@graview/react 0.1.28 → 0.1.30

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 (138) hide show
  1. package/dist/affordances.d.ts +2 -1
  2. package/dist/affordances.d.ts.map +1 -1
  3. package/dist/affordances.js +2 -1
  4. package/dist/affordances.js.map +1 -1
  5. package/dist/beside-the-map.d.ts +13 -0
  6. package/dist/beside-the-map.d.ts.map +1 -0
  7. package/dist/beside-the-map.js +14 -0
  8. package/dist/beside-the-map.js.map +1 -0
  9. package/dist/channels.d.ts +2 -1
  10. package/dist/channels.d.ts.map +1 -1
  11. package/dist/channels.js +121 -22
  12. package/dist/channels.js.map +1 -1
  13. package/dist/connectors.d.ts +27 -2
  14. package/dist/connectors.d.ts.map +1 -1
  15. package/dist/connectors.js +144 -23
  16. package/dist/connectors.js.map +1 -1
  17. package/dist/context.d.ts +91 -2
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +98 -16
  20. package/dist/context.js.map +1 -1
  21. package/dist/drawing.d.ts +2 -0
  22. package/dist/drawing.d.ts.map +1 -1
  23. package/dist/drawing.js +2 -0
  24. package/dist/drawing.js.map +1 -1
  25. package/dist/drawn.d.ts +1 -1
  26. package/dist/drawn.d.ts.map +1 -1
  27. package/dist/drawn.js +1 -1
  28. package/dist/drawn.js.map +1 -1
  29. package/dist/edge-signs.d.ts +12 -2
  30. package/dist/edge-signs.d.ts.map +1 -1
  31. package/dist/edge-signs.js +95 -9
  32. package/dist/edge-signs.js.map +1 -1
  33. package/dist/go.d.ts +28 -10
  34. package/dist/go.d.ts.map +1 -1
  35. package/dist/go.js +14 -5
  36. package/dist/go.js.map +1 -1
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +8 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/last-visit.d.ts +18 -0
  42. package/dist/last-visit.d.ts.map +1 -0
  43. package/dist/last-visit.js +79 -0
  44. package/dist/last-visit.js.map +1 -0
  45. package/dist/linked-reads.d.ts +3 -0
  46. package/dist/linked-reads.d.ts.map +1 -0
  47. package/dist/linked-reads.js +50 -0
  48. package/dist/linked-reads.js.map +1 -0
  49. package/dist/map-choice.d.ts +55 -0
  50. package/dist/map-choice.d.ts.map +1 -0
  51. package/dist/map-choice.js +73 -0
  52. package/dist/map-choice.js.map +1 -0
  53. package/dist/menu-sections.d.ts +67 -0
  54. package/dist/menu-sections.d.ts.map +1 -0
  55. package/dist/menu-sections.js +35 -0
  56. package/dist/menu-sections.js.map +1 -0
  57. package/dist/past.d.ts +58 -0
  58. package/dist/past.d.ts.map +1 -0
  59. package/dist/past.js +70 -0
  60. package/dist/past.js.map +1 -0
  61. package/dist/plots.d.ts +30 -24
  62. package/dist/plots.d.ts.map +1 -1
  63. package/dist/plots.js +168 -149
  64. package/dist/plots.js.map +1 -1
  65. package/dist/popover.d.ts +9 -1
  66. package/dist/popover.d.ts.map +1 -1
  67. package/dist/popover.js +3 -1
  68. package/dist/popover.js.map +1 -1
  69. package/dist/provider.d.ts +6 -2
  70. package/dist/provider.d.ts.map +1 -1
  71. package/dist/provider.js +5 -1
  72. package/dist/provider.js.map +1 -1
  73. package/dist/record-links-layer.d.ts +80 -0
  74. package/dist/record-links-layer.d.ts.map +1 -0
  75. package/dist/record-links-layer.js +127 -0
  76. package/dist/record-links-layer.js.map +1 -0
  77. package/dist/record-links.d.ts +58 -0
  78. package/dist/record-links.d.ts.map +1 -0
  79. package/dist/record-links.js +72 -0
  80. package/dist/record-links.js.map +1 -0
  81. package/dist/resolved-view.d.ts.map +1 -1
  82. package/dist/resolved-view.js +4 -0
  83. package/dist/resolved-view.js.map +1 -1
  84. package/dist/ribbon-width.d.ts +8 -0
  85. package/dist/ribbon-width.d.ts.map +1 -0
  86. package/dist/ribbon-width.js +15 -0
  87. package/dist/ribbon-width.js.map +1 -0
  88. package/dist/ribbons-layer.d.ts +56 -0
  89. package/dist/ribbons-layer.d.ts.map +1 -0
  90. package/dist/ribbons-layer.js +167 -0
  91. package/dist/ribbons-layer.js.map +1 -0
  92. package/dist/ribbons.d.ts +70 -0
  93. package/dist/ribbons.d.ts.map +1 -0
  94. package/dist/ribbons.js +400 -0
  95. package/dist/ribbons.js.map +1 -0
  96. package/dist/routes.d.ts +6 -0
  97. package/dist/routes.d.ts.map +1 -1
  98. package/dist/routes.js +8 -0
  99. package/dist/routes.js.map +1 -1
  100. package/dist/scene-camera.d.ts.map +1 -1
  101. package/dist/scene-camera.js +10 -4
  102. package/dist/scene-camera.js.map +1 -1
  103. package/dist/scene-covered.d.ts +8 -0
  104. package/dist/scene-covered.d.ts.map +1 -0
  105. package/dist/scene-covered.js +118 -0
  106. package/dist/scene-covered.js.map +1 -0
  107. package/dist/scene-hand.d.ts +15 -0
  108. package/dist/scene-hand.d.ts.map +1 -1
  109. package/dist/scene-hand.js +45 -1
  110. package/dist/scene-hand.js.map +1 -1
  111. package/dist/scene-lines.d.ts +15 -2
  112. package/dist/scene-lines.d.ts.map +1 -1
  113. package/dist/scene-lines.js +305 -86
  114. package/dist/scene-lines.js.map +1 -1
  115. package/dist/scene-options.d.ts +10 -3
  116. package/dist/scene-options.d.ts.map +1 -1
  117. package/dist/scene-options.js +26 -9
  118. package/dist/scene-options.js.map +1 -1
  119. package/dist/scene-root.d.ts +7 -0
  120. package/dist/scene-root.d.ts.map +1 -1
  121. package/dist/scene-root.js +224 -158
  122. package/dist/scene-root.js.map +1 -1
  123. package/dist/tie-labels.d.ts +83 -0
  124. package/dist/tie-labels.d.ts.map +1 -0
  125. package/dist/tie-labels.js +98 -0
  126. package/dist/tie-labels.js.map +1 -0
  127. package/dist/view-host.d.ts.map +1 -1
  128. package/dist/view-host.js +7 -2
  129. package/dist/view-host.js.map +1 -1
  130. package/dist/view-registry.d.ts +5 -0
  131. package/dist/view-registry.d.ts.map +1 -1
  132. package/dist/view-registry.js +2 -6
  133. package/dist/view-registry.js.map +1 -1
  134. package/dist/where-drawn.d.ts +3 -1
  135. package/dist/where-drawn.d.ts.map +1 -1
  136. package/dist/where-drawn.js +10 -16
  137. package/dist/where-drawn.js.map +1 -1
  138. package/package.json +5 -5
