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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +11 -11
- package/.next/diagnostics/route-bundle-stats.json +10 -10
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/server/app/_not-found/page/next-font-manifest.json +2 -3
- package/.next/server/app/_not-found/page.js +3 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +27 -20
- package/.next/server/app/_not-found.segments/_full.segment.rsc +27 -20
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +34 -26
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +29 -23
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +33 -24
- package/.next/server/app/index.segments/_full.segment.rsc +29 -23
- package/.next/server/app/index.segments/_tree.segment.rsc +4 -5
- package/.next/server/app/page/build-manifest.json +3 -3
- package/.next/server/app/page/next-font-manifest.json +2 -3
- package/.next/server/app/page.js +4 -4
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +3 -3
- package/.next/server/chunks/_0uqus0ehnijt-._.js +1 -1
- package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0_3l5tdzqy-td._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__10ockmlli-lun._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0dyligeswbb47._.js → [root-of-the-server]__16-gclvix9mjd._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1auodc3c0rppy._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1i9r59yk4i69p._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1l3qp8u9yc_ig._.js +3 -0
- package/.next/server/chunks/ssr/_1p-0wsx9qfm68._.js +13 -0
- package/.next/server/chunks/ssr/app_map_18iu6j19jsqmu._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_1r3iqtbm45lqp._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js +3 -0
- package/.next/server/middleware-build-manifest.js +11 -11
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +4 -6
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{1rljem7o1jtdo.js → 18kfexnrqek07.js} +1 -1
- package/.next/static/chunks/1hwbgd4nvvhg8.js +1 -0
- package/.next/static/chunks/25r4lkkf1shfo.css +3 -0
- package/.next/static/chunks/35ubvk2ivdjgs.js +1 -0
- package/.next/static/chunks/3ieqvmcg3hofj.js +31 -0
- package/.next/static/chunks/3w00e1w2125pp.js +11 -0
- package/.next/static/media/733da6bd8dac84c8-s.3hlisc-pwq13z.woff2 +0 -0
- package/.next/static/media/784318cea1dfcfa4-s.0-oo9_1x_xmvg.woff2 +0 -0
- package/.next/static/media/7e114457660b62ed-s.36fqqi66-3tjb.woff2 +0 -0
- package/.next/static/media/85e86a88f68a624a-s.357dd_6abeqge.woff2 +0 -0
- package/.next/static/media/9a321a2c78eff0b5-s.p.1bkzbscqrt8rl.woff2 +0 -0
- package/.next/static/media/9fecbd3e5ea9bccb-s.3_z45zcoc-xoz.woff2 +0 -0
- package/.next/static/media/c64053b510252f55-s.1kd-r_s9smv5m.woff2 +0 -0
- package/.next/static/media/c6a5c2fdc12da45c-s.p.17-aodiw50953.woff2 +0 -0
- package/.next/static/media/f263782c6a4bddee-s.1gyfqm5yhhzs7.woff2 +0 -0
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/README.md +13 -4
- package/app/globals.css +91 -54
- package/app/layout.tsx +20 -11
- package/app/map/BackButton.tsx +5 -5
- package/app/map/BoxActions.tsx +20 -12
- package/app/map/Breadcrumb.tsx +132 -46
- package/app/map/DetailPanel.tsx +408 -297
- package/app/map/Explorer.tsx +268 -0
- package/app/map/Header.tsx +105 -0
- package/app/map/HelpDialog.tsx +37 -34
- package/app/map/Legend.tsx +19 -75
- package/app/map/LineCard.tsx +187 -0
- package/app/map/MapBox.tsx +73 -38
- package/app/map/MapCanvas.tsx +344 -115
- package/app/map/MapToolbar.tsx +377 -0
- package/app/map/MapView.tsx +217 -213
- package/app/map/RepoMenu.tsx +13 -12
- package/app/map/RoutedEdge.tsx +48 -16
- package/app/map/SearchPalette.tsx +6 -6
- package/app/map/SourceViewer.tsx +19 -17
- package/app/map/StatusBar.tsx +166 -0
- package/app/map/embed.ts +22 -2
- package/app/map/emphasis.ts +94 -8
- package/app/map/explorerRows.ts +85 -0
- package/app/map/flowNodes.ts +12 -7
- package/app/map/format.ts +2 -2
- package/app/map/history.ts +4 -3
- package/app/map/inspector.ts +113 -0
- package/app/map/labels.ts +66 -7
- package/app/map/narrow.ts +165 -85
- package/app/map/opening.ts +16 -0
- package/app/map/portals.ts +2 -2
- package/app/map/routes.ts +1060 -55
- package/app/map/runs.ts +108 -0
- package/app/map/shell.ts +91 -0
- package/app/map/shortcuts.ts +2 -1
- package/app/map/summaries.ts +41 -5
- package/app/map/text.ts +9 -1
- package/app/map/theme.ts +197 -0
- package/app/map/useDismiss.ts +20 -6
- package/app/map/useEditorBridge.ts +12 -1
- package/app/map/useThemeKind.ts +28 -0
- package/app/map/useWindowSize.ts +37 -0
- package/app/map/viewport.ts +134 -16
- package/app/map/weights.ts +80 -0
- package/app/page.tsx +3 -5
- package/lib/background/child.ts +2 -0
- package/lib/background/service.ts +26 -10
- package/lib/contracts.ts +25 -2
- package/lib/db/migrations/0011_plain_summaries_prompt_version.sql +1 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1498 -0
- package/lib/db/migrations/meta/_journal.json +7 -0
- package/lib/db/schema.ts +2 -1
- package/lib/db/store/map.ts +6 -0
- package/lib/db/store/summaries.ts +14 -8
- package/lib/engine/describe.ts +2 -0
- package/lib/index/summarize/budget.ts +5 -2
- package/lib/index/summarize/claude.ts +19 -4
- package/lib/index/summarize/compare.ts +2 -2
- package/lib/index/summarize/plan.ts +73 -13
- package/lib/index/summarize/preview.ts +12 -9
- package/lib/index/summarize/prompts.ts +122 -25
- package/lib/index/summarize/run.ts +48 -18
- package/lib/index/summarize/snapshot.ts +7 -2
- package/lib/index/summarize/status.ts +3 -2
- package/lib/tools/text.ts +3 -2
- package/mcp/server.ts +1 -1
- package/package.json +4 -2
- package/scripts/commands/summarize-command.ts +27 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +0 -3
- package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +0 -3
- package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +0 -9
- package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +0 -3
- package/.next/static/chunks/02x9pxyl2d8om.css +0 -3
- package/.next/static/chunks/0p-0bv2dzer18.js +0 -7
- package/.next/static/chunks/10u7p2gj02qay.js +0 -1
- package/.next/static/chunks/31qp8jfkw0x2h.js +0 -31
- package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
- package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
- package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
- package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
- package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
- package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
- package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
- package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
- package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
- package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
- package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
- package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
- package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
- package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
- package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
- package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
- package/app/map/ActivityBar.tsx +0 -81
- package/app/map/StatusPill.tsx +0 -93
- package/app/map/TopBar.tsx +0 -84
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_buildManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState, type MouseEvent, type ReactNode } from "react";
|
|
4
|
+
import { IconFile, IconFolder, IconX } from "@tabler/icons-react";
|
|
5
|
+
import type { LevelResponse } from "@/lib/contracts";
|
|
6
|
+
import { explorerView, rowTone, type ExplorerRow, type ExplorerView, type RowTone } from "./explorerRows";
|
|
7
|
+
import { formatCount } from "./format";
|
|
8
|
+
import { useDismiss } from "./useDismiss";
|
|
9
|
+
|
|
10
|
+
export type ExplorerProps = {
|
|
11
|
+
/** The level on screen: its folders, files and doorways, its lines, and which page of it this is. */
|
|
12
|
+
level: LevelResponse;
|
|
13
|
+
/** The selected box's path, or null. */
|
|
14
|
+
selected: string | null;
|
|
15
|
+
/** The paths joined to the selection by a drawn line. */
|
|
16
|
+
linked: ReadonlySet<string>;
|
|
17
|
+
/** full is the 236 px list. rail is a 40 px column whose one button opens the list over the canvas. */
|
|
18
|
+
form: "full" | "rail";
|
|
19
|
+
/** A click on a row selects that box on the Map. */
|
|
20
|
+
onSelect: (path: string) => void;
|
|
21
|
+
/** A double click on a folder's row goes into it. */
|
|
22
|
+
onOpen: (folder: string) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How long the rail's list stays open after a click on a row. The second click of a double click has to find the row
|
|
27
|
+
* still there: closed at once, it would land on the canvas underneath and let go of the selection (the pane's click), or
|
|
28
|
+
* open whatever box sits under the pointer.
|
|
29
|
+
*/
|
|
30
|
+
const DOUBLE_CLICK_WAIT = 350; // ms
|
|
31
|
+
|
|
32
|
+
const ROW_TONE: Record<RowTone, string> = {
|
|
33
|
+
selected: "bg-row-sel text-text",
|
|
34
|
+
linked: "text-text hover:bg-node",
|
|
35
|
+
plain: "text-text-2 hover:bg-node",
|
|
36
|
+
};
|
|
37
|
+
const BAR_TONE: Record<RowTone, string> = { selected: "bg-accent", linked: "bg-linked-glyph", plain: "bg-glyph-dim" };
|
|
38
|
+
|
|
39
|
+
const rowOf = (list: HTMLElement, path: string): HTMLElement | null => list.querySelector<HTMLElement>(`[data-explorer-row="${CSS.escape(path)}"]`);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Scrolls the list just far enough to show the selected row, the way VS Code's explorer follows the editor: a box
|
|
43
|
+
* selected on the Map may sit far down a long list. Only the list moves. scrollIntoView would scroll every ancestor
|
|
44
|
+
* that can, the page among them, and the page must never scroll.
|
|
45
|
+
*/
|
|
46
|
+
function reveal(list: HTMLElement | null, path: string | null): void {
|
|
47
|
+
const row = list !== null && path !== null ? rowOf(list, path) : null;
|
|
48
|
+
if (list === null || row === null) return;
|
|
49
|
+
const top = row.offsetTop;
|
|
50
|
+
const bottom = top + row.offsetHeight;
|
|
51
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
52
|
+
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** "Explorer" and how many items, over the list. The rail's list adds its close button at the end. */
|
|
56
|
+
function Header({ count, tall, children }: { count: string; tall?: boolean; children?: ReactNode }){
|
|
57
|
+
return (
|
|
58
|
+
<div className={`flex shrink-0 items-center justify-between gap-2 pl-[14px] text-[11.5px] text-text-3 ${tall ? "h-10 pr-2" : "h-[34px] pr-[14px]"}`}>
|
|
59
|
+
<span className="truncate">Explorer</span>
|
|
60
|
+
<span className="flex shrink-0 items-center gap-1.5 whitespace-nowrap">{count}{children}</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One row, a real button: the icon, the name in mono, and for a folder its bar and how many files it holds. The whole
|
|
67
|
+
* path is its title, since a long name is cut short with an ellipsis. The focus ring is drawn inside, where the list's
|
|
68
|
+
* edges can't clip it.
|
|
69
|
+
*/
|
|
70
|
+
function Row({ row, tone, onPick, onOpen }: {
|
|
71
|
+
row: ExplorerRow;
|
|
72
|
+
tone: RowTone;
|
|
73
|
+
onPick: (path: string, event: MouseEvent<HTMLButtonElement>) => void;
|
|
74
|
+
onOpen: (folder: string) => void;
|
|
75
|
+
}){
|
|
76
|
+
const folder = row.kind === "folder";
|
|
77
|
+
const Icon = folder ? IconFolder : IconFile;
|
|
78
|
+
return (
|
|
79
|
+
<li>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
data-explorer-row={row.path}
|
|
83
|
+
aria-current={tone === "selected" ? "true" : undefined}
|
|
84
|
+
aria-label={folder ? row.label : undefined}
|
|
85
|
+
title={row.path}
|
|
86
|
+
onClick={(event) => onPick(row.path, event)}
|
|
87
|
+
onDoubleClick={folder ? () => onOpen(row.path) : undefined}
|
|
88
|
+
className={`grid h-[26px] w-full grid-cols-[16px_minmax(0,1fr)_36px_minmax(44px,max-content)] items-center gap-2 pl-[14px] pr-3 text-left focus-visible:-outline-offset-2 ${ROW_TONE[tone]}`}
|
|
89
|
+
>
|
|
90
|
+
<Icon aria-hidden size={14} stroke={1.75} className="text-text-3" />
|
|
91
|
+
<span className={`truncate font-mono text-[12.5px] ${folder ? "" : "col-span-3"}`}>{row.name}</span>
|
|
92
|
+
{folder && (
|
|
93
|
+
<>
|
|
94
|
+
<span aria-hidden className={`h-[3px] rounded-[2px] ${BAR_TONE[tone]}`} style={{ width: row.bar }} />
|
|
95
|
+
<span className="whitespace-nowrap text-right text-[11.5px] tabular-nums text-text-3">{formatCount(row.files)}</span>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
</li>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type RowsProps = {
|
|
104
|
+
view: ExplorerView;
|
|
105
|
+
selected: string | null;
|
|
106
|
+
linked: ReadonlySet<string>;
|
|
107
|
+
onPick: (path: string, event: MouseEvent<HTMLButtonElement>) => void;
|
|
108
|
+
onOpen: (folder: string) => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The rows, scrolling inside when there are more than fit, and under them, for a folder drawn a page at a time, which
|
|
113
|
+
* page this is. It keeps the selected row in view as the selection changes.
|
|
114
|
+
*/
|
|
115
|
+
function Rows({ view, selected, linked, onPick, onOpen }: RowsProps){
|
|
116
|
+
const list = useRef<HTMLDivElement>(null);
|
|
117
|
+
useEffect(() => reveal(list.current, selected), [selected, view]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div ref={list} className="relative min-h-0 flex-1 overflow-y-auto pb-[10px]">
|
|
121
|
+
<ul aria-label="Folders and files">
|
|
122
|
+
{view.rows.map((row) => <Row key={row.path} row={row} tone={rowTone(row.path, selected, linked)} onPick={onPick} onOpen={onOpen} />)}
|
|
123
|
+
</ul>
|
|
124
|
+
{view.pageLine !== null && <p className="px-[14px] pt-2 text-[11.5px] leading-4 text-text-3">{view.pageLine}</p>}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The list over the canvas, from the rail's right edge. It's a dialog, so the Map's own keys (Escape, Backspace, the
|
|
131
|
+
* arrows) wait while it's open (MapView asks the page whether one is). The focus goes to the selected row, or the first,
|
|
132
|
+
* so the keyboard can carry on from the button that opened it.
|
|
133
|
+
*/
|
|
134
|
+
function Overlay({ id, view, selected, linked, onPick, onOpen, onClose }: RowsProps & { id: string; onClose: () => void }){
|
|
135
|
+
const self = useRef<HTMLDivElement>(null);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const dialog = self.current;
|
|
138
|
+
const row = dialog?.querySelector<HTMLElement>('[aria-current="true"]') ?? dialog?.querySelector<HTMLElement>("[data-explorer-row]");
|
|
139
|
+
// the list has already scrolled to the selection (Rows), so the focus mustn't scroll anything again
|
|
140
|
+
row?.focus({ preventScroll: true });
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
ref={self}
|
|
146
|
+
id={id}
|
|
147
|
+
role="dialog"
|
|
148
|
+
aria-label="Explorer"
|
|
149
|
+
className="absolute bottom-0 left-full top-0 z-20 flex w-[236px] max-w-[calc(100vw_-_48px)] flex-col border-r border-divider bg-panel"
|
|
150
|
+
>
|
|
151
|
+
<Header count={view.count} tall>
|
|
152
|
+
<button type="button" onClick={onClose} aria-label="Close the explorer" className="grid h-6 w-6 place-items-center rounded-md text-text-2 hover:bg-node hover:text-text">
|
|
153
|
+
<IconX aria-hidden size={16} stroke={1.75} />
|
|
154
|
+
</button>
|
|
155
|
+
</Header>
|
|
156
|
+
<Rows view={view} selected={selected} linked={linked} onPick={onPick} onOpen={onOpen} />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The explorer as a 40 px rail, for a window too narrow to keep the whole list beside the canvas: one button that opens
|
|
163
|
+
* the list over the canvas. Escape, the ×, a click outside or the focus moving away closes it. A click on a row selects
|
|
164
|
+
* that box and closes it a moment later, so a double click still opens a folder (DOUBLE_CLICK_WAIT). The keyboard's
|
|
165
|
+
* Enter has no second click to wait for, so it closes at once.
|
|
166
|
+
*/
|
|
167
|
+
function Rail({ view, selected, linked, onSelect, onOpen }: Omit<RowsProps, "onPick" | "onOpen"> & Pick<ExplorerProps, "onSelect" | "onOpen">){
|
|
168
|
+
const [open, setOpen] = useState(false);
|
|
169
|
+
// bumped by each click on a row: the list closes DOUBLE_CLICK_WAIT after the last
|
|
170
|
+
const [picks, setPicks] = useState(0);
|
|
171
|
+
const id = useId();
|
|
172
|
+
const button = useRef<HTMLButtonElement>(null);
|
|
173
|
+
|
|
174
|
+
/** Closes the list without moving the focus: a click outside, or the focus already gone elsewhere. */
|
|
175
|
+
const dismiss = useCallback((): void => {
|
|
176
|
+
setOpen(false);
|
|
177
|
+
setPicks(0);
|
|
178
|
+
}, []);
|
|
179
|
+
const root = useDismiss<HTMLElement>(open, dismiss);
|
|
180
|
+
/** Closes the list, the focus going back to the rail's button when it was in the list, rather than to the page. */
|
|
181
|
+
const close = useCallback((): void => {
|
|
182
|
+
if (root.current?.contains(document.activeElement)) button.current?.focus();
|
|
183
|
+
dismiss();
|
|
184
|
+
}, [dismiss, root]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (picks === 0) return;
|
|
188
|
+
const timer = setTimeout(close, DOUBLE_CLICK_WAIT);
|
|
189
|
+
return () => clearTimeout(timer);
|
|
190
|
+
}, [picks, close]);
|
|
191
|
+
|
|
192
|
+
// Escape closes just the list. Caught first, on the way down, and stopped there, so it doesn't also reach the Map's
|
|
193
|
+
// Escape, which would let go of the selection the moment the list is gone
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!open) return;
|
|
196
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
197
|
+
if (event.key !== "Escape") return;
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
close();
|
|
200
|
+
};
|
|
201
|
+
window.addEventListener("keydown", onKey, true);
|
|
202
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
203
|
+
}, [open, close]);
|
|
204
|
+
|
|
205
|
+
const pick = (path: string, event: MouseEvent<HTMLButtonElement>): void => {
|
|
206
|
+
onSelect(path);
|
|
207
|
+
// a click's detail counts its clicks, and Enter or Space on the row gives 0
|
|
208
|
+
if (event.detail === 0) close();
|
|
209
|
+
else setPicks((n) => n + 1);
|
|
210
|
+
};
|
|
211
|
+
const openFolder = (folder: string): void => {
|
|
212
|
+
onOpen(folder);
|
|
213
|
+
close();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<aside
|
|
218
|
+
ref={root}
|
|
219
|
+
aria-label="Explorer"
|
|
220
|
+
onBlur={(event) => {
|
|
221
|
+
// the focus left for something outside (Tab past the last row, the search box opening): close behind it
|
|
222
|
+
if (open && event.relatedTarget instanceof Node && !event.currentTarget.contains(event.relatedTarget)) dismiss();
|
|
223
|
+
}}
|
|
224
|
+
className="relative flex h-full w-full flex-col items-center border-r border-divider bg-panel pt-1.5"
|
|
225
|
+
>
|
|
226
|
+
<button
|
|
227
|
+
ref={button}
|
|
228
|
+
type="button"
|
|
229
|
+
aria-label="Open the explorer"
|
|
230
|
+
title="Explorer"
|
|
231
|
+
aria-expanded={open}
|
|
232
|
+
aria-controls={open ? id : undefined}
|
|
233
|
+
onClick={() => (open ? close() : setOpen(true))}
|
|
234
|
+
className={`grid h-7 w-7 shrink-0 place-items-center rounded-[5px] ${open ? "bg-node text-text" : "text-text-2 hover:bg-node hover:text-text"}`}
|
|
235
|
+
>
|
|
236
|
+
<IconFolder aria-hidden size={18} stroke={1.75} />
|
|
237
|
+
</button>
|
|
238
|
+
{open && (
|
|
239
|
+
<Overlay id={id} view={view} selected={selected} linked={linked} onPick={pick} onOpen={openFolder} onClose={close} />
|
|
240
|
+
)}
|
|
241
|
+
</aside>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The explorer (the Workbench redesign, ADR-070): the boxes of the level on screen as a list beside the canvas, the
|
|
247
|
+
* browser's answer to VS Code's own side bar, which the panel inside VS Code has instead. Folders come first, the
|
|
248
|
+
* largest at the top, each with a bar on the same log scale as its dot and how many files it holds, then the files by
|
|
249
|
+
* name. Doorways to other pages aren't rows, and a folder drawn a page at a time says which page under the list. The rows
|
|
250
|
+
* follow the Map: the selection's row is filled, and what's joined to it by a line is brighter with a lilac bar. A click
|
|
251
|
+
* selects that box on the Map, and a double click on a folder goes into it. In a narrower window it's a 40 px rail
|
|
252
|
+
* whose button opens the same list over the canvas. It fills its parent's height, scrolling inside, and the parent sets
|
|
253
|
+
* its width.
|
|
254
|
+
*/
|
|
255
|
+
function Explorer({ level, selected, linked, form, onSelect, onOpen }: ExplorerProps){
|
|
256
|
+
const view = useMemo(() => explorerView(level), [level]);
|
|
257
|
+
|
|
258
|
+
if (form === "rail") return <Rail view={view} selected={selected} linked={linked} onSelect={onSelect} onOpen={onOpen} />;
|
|
259
|
+
return (
|
|
260
|
+
<aside aria-label="Explorer" className="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden border-r border-divider bg-panel">
|
|
261
|
+
<Header count={view.count} />
|
|
262
|
+
<Rows view={view} selected={selected} linked={linked} onPick={(path) => onSelect(path)} onOpen={onOpen} />
|
|
263
|
+
</aside>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// memo: the list draws again only when its own props change, not with everything else MapView redraws
|
|
268
|
+
export default memo(Explorer);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore, type ReactNode } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { IconSearch } from "@tabler/icons-react";
|
|
6
|
+
import BackButton from "./BackButton";
|
|
7
|
+
import { DEMO } from "./mode";
|
|
8
|
+
|
|
9
|
+
type HeaderProps = {
|
|
10
|
+
/** The breadcrumb, with the repo's name and picker first (Breadcrumb, RepoMenu). */
|
|
11
|
+
crumb: ReactNode;
|
|
12
|
+
onSearch: () => void;
|
|
13
|
+
onHelp: () => void;
|
|
14
|
+
/** Inside VS Code's panel: no logo and no help button (the ? key still opens it). */
|
|
15
|
+
embedded: boolean;
|
|
16
|
+
/** Whether Back has one of the Map's own steps to go back to (history.ts's historyDepth). */
|
|
17
|
+
canGoBack: boolean;
|
|
18
|
+
/** A short window's toolbar, folded into this bar (shell.ts). */
|
|
19
|
+
toolbar?: ReactNode;
|
|
20
|
+
/** A very short window's status dots, at the bar's end (shell.ts). */
|
|
21
|
+
status?: ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const noSubscription = () => () => {};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The search field's icon form: under 640 px, or under 1000 where the toolbar's controls share the bar (and gone under
|
|
28
|
+
* 360). As a field it gives way before the breadcrumb does (flex-shrink 1000 to its 1), down to 160 px.
|
|
29
|
+
*/
|
|
30
|
+
const SEARCH_ICON = {
|
|
31
|
+
alone: { field: "max-[640px]:min-w-0 max-[640px]:w-7 max-[640px]:flex-none max-[640px]:cursor-pointer max-[640px]:justify-center max-[640px]:p-0", words: "max-[640px]:hidden" },
|
|
32
|
+
shared: { field: "max-[1000px]:min-w-0 max-[1000px]:w-7 max-[1000px]:flex-none max-[1000px]:cursor-pointer max-[1000px]:justify-center max-[1000px]:p-0 max-[360px]:hidden", words: "max-[1000px]:hidden" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Map's one top bar (the Workbench redesign, ADR-070), 42 px, 38 inside VS Code's panel. Left to right: the logo
|
|
37
|
+
* and name, Back, the breadcrumb in mono with the repo first, the search field, and help. In the demo the logo goes to
|
|
38
|
+
* nebulosity.ca (the owner, 2026-10-27). Inside VS Code's panel it's Back, the breadcrumb and search, since VS Code has
|
|
39
|
+
* its own chrome. Back is the same button in the browser, the demo and the panel, right before the breadcrumb it
|
|
40
|
+
* changes, and greyed out where none of the Map's own steps lie behind it (the owner's pick, 2026-10-29).
|
|
41
|
+
*
|
|
42
|
+
* A narrower window drops secondary parts one step at a time, so nothing sits on anything else and nothing scrolls
|
|
43
|
+
* sideways: under 1000 px the search's shortcut, under 900 the Nebulosity name (the mark stays), under 640 the search's
|
|
44
|
+
* words (the field becomes its icon), and under 480 the help button (the toolbar's … menu has it). Under 360 the mark
|
|
45
|
+
* and its divider go too, and the bar's gaps and Back's box narrow (24 px, 4 px from the breadcrumb), so the folder on
|
|
46
|
+
* screen keeps a readable name at 200 × 200 with the toolbar and the status dots in this bar. Back never goes, so under
|
|
47
|
+
* 360 the browser's bar is the panel's. The breadcrumb takes what's left and folds its middle (under 360 the repo's
|
|
48
|
+
* name with it). In a short window the toolbar's controls and then the status dots join this bar, which then has less
|
|
49
|
+
* to give: the search is its icon under 1000 px, and under 360 it moves into the toolbar's … menu (Ctrl K still opens
|
|
50
|
+
* it).
|
|
51
|
+
*/
|
|
52
|
+
export default function Header({ crumb, onSearch, onHelp, embedded, canGoBack, toolbar, status }: HeaderProps){
|
|
53
|
+
// the server renders "Ctrl K"; a Mac shows "⌘K" once hydrated, without a mismatch
|
|
54
|
+
const isMac = useSyncExternalStore(noSubscription, () => /Mac|iPhone|iPad/.test(navigator.platform), () => false);
|
|
55
|
+
const icon = toolbar ? SEARCH_ICON.shared : SEARCH_ICON.alone;
|
|
56
|
+
const brand = (
|
|
57
|
+
<>
|
|
58
|
+
<Image src="/icon.svg" alt="" width={18} height={18} unoptimized className="shrink-0" />
|
|
59
|
+
<span className="text-[13px] font-semibold text-text max-[900px]:hidden">Nebulosity</span>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<header className={`relative z-20 flex shrink-0 items-center gap-3 border-b border-divider bg-panel pl-3 pr-2.5 max-[360px]:gap-1.5 max-[360px]:pl-2 max-[360px]:pr-1.5 ${embedded ? "h-[38px]" : "h-[42px]"}`}>
|
|
64
|
+
{/* at its natural width, so a long breadcrumb takes room from the search field before it truncates */}
|
|
65
|
+
<div className="flex min-w-0 flex-[1_1_auto] items-center gap-2.5 max-[360px]:gap-1">
|
|
66
|
+
{!embedded && (DEMO
|
|
67
|
+
? <a href="https://nebulosity.ca" title="nebulosity.ca" className="flex shrink-0 items-center gap-2 rounded-[4px] max-[360px]:hidden">{brand}</a>
|
|
68
|
+
: <span className="flex shrink-0 items-center gap-2 max-[360px]:hidden">{brand}</span>)}
|
|
69
|
+
{!embedded && <span aria-hidden className="h-4 w-px shrink-0 bg-divider max-[360px]:hidden" />}
|
|
70
|
+
<BackButton enabled={canGoBack} />
|
|
71
|
+
{crumb}
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={onSearch}
|
|
76
|
+
data-testid="search-button"
|
|
77
|
+
aria-label="Search files and concepts"
|
|
78
|
+
title={`Search files and concepts (${isMac ? "⌘K" : "Ctrl K"})`}
|
|
79
|
+
className={`flex h-7 min-w-[160px] flex-[0_1000_440px] cursor-text items-center gap-2 rounded-[5px] border border-border bg-canvas pl-2.5 pr-1.5 text-[13px] text-text-3 hover:border-text-3 ${icon.field}`}
|
|
80
|
+
>
|
|
81
|
+
<IconSearch aria-hidden size={14} stroke={2} className="shrink-0" />
|
|
82
|
+
<span className={`w-0 flex-1 truncate text-left ${icon.words}`}>Search files and concepts</span>
|
|
83
|
+
<kbd className="shrink-0 rounded-[4px] border border-border px-1.5 font-sans text-[11.5px] leading-[17px] text-text-3 max-[1000px]:hidden">{isMac ? "⌘K" : "Ctrl K"}</kbd>
|
|
84
|
+
</button>
|
|
85
|
+
{/* as wide as the left side while the search field is centred, and only its own width once it's an icon; never
|
|
86
|
+
narrower than what it holds, which would spill left over the search field (its end is aligned) */}
|
|
87
|
+
<div className={`flex items-center justify-end gap-1.5 ${toolbar ? "shrink-0" : "flex-1 max-[640px]:flex-none"}`}>
|
|
88
|
+
{toolbar}
|
|
89
|
+
{!embedded && (
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={onHelp}
|
|
93
|
+
data-testid="help-button"
|
|
94
|
+
aria-label="How to use the Map"
|
|
95
|
+
title="How to use the Map (?)"
|
|
96
|
+
className="grid h-7 w-7 shrink-0 place-items-center rounded-[5px] border border-border text-[13px] font-semibold text-text-2 hover:border-text-3 hover:text-text max-[480px]:hidden"
|
|
97
|
+
>
|
|
98
|
+
?
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
{status}
|
|
102
|
+
</div>
|
|
103
|
+
</header>
|
|
104
|
+
);
|
|
105
|
+
}
|
package/app/map/HelpDialog.tsx
CHANGED
|
@@ -7,11 +7,13 @@ import { DEMO } from "./mode";
|
|
|
7
7
|
import { useScrollLock } from "./useScrollLock";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* How to use the Map (owner review: "it's kinda confusing at first as to what all of this does"). Opens from the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* How to use the Map (owner review: "it's kinda confusing at first as to what all of this does"). Opens from the
|
|
11
|
+
* header's "?" button, the toolbar's … menu where there's no room for it, or the ? key. It no longer opens by itself on
|
|
12
|
+
* a first visit, since the legend sentence under the canvas says what the marks mean (ADR-070). Rendered into the
|
|
13
|
+
* page's body, above everything; Escape or a click outside closes it. In the static demo it opens with what the demo
|
|
14
|
+
* can't do (ADR-049). Inside VS Code's panel (`embedded`) it says how a file opens in the editor.
|
|
13
15
|
*/
|
|
14
|
-
export default function HelpDialog({ onClose }: { onClose: () => void }){
|
|
16
|
+
export default function HelpDialog({ onClose, embedded = false }: { onClose: () => void; embedded?: boolean }){
|
|
15
17
|
useScrollLock();
|
|
16
18
|
useEffect(() => {
|
|
17
19
|
const onKey = (event: KeyboardEvent): void => {
|
|
@@ -50,59 +52,60 @@ export default function HelpDialog({ onClose }: { onClose: () => void }){
|
|
|
50
52
|
at the top right, or the ? key.
|
|
51
53
|
</p>
|
|
52
54
|
</div>
|
|
53
|
-
<button type="button" onClick={onClose} aria-label="Close help" className="shrink-0 rounded-md p-1 text-text-2 hover:bg-
|
|
55
|
+
<button type="button" onClick={onClose} aria-label="Close help" className="shrink-0 rounded-md p-1 text-text-2 hover:bg-node hover:text-text">
|
|
54
56
|
<IconX aria-hidden size={16} stroke={1.75} />
|
|
55
57
|
</button>
|
|
56
58
|
</div>
|
|
57
59
|
|
|
58
60
|
<div className="mt-6 grid gap-x-8 gap-y-6 text-[14px] leading-normal text-text sm:grid-cols-2">
|
|
59
61
|
<Section title="Getting around">
|
|
60
|
-
<li><b>Click</b> a box to see what it is, in the
|
|
61
|
-
<li><b>Double-click</b> a folder to go inside it.</li>
|
|
62
|
-
|
|
63
|
-
<li>
|
|
64
|
-
<li>
|
|
62
|
+
<li><b>Click</b> a box to see what it is, in the inspector beside or under the Map.</li>
|
|
63
|
+
<li><b>Double-click</b> a folder to go inside it, or click a folder in the explorer on the left.</li>
|
|
64
|
+
{/* a click only selects a file in the panel too (the owner, 2026-10-29) */}
|
|
65
|
+
{embedded && <li>In VS Code, <b>double-click</b> a file to open it in the editor, or press <b>Open in VS Code</b> in the inspector.</li>}
|
|
66
|
+
<li>Go back up with the path at the top left, <Key>Backspace</Key>, or your browser's Back button. The repo's name at its start goes back to the top.</li>
|
|
67
|
+
<li>Big folders are split into pages of related boxes. Turn the page with the arrows in the toolbar, or <Key>←</Key> <Key>→</Key>.</li>
|
|
68
|
+
<li>A busy folder opens at a readable size, from its top. Drag to see the rest, or press <b>Fit</b> in the toolbar to see it all.</li>
|
|
65
69
|
<li>Search files and concepts with <Key>Ctrl K</Key> (<Key>⌘K</Key> on a Mac).</li>
|
|
66
70
|
</Section>
|
|
67
71
|
<Section title="Reading the Map">
|
|
68
|
-
<li>A box is a folder
|
|
69
|
-
<li>A line from A to B means A uses code from B
|
|
70
|
-
<li>Hover a box to
|
|
71
|
-
<li>
|
|
72
|
-
<li>
|
|
73
|
-
<li>
|
|
72
|
+
<li>A box is a folder or a file. A folder's dot grows with how many files it holds, and it has a second card behind it. A file has a page.</li>
|
|
73
|
+
<li>A line from A to B means A uses code from B, and the arrow points at what's being used. Its weight, and the number on it, show how many imports it carries.</li>
|
|
74
|
+
<li>Hover a box to bring its lines forward. Hover a line to read it as a sentence, like "editor uses base, 12 imports". Where lines join, it lists every line on that stretch, and a click keeps the list open until you press Escape or click elsewhere.</li>
|
|
75
|
+
<li><b>Strong links only</b>, in the toolbar, hides the lines under 2% of the heaviest line's imports. Every line of the box you select or hover still shows.</li>
|
|
76
|
+
<li>With a box selected, <b>Uses</b> or <b>Used by</b> in the toolbar keeps only its lines to what it uses, or from what uses it, in color, and turns its other lines gray. <b>All</b> colors both, and it's back to All when you select something else.</li>
|
|
77
|
+
<li>Busy folders (more than 60 lines) draw only the lines of the box you hover or select, until you ask for <b>Show all lines</b>.</li>
|
|
78
|
+
<li>Boxes with no lines on the page sit under a line that says "Not linked to anything here". Lines to other pages end at a doorway, a dashed box like "Lines to page 2", and double-clicking it goes there.</li>
|
|
74
79
|
</Section>
|
|
75
80
|
<Section title="Colors">
|
|
76
|
-
<li><Swatch className="border-2 border-
|
|
77
|
-
<li><Swatch className="outline-2 outline-offset-1 outline-agent" /> Orange
|
|
78
|
-
<li><Swatch className="rounded-full bg-
|
|
79
|
-
<li><Swatch className="rounded-full bg-
|
|
80
|
-
<li><Swatch className="rounded-full bg-danger" /> Red: something failed.</li>
|
|
81
|
+
<li><Swatch className="border-2 border-accent bg-node-sel" /> Purple is the box you selected, the lines to what it uses, and what they join. <span className="whitespace-nowrap"><Swatch className="bg-used-by-edge" /> Gold</span> is the lines from what uses it. Everything else fades.</li>
|
|
82
|
+
<li><Swatch className="outline-2 outline-offset-1 outline-agent" /> Orange is what Claude Code is touching right now.</li>
|
|
83
|
+
<li><Swatch className="rounded-full bg-ok" /> The cyan dot at the bottom left says the index is up to date.</li>
|
|
84
|
+
<li><Swatch className="rounded-full bg-danger" /> Red means something failed.</li>
|
|
81
85
|
</Section>
|
|
82
|
-
<Section title="The
|
|
83
|
-
<li>A short summary
|
|
84
|
-
<li><b>
|
|
85
|
-
<li><b>Contents</b>: click a folder to go in, or a file to select it.</li>
|
|
86
|
+
<Section title="The inspector">
|
|
87
|
+
<li>A short summary, and <b>Full summary</b> for the rest.</li>
|
|
88
|
+
<li><b>Uses</b>, <b>Used by</b> and <b>Contents</b> are tabs. Click a row to jump to it on the Map.</li>
|
|
86
89
|
{DEMO
|
|
87
|
-
? <li>For a file, <b>View on GitHub</b> opens its code at the indexed commit
|
|
88
|
-
: <li>For a file, <b>View source</b> shows its code
|
|
90
|
+
? <li>For a file, <b>View on GitHub</b> opens its code at the indexed commit, and a declaration opens there too.</li>
|
|
91
|
+
: <li>For a file, <b>View source</b> shows its code, and a declaration jumps to its line there.</li>}
|
|
89
92
|
</Section>
|
|
90
93
|
<Section title="Claude Code">
|
|
91
94
|
{DEMO
|
|
92
|
-
? <li>In the app, the bar along the bottom shows Claude Code's
|
|
95
|
+
? <li>In the app, the status bar along the bottom shows Claude Code's newest tool call as it works, and the boxes it touched get an orange ring.</li>
|
|
93
96
|
: (
|
|
94
97
|
<>
|
|
95
|
-
<li>The bar along the bottom shows Claude Code's
|
|
98
|
+
<li>The status bar along the bottom shows Claude Code's newest tool call as it works.</li>
|
|
96
99
|
<li>Boxes holding what it touched get an orange ring for 30 seconds.</li>
|
|
97
100
|
</>
|
|
98
101
|
)}
|
|
99
102
|
</Section>
|
|
100
103
|
<Section title="The index">
|
|
101
|
-
<li>The status
|
|
104
|
+
<li>The status bar's first cell says whether the index is up to date. Click it for each stage{DEMO ? "." : ", and to re-index."}</li>
|
|
102
105
|
</Section>
|
|
103
106
|
</div>
|
|
104
107
|
|
|
105
|
-
<h3 className="mt-6 text-[13px] font-medium text-
|
|
108
|
+
<h3 className="mt-6 text-[13px] font-medium text-text">Keyboard</h3>
|
|
106
109
|
<dl className="mt-2 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-[13px] text-text-2 max-[360px]:grid-cols-1 max-[360px]:[&_dd]:mb-1.5">
|
|
107
110
|
<dt><Key>Ctrl K</Key></dt><dd>Search files and concepts</dd>
|
|
108
111
|
<dt><Key>Esc</Key></dt><dd>Close what's open, let go of the selection, then clear Claude Code's highlights</dd>
|
|
@@ -119,14 +122,14 @@ export default function HelpDialog({ onClose }: { onClose: () => void }){
|
|
|
119
122
|
function Section({ title, children }: { title: string; children: ReactNode }){
|
|
120
123
|
return (
|
|
121
124
|
<section>
|
|
122
|
-
<h3 className="text-[13px] font-medium text-
|
|
123
|
-
<ul className="mt-2 space-y-1.5 text-text-
|
|
125
|
+
<h3 className="text-[13px] font-medium text-text">{title}</h3>
|
|
126
|
+
<ul className="mt-2 space-y-1.5 text-text-2 [&_b]:font-medium [&_b]:text-text">{children}</ul>
|
|
124
127
|
</section>
|
|
125
128
|
);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
function Key({ children }: { children: ReactNode }){
|
|
129
|
-
return <kbd className="rounded-md border border-border bg-
|
|
132
|
+
return <kbd className="rounded-md border border-border bg-node px-1.5 py-px font-sans text-[12px] text-text">{children}</kbd>;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
function Swatch({ className }: { className: string }){
|
package/app/map/Legend.tsx
CHANGED
|
@@ -1,82 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, type ReactNode } from "react";
|
|
4
|
-
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
|
5
|
-
|
|
6
|
-
/** Remembers in this browser whether the legend was left open. */
|
|
7
|
-
const LEGEND_OPEN = "nebulosity.legendOpen";
|
|
8
|
-
|
|
9
|
-
function readOpen(): boolean {
|
|
10
|
-
try{
|
|
11
|
-
return window.localStorage.getItem(LEGEND_OPEN) === "1";
|
|
12
|
-
} catch{
|
|
13
|
-
return false; // no storage (a private window): start closed
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
/**
|
|
18
|
-
* What the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* color of everything you click.
|
|
23
|
-
* The canvas only renders in the browser, so reading storage on the first render is safe. On a short canvas (a narrow
|
|
24
|
-
* window's floor), it's at most `maxHeight` tall, its rows scrolling inside, so it never reaches the page pill.
|
|
4
|
+
* What the Map's marks mean, in a sentence under the canvas (the Workbench redesign, ADR-070). It replaces both the
|
|
5
|
+
* legend card in the canvas's corner and the help that opened by itself on a first visit: the full help stays behind
|
|
6
|
+
* the ? button and key. The doorways, Claude Code's rings and a busy folder's hidden lines get a sentence of their own
|
|
7
|
+
* only where they appear. In a short window or over a narrow canvas it goes (shell.ts), the help still having it.
|
|
25
8
|
*/
|
|
26
|
-
export default function Legend({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// nothing to remember it in
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const Chevron = open ? IconChevronDown : IconChevronUp;
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div aria-label="Legend" style={{ maxHeight }} className={`nopan nodrag nowheel absolute bottom-4 left-4 z-[5] flex flex-col ${open ? "w-[252px]" : ""} rounded-lg bg-overlay text-[12px] text-overlay-text-2`}>
|
|
41
|
-
{open && (
|
|
42
|
-
<div className="min-h-0 space-y-1.5 overflow-y-auto border-b border-overlay-line px-3 py-2">
|
|
43
|
-
{/* one sentence for direction: "arrows out / arrows in" rows read as abstract, even to the owner */}
|
|
44
|
-
<Row glyph={<Arrow color="var(--text-2)" />}>A → B: A uses B. The arrow points at what's being used.</Row>
|
|
45
|
-
<Row glyph={<Arrow color="var(--select)" />}>Selected box and its lines</Row>
|
|
46
|
-
<Row glyph={<span className="block h-3 w-3 rounded-sm outline-2 outline-agent" />}>What Claude Code is touching</Row>
|
|
47
|
-
{/* only where there are doorways: a page of a big folder (deferred minor: the legend had no row for them) */}
|
|
48
|
-
{doorways && <Row glyph={<span className="block h-3 w-5 rounded-sm border border-dashed border-text-2" />}>Dashed box: a doorway to another page</Row>}
|
|
49
|
-
{quietLines && <Row glyph={<span aria-hidden className="text-[14px] leading-none">⋯</span>}>Busy folder: hover a box to see its lines</Row>}
|
|
50
|
-
</div>
|
|
51
|
-
)}
|
|
52
|
-
<button
|
|
53
|
-
type="button"
|
|
54
|
-
data-testid="legend-toggle"
|
|
55
|
-
onClick={toggle}
|
|
56
|
-
aria-expanded={open}
|
|
57
|
-
className="flex w-full shrink-0 items-center justify-between gap-3 rounded-lg px-3 py-1.5 text-[12px] font-medium text-control hover:bg-overlay-line"
|
|
58
|
-
>
|
|
59
|
-
Legend
|
|
60
|
-
<Chevron aria-hidden size={16} stroke={1.75} />
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function Row({ glyph, children }: { glyph: ReactNode; children: ReactNode }){
|
|
67
|
-
return (
|
|
68
|
-
<div className="flex items-start gap-2">
|
|
69
|
-
<span aria-hidden className="flex h-4 w-6 shrink-0 items-center justify-center">{glyph}</span>
|
|
70
|
-
<span className="leading-4">{children}</span>
|
|
71
|
-
</div>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function Arrow({ color }: { color: string }){
|
|
9
|
+
export default function Legend({ doorways, agent, quiet }: {
|
|
10
|
+
/** The level has doorways to its other pages. */
|
|
11
|
+
doorways: boolean;
|
|
12
|
+
/** Claude Code is touching boxes on screen. */
|
|
13
|
+
agent: boolean;
|
|
14
|
+
/** A busy level, whose lines show on hover. */
|
|
15
|
+
quiet: boolean;
|
|
16
|
+
}){
|
|
76
17
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
18
|
+
<p data-testid="legend" className="shrink-0 border-t border-divider px-3.5 py-2.5 text-[12px] leading-[1.45] text-text-3">
|
|
19
|
+
Dot size shows how many files a folder has. Line weight, and the number on a line, show how many imports it carries. Arrows point at the code being used.
|
|
20
|
+
Folders are stacked cards, and a page marks a single file.
|
|
21
|
+
{doorways && " A dashed box is a doorway to another page of this folder."}
|
|
22
|
+
{agent && " An orange ring is what Claude Code is touching right now."}
|
|
23
|
+
{quiet && " In a busy folder, hover a box to see its lines."}
|
|
24
|
+
</p>
|
|
81
25
|
);
|
|
82
26
|
}
|