@graview/primitives 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +25 -0
  3. package/dist/arrange-bar.d.ts +49 -0
  4. package/dist/arrange-bar.d.ts.map +1 -0
  5. package/dist/arrange-bar.js +264 -0
  6. package/dist/arrange-bar.js.map +1 -0
  7. package/dist/chat.d.ts +46 -0
  8. package/dist/chat.d.ts.map +1 -0
  9. package/dist/chat.js +343 -0
  10. package/dist/chat.js.map +1 -0
  11. package/dist/companion.d.ts +58 -0
  12. package/dist/companion.d.ts.map +1 -0
  13. package/dist/companion.js +433 -0
  14. package/dist/companion.js.map +1 -0
  15. package/dist/connections.d.ts +23 -0
  16. package/dist/connections.d.ts.map +1 -0
  17. package/dist/connections.js +100 -0
  18. package/dist/connections.js.map +1 -0
  19. package/dist/default-views.d.ts +35 -0
  20. package/dist/default-views.d.ts.map +1 -0
  21. package/dist/default-views.js +605 -0
  22. package/dist/default-views.js.map +1 -0
  23. package/dist/editable.d.ts +71 -0
  24. package/dist/editable.d.ts.map +1 -0
  25. package/dist/editable.js +225 -0
  26. package/dist/editable.js.map +1 -0
  27. package/dist/figure.d.ts +30 -0
  28. package/dist/figure.d.ts.map +1 -0
  29. package/dist/figure.js +66 -0
  30. package/dist/figure.js.map +1 -0
  31. package/dist/find.d.ts +5 -0
  32. package/dist/find.d.ts.map +1 -0
  33. package/dist/find.js +252 -0
  34. package/dist/find.js.map +1 -0
  35. package/dist/index.d.ts +51 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +44 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/installation.d.ts +17 -0
  40. package/dist/installation.d.ts.map +1 -0
  41. package/dist/installation.js +37 -0
  42. package/dist/installation.js.map +1 -0
  43. package/dist/ladder.d.ts +2 -0
  44. package/dist/ladder.d.ts.map +1 -0
  45. package/dist/ladder.js +53 -0
  46. package/dist/ladder.js.map +1 -0
  47. package/dist/lens/arranging.d.ts +41 -0
  48. package/dist/lens/arranging.d.ts.map +1 -0
  49. package/dist/lens/arranging.js +38 -0
  50. package/dist/lens/arranging.js.map +1 -0
  51. package/dist/lens/board.d.ts +146 -0
  52. package/dist/lens/board.d.ts.map +1 -0
  53. package/dist/lens/board.js +658 -0
  54. package/dist/lens/board.js.map +1 -0
  55. package/dist/lens/calendar-dates.d.ts +19 -0
  56. package/dist/lens/calendar-dates.d.ts.map +1 -0
  57. package/dist/lens/calendar-dates.js +55 -0
  58. package/dist/lens/calendar-dates.js.map +1 -0
  59. package/dist/lens/calendar-drawing.d.ts +59 -0
  60. package/dist/lens/calendar-drawing.d.ts.map +1 -0
  61. package/dist/lens/calendar-drawing.js +229 -0
  62. package/dist/lens/calendar-drawing.js.map +1 -0
  63. package/dist/lens/calendar-options.d.ts +107 -0
  64. package/dist/lens/calendar-options.d.ts.map +1 -0
  65. package/dist/lens/calendar-options.js +19 -0
  66. package/dist/lens/calendar-options.js.map +1 -0
  67. package/dist/lens/calendar-placing.d.ts +35 -0
  68. package/dist/lens/calendar-placing.d.ts.map +1 -0
  69. package/dist/lens/calendar-placing.js +113 -0
  70. package/dist/lens/calendar-placing.js.map +1 -0
  71. package/dist/lens/calendar-spans.d.ts +48 -0
  72. package/dist/lens/calendar-spans.d.ts.map +1 -0
  73. package/dist/lens/calendar-spans.js +146 -0
  74. package/dist/lens/calendar-spans.js.map +1 -0
  75. package/dist/lens/calendar-view.d.ts +34 -0
  76. package/dist/lens/calendar-view.d.ts.map +1 -0
  77. package/dist/lens/calendar-view.js +228 -0
  78. package/dist/lens/calendar-view.js.map +1 -0
  79. package/dist/lens/calendar.d.ts +43 -0
  80. package/dist/lens/calendar.d.ts.map +1 -0
  81. package/dist/lens/calendar.js +43 -0
  82. package/dist/lens/calendar.js.map +1 -0
  83. package/dist/lens/coverage.d.ts +183 -0
  84. package/dist/lens/coverage.d.ts.map +1 -0
  85. package/dist/lens/coverage.js +732 -0
  86. package/dist/lens/coverage.js.map +1 -0
  87. package/dist/lens/horizon.d.ts +14 -0
  88. package/dist/lens/horizon.d.ts.map +1 -0
  89. package/dist/lens/horizon.js +20 -0
  90. package/dist/lens/horizon.js.map +1 -0
  91. package/dist/lens/more.d.ts +16 -0
  92. package/dist/lens/more.d.ts.map +1 -0
  93. package/dist/lens/more.js +17 -0
  94. package/dist/lens/more.js.map +1 -0
  95. package/dist/lens/plan-lens.d.ts +17 -0
  96. package/dist/lens/plan-lens.d.ts.map +1 -0
  97. package/dist/lens/plan-lens.js +24 -0
  98. package/dist/lens/plan-lens.js.map +1 -0
  99. package/dist/lens/plan-state.d.ts +122 -0
  100. package/dist/lens/plan-state.d.ts.map +1 -0
  101. package/dist/lens/plan-state.js +152 -0
  102. package/dist/lens/plan-state.js.map +1 -0
  103. package/dist/lens/plan-view.d.ts +10 -0
  104. package/dist/lens/plan-view.d.ts.map +1 -0
  105. package/dist/lens/plan-view.js +576 -0
  106. package/dist/lens/plan-view.js.map +1 -0
  107. package/dist/lens/plan.d.ts +25 -0
  108. package/dist/lens/plan.d.ts.map +1 -0
  109. package/dist/lens/plan.js +25 -0
  110. package/dist/lens/plan.js.map +1 -0
  111. package/dist/lens/reach.d.ts +47 -0
  112. package/dist/lens/reach.d.ts.map +1 -0
  113. package/dist/lens/reach.js +67 -0
  114. package/dist/lens/reach.js.map +1 -0
  115. package/dist/lens/timeline.d.ts +137 -0
  116. package/dist/lens/timeline.d.ts.map +1 -0
  117. package/dist/lens/timeline.js +426 -0
  118. package/dist/lens/timeline.js.map +1 -0
  119. package/dist/markup.d.ts +23 -0
  120. package/dist/markup.d.ts.map +1 -0
  121. package/dist/markup.js +24 -0
  122. package/dist/markup.js.map +1 -0
  123. package/dist/places.d.ts +24 -0
  124. package/dist/places.d.ts.map +1 -0
  125. package/dist/places.js +227 -0
  126. package/dist/places.js.map +1 -0
  127. package/dist/popover.d.ts +28 -0
  128. package/dist/popover.d.ts.map +1 -0
  129. package/dist/popover.js +59 -0
  130. package/dist/popover.js.map +1 -0
  131. package/dist/primitives/index.d.ts +214 -0
  132. package/dist/primitives/index.d.ts.map +1 -0
  133. package/dist/primitives/index.js +381 -0
  134. package/dist/primitives/index.js.map +1 -0
  135. package/dist/profile.d.ts +65 -0
  136. package/dist/profile.d.ts.map +1 -0
  137. package/dist/profile.js +233 -0
  138. package/dist/profile.js.map +1 -0
  139. package/dist/quick-relations.d.ts +16 -0
  140. package/dist/quick-relations.d.ts.map +1 -0
  141. package/dist/quick-relations.js +183 -0
  142. package/dist/quick-relations.js.map +1 -0
  143. package/dist/relation-key.d.ts +44 -0
  144. package/dist/relation-key.d.ts.map +1 -0
  145. package/dist/relation-key.js +196 -0
  146. package/dist/relation-key.js.map +1 -0
  147. package/dist/seat.d.ts +137 -0
  148. package/dist/seat.d.ts.map +1 -0
  149. package/dist/seat.js +162 -0
  150. package/dist/seat.js.map +1 -0
  151. package/dist/seats.d.ts +21 -0
  152. package/dist/seats.d.ts.map +1 -0
  153. package/dist/seats.js +62 -0
  154. package/dist/seats.js.map +1 -0
  155. package/dist/seeding.d.ts +150 -0
  156. package/dist/seeding.d.ts.map +1 -0
  157. package/dist/seeding.js +538 -0
  158. package/dist/seeding.js.map +1 -0
  159. package/dist/shell.d.ts +74 -0
  160. package/dist/shell.d.ts.map +1 -0
  161. package/dist/shell.js +86 -0
  162. package/dist/shell.js.map +1 -0
  163. package/dist/theme.d.ts +50 -0
  164. package/dist/theme.d.ts.map +1 -0
  165. package/dist/theme.js +1692 -0
  166. package/dist/theme.js.map +1 -0
  167. package/dist/wordmark.d.ts +15 -0
  168. package/dist/wordmark.d.ts.map +1 -0
  169. package/dist/wordmark.js +55 -0
  170. package/dist/wordmark.js.map +1 -0
  171. package/dist/workbench/activity.d.ts +75 -0
  172. package/dist/workbench/activity.d.ts.map +1 -0
  173. package/dist/workbench/activity.js +259 -0
  174. package/dist/workbench/activity.js.map +1 -0
  175. package/dist/workbench/agent-seat.d.ts +51 -0
  176. package/dist/workbench/agent-seat.d.ts.map +1 -0
  177. package/dist/workbench/agent-seat.js +112 -0
  178. package/dist/workbench/agent-seat.js.map +1 -0
  179. package/dist/workbench/answer-args.d.ts +31 -0
  180. package/dist/workbench/answer-args.d.ts.map +1 -0
  181. package/dist/workbench/answer-args.js +257 -0
  182. package/dist/workbench/answer-args.js.map +1 -0
  183. package/dist/workbench/back-out.d.ts +71 -0
  184. package/dist/workbench/back-out.d.ts.map +1 -0
  185. package/dist/workbench/back-out.js +238 -0
  186. package/dist/workbench/back-out.js.map +1 -0
  187. package/dist/workbench/following.d.ts +7 -0
  188. package/dist/workbench/following.d.ts.map +1 -0
  189. package/dist/workbench/following.js +26 -0
  190. package/dist/workbench/following.js.map +1 -0
  191. package/dist/workbench/index.d.ts +23 -0
  192. package/dist/workbench/index.d.ts.map +1 -0
  193. package/dist/workbench/index.js +23 -0
  194. package/dist/workbench/index.js.map +1 -0
  195. package/dist/workbench/inspector.d.ts +27 -0
  196. package/dist/workbench/inspector.d.ts.map +1 -0
  197. package/dist/workbench/inspector.js +745 -0
  198. package/dist/workbench/inspector.js.map +1 -0
  199. package/dist/workbench/standing.d.ts +16 -0
  200. package/dist/workbench/standing.d.ts.map +1 -0
  201. package/dist/workbench/standing.js +102 -0
  202. package/dist/workbench/standing.js.map +1 -0
  203. package/dist/workbench/trail.d.ts +35 -0
  204. package/dist/workbench/trail.d.ts.map +1 -0
  205. package/dist/workbench/trail.js +150 -0
  206. package/dist/workbench/trail.js.map +1 -0
  207. package/package.json +62 -0
