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
@@ -1,83 +1,169 @@
1
1
  "use client";
2
2
 
3
- import { useLayoutEffect, useRef, useState } from "react";
3
+ import { useCallback, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
4
4
  import { trailStart } from "./narrow";
5
5
  import { nameOf } from "./paths";
6
+ import { placePopover, usePlaceOnResize } from "./popover";
7
+ import { useDismiss } from "./useDismiss";
6
8
 
7
- type BreadcrumbProps = { repo: string; trail: readonly string[]; onOpen: (folder: string) => void };
9
+ type BreadcrumbProps = {
10
+ /** The repo's name, which goes to the top level, with its picker when several repos are indexed (RepoMenu). */
11
+ repo: ReactNode;
12
+ /** The repo's root path, for the steps' titles. */
13
+ repoRoot: string;
14
+ /** Folder paths from the root down to the one on screen: ["", "src", "src/vs"]. */
15
+ trail: readonly string[];
16
+ onOpen: (folder: string) => void;
17
+ /**
18
+ * A window under 360 px: the repo's name folds into the "…" with every folder but the one on screen, which keeps the
19
+ * room. At the top level the repo's name is the whole trail.
20
+ */
21
+ compact: boolean;
22
+ };
8
23
 
9
- /** The space between steps (gap-1). */
24
+ /** The space between steps (gap-1), and the folded folders' menu width. */
10
25
  const STEP_GAP = 4;
26
+ const MENU_WIDTH = 260;
11
27
 
12
28
  /**
13
- * Where the Map is: "vscode / src / vs" (VISUAL_REFRESH.md §4). Every step but the last goes back up to it.
29
+ * Where the Map is, in mono: "microsoft/vscode / src / vs / workbench" (the Workbench redesign, ADR-070). The repo
30
+ * comes first and every step but the last goes back up to it; at the top level it reads "microsoft/vscode / root".
14
31
  *
15
- * When the trail doesn't fit (a deep folder, a narrow window), it truncates from its start: the first steps fold into
16
- * one "…", which goes up to the last of them and names them all in its title, so the current folder always shows
17
- * (truncating itself, whole in its title, if even it is too wide). The steps' natural widths come from a hidden copy
18
- * of the trail, measured whenever it or the room for it changes (narrow.ts's trailStart decides how many fold).
32
+ * When the trail doesn't fit (a deep folder, a narrow window), it keeps the repo and the folder on screen and folds the
33
+ * folders between into a "…" that opens a list of them to jump to (the owner, 2026-10-27). It never scrolls sideways.
34
+ * When even that is too wide, the repo's owner goes (whole or not at all, never a sliver of it), then the repo's name
35
+ * truncates, then the folder on screen, each whole in its title. The steps' natural widths come from a hidden copy of
36
+ * the trail, measured whenever it or the room for it changes. In a window under 360 px the repo's name folds in too,
37
+ * first in the list (it goes to the top level, where the repo's name and its picker are the whole trail).
19
38
  */
20
- export default function Breadcrumb({ repo, trail, onOpen }: BreadcrumbProps){
39
+ export default function Breadcrumb({ repo, repoRoot, trail, onOpen, compact }: BreadcrumbProps){
40
+ const steps = trail.slice(1);
21
41
  const nav = useRef<HTMLElement>(null);
42
+ const repoBox = useRef<HTMLSpanElement>(null);
22
43
  const ruler = useRef<HTMLDivElement>(null);
23
- // how many steps from the start are folded into "…", for the trail it was measured on
24
- const [fold, setFold] = useState<{ trail: readonly string[]; count: number }>({ trail, count: 0 });
25
- const folded = fold.trail === trail ? fold.count : 0;
44
+ // how many folders after the repo are folded into "…", and whether the repo's owner shows, for the trail measured
45
+ const [fold, setFold] = useState<{ trail: readonly string[]; count: number; owner: boolean }>({ trail, count: 0, owner: true });
46
+ const repoFolded = compact && steps.length > 0;
47
+ const folded = repoFolded ? steps.length - 1 : fold.trail === trail ? fold.count : 0;
48
+ const ownerShown = fold.trail !== trail || fold.owner;
49
+ // the "…" list: the repo's top level first when its name folded too, then the folders between
50
+ const foldedSteps = repoFolded ? ["", ...steps.slice(0, folded)] : steps.slice(0, folded);
26
51
 
27
52
  // measured before the browser paints the new trail, so a deep folder's whole trail never shows for a frame over its
28
- // neighbours; then again whenever the room or a step's width changes (the window resized, the font arrived)
53
+ // neighbours, then again whenever the room or a step's width changes (the window resized, the font arrived)
29
54
  useLayoutEffect(() => {
30
- const navEl = nav.current, rulerEl = ruler.current;
31
- if (!navEl || !rulerEl) return;
55
+ const navEl = nav.current, rulerEl = ruler.current, repoEl = repoBox.current;
56
+ if (!navEl || !rulerEl || !repoEl) return;
57
+ // the header's search field gives way before the trail does (Header), down to its least width: what it can still
58
+ // give is the trail's too. Measured from the nav alone, a trail folded for a narrower window, or a font not yet
59
+ // loaded, would stay folded, its nav only as wide as what it shows.
60
+ const search = navEl.closest("header")?.querySelector<HTMLElement>('[data-testid="search-button"]') ?? null;
61
+ const slack = (): number => {
62
+ if (search === null) return 0;
63
+ const style = getComputedStyle(search);
64
+ return style.flexShrink === "0" ? 0 : Math.max(0, search.getBoundingClientRect().width - (parseFloat(style.minWidth) || 0));
65
+ };
32
66
  const measure = (): void => {
33
67
  const widths = [...rulerEl.children].map((step) => step.getBoundingClientRect().width);
34
68
  const ellipsis = widths.pop() ?? 0; // the ruler ends with the "…" step
35
- // the steps' widths are fractional, so the room is too: a whole-pixel clientWidth could fold a step that fits
36
- const count = trailStart(widths, navEl.getBoundingClientRect().width, ellipsis, STEP_GAP);
37
- setFold((last) => last.trail === trail && last.count === count ? last : { trail, count });
69
+ // the repo at its natural width, what it takes now plus what its truncated names gave up (the owner too, which
70
+ // is only narrowed to nothing when it goes): squeezed first, it would otherwise make room for the folders
71
+ // between rather than have them fold
72
+ const given = [...repoEl.querySelectorAll<HTMLElement>(".truncate")].reduce((sum, el) => sum + Math.max(0, el.scrollWidth - el.clientWidth), 0);
73
+ const room = navEl.getBoundingClientRect().width + slack() - repoEl.getBoundingClientRect().width - given - STEP_GAP;
74
+ // the steps fold from the one after the repo onward; the last always stays (narrow.ts's trailStart)
75
+ const start = (space: number): number => widths.length === 0 ? 0 : trailStart(widths, space, ellipsis, STEP_GAP);
76
+ // what the trail after the repo takes from `from` on: the "…" for what's folded, the steps, or the top level's "/ root"
77
+ const needs = (from: number): number => widths.length === 0
78
+ ? navEl.querySelector("[data-crumb-root]")?.getBoundingClientRect().width ?? 0
79
+ : widths.slice(from).reduce((sum, w) => sum + w + STEP_GAP, from > 0 ? ellipsis + STEP_GAP : 0) - STEP_GAP;
80
+ // the owner stays while the trail fits with it whole; otherwise it goes and the trail folds against the rest
81
+ const owned = start(room);
82
+ const owner = needs(owned) <= room;
83
+ const ownerWidth = repoEl.querySelector("[data-repo-owner]")?.scrollWidth ?? 0;
84
+ const count = owner ? owned : start(room + ownerWidth);
85
+ setFold((last) => last.trail === trail && last.count === count && last.owner === owner ? last : { trail, count, owner });
38
86
  };
39
87
  measure();
40
88
  const observer = new ResizeObserver(measure);
41
89
  observer.observe(navEl);
42
- // each step, not the ruler itself: the ruler is the nav's width whatever its text, so only the steps change size
90
+ observer.observe(repoEl);
91
+ if (search !== null) observer.observe(search);
43
92
  for (const step of rulerEl.children) observer.observe(step);
44
93
  return () => observer.disconnect();
45
94
  }, [trail]);
46
95
 
47
- const label = (step: string, last: boolean) => last
48
- ? <span aria-current="page" title={step || nameOf(step, repo)} className="truncate px-1 font-medium text-text">{nameOf(step, repo)}</span>
49
- : <button type="button" onClick={() => onOpen(step)} title={step || nameOf(step, repo)} className="truncate rounded-md px-1 hover:bg-surface hover:text-text">{nameOf(step, repo)}</button>;
50
-
96
+ const here = steps.length === 0 ? "root" : nameOf(steps[steps.length - 1], repoRoot);
51
97
  return (
52
- <nav ref={nav} aria-label="Where you are" className="relative flex min-w-0 flex-1 items-center gap-1 text-[14px] text-text-2">
53
- {folded > 0 && (
54
- <button
55
- type="button"
56
- data-testid="breadcrumb-folded"
57
- onClick={() => onOpen(trail[folded - 1])}
58
- title={`Up to ${nameOf(trail[folded - 1], repo)}: ${trail.slice(0, folded).map((step) => nameOf(step, repo)).join(" / ")}`}
59
- aria-label={`Up to ${nameOf(trail[folded - 1], repo)}`}
60
- className="shrink-0 rounded-md px-1 hover:bg-surface hover:text-text"
61
- >
62
- …
63
- </button>
98
+ <nav ref={nav} aria-label="Where you are" data-owner={ownerShown ? undefined : "hidden"} className="group/crumb relative flex min-w-0 flex-1 items-center gap-1 font-mono text-[12.5px] text-text-2">
99
+ {/* Short of room it gives way before the folder on screen (shrink 1000 to the folder's 1), down to a floor
100
+ that keeps a few letters of its name and its ▾. Hidden rather than gone while it's folded, so an open
101
+ repo list keeps its place for a wider window. */}
102
+ <span ref={repoBox} className={`${repoFolded ? "hidden" : "flex"} min-w-[30px] shrink-[1000] items-center has-[[data-testid=repo-menu]]:min-w-[50px]`}>{repo}</span>
103
+ {foldedSteps.length > 0 && (
104
+ <>
105
+ {!repoFolded && <span aria-hidden className="shrink-0">/</span>}
106
+ <FoldedSteps steps={foldedSteps} repoRoot={repoRoot} onOpen={onOpen} />
107
+ </>
64
108
  )}
65
- {trail.map((step, i) => i < folded ? null : (
66
- <span key={step || "(root)"} className={`flex items-center gap-1 ${i === trail.length - 1 ? "min-w-0" : "shrink-0"}`}>
67
- {i > 0 && <span aria-hidden>/</span>}
68
- {label(step, i === trail.length - 1)}
109
+ {steps.map((step, i) => i < folded ? null : (
110
+ <span key={step} className={`flex items-center gap-1 ${i === steps.length - 1 ? "min-w-0" : "shrink-0"}`}>
111
+ <span aria-hidden>/</span>
112
+ {i === steps.length - 1
113
+ ? <span aria-current="page" title={step} className="truncate text-text">{nameOf(step, repoRoot)}</span>
114
+ : <button type="button" onClick={() => onOpen(step)} title={step} className="truncate rounded-[4px] px-0.5 hover:bg-node hover:text-text">{nameOf(step, repoRoot)}</button>}
69
115
  </span>
70
116
  ))}
71
- {/* the trail at its natural width, unseen and unclickable, for measuring; the "…" step last */}
117
+ {steps.length === 0 && !compact && (
118
+ <span data-crumb-root className="flex shrink-0 items-center gap-1"><span aria-hidden>/</span><span aria-current="page" className="text-text-2">{here}</span></span>
119
+ )}
120
+ {/* the steps at their natural width, unseen and unclickable, for measuring; the "…" step last */}
72
121
  <div ref={ruler} aria-hidden className="pointer-events-none invisible absolute inset-x-0 top-0 flex h-0 gap-1 overflow-hidden whitespace-nowrap">
73
- {trail.map((step, i) => (
74
- <span key={step || "(root)"} className="flex shrink-0 items-center gap-1">
75
- {i > 0 && <span>/</span>}
76
- <span className={`px-1 ${i === trail.length - 1 ? "font-medium" : ""}`}>{nameOf(step, repo)}</span>
77
- </span>
122
+ {steps.map((step) => (
123
+ <span key={step} className="flex shrink-0 items-center gap-1"><span>/</span><span className="px-0.5">{nameOf(step, repoRoot)}</span></span>
78
124
  ))}
79
- <span className="shrink-0 px-1">…</span>
125
+ <span className="flex shrink-0 items-center gap-1"><span>/</span><span className="px-1.5">…</span></span>
80
126
  </div>
81
127
  </nav>
82
128
  );
83
129
  }
130
+
131
+ /** The "…" for the folders folded out of the trail, and the list of them it opens, each one a way up to it. */
132
+ function FoldedSteps({ steps, repoRoot, onOpen }: { steps: readonly string[]; repoRoot: string; onOpen: (folder: string) => void }){
133
+ const [open, setOpen] = useState(false);
134
+ const [place, setPlace] = useState<CSSProperties>({ left: 0, width: MENU_WIDTH });
135
+ const ref = useDismiss<HTMLSpanElement>(open, () => setOpen(false));
136
+ const placeMenu = useCallback(() => {
137
+ if (ref.current) setPlace(placePopover(ref.current, MENU_WIDTH, "start"));
138
+ }, [ref]);
139
+ usePlaceOnResize(open, placeMenu);
140
+ return (
141
+ <span ref={ref} className="relative shrink-0">
142
+ <button
143
+ type="button"
144
+ data-testid="breadcrumb-folded"
145
+ aria-expanded={open}
146
+ aria-label={`${steps.length} more ${steps.length === 1 ? "folder" : "folders"} on the way here`}
147
+ title={steps.map((step) => nameOf(step, repoRoot)).join(" / ")}
148
+ onClick={() => {
149
+ if (!open) placeMenu();
150
+ setOpen((o) => !o);
151
+ }}
152
+ className="rounded-[4px] border border-border px-1.5 leading-[18px] hover:bg-node hover:text-text"
153
+ >
154
+ …
155
+ </button>
156
+ {open && (
157
+ <ul data-testid="breadcrumb-menu" data-covers style={place} className="absolute top-7 z-30 overflow-y-auto rounded-md border border-border bg-panel py-1 font-sans text-[13px]">
158
+ {steps.map((step) => (
159
+ <li key={step}>
160
+ <button type="button" onClick={() => { setOpen(false); onOpen(step); }} title={step} className="block w-full truncate px-3 py-1.5 text-left font-mono text-[12.5px] text-text hover:bg-node">
161
+ {nameOf(step, repoRoot)}
162
+ </button>
163
+ </li>
164
+ ))}
165
+ </ul>
166
+ )}
167
+ </span>
168
+ );
169
+ }