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
package/app/map/narrow.ts
CHANGED
|
@@ -1,36 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Map at any window size (VISUAL_REFRESH.md, "Narrow windows
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The Map's small placements at any window size (VISUAL_REFRESH.md, "Narrow windows"): a selected box's actions, a
|
|
3
|
+
* folded breadcrumb, a popover kept inside the window, and the error line's form. The layout itself, what gives way as
|
|
4
|
+
* the window narrows or shortens, is shell.ts's (the Workbench redesign, ADR-070). Pure, so each rule is tested alone.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { TOP_INSET } from "./viewport";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The floor: the smallest the Map lays itself out at. Narrower or shorter, the page scrolls sideways or down.
|
|
11
|
-
* 360 px wide holds the top bar's essentials (logo, repo, search, help, status dot) and a canvas as wide as the floor
|
|
12
|
-
* beside nothing. 320 px tall leaves a 200 px canvas under the bars (48 + 40 + 32 px), which stacks a paged level's
|
|
13
|
-
* pill (16 + 44 px from the top) over the zoom controls (104 px, 15 px from the bottom) with about 20 px between them.
|
|
14
|
-
*/
|
|
15
|
-
export const MIN_WIDTH = 360;
|
|
16
|
-
export const MIN_HEIGHT = 320;
|
|
17
|
-
|
|
18
|
-
/** The details panel's width, when there's room beside it (VISUAL_REFRESH.md §5). */
|
|
19
|
-
export const PANEL_WIDTH = 400;
|
|
20
|
-
/**
|
|
21
|
-
* The least canvas worth keeping beside the panel: the floor's own width. With less left, the panel covers the whole
|
|
22
|
-
* canvas (its × closes it), since a sliver of Map beside it can't be read or used.
|
|
23
|
-
*/
|
|
24
|
-
export const MIN_CANVAS_BESIDE_PANEL = MIN_WIDTH;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Whether the panel covers the whole canvas in a Map area `mainWidth` px wide; beside it, it's PANEL_WIDTH wide.
|
|
28
|
-
* `null`, before the area is measured, is the normal case: beside the canvas.
|
|
29
|
-
*/
|
|
30
|
-
export function panelLayout(mainWidth: number | null): { covers: boolean } {
|
|
31
|
-
return { covers: mainWidth !== null && mainWidth - PANEL_WIDTH < MIN_CANVAS_BESIDE_PANEL };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
7
|
/**
|
|
35
8
|
* Where a selected box's actions go while the panel would cover the canvas (owner review, 2026-10-25). A click then only
|
|
36
9
|
* selects, so a double click still reaches the box and the Map stays in sight, and the box shows Open and Details beside
|
|
@@ -47,7 +20,9 @@ const ACTIONS_GAP = 8;
|
|
|
47
20
|
/**
|
|
48
21
|
* Where the selected box's actions go on the canvas (their top-left corner, canvas px), given the box and the actions'
|
|
49
22
|
* size on screen: on actionsSide's side, 8 px off the box, centred on it, but kept 8 px inside the canvas's sides, so a
|
|
50
|
-
* box hanging off an edge (a level opened from its top at 75%) never takes them past it. Each axis is its own rule.
|
|
23
|
+
* box hanging off an edge (a level opened from its top at 75%) never takes them past it. Each axis is its own rule. A
|
|
24
|
+
* box out of sight above or below (selected by a search pick or Follow in a short window, or panned away) keeps them 8 px
|
|
25
|
+
* inside the canvas's top or bottom, since they stand in for the inspector, which is always in sight (2026-10-29).
|
|
51
26
|
*/
|
|
52
27
|
export function actionsPlace(
|
|
53
28
|
box: { left: number; top: number; width: number; height: number },
|
|
@@ -55,27 +30,13 @@ export function actionsPlace(
|
|
|
55
30
|
canvasWidth: number,
|
|
56
31
|
canvasHeight: number,
|
|
57
32
|
): { left: number; top: number } {
|
|
58
|
-
const
|
|
33
|
+
const beside = actionsSide(box.top, box.height, canvasHeight) === "bottom" ? box.top + box.height + ACTIONS_GAP : box.top - ACTIONS_GAP - actions.height;
|
|
34
|
+
const top = Math.max(Math.min(beside, canvasHeight - ACTIONS_GAP - actions.height), ACTIONS_GAP);
|
|
59
35
|
const centred = box.left + box.width / 2 - actions.width / 2;
|
|
60
36
|
const left = Math.min(Math.max(centred, ACTIONS_GAP), canvasWidth - ACTIONS_GAP - actions.width);
|
|
61
37
|
return { left, top };
|
|
62
38
|
}
|
|
63
39
|
|
|
64
|
-
/**
|
|
65
|
-
* How much the page pill says, by the canvas width it sits in (`null`: not measured yet, the normal case):
|
|
66
|
-
* "Page 2 of 5 · 44 of 182 folders and files", "Page 2 of 5", or "2 / 5". The whole note is about 350 px, 370 at
|
|
67
|
-
* most with four-digit counts; with its 16 px inset on the right and as much again on its left that's about 400 px,
|
|
68
|
-
* and 440 leaves room for a wider font.
|
|
69
|
-
*/
|
|
70
|
-
export type PillForm = "full" | "page" | "numbers";
|
|
71
|
-
const FULL_PILL_ROOM = 440;
|
|
72
|
-
const PAGE_PILL_ROOM = 240;
|
|
73
|
-
|
|
74
|
-
export function pillForm(room: number | null): PillForm {
|
|
75
|
-
if (room === null || room >= FULL_PILL_ROOM) return "full";
|
|
76
|
-
return room >= PAGE_PILL_ROOM ? "page" : "numbers";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
40
|
/**
|
|
80
41
|
* The first breadcrumb step to show so the trail fits in `available` px: the steps before it fold into one "…" step
|
|
81
42
|
* (`ellipsis` px), so the trail truncates from its start and the current folder stays. `widths` are the steps' natural
|
|
@@ -95,6 +56,151 @@ export function trailStart(widths: readonly number[], available: number, ellipsi
|
|
|
95
56
|
return last;
|
|
96
57
|
}
|
|
97
58
|
|
|
59
|
+
/** A rectangle on the canvas, in canvas px. */
|
|
60
|
+
export type CanvasRect = { left: number; top: number; width: number; height: number };
|
|
61
|
+
|
|
62
|
+
/** The least room the card keeps from the canvas's sides. */
|
|
63
|
+
export const CARD_EDGE = 8;
|
|
64
|
+
/**
|
|
65
|
+
* The tail a card points at its line with, a comic's speech bubble's (the owner, 2026-10-29): this wide at its base and
|
|
66
|
+
* this long, one size whatever the line, its tip TAIL_GAP short of the line's edge, its middle TAIL_INSET along the
|
|
67
|
+
* card's side from the corner nearest the line, so the card keeps the same distance from the spot it's about.
|
|
68
|
+
*/
|
|
69
|
+
export const TAIL_BASE = 12;
|
|
70
|
+
export const TAIL_LENGTH = 7;
|
|
71
|
+
const TAIL_GAP = 1;
|
|
72
|
+
const TAIL_INSET = 16;
|
|
73
|
+
/** The least a tail's middle keeps from the card's corners, its base clear of their 6 px rounding. */
|
|
74
|
+
const TAIL_END = 12;
|
|
75
|
+
/**
|
|
76
|
+
* The bit of line right at the spot, each way from it, which the card covers in no window it fits, as wide as the
|
|
77
|
+
* heaviest line is drawn.
|
|
78
|
+
*/
|
|
79
|
+
const CARD_SPOT = 6;
|
|
80
|
+
|
|
81
|
+
/** Which side of the card its tail comes out of. */
|
|
82
|
+
export type TailSide = "top" | "bottom" | "left" | "right";
|
|
83
|
+
/**
|
|
84
|
+
* A card's place (its top-left corner, canvas px), and its tail: the side it comes out of and how far along that side
|
|
85
|
+
* its middle is, from the card's left or top. No tail where the card couldn't keep its distance from the spot.
|
|
86
|
+
* `rises` says the card runs up from its tail, so a pinned card grows up from it.
|
|
87
|
+
*/
|
|
88
|
+
export type BubblePlace = { left: number; top: number; tail: { side: TailSide; at: number } | null; rises: boolean };
|
|
89
|
+
|
|
90
|
+
const overlaps = (a: CanvasRect, b: CanvasRect): boolean =>
|
|
91
|
+
a.left < b.left + b.width && b.left < a.left + a.width && a.top < b.top + b.height && b.top < a.top + a.height;
|
|
92
|
+
|
|
93
|
+
/** How far the card's side keeps from the middle of a line `half` px wide each side, its tail between them. */
|
|
94
|
+
const tailReach = (half: number): number => half + TAIL_GAP + TAIL_LENGTH;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Where the card about the lines under the pointer goes, pointing at `spot`, the point of the line nearest the pointer
|
|
98
|
+
* (canvas px), with its tail across the line, never along it. Beside a line that runs up and down there (`upright`),
|
|
99
|
+
* the tail comes out of the card's side, and over or under a level one out of its bottom or top, so the card never
|
|
100
|
+
* lies on the line it's about. `half` is half the line's width on screen. The places are tried above and left first
|
|
101
|
+
* (the owner's pick of 2026-10-29, closest to the pointer's tip, where nothing of the pointer is, and clear of the box
|
|
102
|
+
* a line runs down into): left of an upright line rising from the tail, then right of it, then left and right going
|
|
103
|
+
* down; over a level line reaching left from the tail, then right, then under it; then each of those slid along the
|
|
104
|
+
* line, its tail still on the spot, to fit the canvas near its edge. The first inside the canvas, CARD_EDGE in from its
|
|
105
|
+
* sides, and clear of `line` (the bit of line pointed at and its arrowhead, as rectangles as wide
|
|
106
|
+
* as they're drawn) and of `clear` (what's over the canvas: a drawer, a box's actions, the banner) wins, then the first
|
|
107
|
+
* inside and clear of the rest that keeps off the line right at the spot. Where none fits, the first moved inside the
|
|
108
|
+
* canvas that's clear, then the first moved inside that keeps off the spot, then just over or under the spot (a canvas
|
|
109
|
+
* too narrow for the card beside an upright line), then the first moved inside: the card never leaves the canvas, and
|
|
110
|
+
* moved, it can't keep its tail.
|
|
111
|
+
*/
|
|
112
|
+
export function bubblePlace(
|
|
113
|
+
spot: { x: number; y: number },
|
|
114
|
+
upright: boolean,
|
|
115
|
+
half: number,
|
|
116
|
+
card: { width: number; height: number },
|
|
117
|
+
canvas: { width: number; height: number },
|
|
118
|
+
line: readonly CanvasRect[],
|
|
119
|
+
clear: readonly CanvasRect[],
|
|
120
|
+
): BubblePlace {
|
|
121
|
+
const { width, height } = card;
|
|
122
|
+
const reach = tailReach(half);
|
|
123
|
+
const tries: BubblePlace[] = upright ? [
|
|
124
|
+
{ left: spot.x - reach - width, top: spot.y + TAIL_INSET - height, tail: { side: "right", at: height - TAIL_INSET }, rises: true },
|
|
125
|
+
{ left: spot.x + reach, top: spot.y + TAIL_INSET - height, tail: { side: "left", at: height - TAIL_INSET }, rises: true },
|
|
126
|
+
{ left: spot.x - reach - width, top: spot.y - TAIL_INSET, tail: { side: "right", at: TAIL_INSET }, rises: false },
|
|
127
|
+
{ left: spot.x + reach, top: spot.y - TAIL_INSET, tail: { side: "left", at: TAIL_INSET }, rises: false },
|
|
128
|
+
] : [
|
|
129
|
+
{ left: spot.x + TAIL_INSET - width, top: spot.y - reach - height, tail: { side: "bottom", at: width - TAIL_INSET }, rises: true },
|
|
130
|
+
{ left: spot.x - TAIL_INSET, top: spot.y - reach - height, tail: { side: "bottom", at: TAIL_INSET }, rises: true },
|
|
131
|
+
{ left: spot.x + TAIL_INSET - width, top: spot.y + reach, tail: { side: "top", at: width - TAIL_INSET }, rises: false },
|
|
132
|
+
{ left: spot.x - TAIL_INSET, top: spot.y + reach, tail: { side: "top", at: TAIL_INSET }, rises: false },
|
|
133
|
+
];
|
|
134
|
+
// near the canvas's edge, each place also slides along the line, inside the canvas, with its tail still on the spot,
|
|
135
|
+
// as far as the tail keeps TAIL_END from the card's corners: the tail stays one size
|
|
136
|
+
const slid = tries.map((at): BubblePlace => {
|
|
137
|
+
const along = upright
|
|
138
|
+
? Math.max(CARD_EDGE, Math.min(at.top, canvas.height - CARD_EDGE - height))
|
|
139
|
+
: Math.max(CARD_EDGE, Math.min(at.left, canvas.width - CARD_EDGE - width));
|
|
140
|
+
const tailAt = (upright ? spot.y : spot.x) - along;
|
|
141
|
+
if (at.tail === null || tailAt < TAIL_END || tailAt > (upright ? height : width) - TAIL_END) return at;
|
|
142
|
+
return { ...at, ...(upright ? { top: along } : { left: along }), tail: { side: at.tail.side, at: tailAt } };
|
|
143
|
+
});
|
|
144
|
+
const placed = [...tries, ...slid];
|
|
145
|
+
const box = (at: { left: number; top: number }): CanvasRect => ({ ...at, width, height });
|
|
146
|
+
const inside = (at: BubblePlace): boolean =>
|
|
147
|
+
at.left >= CARD_EDGE && at.top >= CARD_EDGE && at.left + width <= canvas.width - CARD_EDGE && at.top + height <= canvas.height - CARD_EDGE;
|
|
148
|
+
const free = (at: { left: number; top: number }): boolean => [...line, ...clear].every((r) => !overlaps(box(at), r));
|
|
149
|
+
const near = { left: spot.x - CARD_SPOT, top: spot.y - CARD_SPOT, width: 2 * CARD_SPOT, height: 2 * CARD_SPOT };
|
|
150
|
+
const offSpot = (at: { left: number; top: number }): boolean => !overlaps(box(at), near) && clear.every((r) => !overlaps(box(at), r));
|
|
151
|
+
const movedIn = tries.map((at): BubblePlace => ({ ...insideCanvas(at, card, canvas), tail: null, rises: at.rises }));
|
|
152
|
+
const overUnder = [spot.y - CARD_SPOT - 1 - height, spot.y + CARD_SPOT + 1]
|
|
153
|
+
.map((top): BubblePlace => ({ ...insideCanvas({ left: spot.x - width / 2, top }, card, canvas), tail: null, rises: top < spot.y }));
|
|
154
|
+
return placed.find((at) => inside(at) && free(at)) ?? placed.find((at) => inside(at) && offSpot(at))
|
|
155
|
+
?? movedIn.find(free) ?? [...movedIn, ...overUnder].find(offSpot) ?? movedIn[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether a card `cardWidth` wide fits beside an upright line at `spotX`, its tail between them, on either side. */
|
|
159
|
+
const fitsBeside = (spotX: number, half: number, cardWidth: number, canvasWidth: number): boolean =>
|
|
160
|
+
Math.max(spotX - tailReach(half) - CARD_EDGE, canvasWidth - CARD_EDGE - spotX - tailReach(half)) >= cardWidth;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A card's place moved inside the canvas, CARD_EDGE in from its sides, the least it takes: a pinned card the wheel's
|
|
164
|
+
* zoom took to the canvas's edge stays in sight (LineCard), as a card with no place that fits does.
|
|
165
|
+
*/
|
|
166
|
+
export function insideCanvas(at: { left: number; top: number }, card: { width: number; height: number }, canvas: { width: number; height: number }): { left: number; top: number } {
|
|
167
|
+
return {
|
|
168
|
+
left: Math.max(CARD_EDGE, Math.min(at.left, canvas.width - CARD_EDGE - card.width)),
|
|
169
|
+
top: Math.max(CARD_EDGE, Math.min(at.top, canvas.height - CARD_EDGE - card.height)),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The height a card `cardWidth` wide has for itself on `canvas`, pointing at `spot` (bubblePlace): over or under a level
|
|
175
|
+
* line, past its tail, whichever has more, or beside an upright one the canvas's height inside its edges, the card
|
|
176
|
+
* sliding along the line to fit. Where the card can't be beside an upright line, too wide for the canvas there or the
|
|
177
|
+
* spot within TAIL_END of the canvas's top or bottom, it's the room just over or under the spot. So a card that fits it
|
|
178
|
+
* stays off the line at the spot in any window, and keeps its tail wherever it can.
|
|
179
|
+
*/
|
|
180
|
+
export function bubbleRoom(spot: { x: number; y: number }, upright: boolean, half: number, cardWidth: number, canvas: { width: number; height: number }): number {
|
|
181
|
+
if (!upright) return Math.max(spot.y - tailReach(half) - CARD_EDGE, canvas.height - CARD_EDGE - spot.y - tailReach(half));
|
|
182
|
+
const tailFits = spot.y >= CARD_EDGE + TAIL_END && spot.y <= canvas.height - CARD_EDGE - TAIL_END;
|
|
183
|
+
return fitsBeside(spot.x, half, cardWidth, canvas.width) && tailFits
|
|
184
|
+
? canvas.height - 2 * CARD_EDGE
|
|
185
|
+
: Math.max(spot.y - CARD_SPOT - 1 - CARD_EDGE, canvas.height - CARD_EDGE - spot.y - CARD_SPOT - 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The most lines the hover card lists before "and N more" (the owner, 2026-10-29), and each row's height (leading-[18px]). */
|
|
189
|
+
export const CARD_ROWS = 5;
|
|
190
|
+
export const CARD_ROW = 18;
|
|
191
|
+
/** The card's border and padding, top and bottom together (border and py-1). */
|
|
192
|
+
export const CARD_FRAME = 10;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* How many of a run's `total` lines the hover card lists in `room` px of height: CARD_ROWS, or all of them where one
|
|
196
|
+
* more would only replace the "and 1 more" row, but no more than fit under the title with the "and N more" row.
|
|
197
|
+
*/
|
|
198
|
+
export function hoverCardRows(total: number, room: number): number {
|
|
199
|
+
let rows = total <= CARD_ROWS + 1 ? total : CARD_ROWS;
|
|
200
|
+
while (rows > 0 && CARD_FRAME + CARD_ROW * (1 + rows + (rows < total ? 1 : 0)) > room) rows--;
|
|
201
|
+
return rows;
|
|
202
|
+
}
|
|
203
|
+
|
|
98
204
|
/** The space a popover keeps from the Map's edges. */
|
|
99
205
|
const POPOVER_GUTTER = 16;
|
|
100
206
|
|
|
@@ -117,52 +223,26 @@ export function fitPopover(
|
|
|
117
223
|
}
|
|
118
224
|
|
|
119
225
|
/*
|
|
120
|
-
* The Map's error line (a failed jump, a level that wouldn't load), over the canvas
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* in the canvas's top-left corner instead, clear of the pill and the zoom controls. Over a panel that covers the
|
|
124
|
-
* canvas it's the panel's own last row (DetailPanel). It never takes a click.
|
|
226
|
+
* The Map's error line (a failed jump, a level that wouldn't load), over the canvas: 24 px down, centred, and where
|
|
227
|
+
* that can't hold it in two lines, a solid card in the canvas's top-left corner. Over an inspector that covers the
|
|
228
|
+
* canvas it's the inspector's own last row (DetailPanel). It never takes a click.
|
|
125
229
|
*/
|
|
126
230
|
|
|
127
|
-
/** The
|
|
128
|
-
const
|
|
129
|
-
/** The
|
|
130
|
-
const OVERLAY_GAP = 8;
|
|
131
|
-
/** React Flow's zoom controls: 26 px wide, 15 px in from the canvas's right edge. */
|
|
132
|
-
const ZOOM_COLUMN = 15 + 26;
|
|
133
|
-
/** The banner's distance from the canvas's top: as a line in the pill's band (top-6), and as a card (top-4). */
|
|
231
|
+
/** The card's inset from the canvas's edges. */
|
|
232
|
+
const CARD_INSET = 16;
|
|
233
|
+
/** The line's distance from the canvas's top (top-6), and how tall it may grow before it becomes the card. */
|
|
134
234
|
export const BANNER_TOP = 24;
|
|
135
|
-
|
|
235
|
+
const BANNER_BAND = 64;
|
|
136
236
|
|
|
137
237
|
export type BannerForm = "band" | "card";
|
|
138
238
|
|
|
139
239
|
/**
|
|
140
|
-
* Which form the banner takes, by the height it has as a line
|
|
141
|
-
*
|
|
240
|
+
* Which form the banner takes, by the height it has as a line (`null`: not measured yet, the normal case): the line
|
|
241
|
+
* while it ends inside the canvas's top 64 px (two lines), else the card.
|
|
142
242
|
*/
|
|
143
243
|
export function bannerForm(bandHeight: number | null): BannerForm {
|
|
144
|
-
return bandHeight === null || BANNER_TOP + bandHeight <=
|
|
244
|
+
return bandHeight === null || BANNER_TOP + bandHeight <= BANNER_BAND ? "band" : "card";
|
|
145
245
|
}
|
|
146
246
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
* and as much on its left, so it stays centred over the canvas while it can (`null`: no pill, or not measured yet).
|
|
150
|
-
*/
|
|
151
|
-
export function bandClear(pillWidth: number | null): number {
|
|
152
|
-
return pillWidth === null ? 0 : pillWidth + OVERLAY_GAP;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** How far the card's right edge keeps from the canvas's right edge: past the pill, and always past the zoom controls. */
|
|
156
|
-
export function cardClear(pillWidth: number | null): number {
|
|
157
|
-
return Math.max(ZOOM_COLUMN + OVERLAY_GAP, pillWidth === null ? 0 : OVERLAY_INSET + pillWidth + OVERLAY_GAP);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* The open legend's greatest height on a canvas `canvasHeight` px tall (`0`: not measured yet, then no cap). It sits
|
|
162
|
-
* 16 px from the bottom and stops short of what's above it: 16 px from the top, a paged level's pill band, or the
|
|
163
|
-
* banner's bottom (`bannerBottom`, 0 with none) and a gap. Its rows scroll inside.
|
|
164
|
-
*/
|
|
165
|
-
export function legendMaxHeight(canvasHeight: number, pillBand: number, bannerBottom: number): number | undefined {
|
|
166
|
-
if (canvasHeight <= 0) return undefined;
|
|
167
|
-
return canvasHeight - OVERLAY_INSET - Math.max(OVERLAY_INSET, pillBand, bannerBottom > 0 ? bannerBottom + OVERLAY_GAP : 0);
|
|
168
|
-
}
|
|
247
|
+
/** How far the card keeps from the canvas's sides, together: its inset on the left and as much on the right. */
|
|
248
|
+
export const CARD_CLEAR = 2 * CARD_INSET;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MapNode } from "@/lib/contracts";
|
|
2
|
+
|
|
3
|
+
/** What opening a box does: a folder opens on the Map, a doorway turns to its page, a file opens in the editor. */
|
|
4
|
+
export type BoxOpening = { type: "folder"; path: string } | { type: "page"; page: number } | { type: "editor"; path: string };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What opening `box` does, by a double-click on it or the Open beside it in a narrow window (BoxActions); null when it
|
|
8
|
+
* has nothing to open. Inside VS Code's panel (`inEditor`) a click only selects a file, and opening it is how it reaches
|
|
9
|
+
* the editor (the owner, 2026-10-29, ADR-062 amended); a browser tab has no editor to open it in. The merged doorway
|
|
10
|
+
* stands for several pages, so it stays selected for its panel to pick one.
|
|
11
|
+
*/
|
|
12
|
+
export function openingOf(box: MapNode, inEditor: boolean): BoxOpening | null {
|
|
13
|
+
if (box.kind === "folder") return { type: "folder", path: box.path };
|
|
14
|
+
if (box.kind === "portal") return box.page === undefined ? null : { type: "page", page: box.page };
|
|
15
|
+
return inEditor ? { type: "editor", path: box.path } : null;
|
|
16
|
+
}
|
package/app/map/portals.ts
CHANGED
|
@@ -29,11 +29,11 @@ export function portalTitle(box: Pick<MapNode, "page" | "pages">): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Under a doorway's name: "44 folders and files
|
|
32
|
+
* Under a doorway's name: "44 folders and files, 6 imports" (the owner's wording; "boxes" read as jargon). Imports, as a
|
|
33
33
|
* line's hover sentence counts them; "links" read as a count of lines (deferred minor, 2026-10-13).
|
|
34
34
|
*/
|
|
35
35
|
export function portalCaption(box: Pick<MapNode, "hiddenCount">, imports: number): string {
|
|
36
|
-
return `${plural(box.hiddenCount ?? 0, "folder or file", "folders and files")}
|
|
36
|
+
return `${plural(box.hiddenCount ?? 0, "folder or file", "folders and files")}, ${plural(imports, "import", "imports")}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|