@@ -0,0 +1,576 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { areaOf, boxOf, fitLabel, overlaps, spanAt } from "@graview/layout";
3
+ import { hueFor } from "../default-views.js";
4
+ import { Chip, Panel } from "../primitives/index.js";
5
+ import { useDrawnSize, useGraview, useTextMeasure } from "@graview/react";
6
+ import { useMemo, useRef, useState } from "react";
7
+ import { buildPlanLens } from "./plan-state.js";
8
+ /**
9
+ * THE SAME GROUND AS A LIST — what a map says when it cannot be drawn.
10
+ *
11
+ * A picture is not the only honest way to show ground, and below a certain
12
+ * size it stops being one at all. This is what the summary fidelity draws,
13
+ * and what the full one falls back to when the scene hands it a hundred and
14
+ * thirty-six pixels: every area, what is standing in it, and what was named
15
+ * and never drawn. Denser than the map, and at that size, more.
16
+ */
17
+ function Roster({ map, options, implicated, flagged, }) {
18
+ return (_jsxs("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "grid", gap: ".25rem" }, children: [map.regions.map((region) => {
19
+ const emphasis = emphasisOf(region.id, implicated, flagged);
20
+ return (_jsxs("li", { "data-graview-pick": region.id, ...(emphasis ? { "data-graview-emphasis": emphasis } : {}), style: {
21
+ display: "flex",
22
+ flexWrap: "wrap",
23
+ gap: ".5rem",
24
+ justifyContent: "space-between",
25
+ minWidth: 0,
26
+ padding: ".25rem .4rem",
27
+ borderRadius: ".35rem",
28
+ background: emphasis === "flagged" ? fillFor(region.what, emphasis, options.hues) : undefined,
29
+ }, children: [_jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: region.label }), _jsxs("span", { style: { opacity: 0.7, whiteSpace: "nowrap" }, children: [region.what ? `${region.what} · ` : "", region.markers.length] })] }, region.id));
30
+ }), map.undrawn.map((region) => (_jsxs("li", { "data-graview-pick": region.id, style: { display: "flex", gap: ".5rem", opacity: 0.7, padding: ".25rem .4rem" }, children: [_jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: region.label }), _jsx("span", { style: { whiteSpace: "nowrap" }, children: options.undrawnLabel ?? "not drawn" })] }, region.id)))] }));
31
+ }
32
+ /** The viewBox is in hundredths, so every number below reads as a percent. */
33
+ const W = 1000;
34
+ /** A marker's hit radius, in canvas units. Twice the dot it draws. */
35
+ const HIT = 0.022;
36
+ /** A tool button: a control, never a pick target, and never under 24px. */
37
+ const TOOL = {
38
+ minHeight: "1.9rem",
39
+ padding: ".25rem .65rem",
40
+ borderRadius: ".35rem",
41
+ border: "1px solid var(--graview-edge)",
42
+ background: "var(--graview-panel)",
43
+ color: "var(--graview-ink)",
44
+ font: "inherit",
45
+ fontSize: "0.9375rem",
46
+ cursor: "pointer",
47
+ };
48
+ const emphasisOf = (id, implicated, flagged) => {
49
+ if (flagged?.includes(id))
50
+ return "flagged";
51
+ if (implicated?.includes(id))
52
+ return "implicated";
53
+ return undefined;
54
+ };
55
+ /** A hue for a category: the app's declared degrees, or the framework's stable hash (degrees since F-014). */
56
+ const degrees = (what, hues) => {
57
+ const named = what === undefined ? undefined : hues?.[what];
58
+ return named ?? hueFor(what ?? "region");
59
+ };
60
+ const fillFor = (what, emphasis, hues) => emphasis === "flagged"
61
+ ? "color-mix(in oklab, var(--graview-warn) 26%, var(--graview-panel))"
62
+ : `color-mix(in oklab, hsl(${degrees(what, hues)} 52% 48%) 24%, var(--graview-panel))`;
63
+ const strokeFor = (what, emphasis, hues) => emphasis === "flagged"
64
+ ? "var(--graview-warn)"
65
+ : `color-mix(in oklab, hsl(${degrees(what, hues)} 48% 42%) 78%, var(--graview-ink))`;
66
+ /** Where a click landed, 0..1 across and down the drawn site. */
67
+ function pointOf(event) {
68
+ const box = event.currentTarget.getBoundingClientRect();
69
+ const clamp = (n) => Math.round(Math.max(0, Math.min(1, n)) * 1000) / 1000;
70
+ return {
71
+ x: clamp(box.width === 0 ? 0 : (event.clientX - box.left) / box.width),
72
+ y: clamp(box.height === 0 ? 0 : (event.clientY - box.top) / box.height),
73
+ };
74
+ }
75
+ export function PlanView({ schema, options, nodes, label, fidelity, selected, implicated, flagged, }) {
76
+ const { store } = useGraview();
77
+ const edges = store.graph.allEdges();
78
+ /*
79
+ * REGIONS COME FROM THE GROUP; MARKERS CANNOT.
80
+ *
81
+ * Registered over the region kind's `many` cell, this view is handed
82
+ * `nodes` — that kind's members, already filtered by the horizon. The
83
+ * markers are a DIFFERENT kind, so they are never in that array, and
84
+ * building from `nodes` alone drew every piece of ground with nothing
85
+ * standing in it. Reading the whole graph and narrowing the regions back
86
+ * to what the group gave keeps both: the scene's filtering, and the
87
+ * things that stand on the filtered ground.
88
+ */
89
+ const everything = store.graph.allNodes();
90
+ const shown = nodes === undefined ? undefined : new Set(nodes.map((node) => node.id));
91
+ const visible = shown === undefined
92
+ ? everything
93
+ : everything.filter((node) => node.kind !== options.regions || shown.has(node.id));
94
+ const map = buildPlanLens(visible, edges, options, schema);
95
+ const drawn = map.regions.length;
96
+ const things = map.regions.reduce((n, region) => n + region.markers.length, 0);
97
+ /*
98
+ * A CHIP. Standing in for the whole map at the smallest fidelity, the
99
+ * useful thing to say is how much ground there is — and, when some of it
100
+ * is not drawn, that there is ground this picture is not showing.
101
+ */
102
+ if (fidelity === "glyph") {
103
+ return (_jsx(Chip, { label: `${label ?? "Ground"} · ${drawn}`, ...(map.undrawn.length > 0 ? { meta: `+${map.undrawn.length} undrawn` } : {}), selected: selected }));
104
+ }
105
+ /*
106
+ * A SUMMARY IS DENSER CONTENT, NOT THE PICTURE SCALED DOWN. A map at a
107
+ * third of its size is a map with no legible labels, which answers
108
+ * nothing; a roster of the same ground with what stands in it and what is
109
+ * wrong with it answers several things in the same space.
110
+ */
111
+ if (fidelity === "summary") {
112
+ return (_jsx(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, children: _jsx(Roster, { map: map, options: options, ...(implicated ? { implicated } : {}), ...(flagged ? { flagged } : {}) }) }));
113
+ }
114
+ /*
115
+ * THE DRAWING TAKES ITS PROPORTIONS FROM WHAT WAS DRAWN.
116
+ *
117
+ * The canvas used to be a fixed 1.45 to 1 and the coordinate space was
118
+ * "0–1 across and 0–1 down" — which is not a space, it is two independent
119
+ * scales, and it cannot say that a property is three times as long as it
120
+ * is wide. A surveyed town lot, deep and narrow, came back as squat
121
+ * horizontal bands: every shape stretched sideways to fill a canvas whose
122
+ * shape had nothing to do with the ground.
123
+ *
124
+ * So one unit across is one unit down — a SQUARE space, in which a long
125
+ * lot simply uses less of one axis — and the picture is cropped to what
126
+ * is actually in it. The canvas is then the shape of the property, and a
127
+ * lawn twice as long as it is wide is drawn twice as long as it is wide.
128
+ *
129
+ * `aspect` remains as an override for a domain that knows better, and is
130
+ * no longer a default that quietly lies.
131
+ */
132
+ const extent = useMemo(() => {
133
+ let x0 = Infinity;
134
+ let y0 = Infinity;
135
+ let x1 = -Infinity;
136
+ let y1 = -Infinity;
137
+ const see = (p) => {
138
+ if (p.x < x0)
139
+ x0 = p.x;
140
+ if (p.x > x1)
141
+ x1 = p.x;
142
+ if (p.y < y0)
143
+ y0 = p.y;
144
+ if (p.y > y1)
145
+ y1 = p.y;
146
+ };
147
+ for (const region of map.regions) {
148
+ for (const corner of region.outline)
149
+ see(corner);
150
+ for (const marker of region.markers)
151
+ see(marker.at);
152
+ }
153
+ if (!Number.isFinite(x0) || x1 - x0 <= 0 || y1 - y0 <= 0)
154
+ return { x0: 0, y0: 0, x1: 1, y1: 1 };
155
+ /* A margin, so nothing is drawn against the frame. */
156
+ const pad = Math.max(x1 - x0, y1 - y0) * 0.04;
157
+ return { x0: x0 - pad, y0: y0 - pad, x1: x1 + pad, y1: y1 + pad };
158
+ }, [map.regions]);
159
+ /*
160
+ * Clamped, because a property really can be ten to one and a picture that
161
+ * is ten to one is a ribbon nothing is legible in. Past the clamp the
162
+ * drawing stops being true rather than stops being readable, and the true
163
+ * one nobody can read is the worse of the two.
164
+ */
165
+ const raw = (extent.x1 - extent.x0) / (extent.y1 - extent.y0);
166
+ const aspect = options.aspect ?? Math.min(3.2, Math.max(0.55, raw));
167
+ /* U is the scale of the square space; the view is the window onto it. */
168
+ const U = Math.round(W / (extent.x1 - extent.x0));
169
+ const viewW = Math.round(U * (extent.x1 - extent.x0));
170
+ const viewH = Math.round(viewW / aspect);
171
+ const view = {
172
+ x: extent.x0 * U,
173
+ y: extent.y0 * U - (viewH - U * (extent.y1 - extent.y0)) / 2,
174
+ w: viewW,
175
+ h: viewH,
176
+ };
177
+ /*
178
+ * Two scales, and keeping them apart is the whole of this.
179
+ *
180
+ * `U` turns a coordinate into a place: it is how many units of the
181
+ * drawing one unit of the square space is worth, and it grows as the
182
+ * window narrows. `S` is how big a thing should LOOK — a font, a dot, a
183
+ * margin — and it is a fraction of the window, so a name on a narrow lot
184
+ * is the same size on screen as a name on a square one. Using the space's
185
+ * scale for sizes is how a long thin property ends up with enormous type.
186
+ */
187
+ const H = U;
188
+ const S = view.w;
189
+ const anyEmphasis = (implicated?.length ?? 0) > 0 || (flagged?.length ?? 0) > 0;
190
+ /*
191
+ * WHAT CAN BE WRITTEN ON THE PICTURE, worked out before any of it is
192
+ * drawn — because whether a name fits decides whether the key below has
193
+ * to say it, and a component cannot find that out while rendering.
194
+ *
195
+ * Regions first and markers into the gaps left over: a marker is a dot
196
+ * with a name beside it, and the dot is the thing that carries the
197
+ * meaning. A name that will not fit is dropped rather than overlapped,
198
+ * which is the whole difference between a plan and a pile of words.
199
+ */
200
+ /*
201
+ * WHETHER THIS IS A CONTROL SURFACE, A PICTURE, OR NEITHER.
202
+ *
203
+ * The framework asks this now — `useDrawnSize` — because it turned out
204
+ * not to be a question about grounds. A drawing written in its own units
205
+ * and handed whatever room the page has hides it: at THIS size, is what
206
+ * I am drawing still a thing a person can read, or press? The scene lays
207
+ * this panel out at 136 pixels on a phone, where the answer is no twice.
208
+ */
209
+ const canvas = useRef(null);
210
+ const { touchable, legible: drawable } = useDrawnSize(canvas, { units: view.w, target: S * HIT * 2 });
211
+ const display = useTextMeasure("--graview-font-display", "serif");
212
+ const body = useTextMeasure("--graview-font-body", "sans-serif");
213
+ const { named, placed, tagged } = useMemo(() => {
214
+ const taken = [];
215
+ /*
216
+ * THE NAME GOES IN THE CORNER, NOT ACROSS THE MIDDLE.
217
+ *
218
+ * A name set large and centred looks like a title and behaves like a
219
+ * wall: it owns the widest part of the shape, which is exactly where
220
+ * the things standing in that shape are, so twenty-two markers had
221
+ * nowhere to put their own names and went unlabelled. Every site plan
222
+ * ever drawn does the opposite — the area is named quietly along its
223
+ * top edge, and the middle is left for what is in it.
224
+ */
225
+ const inset = S * 0.012;
226
+ /* A hair of air: two names that merely touch read as one long word. */
227
+ const air = W * 0.004;
228
+ const clear = (box, mine) => !taken.some((other) => other !== mine &&
229
+ overlaps({ x0: box.x0 - air, y0: box.y0 - air, x1: box.x1 + air, y1: box.y1 + air }, other));
230
+ /*
231
+ * BIGGEST GROUND FIRST. Where two names cannot both be drawn, the one
232
+ * that keeps its place should be the one with more room to keep it in —
233
+ * and the one that loses goes to the key, which is a demotion a small
234
+ * shape can afford and a large one cannot.
235
+ */
236
+ const byRoom = [...map.regions].sort((a, b) => areaOf(b.outline) - areaOf(a.outline));
237
+ const named = new Map();
238
+ const placed = new Map();
239
+ for (const region of byRoom) {
240
+ const box = boxOf(region.outline);
241
+ const height = Math.max(0, (box.bottom - box.top) * H * 0.55);
242
+ /*
243
+ * Down the inside of the top edge, looking for a line with room. Two
244
+ * areas that overlap — which a survey produces more often than
245
+ * anybody would like — have their top edges close together, and a
246
+ * name drawn over another name belongs to neither of them.
247
+ */
248
+ let put = null;
249
+ for (const down of [0.12, 0.26, 0.42, 0.6, 0.78]) {
250
+ const y = box.top + down * (box.bottom - box.top);
251
+ const span = spanAt(region.outline, y);
252
+ const room = Math.max(0, (span.x1 - span.x0) * U - inset * 2);
253
+ const fitted = fitLabel(region.label, {
254
+ room,
255
+ height,
256
+ size: S * 0.023,
257
+ floor: S * 0.015,
258
+ measure: display,
259
+ });
260
+ if (fitted === null)
261
+ continue;
262
+ const wide = Math.max(...fitted.lines.map((line) => display(line, fitted.fontSize)));
263
+ const tall = fitted.lines.length * fitted.fontSize * 1.15;
264
+ /*
265
+ * OFF THE EDGE IT IS DRAWN ON. A short band's tenth-of-the-way-down
266
+ * is a couple of pixels, so the first row of names came back with
267
+ * the outline stroke ruled straight through them — a name on a
268
+ * boundary reads as belonging to whichever side you looked at first.
269
+ */
270
+ const top = box.top * H + inset + tall / 2;
271
+ const bottom = box.bottom * H - inset - tall / 2;
272
+ const mid = bottom < top ? ((box.top + box.bottom) * H) / 2 : Math.min(Math.max(y * H, top), bottom);
273
+ /*
274
+ * AND MEASURED WHERE IT IS ACTUALLY DRAWN.
275
+ *
276
+ * The room was measured at `y` and the clamp above moves the label
277
+ * to `mid`, which is a different height — and a shape is a
278
+ * different width at a different height. That is how "The Tree Bed"
279
+ * came to be lettered across the patio next door: the widest run at
280
+ * the height it was measured, drawn at a height where the bed is
281
+ * not. So the span is re-asked where the words will land, and a
282
+ * label that does not fit THERE tries the next line down instead.
283
+ */
284
+ const here = spanAt(region.outline, mid / H);
285
+ const x = here.x0 * U + inset;
286
+ if (wide > (here.x1 - here.x0) * U - inset * 2)
287
+ continue;
288
+ const candidate = { x0: x, y0: mid - tall / 2, x1: x + wide, y1: mid + tall / 2 };
289
+ if (!clear(candidate))
290
+ continue;
291
+ taken.push(candidate);
292
+ put = { fitted, x, y: mid };
293
+ break;
294
+ }
295
+ named.set(region.id, put?.fitted ?? null);
296
+ if (put !== null)
297
+ placed.set(region.id, { x: put.x, y: put.y });
298
+ }
299
+ /*
300
+ * TWENTY-TWO COLOURED DOTS AND NO WORDS is not a map of anything. The
301
+ * first survey drew exactly that, and every one of them was reachable,
302
+ * announced and pickable — and unreadable, because knowing a thing is
303
+ * THERE is not knowing what it is.
304
+ *
305
+ * A marker's name dodges everything already on the picture, including
306
+ * the area names. Letting them overlap was tried and is worse than
307
+ * silence: "Cedar ra…" under "Perimeter raised" reads as one damaged
308
+ * sentence rather than as two things. What buys the room is the area
309
+ * names moving out of the middle, above.
310
+ */
311
+ const size = S * 0.018;
312
+ const tagged = new Map();
313
+ /*
314
+ * THE DOTS ARE IN THE WAY TOO.
315
+ *
316
+ * Names dodged other names and sailed straight over the marks they
317
+ * belong to: "Back Fence Beds" written across the alley gate's dot,
318
+ * "Deck Shrub" across the drain's. A dot is the thing carrying the
319
+ * meaning — the name is only there to say which dot — so a name over
320
+ * somebody else's dot mislabels it, which is worse than the dot having
321
+ * no name at all. Every mark goes into the set before any name does.
322
+ */
323
+ const dot = S * 0.011 + S * 0.004;
324
+ const marks = new Map();
325
+ for (const region of map.regions) {
326
+ for (const marker of region.markers) {
327
+ const box = {
328
+ x0: marker.at.x * U - dot,
329
+ y0: marker.at.y * H - dot,
330
+ x1: marker.at.x * U + dot,
331
+ y1: marker.at.y * H + dot,
332
+ };
333
+ marks.set(marker.id, box);
334
+ taken.push(box);
335
+ }
336
+ }
337
+ /*
338
+ * And none of them at all when the drawing is too small to press,
339
+ * because it is then also too small to read: at phone width this canvas
340
+ * lands in about a hundred and forty pixels, where an eighteen-unit
341
+ * name renders at two and a half. Two-and-a-half-pixel words are not a
342
+ * smaller version of the information; they are a texture. The key
343
+ * carries every marker at that size, in rows a finger can hit.
344
+ */
345
+ for (const region of touchable ? map.regions : []) {
346
+ for (const marker of region.markers) {
347
+ const gap = S * 0.018;
348
+ const wide = body(marker.label, size);
349
+ const cx = marker.at.x * U;
350
+ const cy = marker.at.y * H;
351
+ const rise = size * 1.15;
352
+ /*
353
+ * EIGHT PLACES TO TRY, not one.
354
+ *
355
+ * The first version put every name to the right of its dot and gave
356
+ * up if that was taken — so on a real plan, where twenty-two things
357
+ * stand in eight small areas, it gave up twenty-two times and the
358
+ * drawing was dots. A name beside its dot is worth a good deal of
359
+ * looking for: right first because it reads most naturally, then
360
+ * left, then the diagonals, then straight above and below.
361
+ */
362
+ const tries = [
363
+ { x: cx + gap, y: cy, anchor: "start" },
364
+ { x: cx - gap, y: cy, anchor: "end" },
365
+ { x: cx + gap * 0.7, y: cy - rise, anchor: "start" },
366
+ { x: cx - gap * 0.7, y: cy - rise, anchor: "end" },
367
+ { x: cx + gap * 0.7, y: cy + rise, anchor: "start" },
368
+ { x: cx - gap * 0.7, y: cy + rise, anchor: "end" },
369
+ { x: cx, y: cy - rise * 1.1, anchor: "start" },
370
+ { x: cx, y: cy + rise * 1.1, anchor: "start" },
371
+ ];
372
+ for (const put of tries) {
373
+ const x0 = put.anchor === "start" ? put.x : put.x - wide;
374
+ const box = { x0, y0: put.y - size * 0.62, x1: x0 + wide, y1: put.y + size * 0.62 };
375
+ if (x0 < view.x + 4 || x0 + wide > view.x + view.w - 4)
376
+ continue;
377
+ if (box.y0 < view.y + 2 || box.y1 > view.y + view.h - 2)
378
+ continue;
379
+ /*
380
+ * A MARK DOES NOT BLOCK ITS OWN NAME.
381
+ *
382
+ * Every dot went into the set so that no name would be written
383
+ * across somebody else's — and then every name was written
384
+ * eighteen units from a dot whose blocked square reaches fifteen,
385
+ * with four units of air between labels on top. Nineteen against
386
+ * eighteen: each of the twenty-two names was refused by the one
387
+ * mark it belongs to, and the drawing came back as dots with a
388
+ * key of everything on it.
389
+ */
390
+ if (!clear(box, marks.get(marker.id)))
391
+ continue;
392
+ taken.push(box);
393
+ tagged.set(marker.id, { at: { x: put.x, y: put.y }, anchor: put.anchor, size });
394
+ break;
395
+ }
396
+ }
397
+ }
398
+ return { named, placed, tagged };
399
+ }, [map.regions, W, H, touchable, display, body]);
400
+ /** Everything the drawing could not say for itself, in the order it is drawn. */
401
+ const unsaid = useMemo(() => {
402
+ const out = [];
403
+ for (const region of map.regions) {
404
+ if ((named.get(region.id)?.whole ?? false) === false) {
405
+ out.push({ id: region.id, label: region.label, what: region.what, round: false });
406
+ }
407
+ }
408
+ for (const region of map.regions) {
409
+ for (const marker of region.markers) {
410
+ /* Everything, when the drawing is too small to be pressed: the key
411
+ is then not a footnote about what would not fit, it is the only
412
+ way to reach anything standing on the ground. */
413
+ if (!touchable || !tagged.has(marker.id)) {
414
+ out.push({ id: marker.id, label: marker.label, what: marker.what, round: true });
415
+ }
416
+ }
417
+ }
418
+ return out;
419
+ }, [map.regions, named, tagged, touchable]);
420
+ /*
421
+ * STAKING OUT, ON THE MAP ITSELF.
422
+ *
423
+ * The vision's sentence was "dragging corners on a sketch", and the first
424
+ * real survey — seven areas, none drawn — landed a person on an empty
425
+ * rectangle with a paragraph of names under it. This is where drawing
426
+ * belongs: choose an area, click its corners, done. Each finished outline
427
+ * is the domain's own act through the store, authored by whoever is in
428
+ * the seat, so it lands in the log and undoes like anything else.
429
+ */
430
+ const { principal } = useGraview();
431
+ const [drawing, setDrawing] = useState(null);
432
+ const canDraw = options.draw !== undefined;
433
+ const canPlace = options.place !== undefined;
434
+ const onCanvasClick = (event) => {
435
+ if (drawing === null)
436
+ return;
437
+ // A click that is laying a corner must not also select or travel.
438
+ event.stopPropagation();
439
+ const point = pointOf(event);
440
+ if (drawing.kind === "marker") {
441
+ store.apply({ name: options.place.act, args: { [options.place.id]: drawing.id, [options.place.at]: point } }, { author: principal, intent: `Placed ${drawing.label} on the map` });
442
+ setDrawing(null);
443
+ return;
444
+ }
445
+ setDrawing({ ...drawing, corners: [...drawing.corners, point] });
446
+ };
447
+ const finishRegion = () => {
448
+ if (drawing === null || drawing.kind !== "region" || drawing.corners.length < 3)
449
+ return;
450
+ store.apply({
451
+ name: options.draw.act,
452
+ args: { [options.draw.id]: drawing.id, [options.draw.outline]: drawing.corners },
453
+ }, { author: principal, intent: `Drew ${drawing.label} on the map — ${drawing.corners.length} corners` });
454
+ setDrawing(null);
455
+ };
456
+ const hint = drawing === null
457
+ ? map.regions.length === 0
458
+ ? "Nothing drawn yet. Choose an area below, then click its corners here."
459
+ : null
460
+ : drawing.kind === "marker"
461
+ ? `Click where ${drawing.label} stands.`
462
+ : drawing.corners.length < 3
463
+ ? `Click the corners of ${drawing.label} — ${3 - drawing.corners.length} more to make an area.`
464
+ : `${drawing.corners.length} corners. Keep clicking, or press Done.`;
465
+ if (!drawable) {
466
+ return (_jsx(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, selected: selected, children: _jsx(Roster, { map: map, options: options, ...(implicated ? { implicated } : {}), ...(flagged ? { flagged } : {}) }) }));
467
+ }
468
+ return (_jsxs(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, ...(map.undrawn.length > 0
469
+ ? { meta: `${map.undrawn.length} ${options.undrawnLabel ?? "not drawn yet"}` }
470
+ : {}), selected: selected, fit: true, children: [_jsx("div", { style: { position: "relative", width: "100%", aspectRatio: `${view.w} / ${view.h}`, overflow: "hidden" }, children: _jsxs("svg", { ref: canvas, viewBox: `${view.x} ${view.y} ${view.w} ${view.h}`, width: "100%", height: "100%", preserveAspectRatio: "xMidYMid meet",
471
+ /*
472
+ * A DRAWING WHOSE PARTS ARE CONTROLS IS NOT AN IMAGE.
473
+ *
474
+ * `role="img"` is a leaf: it promises a screen reader that what
475
+ * is inside is decoration described by the label, so a focusable
476
+ * shape in there is a control the reader has been told is not
477
+ * there. axe calls it nested-interactive and it is right. Every
478
+ * area here is a button, so this is a group — and an image only
479
+ * when the picture is too small to have given anything a target,
480
+ * which is exactly when it IS decoration.
481
+ */
482
+ role: drawing !== null ? "application" : touchable ? "group" : "img", "aria-label": drawing === null
483
+ ? `${label ?? "The plan"}: ${drawn} drawn, ${things} standing in them`
484
+ : hint ?? "Drawing", onClick: onCanvasClick, "data-testid": "plan-canvas", style: {
485
+ display: "block",
486
+ position: "absolute",
487
+ inset: 0,
488
+ /*
489
+ * THE PICTURE STAYS INSIDE ITS BOX.
490
+ *
491
+ * At phone width the scene lays this panel out at 136 pixels and
492
+ * something in that chain resolved the canvas to 138 by TWO — at
493
+ * which point the drawing did not scale down, it OVERFLOWED: a
494
+ * thousand units of ground painted across the page, region names
495
+ * ending nine hundred pixels past the right edge of a 390-pixel
496
+ * screen, invisible because they sat behind everything else and
497
+ * caught only because a harness measures where text actually
498
+ * lands. `aspect-ratio` makes the height follow the width even
499
+ * where a flex parent would rather it did not, and `overflow`
500
+ * makes the escape impossible rather than unlikely.
501
+ */
502
+ cursor: drawing === null ? undefined : "crosshair",
503
+ // A dashed edge and a faint grid: a sheet to draw on, not a void.
504
+ border: `1px dashed color-mix(in oklab, var(--graview-edge) 80%, transparent)`,
505
+ borderRadius: ".4rem",
506
+ backgroundImage: "linear-gradient(color-mix(in oklab, var(--graview-edge) 35%, transparent) 1px, transparent 1px)," +
507
+ "linear-gradient(90deg, color-mix(in oklab, var(--graview-edge) 35%, transparent) 1px, transparent 1px)",
508
+ backgroundSize: "10% 10%",
509
+ }, children: [_jsx("text", { x: view.x + view.w - S * 0.028, y: view.y + S * 0.034, fontSize: S * 0.022, textAnchor: "middle", fill: "var(--graview-ink)", opacity: 0.55, children: "N" }), hint !== null && map.regions.length === 0 ? (_jsx("text", { x: view.x + view.w / 2, y: view.y + view.h / 2, textAnchor: "middle", dominantBaseline: "middle", fontSize: S * 0.026, fill: "var(--graview-ink)", opacity: 0.7, style: { pointerEvents: "none", fontFamily: "var(--graview-font-body)" }, children: hint })) : null, drawing !== null && drawing.kind === "region" && drawing.corners.length > 0 ? (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("polyline", { points: drawing.corners.map((p) => `${p.x * U},${p.y * H}`).join(" "), fill: drawing.corners.length >= 3 ? "color-mix(in oklab, var(--graview-accent) 22%, transparent)" : "none", stroke: "var(--graview-accent)", strokeWidth: 3, strokeDasharray: "8 6" }), drawing.corners.map((p, index) => (_jsx("circle", { cx: p.x * U, cy: p.y * H, r: S * 0.008, fill: "var(--graview-accent)" }, index)))] })) : null, map.regions.map((region) => {
510
+ const emphasis = emphasisOf(region.id, implicated, flagged);
511
+ const dim = anyEmphasis && emphasis === undefined ? 0.34 : 1;
512
+ const points = region.outline.map((p) => `${p.x * U},${p.y * H}`).join(" ");
513
+ return (_jsx("g", { "data-graview-pick": region.id, ...(emphasis ? { "data-graview-emphasis": emphasis } : {}),
514
+ /*
515
+ * The host's `usePickTargets` gives every pick target a tab
516
+ * stop, but it only grants `role="button"` to a closed set of
517
+ * generic HTML tags — no SVG element is on it. So an SVG
518
+ * target would be focusable and announced as nothing at all.
519
+ * Saying it here is the fix; the host leaves an attribute
520
+ * alone once it is set. (docs/graview-feedback.md F-013.)
521
+ */
522
+ role: "button", "aria-label": `${region.label}${region.what ? `, ${region.what}` : ""}, ${region.markers.length} standing in it`, style: { opacity: dim, cursor: "pointer" }, children: _jsx("polygon", { points: points, fill: fillFor(region.what, emphasis, options.hues), stroke: strokeFor(region.what, emphasis, options.hues), strokeWidth: emphasis === "flagged" ? 5 : 2.5, strokeLinejoin: "round" }) }, region.id));
523
+ }), map.regions.flatMap((region) => region.markers.map((marker) => {
524
+ const emphasis = emphasisOf(marker.id, implicated, flagged);
525
+ const dim = anyEmphasis && emphasis === undefined ? 0.34 : 1;
526
+ return (_jsxs("g", { ...(touchable
527
+ ? {
528
+ "data-graview-pick": marker.id,
529
+ role: "button",
530
+ "aria-label": `${marker.label}${marker.what ? `, ${marker.what}` : ""}, in ${region.label}`,
531
+ }
532
+ : { "aria-hidden": true }), ...(emphasis ? { "data-graview-emphasis": emphasis } : {}), style: { opacity: dim, cursor: touchable ? "pointer" : "default" }, children: [touchable ? (_jsx("circle", { cx: marker.at.x * U, cy: marker.at.y * H, r: S * HIT, fill: "transparent" })) : null, _jsx("circle", { cx: marker.at.x * U, cy: marker.at.y * H, r: S * 0.011, fill: emphasis === "flagged"
533
+ ? "var(--graview-warn)"
534
+ : `hsl(${degrees(marker.what, options.hues)} 55% 45%)`, stroke: "var(--graview-panel)", strokeWidth: S * 0.004 }), tagged.has(marker.id) ? (_jsx("text", { x: tagged.get(marker.id).at.x, y: tagged.get(marker.id).at.y, textAnchor: tagged.get(marker.id).anchor, dominantBaseline: "middle", fontSize: tagged.get(marker.id).size, fill: "var(--graview-ink)", stroke: "var(--graview-panel)", strokeWidth: Math.max(2, tagged.get(marker.id).size * 0.3), paintOrder: "stroke", "data-testid": `marker-label-${marker.id}`, style: { pointerEvents: "none", fontFamily: "var(--graview-font-body)" }, children: marker.label })) : null] }, marker.id));
535
+ })), map.regions.map((region) => {
536
+ const fitted = named.get(region.id);
537
+ const at = placed.get(region.id);
538
+ if (!fitted || at === undefined)
539
+ return null;
540
+ const emphasis = emphasisOf(region.id, implicated, flagged);
541
+ return (_jsx("text", { x: at.x, y: at.y - ((fitted.lines.length - 1) * fitted.fontSize * 1.15) / 2, textAnchor: "start", dominantBaseline: "middle", fontSize: fitted.fontSize, fill: "var(--graview-ink)", opacity: anyEmphasis && emphasis === undefined ? 0.34 : 1,
542
+ /* Painted stroke-first so a label stays legible wherever it
543
+ lands — over a dark fill, over a neighbour's edge, over
544
+ another label. Without it the name of the ground is the
545
+ first thing the picture loses. */
546
+ stroke: "var(--graview-panel)", strokeWidth: Math.max(2, fitted.fontSize * 0.26), paintOrder: "stroke", "data-testid": `region-label-${region.id}`, style: { pointerEvents: "none", fontFamily: "var(--graview-font-display)" }, children: fitted.lines.map((line, index) => (_jsx("tspan", { x: at.x, dy: index === 0 ? 0 : fitted.fontSize * 1.15, children: line }, line + String(index)))) }, `label-${region.id}`));
547
+ })] }) }), unsaid.length > 0 ? (_jsx("ul", { "data-testid": "map-key", style: {
548
+ margin: ".5rem 0 0",
549
+ padding: 0,
550
+ listStyle: "none",
551
+ display: "flex",
552
+ flexWrap: "wrap",
553
+ gap: ".2rem .75rem",
554
+ fontSize: "0.875rem",
555
+ }, children: unsaid.map((one) => (_jsxs("li", { "data-graview-pick": one.id, style: {
556
+ display: "flex",
557
+ alignItems: "center",
558
+ gap: ".35rem",
559
+ minWidth: 0,
560
+ /* A row in the key is a real target, because for a marker
561
+ on a small screen it is the only one there is. */
562
+ minHeight: "max(1.5rem, 24px)",
563
+ padding: "0 .15rem",
564
+ cursor: "pointer",
565
+ }, children: [_jsx("span", { "aria-hidden": "true", style: {
566
+ width: one.round ? ".55rem" : ".7rem",
567
+ height: one.round ? ".55rem" : ".7rem",
568
+ flex: "0 0 auto",
569
+ borderRadius: one.round ? "50%" : ".15rem",
570
+ background: one.round
571
+ ? `hsl(${degrees(one.what, options.hues)} 55% 45%)`
572
+ : fillFor(one.what, undefined, options.hues),
573
+ border: one.round ? undefined : `1px solid ${strokeFor(one.what, undefined, options.hues)}`,
574
+ } }), _jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: one.label })] }, one.id))) })) : null, drawing !== null ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".5rem", marginTop: ".5rem", alignItems: "center" }, children: [map.regions.length > 0 && hint !== null ? (_jsx("span", { style: { fontSize: "0.9375rem", opacity: 0.8, flexBasis: "100%" }, "aria-live": "polite", children: hint })) : null, drawing.kind === "region" ? (_jsxs("button", { type: "button", onClick: finishRegion, disabled: drawing.corners.length < 3, "data-testid": "draw-done", style: { ...TOOL, background: "var(--graview-accent)", color: "var(--graview-accent-ink)", borderColor: "transparent" }, children: ["Done \u2014 keep ", drawing.label] })) : null, drawing.kind === "region" && drawing.corners.length > 0 ? (_jsx("button", { type: "button", onClick: () => setDrawing({ ...drawing, corners: drawing.corners.slice(0, -1) }), style: TOOL, children: "Undo a corner" })) : null, _jsx("button", { type: "button", onClick: () => setDrawing(null), style: TOOL, "data-testid": "draw-cancel", children: "Stop" })] })) : (_jsxs(_Fragment, { children: [canDraw && map.undrawn.length > 0 ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".4rem", marginTop: ".5rem", alignItems: "center" }, children: [_jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.75 }, children: [options.undrawnLabel ?? "Not drawn yet", ":"] }), map.undrawn.map((region) => (_jsxs("button", { type: "button", style: TOOL, onClick: () => setDrawing({ kind: "region", id: region.id, label: region.label, corners: [] }), "data-testid": `draw-${region.id}`, children: ["Draw ", region.label] }, region.id)))] })) : null, canPlace && map.strays.length > 0 ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".4rem", marginTop: ".5rem", alignItems: "center" }, children: [_jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.75 }, children: [options.strayLabel ?? "Not placed", ":"] }), map.strays.map((stray) => (_jsxs("button", { type: "button", style: TOOL, onClick: () => setDrawing({ kind: "marker", id: stray.id, label: stray.label }), "data-testid": `place-${stray.id}`, children: ["Place ", stray.label] }, stray.id)))] })) : null, options.help !== undefined && (map.undrawn.length > 0 || map.strays.length > 0) ? (_jsx("p", { style: { margin: ".5rem 0 0", fontSize: "0.875rem" }, children: _jsx("a", { href: options.help.href, "data-testid": "map-help", children: options.help.label }) })) : null, !canDraw && (map.undrawn.length > 0 || map.strays.length > 0) ? (_jsxs("p", { style: { margin: ".5rem 0 0", fontSize: "0.875rem", opacity: 0.75 }, children: [map.undrawn.map((r) => r.label).join(", "), map.undrawn.length > 0 ? ` — ${options.undrawnLabel ?? "not drawn yet"}. ` : "", map.strays.map((s) => s.label).join(", "), map.strays.length > 0 ? ` — ${options.strayLabel ?? "not placed"}.` : ""] })) : null] }))] }));
575
+ }
576
+ //# sourceMappingURL=plan-view.js.map