@@ -1,22 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SCENE_LAYERS } from "@graview/core";
3
- import { toIso } from "@graview/core/scene";
4
- import { aggregateId, cameraLimit, panForZoom, kindOfCard, kindsOf, layout, marqueeHeightFor, withFocus, withOverview, withPan, withPin, panLayout, withRelation, withJackIn, holdLayout, isBandAggregate, toggleExpanded, withWithin, } from "@graview/layout";
5
- import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { toIso, TOP_DOWN } from "@graview/core/city";
4
+ import { aggregateId, cameraLimit, panForZoom, kindOfCard, kindsOf, layout, withFocus, withOverview, panLayout, withRelation, withJackIn, holdLayout, isBandAggregate, toggleExpanded, withWithin, } from "@graview/layout";
5
+ import { withPan, withPin } from "@graview/layout/view-moves";
6
+ import { Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
7
+ import { retryingImport } from "@graview/core/retry";
8
+ import { lazyModule } from "./lazy-part.js";
9
+ import { BesideTheMap } from "./beside-the-map.js";
10
+ import { mapChoice } from "./map-choice.js";
6
11
  import { useActivity } from "./attention.js";
7
12
  import { useAnimatedLayout, useSeatWork, useTouched } from "./animation.js";
8
13
  import { SeatMarks } from "./seat-marks.js";
9
- import { useFound, useGraph, useGraview } from "./context.js";
10
- import { MarqueeRoomContext, useMarqueeNameWidth } from "./drawn.js";
11
- import { Plots } from "./plots.js";
14
+ import { matchWith, useFound, useGraph, useGraview } from "./context.js";
15
+ import { search, touchWeights } from "@graview/core/search";
16
+ import { kitConnector, useKit } from "./kit.js";
17
+ // The scene searches: it hands the provider the matcher as it is fetched (FR-179).
18
+ matchWith({ search, touchWeights });
12
19
  import { Occupants } from "./occupants.js";
13
20
  import { EdgeSigns } from "./edge-signs.js";
14
21
  import { useCameraFlights } from "./scene-camera.js";
15
- import { useHeldDistrict, useSceneDrag, useWheelAndPinch, useWorldShift } from "./scene-hand.js";
22
+ import { useKeyboardKeepsToWhatIsSeen } from "./scene-covered.js";
23
+ import { useDock, useHeldDistrict, useSceneDrag, useWheelAndPinch, useWorldShift } from "./scene-hand.js";
16
24
  import { whereIsIn } from "./where-drawn.js";
17
25
  import { BandCard, BeyondCard, SettledView } from "./resolved-view.js";
18
26
  import { selectionFor, useElementSize, useRootUnit } from "./scene-helpers.js";
19
- import { Lines, RelationCaptions } from "./scene-lines.js";
27
+ import { Lines, provisionalLines, RelationCaptions } from "./scene-lines.js";
20
28
  import { railInset } from "./rails.js";
21
29
  import { useSeatDrawn } from "./seat-talk.js";
22
30
  import { useLayoutOptions } from "./scene-options.js";
@@ -66,25 +74,6 @@ function useHandZoom(overview) {
66
74
  }, [overview]);
67
75
  return [zoom, zoomLive, setZoom];
68
76
  }
69
- /**
70
- * THE BILLBOARD IS CUT TO ITS PICTURE. The lens draws in a box as tall as
71
- * the window; the screen's host reports how much of it the lens actually
72
- * used, and the layout sizes the billboard to that — so the picture's foot
73
- * is on the curb instead of a village's height above it. Whole pixels,
74
- * and only a change re-lays the city.
75
- *
76
- * Reported by the layout's CURRENT screen only. A billboard on its way
77
- * into its village is still drawn as one, but for the length of the tween
78
- * two hosts would report, the layout would flip between their two heights,
79
- * and every flip restarted the tween from where it was — a crawl of a
80
- * pixel a frame until the old picture had faded.
81
- */
82
- function useScreenHeight() {
83
- const [screenHeight, setScreenHeight] = useState(undefined);
84
- // Whole pixels: React keeps the height it has when the new one is the same, so only a change re-lays the city.
85
- const noteScreenHeight = useCallback((height) => setScreenHeight(height && Math.round(height)), []);
86
- return [screenHeight, noteScreenHeight];
87
- }
88
77
  /**
89
78
  * THE CAMERA IS NOT A MOVE. A drive-in on the far side of a large city
90
79
  * lights up off-screen unless the camera goes to it, and the camera's
@@ -299,7 +288,7 @@ function usePointerReport(pointer, stage) {
299
288
  * A group is stale when any of its members changed, since its own view
300
289
  * is a summary of them.
301
290
  */
302
- function useAttachedRenderer(renderer, attachRenderer, canvasRef, frame, touched) {
291
+ function useAttachedRenderer(renderer, attachRenderer, canvasRef, frame, touched, drawOnDom) {
303
292
  useEffect(() => {
304
293
  if (renderer === "dom")
305
294
  return;
@@ -317,12 +306,25 @@ function useAttachedRenderer(renderer, attachRenderer, canvasRef, frame, touched
317
306
  canvas,
318
307
  layout: frame,
319
308
  dirty,
320
- hostOf: (id) => canvas.querySelector(`[data-graview-view="${CSS.escape(id)}"]`),
309
+ // In the canvas, or on the DOM path beside it.
310
+ hostOf: (id) => (canvas.parentElement ?? canvas).querySelector(`[data-graview-view="${CSS.escape(id)}"]`),
311
+ drawOnDom,
321
312
  });
322
313
  return () => {
323
314
  detach?.();
324
315
  };
325
- }, [renderer, attachRenderer, frame, touched]);
316
+ }, [renderer, attachRenderer, frame, touched, drawOnDom]);
317
+ }
318
+ /**
319
+ * The views a renderer handed to the DOM path, as state. A set equal to the
320
+ * last one is not a new render: only a change of hands should cost one.
321
+ */
322
+ function useDrawnOnDom() {
323
+ const [onDom, setOnDom] = useState(() => new Set());
324
+ const drawOnDom = useCallback((ids) => {
325
+ setOnDom((current) => (current.size === ids.size && [...ids].every((id) => current.has(id)) ? current : new Set(ids)));
326
+ }, []);
327
+ return [onDom, drawOnDom];
326
328
  }
327
329
  /**
328
330
  * What just happened, resolved onto whatever is DRAWN.
@@ -373,6 +375,8 @@ const toggled = (current, id) => current.includes(id) ? current.filter((other) =
373
375
  function useCardActs() {
374
376
  const { views, view, setView, setSelection } = useGraview();
375
377
  const found = useFound();
378
+ // Whether a panel beside the map lists a district opened from altitude.
379
+ const besideTheMap = useContext(BesideTheMap);
376
380
  /*
377
381
  * Whether this card stands for a kind the app gave a picture of its own —
378
382
  * the same question `ResolvedView` asks to draw the ◆, asked here so the
@@ -481,6 +485,20 @@ function useCardActs() {
481
485
  // A district the search lit opens narrowed by the same words.
482
486
  const card = kindOfCard(node.id);
483
487
  const lit = card !== null && (found?.byKind[card] ?? 0) > 0;
488
+ /*
489
+ * A DISTRICT WITH NO PICTURE OF ITS OWN, OPENED FROM ALTITUDE, OPENS
490
+ * TO ITS STOP BESIDE THE MAP, in the panel a lens opens in: its own
491
+ * focus, at altitude, so it takes the place of a lens or another
492
+ * kind's stop open there (one panel at a time), the address says so,
493
+ * and Back returns to what was open before. It used to be chosen,
494
+ * which listed it beside the map only when nothing was open — with a
495
+ * lens open, the lens stayed and the district opened behind it.
496
+ */
497
+ if (besideTheMap && card !== null && view.overview && !ownPictureOf(node) && !(lit && view.q)) {
498
+ setView((current) => withWithin({ ...withFocus(current, `aggregate:${card}`), relation: null }, "view", null));
499
+ setSelection([]);
500
+ return;
501
+ }
484
502
  setView((current) => withJackIn(current, node.id, { ownPicture: ownPictureOf(node), ...(lit && current.q ? { carry: current.q } : {}) }));
485
503
  /*
486
504
  * A zoomed RECORD is selected — reading closely is when you act.
@@ -492,37 +510,47 @@ function useCardActs() {
492
510
  },
493
511
  });
494
512
  }
495
- /**
496
- * THE RAIL YOU MOVE THE BOARD BY.
497
- *
498
- * Dragging the picture itself looks around the city, and must go on
499
- * doing so: the billboard is the biggest thing on screen and the one
500
- * most likely to be under the hand, and taking panning away from it
501
- * was how the far side of a city became unreachable once. So the
502
- * board gets a rail along its top edge, the way a window has a title
503
- * bar — the picture pans, the rail moves the board, and neither
504
- * gesture has to be discovered from the other.
505
- *
506
- * How far it may go is not this rail's business: `layout` leashes
507
- * the pin to its own plot, so a hand and a pasted link are held to
508
- * the same distance.
509
- *
510
- * A TITLE BAR, not a gray strip with a pill floating over it: the
511
- * picture's name on the left, the one way down on the right, and
512
- * the whole bar the handle that moves the board.
513
+ /*
514
+ * THE RELATIONS FROM ALTITUDE, one ribbon each (`ribbons-layer.tsx`),
515
+ * fetched the first time the city is drawn: a page that stays on the
516
+ * ground carries none of them.
513
517
  */
514
- function ScreenGrip({ node }) {
515
- const { views, view, setView } = useGraview();
516
- return (_jsxs("div", { className: "graview-screen-grip", "data-graview-grip": node.id, "data-testid": "screen-grip", title: "Move this picture \u2014 it stays by its own village", children: [_jsx("span", { className: "graview-screen-title", "aria-hidden": "true", children: views.places().find((place) => place.as === view.within?.["view"] && place.kind === node.screenOf)?.title ?? "" }), _jsx("button", { type: "button", className: "graview-screen-fullscreen", "data-testid": "screen-fullscreen", title: "Open this picture on its own \u2014 leave the graview with it", onClick: (event) => {
517
- event.stopPropagation();
518
- setView((current) => withOverview(current, false));
519
- }, onPointerDown: stop, onDoubleClick: stop, children: "Open \u2197" })] }));
518
+ const ribbonsModule = /* @__PURE__ */ lazyModule(retryingImport(() => import("./ribbons-layer.js")));
519
+ const Ribbons = /* @__PURE__ */ ribbonsModule.part((module, props) => _jsx(module.Ribbons, { ...props }), { what: "The relations", quiet: true });
520
+ const ABOVE_KEY = "graview:map-from-above";
521
+ /** Whether this person looks at the city from straight above: their own way of looking, kept in this browser when it can be. */
522
+ function useFromAbove() {
523
+ const [above, setAbove] = useState(() => {
524
+ try {
525
+ return globalThis.localStorage?.getItem(ABOVE_KEY) === "1";
526
+ }
527
+ catch {
528
+ return false;
529
+ }
530
+ });
531
+ const choose = useCallback((next) => {
532
+ setAbove(next);
533
+ try {
534
+ globalThis.localStorage?.setItem(ABOVE_KEY, next ? "1" : "0");
535
+ }
536
+ catch {
537
+ // Not kept: it holds for this visit.
538
+ }
539
+ }, []);
540
+ return [above, choose];
520
541
  }
542
+ /*
543
+ * THE GROUND OF THE CITY — its plots, their blocks, the roads and their
544
+ * rules — fetched the first time the scene rises: a page that never looks
545
+ * from altitude never carries it (`plots.tsx`).
546
+ */
547
+ export const cityGround = lazyModule(retryingImport(() => import("./plots.js")));
548
+ const Plots = cityGround.part((module, props) => _jsx(module.Plots, { ...props }), { quiet: true });
521
549
  /** SCENE FURNITURE in the ground's other corner: the way a map carries its own zoom. */
522
- function ZoomControls({ zoom, zoomBy }) {
550
+ function ZoomControls({ zoom, zoomBy, above, setAbove }) {
523
551
  /** One way to zoom: its sign, the step it takes, and whether the zoom has gone as far as it goes that way. */
524
552
  const button = (way, sign, factor, disabled) => (_jsx("button", { type: "button", className: "graview-zoom-button", "data-testid": `zoom-${way}`, "aria-label": `Zoom ${way}`, title: `Zoom ${way} — or pinch, or ctrl+wheel`, disabled: disabled, onClick: () => zoomBy(factor), children: sign }));
525
- return (_jsxs("div", { className: "graview-zoom", "data-graview-foot": "", role: "group", "aria-label": "Zoom", onPointerDown: stop, onClick: stop, children: [button("out", "−", 1 / 1.25, zoom <= ZOOM_MIN + 1e-6), _jsxs("span", { className: "graview-zoom-level", "data-testid": "zoom-level", "aria-live": "polite", children: [Math.round(zoom * 100), "%"] }), button("in", "+", 1.25, zoom >= ZOOM_MAX - 1e-6)] }));
553
+ return (_jsxs("div", { className: "graview-zoom", "data-graview-foot": "", role: "group", "aria-label": "Zoom", onPointerDown: stop, onClick: stop, children: [button("out", "−", 1 / 1.25, zoom <= ZOOM_MIN + 1e-6), _jsxs("span", { className: "graview-zoom-level", "data-testid": "zoom-level", "aria-live": "polite", children: [Math.round(zoom * 100), "%"] }), button("in", "+", 1.25, zoom >= ZOOM_MAX - 1e-6), _jsx("button", { type: "button", className: "graview-zoom-button graview-zoom-above", "data-testid": "from-above", "aria-pressed": above, title: above ? "See the city at an angle again" : "See the city from straight above", onClick: () => setAbove(!above), children: "From above" })] }));
526
554
  }
527
555
  /**
528
556
  * The spatial scene: one `<canvas layoutsubtree>` with the views as its
@@ -535,16 +563,14 @@ function ZoomControls({ zoom, zoomBy }) {
535
563
  export function Scene({ options, renderer = "auto", attachRenderer, className, style, animate = true, children, }) {
536
564
  const context = useGraview();
537
565
  const { store, scheme, views, view, setView, selection, setSelection, setMenuAt, emphasis, brand, noteMoved, robots, seatTalk, motion } = context;
566
+ // A panel beside the map shows what is chosen from altitude, a record in focus among it (`mapChoice`).
567
+ const besideTheMap = useContext(BesideTheMap);
538
568
  /* Whether an ask field stands at the picture's foot: the layout keeps that strip clear of cards. */
539
569
  const seated = useSeatDrawn(seatTalk);
540
570
  const canvasRef = useRef(null);
541
571
  const wrapperRef = useRef(null);
542
572
  const size = useElementSize(wrapperRef);
543
573
  const unit = useRootUnit();
544
- // The marquee's names measured in the face the scene draws them in (FR-118), for the room under each signpost.
545
- const nameWidth = useMarqueeNameWidth(wrapperRef, unit, brand);
546
- // The room the layout reserves for a marquee of those names, handed to the districts that draw one (132: the district's own width, unused).
547
- const marqueeRoom = useMemo(() => (titles) => marqueeHeightFor(titles, 132, nameWidth), [nameWidth]);
548
574
  // Pinch and ctrl+wheel: altitude from the ground, zoom about the pointer from above; the plain wheel pans. See scene-hand.ts.
549
575
  const { zoomAbout, panBy } = useWheelAndPinch({ stage: wrapperRef, view, setView });
550
576
  // `nodes` is a cached snapshot that only changes when the graph does, so
@@ -556,12 +582,28 @@ export function Scene({ options, renderer = "auto", attachRenderer, className, s
556
582
  * bigger on its plot, and the camera keeps the picture in view. Derived
557
583
  * from the stop, never stored: leaving the picture flies back out.
558
584
  */
559
- const closer = view.overview && view.within?.["view"] !== undefined ? 1.5 : 1;
585
+ /*
586
+ * A PICTURE CHOSEN FROM ALTITUDE OPENS BESIDE THE MAP, at a size a person
587
+ * reads (the lens panel), so the camera no longer flies closer to a
588
+ * billboard on the plot: the city stays where it was, with the district
589
+ * the picture is of lit and the rest receded.
590
+ */
591
+ const closer = 1;
560
592
  const [zoom, zoomLive, setZoom] = useHandZoom(view.overview);
561
- const [screenHeight, noteScreenHeight] = useScreenHeight();
562
- const sized = useLayoutOptions(options, size, unit, nameWidth, seated, closer * zoom, screenHeight, nodes);
593
+ const [above, setAbove] = useFromAbove();
594
+ const seenFrom = useMemo(() => (above ? { ...options, tilt: TOP_DOWN } : options), [options, above]);
595
+ const { probe: dockProbe, dock } = useDock();
596
+ /*
597
+ * THE TRAIL'S ROOM. On the ground, where the picture's trail says
598
+ * something — "zoomed in ×", "the past ×", a raised kind — the focused
599
+ * card begins below it rather than under it: its top, 14 pixels down, and
600
+ * a chip of the reader's text size (the tenth walk: "Shifts ×" over "Sort").
601
+ */
602
+ const headroom = !view.overview && (view.zoom || view.past || view.relation) ? Math.ceil(14 + (34 * unit) / 16 + 6) : 0;
603
+ const sized = useLayoutOptions(seenFrom, size, unit, seated, closer * zoom, dock, nodes, headroom);
563
604
  const [panned, seen, panWithin, cameraLive, setCamera] = useCamera(view);
564
- const pinnedIds = useMemo(() => new Set(Object.keys(view.pins)), [view.pins]);
605
+ // A district cannot be placed by hand from altitude, so no plot wears the pinned curb for one (a pin in an old bookmark is ignored).
606
+ const pinnedIds = useMemo(() => new Set(Object.keys(view.pins).filter((id) => kindOfCard(id) === null)), [view.pins]);
565
607
  // A district under the hand is placed here while it is dragged, and written into the view on release.
566
608
  const district = useHeldDistrict();
567
609
  const [result, inPlace] = useLaidOut(store, view, district.held, sized, nodes, seen.pan);
@@ -576,11 +618,11 @@ export function Scene({ options, renderer = "auto", attachRenderer, className, s
576
618
  const touched = useTouched();
577
619
  const seatWork = useSeatWork();
578
620
  // Where the camera goes on its own: to a drive-in off the edge, and down into a village. See scene-camera.ts.
579
- const screenId = result.nodes.find((node) => node.screenOf !== undefined)?.id ?? null;
580
- const flights = useCameraFlights({ view, result, frame, panned, closer, screenId, setCamera });
621
+ const flights = useCameraFlights({ view, result, frame, panned, closer, screenId: null, setCamera });
581
622
  useLentWhereIs(context, frame, wrapperRef);
582
623
  usePointerReport(context.pointer, wrapperRef);
583
- useAttachedRenderer(renderer, attachRenderer, canvasRef, frame, touched);
624
+ const [onDom, drawOnDom] = useDrawnOnDom();
625
+ useAttachedRenderer(renderer, attachRenderer, canvasRef, frame, touched, drawOnDom);
584
626
  const useDom = renderer === "dom" || (renderer === "auto" && !attachRenderer);
585
627
  const activityOf = useActivityOf();
586
628
  /* THE HAND: dragging the ground to look around and a district to place it — see scene-hand.ts. */
@@ -621,7 +663,9 @@ export function Scene({ options, renderer = "auto", attachRenderer, className, s
621
663
  steer();
622
664
  setZoom(next);
623
665
  const box = wrapperRef.current?.getBoundingClientRect();
624
- const center = { x: result.width / 2, y: result.height / 2 };
666
+ // The middle of what the reader sees: a panel over the scene is not part of it.
667
+ const seenBox = result.visible ?? result;
668
+ const center = { x: seenBox.width / 2, y: seenBox.height / 2 };
625
669
  const pointer = box && clientX !== undefined && clientY !== undefined ? { x: clientX - box.left, y: clientY - box.top } : center;
626
670
  const ratio = next / current;
627
671
  setView((current) => withPan(current, panForZoom(current.pan ?? { x: 0, y: 0 }, cameraLive.current, pointer, center, ratio)));
@@ -629,6 +673,8 @@ export function Scene({ options, renderer = "auto", attachRenderer, className, s
629
673
  };
630
674
  /** The view's own pan moved by so much, held within the camera's reach. */
631
675
  const nudge = (dx, dy, limit = cameraLimit(result)) => setView((current) => withPan(current, panWithin(limit, { x: (current.pan?.x ?? 0) + dx, y: (current.pan?.y ?? 0) + dy })));
676
+ // Tab passes over a card the panel covers, and the camera brings the one it lands on into the part that is seen.
677
+ useKeyboardKeepsToWhatIsSeen(wrapperRef, dock, (dx, dy) => nudge(dx, dy));
632
678
  panBy.current = (dx, dy) => {
633
679
  steer();
634
680
  const limit = cameraLimit(result);
@@ -649,114 +695,134 @@ export function Scene({ options, renderer = "auto", attachRenderer, className, s
649
695
  noteMoved();
650
696
  };
651
697
  const actsOn = useCardActs();
698
+ // Whether the kit draws a relation at all, for the ribbons (handed in: see ribbons-layer.tsx).
699
+ const kit = useKit();
700
+ const kitDraws = useCallback((edge) => kitConnector(kit, edge).connector.visible, [kit]);
652
701
  const drawnActivity = (id) => activityOf(frame.nodes.find((node) => node.id === id));
653
702
  /** How far down its card a plot's foot (`share` 1) or middle (2) stands, from the city's own origin and the pan. */
654
- const plotY = (city, node, plot, share) => city.originY + panned.y + toIso(plot.col + plot.side / 2, plot.row + plot.side / share, city.cell).y - node.y;
703
+ const plotY = (city, node, plot, share) => city.originY + panned.y + toIso(plot.col + plot.side / share, plot.row + plot.side / share, city.cell, city.tilt).y - node.y;
655
704
  // The stage's extent: what the ground, its layers and its lines are all drawn over.
656
705
  const extent = { width: result.width, height: result.height };
657
706
  const stage = { position: "relative", zIndex: SCENE_LAYERS.stage, ...extent };
658
707
  const whereIs = (id) => whereIsIn(frame, wrapperRef.current, scheme, views, id);
659
- const hosts = frame.nodes.map((node) => (_jsxs(SceneViewHost, { node: node, crowded: crowded(node), useDom: useDom, ...(node.plot && frame.city && Math.round(node.plane) === 2
708
+ /*
709
+ * THE BILLBOARD IS NOT DRAWN. A picture chosen from altitude was its lens
710
+ * drawn at the city's scale on its plot — eight-pixel text over the
711
+ * districts, unreadable until it was opened. It opens beside the map now,
712
+ * at full size (the lens panel); the stop is the same, so an address, the
713
+ * bar and Back still name it.
714
+ */
715
+ // On the capture path a view the renderer handed on is drawn as the DOM path draws it.
716
+ const hostedOnDom = (id) => useDom || onDom.has(id);
717
+ const hosts = frame.nodes.filter((node) => node.screenOf === undefined).map((node) => (_jsx(SceneViewHost, { node: node, crowded: crowded(node), useDom: hostedOnDom(node.id), ...(node.plot && frame.city && Math.round(node.plane) === 2
660
718
  ? { frontY: plotY(frame.city, node, node.plot, 1), centerY: plotY(frame.city, node, node.plot, 2) }
661
- : {}), ...(node.screenOf !== undefined ? { screen: true, ...(node.id === screenId ? { onDrawnHeight: noteScreenHeight } : {}) } : {}), touched: touched.has(node.id), ...withActivity(activityOf(node)), scheme: scheme, canvasWidth: result.width, canvasHeight: result.height, selected: selection.includes(node.id), ...actsOn(node), onMenu: setMenuAt, selection: selection, onDragStart: (event) => onCardDown(node, event), onDragMove: onDragMove, onDragEnd: onDragUp, swallowClick: swallow, children: [node.screenOf !== undefined ? _jsx(ScreenGrip, { node: node }) : null, node.beyond ? (_jsx(BeyondCard, { kinds: node.beyond })) : node.aggregate?.opens ? (_jsx(BandCard, { node: node })) : (_jsx(SettledView, { node: node, mode: "scene", selected: selection.includes(node.id), ...(crowded(node) ? { fidelity: "glyph" } : {}) }))] }, node.id)));
662
- return (_jsx(MarqueeRoomContext.Provider, { value: marqueeRoom, children: _jsxs("div", { ref: wrapperRef, className: `graview-ground${className ? ` ${className}` : ""}`, "data-graview-altitude": view.overview ? "" : undefined, "data-graview-reach": frame.city ? reachOf(cameraLimit(result)) : undefined, "data-graview-settled": frame.t >= 1 && !flights.gliding() ? "" : undefined, "data-graview-box": size ? `${size.width}x${size.height}` : undefined, onPointerDown: onGroundDown, onPointerMove: onDragMove, onPointerUp: onDragUp, onPointerCancel: onDragUp,
719
+ : {}), touched: touched.has(node.id), ...withActivity(activityOf(node)), scheme: scheme, canvasWidth: result.width, canvasHeight: result.height, selected: selection.includes(node.id), ...actsOn(node), onMenu: setMenuAt, selection: selection, onDragStart: (event) => onCardDown(node, event), onDragMove: onDragMove, onDragEnd: onDragUp, swallowClick: swallow, children: node.beyond ? (_jsx(BeyondCard, { kinds: node.beyond })) : node.aggregate?.opens ? (_jsx(BandCard, { node: node })) : (_jsx(SettledView, { node: node, mode: "scene", selected: selection.includes(node.id), ...(crowded(node) ? { fidelity: "glyph" } : {}) })) }, node.id)));
720
+ return (_jsxs("div", { ref: wrapperRef, className: `graview-ground${className ? ` ${className}` : ""}`, "data-graview-altitude": view.overview ? "" : undefined, "data-graview-reach": frame.city ? reachOf(cameraLimit(result)) : undefined, "data-graview-settled": frame.t >= 1 && !flights.gliding() ? "" : undefined, "data-graview-box": size ? `${size.width}x${size.height}` : undefined, onPointerDown: onGroundDown, onPointerMove: onDragMove, onPointerUp: onDragUp, onPointerCancel: onDragUp,
721
+ /*
722
+ * CLICKING EMPTY GROUND puts the selection down — the gesture every
723
+ * canvas tool teaches, and the graceful half of deselection the ×
724
+ * and Escape were carrying alone. Only the bare ground: a card, a
725
+ * control or a piece of chrome keeps its own meaning, and a drag
726
+ * that ends on the ground is still a pan, not a deselection.
727
+ */
728
+ onClick: (event) => {
729
+ if (swallow.current)
730
+ return;
731
+ const target = event.target;
732
+ // The panes are labeled sections now, not asides (FR-40); chrome is chrome either way.
733
+ if (target.closest("[data-graview-view], button, aside, section[aria-label], [data-graview-offstage], a, input, select"))
734
+ return;
735
+ setSelection([]);
736
+ setMenuAt(null);
737
+ }, style: {
738
+ position: "relative",
663
739
  /*
664
- * CLICKING EMPTY GROUND puts the selection down — the gesture every
665
- * canvas tool teaches, and the graceful half of deselection the ×
666
- * and Escape were carrying alone. Only the bare ground: a card, a
667
- * control or a piece of chrome keeps its own meaning, and a drag
668
- * that ends on the ground is still a pan, not a deselection.
740
+ * THE SCENE FILLS THE WHOLE PICTURE; A PANEL FLOATS OVER IT. The
741
+ * ground, the grid, the depth and the tint run under the panel and
742
+ * show in the gap around it. What a panel takes from the map is the
743
+ * city's fit and the camera's bounds (`useDock`, `--graview-dock`,
744
+ * `--graview-sheet`), never the scene's own box.
669
745
  */
670
- onClick: (event) => {
671
- if (swallow.current)
672
- return;
673
- const target = event.target;
674
- // The panes are labeled sections now, not asides (FR-40); chrome is chrome either way.
675
- if (target.closest("[data-graview-view], button, aside, section[aria-label], [data-graview-offstage], a, input, select"))
676
- return;
677
- setSelection([]);
678
- setMenuAt(null);
679
- }, style: {
680
- position: "relative",
681
- width: "100%",
682
- height: "100%",
683
- cursor: dragging ? "grabbing" : "grab",
684
- touchAction: "none",
685
- ["--graview-altitude"]: view.overview ? 1 : 0,
686
- /*
687
- * THE LATTICE THE CITY STANDS ON. The ground draws its diamonds at
688
- * the cell the map was placed with, anchored where cell (0,0) meets
689
- * the canvas — and the anchor pans with the picture, since the pan
690
- * is baked into every coordinate — so a plot sits on a grid line a
691
- * person can see. The kit's own size stands when no city is drawn.
692
- */
693
- ...(frame.city
694
- ? {
695
- ["--graview-lattice-cell"]: `${frame.city.cell.toFixed(2)}px`,
696
- // The tween's own pan, not the live one: the ground moves with the cards it is under.
697
- ["--graview-lattice-x"]: `${(frame.city.originX + frame.city.pan.x).toFixed(1)}px`,
698
- ["--graview-lattice-y"]: `${(frame.city.originY + frame.city.pan.y).toFixed(1)}px`,
699
- }
700
- : {}),
701
- ...(dragging ? { userSelect: "none" } : {}),
702
- // The stage is sized to the measurement, but a stale measurement
703
- // during a resize can briefly exceed it. Clipping keeps the scene
704
- // inside its own bounds instead of pushing the page taller and
705
- // cutting off anything floating over it.
706
- overflow: "hidden",
707
- ...style,
708
- }, children: [_jsx(Plots, { frame: frame, ...extent,
746
+ width: "100%",
747
+ height: "100%",
748
+ cursor: dragging ? "grabbing" : "grab",
749
+ touchAction: "none",
750
+ ["--graview-altitude"]: view.overview ? 1 : 0,
751
+ /*
752
+ * THE LATTICE THE CITY STANDS ON. The ground draws its diamonds at
753
+ * the cell the map was placed with, anchored where cell (0,0) meets
754
+ * the canvas — and the anchor pans with the picture, since the pan
755
+ * is baked into every coordinate — so a plot sits on a grid line a
756
+ * person can see. The kit's own size stands when no city is drawn.
757
+ */
758
+ ...(frame.city
759
+ ? {
760
+ ["--graview-lattice-cell"]: `${frame.city.cell.toFixed(2)}px`,
761
+ ["--graview-lattice-tilt"]: (frame.city.tilt ?? 0.5).toFixed(3),
762
+ // The tween's own pan, not the live one: the ground moves with the cards it is under.
763
+ ["--graview-lattice-x"]: `${(frame.city.originX + frame.city.pan.x).toFixed(1)}px`,
764
+ ["--graview-lattice-y"]: `${(frame.city.originY + frame.city.pan.y).toFixed(1)}px`,
765
+ }
766
+ : {}),
767
+ ...(dragging ? { userSelect: "none" } : {}),
768
+ // The stage is sized to the measurement, but a stale measurement
769
+ // during a resize can briefly exceed it. Clipping keeps the scene
770
+ // inside its own bounds instead of pushing the page taller and
771
+ // cutting off anything floating over it.
772
+ overflow: "hidden",
773
+ ...style,
774
+ }, children: [view.overview && frame.city ? (_jsx(Suspense, { fallback: null, children: _jsx(Ribbons, { frame: frame, ...extent, pan: frame.city.pan, store: store, graphVersion: nodes, selection: selection, view: { overview: view.overview, focusId: view.focusId, beside: besideTheMap }, emphasis: emphasis, hiddenKinds: context.hiddenKinds, drawn: kitDraws }) })) : null, _jsx("div", { ref: dockProbe, "aria-hidden": "true", style: { position: "absolute", top: 0, right: 0, width: "var(--graview-dock, 0px)", height: "var(--graview-sheet, 0px)", visibility: "hidden", pointerEvents: "none" } }), frame.city ? (_jsx(Suspense, { fallback: null, children: _jsx(Plots, { frame: frame, ...extent,
709
775
  // The tween's own pan: the tiles, the villages and the roads move with the cards on them.
710
- pan: frame.city ? frame.city.pan : panned, brand: brand, pinned: pinnedIds, swallowed: swallow,
776
+ pan: frame.city ? frame.city.pan : panned, brand: brand, pinned: pinnedIds, close: closer * zoom > 1.2, swallowed: swallow, ...(view.overview ? { chosen: mapChoice(selection, { overview: view.overview, focusId: view.focusId, beside: besideTheMap }, (id) => store.graph.getNode(id)?.kind).records } : {}),
711
777
  // A tile is its district: focusing it means the kind's aggregate, never the card's own id.
712
778
  onFocus: (id) => {
713
779
  const kind = kindOfCard(id);
714
780
  if (kind !== null)
715
781
  setView((current) => withFocus(current, aggregateId(kind)));
716
- } }), useDom ? (
782
+ } }) })) : null, useDom ? (
783
+ /*
784
+ * The DOM path uses an ORDINARY container, not a capture canvas.
785
+ *
786
+ * `layoutsubtree` exists so the GPU can capture these elements, and
787
+ * it changes how the browser lays them out. Combining it with the CSS
788
+ * transforms and filters this path applies crashes the renderer
789
+ * process in Chromium 154 — silently, on first paint. Since the DOM
790
+ * path never captures anything, the canvas has no job here, and not
791
+ * creating one removes the whole interaction.
792
+ */
793
+ _jsx("div", { "data-graview-stage": "dom", "data-graview-world": "", style: { ...stage, overflow: "hidden" }, children: hosts })) : (_jsxs("div", { style: stage, children: [_jsx("div", { "data-graview-stage": "dom", "data-graview-under-canvas": "", style: { position: "absolute", left: 0, top: 0, ...extent, overflow: "hidden" }, children: hosts.filter((host) => onDom.has(String(host.key))) }), _jsx("canvas", { ref: canvasRef, "data-graview-stage": "gpu", content: "drawable", layoutsubtree: "", ...extent, style: { display: "block", position: "relative", ...extent }, children: hosts.filter((host) => !onDom.has(String(host.key))) })] })), view.overview && !dragging ? (
794
+ /* What the window does not reach, named on the edge it lies past (see edge-signs.tsx). */
795
+ _jsx(EdgeSigns, { result: result, schema: store.schema, ...(sized.chrome ? { chrome: sized.chrome } : {}), onGo: (by) => {
796
+ nudge(by.x, by.y);
797
+ noteMoved();
798
+ } })) : null, view.overview ? _jsx(ZoomControls, { zoom: zoom, zoomBy: (factor) => zoomAbout.current(factor), above: above, setAbove: setAbove }) : null, _jsx(Lines, { frame: frame, focusId: view.focusId, beside: besideTheMap, holding: inPlace, ...extent, scheme: scheme, overview: !!view.overview, selection: selection, emphasis: emphasis, stageRef: wrapperRef, store: store, graphNodes: nodes, ...((provisional) => (provisional ? { provisional } : {}))(provisionalLines(store)),
717
799
  /*
718
- * The DOM path uses an ORDINARY container, not a capture canvas.
719
- *
720
- * `layoutsubtree` exists so the GPU can capture these elements, and
721
- * it changes how the browser lays them out. Combining it with the CSS
722
- * transforms and filters this path applies crashes the renderer
723
- * process in Chromium 154 — silently, on first paint. Since the DOM
724
- * path never captures anything, the canvas has no job here, and not
725
- * creating one removes the whole interaction.
800
+ * A LINE IS A THING. Clicking one that stands for exactly one edge
801
+ * selects the relation itself — the inspector then says what it is
802
+ * and what may lawfully be done to it; right-click opens the same
803
+ * actions at the pointer. Bundled lines stay scenery: "some of
804
+ * these" is not an honest thing to act on.
726
805
  */
727
- _jsx("div", { "data-graview-stage": "dom", "data-graview-world": "", style: { ...stage, overflow: "hidden" }, children: hosts })) : (_jsx("canvas", { ref: canvasRef, "data-graview-stage": "gpu", layoutsubtree: "", ...extent, style: { display: "block", ...stage }, children: hosts })), view.overview && !dragging ? (
728
- /* What the window does not reach, named on the edge it lies past (see edge-signs.tsx). */
729
- _jsx(EdgeSigns, { result: result, schema: store.schema, onGo: (by) => {
730
- nudge(by.x, by.y);
731
- noteMoved();
732
- } })) : null, view.overview ? _jsx(ZoomControls, { zoom: zoom, zoomBy: (factor) => zoomAbout.current(factor) }) : null, _jsx(Lines, { frame: frame, holding: inPlace, ...extent, scheme: scheme, overview: !!view.overview, selection: selection, emphasis: emphasis, stageRef: wrapperRef, store: store, graphNodes: nodes,
806
+ onPickEdge: (edgeId, at) => {
807
+ // A pan that happened to start on a line is a pan, not a pick.
808
+ if (swallow.current)
809
+ return;
810
+ setSelection([edgeId]);
811
+ setMenuAt(at ? { ...at, on: edgeId } : null);
812
+ }, liveOf: (connector) => {
733
813
  /*
734
- * A LINE IS A THING. Clicking one that stands for exactly one edge
735
- * selects the relation itself — the inspector then says what it is
736
- * and what may lawfully be done to it; right-click opens the same
737
- * actions at the pointer. Bundled lines stay scenery: "some of
738
- * these" is not an honest thing to act on.
814
+ * A relation PULSES where it was just made or broken.
815
+ *
816
+ * An edge write touches both of its ends, so a connector is live
817
+ * exactly when both of the things it joins were written in the same
818
+ * window — which is what making or breaking a relation looks like in
819
+ * the log, and is not what changing one node's field looks like.
739
820
  */
740
- onPickEdge: (edgeId, at) => {
741
- // A pan that happened to start on a line is a pan, not a pick.
742
- if (swallow.current)
743
- return;
744
- setSelection([edgeId]);
745
- setMenuAt(at ? { ...at, on: edgeId } : null);
746
- }, liveOf: (connector) => {
747
- /*
748
- * A relation PULSES where it was just made or broken.
749
- *
750
- * An edge write touches both of its ends, so a connector is live
751
- * exactly when both of the things it joins were written in the same
752
- * window — which is what making or breaking a relation looks like in
753
- * the log, and is not what changing one node's field looks like.
754
- */
755
- const from = drawnActivity(connector.from);
756
- const to = drawnActivity(connector.to);
757
- return from?.wrote && to?.wrote ? (from.at > to.at ? from : to) : undefined;
758
- } }), _jsx(Occupants, { width: result.width, whereIs: whereIs }), _jsx(SeatMarks, { marks: seatWork.marks, questions: [...robots.values()]
759
- .filter((one) => one.mode === "asking" && one.at !== null && one.say)
760
- .map((one) => ({ id: one.at, who: one.who, asks: one.say })), whereIs: whereIs, stageRef: wrapperRef, ...extent }), _jsx(RelationCaptions, { nodes: frame.nodes, moving: frame.t < 1, holding: inPlace, scheme: scheme, width: result.width, stageRef: wrapperRef }), children] }) }));
821
+ const from = drawnActivity(connector.from);
822
+ const to = drawnActivity(connector.to);
823
+ return from?.wrote && to?.wrote ? (from.at > to.at ? from : to) : undefined;
824
+ } }), _jsx(Occupants, { width: result.width, whereIs: whereIs }), _jsx(SeatMarks, { marks: seatWork.marks, questions: [...robots.values()]
825
+ .filter((one) => one.mode === "asking" && one.at !== null && one.say)
826
+ .map((one) => ({ id: one.at, who: one.who, asks: one.say })), whereIs: whereIs, stageRef: wrapperRef, ...extent }), _jsx(RelationCaptions, { nodes: frame.nodes, moving: frame.t < 1, holding: inPlace, scheme: scheme, width: result.width, stageRef: wrapperRef }), children] }));
761
827
  }
762
828
  //# sourceMappingURL=scene-root.js.map