@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,658 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { withMore } from "./more.js";
3
+ import { labelOf } from "@graview/core";
4
+ import { useArranging } from "./arranging.js";
5
+ import { useGraview, useViolations } from "@graview/react";
6
+ import { onTheHorizon } from "./horizon.js";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { hueFor } from "../default-views.js";
9
+ import { Chip, Panel, Roster } from "../primitives/index.js";
10
+ /**
11
+ * The board lens: things arranged where the DOMAIN says they go.
12
+ *
13
+ * The third picture, and the first one whose geometry the framework does not
14
+ * own. A timeline places by time; a matrix places by two sets; a board places
15
+ * by coordinates the app declares on its own nodes — a formation on a pitch,
16
+ * desks in an office, a stage plot, seats at a table, bays in a warehouse.
17
+ *
18
+ * That distinction matters more than it looks. Everywhere else, layout is a
19
+ * pure function of (focus, relation, graph) and the framework decides where
20
+ * things sit. Here the arrangement is DATA: a left back is at the left back's
21
+ * place because the position node says so, and moving it is an ordinary
22
+ * mutation rather than a change to the layout engine.
23
+ *
24
+ * A slot with nothing in it is the point. On a pitch it is the hole in the
25
+ * team; on a seating plan it is the empty chair. That reads instantly in an
26
+ * arrangement and not at all in a list.
27
+ */
28
+ export const BOARD_REQUIRED_ROLES = ["slots", "x", "y", "fill"];
29
+ export class BoardBindingError extends Error {
30
+ hint;
31
+ constructor(message, hint) {
32
+ super(`${message}\n ${hint}`);
33
+ this.hint = hint;
34
+ this.name = "BoardBindingError";
35
+ }
36
+ }
37
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
38
+ /**
39
+ * Reads the arrangement out of the graph. Pure and exported, so a test can
40
+ * assert on the holes rather than on the drawing of them.
41
+ */
42
+ export function buildBoard(nodes, edges, options, schema) {
43
+ const record = (node) => node;
44
+ const name = (node) => schema
45
+ ? labelOf(schema.tryDefinition(node.kind), node)
46
+ : String(record(node)["label"] ?? node.id);
47
+ /*
48
+ * AN EMPTY GRAPH IS NOT A MISBINDING. See the same note in coverage.tsx:
49
+ * a blank app has no slots yet and its board's title is already in the
50
+ * bar, so throwing on "no slot nodes" took the scene down at exactly the
51
+ * moment somebody was looking for the empty picture.
52
+ */
53
+ if (schema && schema.tryDefinition(options.slots) === undefined) {
54
+ throw new BoardBindingError(`No kind is declared for "${options.slots}".`, `Check the lens bindings: { slots: "<kind>", x: "<field>", y: "<field>", fill: "<edge kind>" }`);
55
+ }
56
+ const slotNodes = nodes.filter((node) => node.kind === options.slots);
57
+ if (!schema && slotNodes.length === 0) {
58
+ throw new BoardBindingError(`Nothing to arrange: no "${options.slots}" nodes.`, `Check the lens bindings: { slots: "<kind>", x: "<field>", y: "<field>", fill: "<edge kind>" }`);
59
+ }
60
+ const filling = edges.filter((edge) => edge.kind === options.fill);
61
+ const byId = new Map(nodes.map((node) => [node.id, node]));
62
+ // Occupants are drawn in the order the nodes came: an arrangement that
63
+ // sorted them holds within a slot, and the graph's order otherwise.
64
+ const rank = new Map(nodes.map((node, index) => [node.id, index]));
65
+ const slots = slotNodes
66
+ .map((node) => {
67
+ const fields = record(node);
68
+ const fromOccupant = options.fillFrom === "occupant";
69
+ const occupants = filling
70
+ .filter((candidate) => (fromOccupant ? candidate.to : candidate.from) === node.id)
71
+ .map((edge) => byId.get(fromOccupant ? edge.from : edge.to))
72
+ .filter((occupant) => occupant !== undefined)
73
+ .sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0))
74
+ .map((occupant) => ({ id: occupant.id, label: name(occupant) }));
75
+ return {
76
+ id: node.id,
77
+ code: String(fields[options.slotCode ?? ""] ?? "").trim() || name(node),
78
+ label: name(node),
79
+ x: clamp01(Number(fields[options.x] ?? 0.5)),
80
+ y: clamp01(Number(fields[options.y] ?? 0.5)),
81
+ occupants,
82
+ };
83
+ })
84
+ // Top to bottom, then left to right: a stable reading order for the
85
+ // accessibility tree, which otherwise gets whatever order the graph
86
+ // happened to be in.
87
+ .sort((a, b) => a.y - b.y || a.x - b.x);
88
+ const placed = new Set(slots.flatMap((slot) => slot.occupants.map((occupant) => occupant.id)));
89
+ const occupantKinds = new Set([...placed]
90
+ .map((id) => byId.get(id)?.kind)
91
+ .filter((kind) => kind !== undefined));
92
+ const spare = nodes
93
+ .filter((node) => occupantKinds.has(node.kind) && !placed.has(node.id))
94
+ .map((node) => ({ id: node.id, label: name(node) }))
95
+ .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
96
+ return {
97
+ slots,
98
+ empty: slots.filter((slot) => slot.occupants.length === 0).map((slot) => slot.id),
99
+ spare,
100
+ };
101
+ }
102
+ export function BoardView({ nodes, occupants, label, fidelity, mode, options, schema, implicated = [], flagged = [], }) {
103
+ const { store } = useGraview();
104
+ /*
105
+ * The whole graph, not the aggregate's members.
106
+ *
107
+ * A board's occupants are a DIFFERENT kind from its slots, and an aggregate
108
+ * of positions does not contain the players in them — so reading occupants
109
+ * out of `props.nodes` made every slot report itself empty. The coverage
110
+ * matrix gets away with `props.nodes` because both its kinds are in the
111
+ * aggregate; a board never can. The aggregate still decides what is
112
+ * focused; it just does not decide what can be looked up.
113
+ */
114
+ /*
115
+ * WHO IS DRAWN. Every slot, always — a filter that hid a seat would redraw
116
+ * the room — and the occupants an arrangement kept, in the order it put
117
+ * them; everyone, when nothing was asked.
118
+ */
119
+ const everyone = onTheHorizon(store.graph, store.schema, nodes);
120
+ const forBoard = occupants
121
+ ? [
122
+ ...everyone.filter((node) => node.kind === options.slots),
123
+ ...occupants.map((id) => store.graph.getNode(id)).filter((node) => node !== undefined),
124
+ ]
125
+ : everyone;
126
+ const board = buildBoard(forBoard, store.graph.allEdges(), options, schema);
127
+ const lit = new Set(implicated);
128
+ const broken = new Set(flagged);
129
+ /*
130
+ * WHY a thing is marked, not only that it is.
131
+ *
132
+ * A slot drawn in the warning tone with nothing to explain it is the worst
133
+ * kind of mark: "why is Hana a different colour?" is the question it
134
+ * provokes, and the honest answer — the LEFT MIDFIELD position demands a
135
+ * skill the week does not train — is nothing to do with Hana at all. The
136
+ * violation messages are already computed; the picture just never read
137
+ * them.
138
+ */
139
+ const violations = useViolations();
140
+ const why = new Map();
141
+ for (const violation of violations) {
142
+ for (const id of violation.nodeIds) {
143
+ why.set(id, [...(why.get(id) ?? []), violation.message]);
144
+ }
145
+ }
146
+ const reasons = (id) => (id ? (why.get(id) ?? []) : []);
147
+ if (fidelity === "glyph") {
148
+ return (_jsx(Chip, { label: `${label ?? "Board"} · ${board.slots.length - board.empty.length}/${board.slots.length}`, hue: hueFor("board") }));
149
+ }
150
+ if (fidelity === "summary") {
151
+ const holes = board.slots.filter((slot) => board.empty.includes(slot.id));
152
+ return (_jsx(Panel, { title: label ?? "Board", meta: `${board.slots.length - board.empty.length}/${board.slots.length}`, tone: holes.length > 0 ? "warning" : "muted", fit: true, children: holes.length > 0 ? (_jsx(Roster, { pick: true, max: 5, items: holes.map((slot) => ({ id: slot.id, label: slot.code })) })) : (_jsx("span", { style: { fontSize: "0.8125rem", color: "var(--graview-ink-faint)" }, children: "Every slot filled." })) }));
153
+ }
154
+ const aspect = options.aspect ?? 0.68;
155
+ /*
156
+ * The arrangement TURNS to fit the room it is given.
157
+ *
158
+ * A portrait pitch in a landscape band shrinks in both directions at once:
159
+ * height drives it, the aspect follows, and a 1040-pixel card ends up with
160
+ * a 200-pixel pitch and four hundred empty pixels either side — thirteen
161
+ * per cent of the card doing all of the card's work. A pitch has no
162
+ * intrinsic reading direction; drawn side-on with attack to the right it
163
+ * is the same arrangement, television's way round, using the width it was
164
+ * actually given. Measured rather than guessed, because the same lens is
165
+ * drawn in a band, on a page, and in a shrunk copy of either.
166
+ */
167
+ const room = useRef(null);
168
+ const [turned, setTurned] = useState(false);
169
+ /*
170
+ * The room's measured height, because `height: 100%` + `aspect-ratio`
171
+ * cannot be trusted to produce a width here. The board sits in a flex row
172
+ * inside a stretched flex row inside a flexed panel, and in that chain
173
+ * Firefox and WebKit treat the percentage height as indefinite when
174
+ * transferring it through the aspect ratio — the pitch collapsed to its
175
+ * two border pixels, taking every slot's hit target with it. Chromium was
176
+ * lenient, which is how it shipped. The observer below already watches
177
+ * the room, so the width is set from the same measurement.
178
+ */
179
+ const [tall, setTall] = useState(null);
180
+ /*
181
+ * A ROOM WITH NO HEIGHT OF ITS OWN drives the board by its WIDTH.
182
+ *
183
+ * The height-driven sizing above assumes the room was given a height — a
184
+ * band in the scene, a page region. In a chapter embed on the docs site
185
+ * the panel sits in a column that is as tall as its content, so the room
186
+ * measured four pixels, the board became six by four, and every slot on
187
+ * it was a four-pixel target hanging off a nothing. Below a height a
188
+ * board could be read at, the board takes the room's width instead and
189
+ * its aspect gives it a height, which is what a board in a document is.
190
+ */
191
+ const LEAST_HEIGHT = 160;
192
+ const [byWidth, setByWidth] = useState(false);
193
+ useEffect(() => {
194
+ const element = room.current;
195
+ if (!element || typeof ResizeObserver === "undefined")
196
+ return;
197
+ const observer = new ResizeObserver((entries) => {
198
+ const box = entries[0]?.contentRect;
199
+ if (!box)
200
+ return;
201
+ const shallow = box.height < LEAST_HEIGHT;
202
+ setByWidth(shallow);
203
+ setTurned(!shallow && aspect < 1 && box.width / box.height > 1.3);
204
+ setTall(shallow ? null : box.height);
205
+ });
206
+ observer.observe(element);
207
+ return () => observer.disconnect();
208
+ }, [aspect]);
209
+ /** A slot's place on the DRAWN board: turned, attack ends up on the right. */
210
+ const at = (x, y) => (turned ? { x: 1 - y, y: x } : { x, y });
211
+ /**
212
+ * Every slot carrying a mark, with the sentence that put it there — the
213
+ * slot's own trouble first, then its occupant's, since that is the order a
214
+ * reader's eye meets them on the board.
215
+ */
216
+ /*
217
+ * ONE LINE PER SENTENCE, naming everyone it implicates.
218
+ *
219
+ * A violation about two positions used to print the same sentence twice,
220
+ * once per code — "LM … Recovery runs …" directly above "RM … Recovery
221
+ * runs …" — which reads as a stutter, not as two facts. The sentence is
222
+ * the fact; the codes are who it touches.
223
+ */
224
+ const marked = (() => {
225
+ const byText = new Map();
226
+ const note = (id, code, text) => {
227
+ const entry = byText.get(text) ?? { ids: [], codes: [] };
228
+ if (!entry.ids.includes(id)) {
229
+ entry.ids.push(id);
230
+ if (!entry.codes.includes(code))
231
+ entry.codes.push(code);
232
+ }
233
+ byText.set(text, entry);
234
+ };
235
+ for (const slot of board.slots) {
236
+ for (const text of reasons(slot.id))
237
+ note(slot.id, slot.code, text);
238
+ for (const occupant of slot.occupants)
239
+ for (const text of reasons(occupant.id))
240
+ note(occupant.id, occupant.label, text);
241
+ }
242
+ return [...byText.entries()].map(([text, entry]) => ({
243
+ id: entry.ids[0],
244
+ code: entry.codes.join(", "),
245
+ text,
246
+ }));
247
+ })();
248
+ /*
249
+ * On a full page the board is the PAGE, not a card sitting on one.
250
+ *
251
+ * Lifted out, it kept its border, its shadow and its content-sized height,
252
+ * so "full screen" meant a 477-pixel card marooned in the middle of a
253
+ * 1560-pixel window. `page` is the same view with the card furniture
254
+ * dropped and permission to use the height it was given — which for a view
255
+ * whose entire content is WHERE THINGS ARE is the only version worth
256
+ * lifting out.
257
+ */
258
+ const page = mode === "fullscreen";
259
+ /**
260
+ * The KEY to the marks, in the app's own words.
261
+ *
262
+ * Every other mark on this board explains itself — a dashed outline is
263
+ * visibly a hole, a name is visibly a name — and the warning tint was the
264
+ * one that did not. Naming the flagged slots and the reason turns "why is
265
+ * that one orange" into a sentence, and it is free: these are the same
266
+ * violation messages the problems list already shows.
267
+ */
268
+ const keyList = marked.length === 0 ? null : (_jsxs("ul", { "data-graview-primitive": "board-key", style: {
269
+ margin: "2px 0 0",
270
+ padding: 0,
271
+ listStyle: "none",
272
+ display: "grid",
273
+ gap: 3,
274
+ flex: "0 0 auto",
275
+ }, children: [marked.slice(0, page ? 6 : 2).map((entry) => (_jsxs("li", { "data-graview-pick": entry.id, "data-graview-emphasis": lit.size === 0 ? "plain" : lit.has(entry.id) ? "lit" : "dimmed", title: entry.text, style: {
276
+ display: page ? "grid" : "flex",
277
+ gap: page ? 1 : 7,
278
+ alignItems: page ? "start" : "baseline",
279
+ fontSize: "0.8125rem",
280
+ lineHeight: 1.45,
281
+ color: "var(--graview-ink-muted)",
282
+ /*
283
+ * A pick target is a CONTROL, and a control seventeen pixels
284
+ * tall and a thousand wide is neither hittable nor readable as
285
+ * one. Marking an element `data-graview-pick` makes the
286
+ * surface turn it into a button, so it has to be shaped like
287
+ * one — width to its content, height to a fingertip.
288
+ */
289
+ minHeight: 24,
290
+ width: "fit-content",
291
+ maxWidth: "100%",
292
+ padding: page ? "2px 0" : "3px 6px",
293
+ marginLeft: page ? 0 : -6,
294
+ borderRadius: 7,
295
+ cursor: "pointer",
296
+ }, children: [_jsxs("span", { style: {
297
+ flex: "0 0 auto",
298
+ fontWeight: 600,
299
+ color: "var(--graview-warn)",
300
+ letterSpacing: "0.02em",
301
+ }, children: ["\u26A0 ", entry.code] }), _jsx("span", { style: {
302
+ minWidth: 0,
303
+ // A column has room to wrap; a line under a card does not.
304
+ ...(page
305
+ ? {}
306
+ : { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }),
307
+ }, children: entry.text })] }, entry.id))), marked.length > (page ? 6 : 2) ? (_jsxs("li", { style: { fontSize: "0.8125rem", color: "var(--graview-ink-faint)" }, children: ["+", marked.length - (page ? 6 : 2), " more"] })) : null] }));
308
+ const shelf = options.arrange === "shelf";
309
+ /** Discs hold three characters; anything longer is a word, and a word gets a token. */
310
+ const discs = board.slots.every((slot) => slot.code.length <= 3 && !/\s/.test(slot.code));
311
+ /**
312
+ * ONE SLOT, DRAWN. The mark is a disc or a token — decided for the whole
313
+ * board above — and the wrapper takes its place from the caller: a point on
314
+ * the field, or a spot in a shelf's row.
315
+ */
316
+ const renderSlot = (slot, placement) => {
317
+ const hole = slot.occupants.length === 0;
318
+ const dim = lit.size > 0 && !lit.has(slot.id) && !slot.occupants.some((occupant) => lit.has(occupant.id));
319
+ /*
320
+ * TWO different marks, because they are two different facts.
321
+ *
322
+ * The disc is the SLOT and the name under it is the PERSON, so a
323
+ * rule that fails about the position must not paint the person's
324
+ * badge — that is what made a midfielder look injured when what
325
+ * was actually wrong was that nothing in the week trained the
326
+ * skill her position demands. Marking both the same way was a
327
+ * picture stating something untrue.
328
+ */
329
+ const slotBad = broken.has(slot.id);
330
+ const occupantBad = slot.occupants.some((occupant) => broken.has(occupant.id));
331
+ const bad = slotBad;
332
+ const said = [
333
+ ...reasons(slot.id),
334
+ ...slot.occupants.flatMap((occupant) => reasons(occupant.id)),
335
+ ];
336
+ /*
337
+ * WHO THE MARK STANDS FOR. Default (occupant): one occupant and
338
+ * the mark is them; several and the mark is the slot, each name
339
+ * its own target. pickTarget "slot" always selects the slot —
340
+ * names on a multi-occupant mark stay person targets.
341
+ */
342
+ const one = slot.occupants.length === 1 ? slot.occupants[0] : null;
343
+ const pickId = options.pickTarget === "slot" ? slot.id : one ? one.id : slot.id;
344
+ const nameProps = (occupant) => one
345
+ ? {}
346
+ : {
347
+ "data-graview-pick": occupant.id,
348
+ /*
349
+ * A MARK SAYS WHAT IT CLAIMS. The names on a slot holding
350
+ * SEVERAL occupants are targets of their own, and they say
351
+ * so in the tree, not only in opacity.
352
+ */
353
+ "data-graview-emphasis": lit.size === 0 ? "plain" : dim ? "dimmed" : "lit",
354
+ };
355
+ // The NAME carries the person's own trouble — an injury, a suspension —
356
+ // and nothing else. A rule about the position has no business marking
357
+ // the person in it.
358
+ const nameColor = (occupant) => broken.has(occupant.id) ? "var(--graview-warn)" : "var(--graview-ink-muted)";
359
+ const emptyWord = options.emptyLabel ?? "empty";
360
+ return (_jsxs("div", { "data-graview-pick": pickId, "data-graview-slot": slot.id, "data-graview-mark": discs ? "disc" : "token", "data-graview-flagged": slotBad && occupantBad ? "both" : slotBad ? "slot" : occupantBad ? "occupant" : undefined, "data-graview-emphasis": lit.size === 0 ? "plain" : dim ? "dimmed" : "lit", title: [
361
+ hole
362
+ ? `${slot.label} — nobody in it`
363
+ : `${slot.occupants.map((occupant) => occupant.label).join(", ")} at ${slot.label}`,
364
+ ...said,
365
+ ].join("\n"), style: {
366
+ ...placement,
367
+ display: "grid",
368
+ justifyItems: discs ? "center" : "start",
369
+ gap: 3,
370
+ opacity: dim ? 0.5 : 1,
371
+ transition: "opacity 160ms ease",
372
+ }, children: [discs ? (_jsxs("span", { style: {
373
+ position: "relative",
374
+ width: 34,
375
+ height: 34,
376
+ borderRadius: 999,
377
+ display: "grid",
378
+ placeItems: "center",
379
+ fontSize: "0.75rem",
380
+ letterSpacing: "0.02em",
381
+ // An empty slot is drawn as an OUTLINE, not as a filled
382
+ // shape with no name: the hole should look like a hole.
383
+ border: hole
384
+ ? "1.5px dashed var(--graview-warn)"
385
+ : `1px solid ${bad ? "var(--graview-warn)" : "var(--graview-edge-bright)"}`,
386
+ background: hole ? "transparent" : bad ? "var(--graview-panel-warning)" : "var(--graview-panel)",
387
+ color: hole || bad ? "var(--graview-warn)" : "var(--graview-ink)",
388
+ boxShadow: hole ? undefined : "var(--graview-lift-low)",
389
+ }, children: [slot.code, slotBad && !hole ? (_jsx("span", { "aria-hidden": "true", style: { position: "absolute", top: -3, right: -3, fontSize: "0.6875rem", lineHeight: 1, color: "var(--graview-warn)" }, children: "\u26A0" })) : null] })) : (
390
+ /*
391
+ * THE TOKEN: the code on one line, cut with an ellipsis past the
392
+ * width of a long word and carried whole in the title; who is in
393
+ * it on the line beneath, INSIDE the mark, so the mark is one box
394
+ * whose size is known and nothing under it can collide with the
395
+ * row below.
396
+ */
397
+ _jsxs("span", { style: {
398
+ display: "grid",
399
+ gap: 1,
400
+ minWidth: 0,
401
+ maxWidth: 180,
402
+ padding: "5px 10px 6px",
403
+ borderRadius: 8,
404
+ border: hole
405
+ ? "1.5px dashed var(--graview-warn)"
406
+ : `1px solid ${bad ? "var(--graview-warn)" : "var(--graview-edge-bright)"}`,
407
+ background: hole ? "transparent" : bad ? "var(--graview-panel-warning)" : "var(--graview-panel)",
408
+ boxShadow: hole ? undefined : "var(--graview-lift-low)",
409
+ }, children: [_jsxs("span", { style: {
410
+ fontSize: "0.8125rem",
411
+ fontWeight: 550,
412
+ lineHeight: 1.25,
413
+ whiteSpace: "nowrap",
414
+ overflow: "hidden",
415
+ textOverflow: "ellipsis",
416
+ color: hole || bad ? "var(--graview-warn)" : "var(--graview-ink)",
417
+ }, children: [slot.code, slotBad && !hole ? " ⚠" : null] }), hole ? (_jsx("span", { style: { fontSize: "0.75rem", lineHeight: 1.3, color: "var(--graview-warn)" }, children: emptyWord })) : (_jsx("span", { style: { display: "flex", flexWrap: "wrap", gap: "0 8px", fontSize: "0.75rem", lineHeight: 1.3 }, children: slot.occupants.map((occupant) => (_jsxs("span", { ...nameProps(occupant), style: { whiteSpace: "nowrap", color: nameColor(occupant) }, children: [occupant.label, broken.has(occupant.id) ? " ⚠" : ""] }, occupant.id))) }))] })), discs ? (hole ? (_jsx("span", { style: { fontSize: "0.75rem", whiteSpace: "nowrap", color: "var(--graview-warn)" }, children: emptyWord })) : (slot.occupants.map((occupant) => (_jsxs("span", { ...nameProps(occupant), style: { fontSize: "0.75rem", whiteSpace: "nowrap", color: nameColor(occupant) }, children: [occupant.label, broken.has(occupant.id) ? " ⚠" : ""] }, occupant.id))))) : null] }, slot.id));
418
+ };
419
+ /**
420
+ * THE SHELF'S BANDS: each zone with the slots whose y falls in it, in the
421
+ * order the domain gave them (top to bottom, then left to right — the
422
+ * board is already sorted so). A slot no zone claims, or a board with no
423
+ * zones, goes in one band with no heading.
424
+ */
425
+ const zones = options.zones ?? [];
426
+ const bands = zones.map((zone, index) => ({
427
+ label: zone.label,
428
+ slots: board.slots.filter((slot) => slot.y >= zone.from && (slot.y < zone.to || (index === zones.length - 1 && slot.y <= zone.to))),
429
+ }));
430
+ const claimed = new Set(bands.flatMap((band) => band.slots.map((slot) => slot.id)));
431
+ const unclaimed = board.slots.filter((slot) => !claimed.has(slot.id));
432
+ if (unclaimed.length > 0 || bands.length === 0)
433
+ bands.push({ label: "", slots: unclaimed });
434
+ const field = {
435
+ boxSizing: "border-box",
436
+ borderRadius: 12,
437
+ border: "1px solid var(--graview-edge)",
438
+ // A ground of its own, so the arrangement reads as a place rather
439
+ // than as dots floating on the panel.
440
+ background: "linear-gradient(var(--graview-panel-muted), var(--graview-panel-muted)), var(--graview-panel)",
441
+ overflow: "hidden",
442
+ };
443
+ return (_jsxs(Panel, { title: label ?? "Board",
444
+ /*
445
+ * Occupancy, stated as occupancy. "Complete" beside a key listing two
446
+ * warnings read as a contradiction — both were true, because
447
+ * "complete" was quietly about slots being filled and never said so.
448
+ */
449
+ meta: board.empty.length === 0
450
+ ? `all ${board.slots.length} filled`
451
+ : `${board.empty.length} unfilled`, ...(page ? { style: { flex: "1 1 auto", minHeight: 0, height: "100%" } } : {}), children: [_jsxs("div", { ref: room, style: {
452
+ display: "flex",
453
+ gap: 28,
454
+ alignItems: "stretch",
455
+ justifyContent: "center",
456
+ flex: "1 1 auto",
457
+ minHeight: 0,
458
+ }, children: [page && keyList ? (_jsxs("div", { style: {
459
+ width: 260,
460
+ flex: "0 0 auto",
461
+ alignSelf: "center",
462
+ display: "grid",
463
+ gap: 8,
464
+ }, children: [_jsx("span", { style: {
465
+ fontSize: "0.75rem",
466
+ letterSpacing: "0.14em",
467
+ textTransform: "uppercase",
468
+ color: "var(--graview-ink-faint)",
469
+ }, children: "What is marked" }), keyList] })) : null, shelf ? (
470
+ /*
471
+ * THE SHELF. Each zone is a band with its name as a heading —
472
+ * a heading row of its own, so it can never sit on a slot — and
473
+ * its slots flow into rows sized to what they hold.
474
+ */
475
+ _jsx("div", { "data-graview-primitive": "board", "data-graview-arrange": "shelf", style: {
476
+ ...field,
477
+ overflow: "auto",
478
+ flex: "1 1 auto",
479
+ minWidth: 0,
480
+ alignSelf: "stretch",
481
+ display: "grid",
482
+ alignContent: "start",
483
+ }, children: bands.map((band, index) => (_jsxs("section", { "data-graview-zone": band.label || undefined, style: {
484
+ display: "grid",
485
+ gap: 8,
486
+ padding: "10px 14px 14px",
487
+ ...(index > 0 ? { borderTop: "1px dashed var(--graview-edge)" } : {}),
488
+ }, children: [band.label ? (_jsx("span", { style: {
489
+ fontSize: "0.6875rem",
490
+ letterSpacing: "0.14em",
491
+ textTransform: "uppercase",
492
+ color: "var(--graview-ink-faint)",
493
+ }, children: band.label })) : null, band.slots.length > 0 ? (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "10px 12px", alignItems: "flex-start" }, children: band.slots.map((slot) => renderSlot(slot, {})) })) : (_jsx("span", { style: { fontSize: "0.75rem", color: "var(--graview-ink-faint)" }, children: "nothing here" }))] }, band.label || `band-${index}`))) })) : (
494
+ /*
495
+ * The zone names live OUTSIDE the field.
496
+ *
497
+ * Inside, they were absolutely positioned at the top-left of each
498
+ * band — which is exactly where a left back stands. "DEFENCE" and
499
+ * the LB slot were drawn on top of each other and read "DEFENCELB",
500
+ * and no arrangement of insets fixes that, because where the slots
501
+ * go is the DOMAIN's decision and the label has no claim on it. A
502
+ * rail beside the field can never collide with anything, and the
503
+ * field stays purely what the graph says.
504
+ */
505
+ /* The rail belongs TO the field, so it travels with it rather than
506
+ sitting a gap away looking like a separate column. Turned, it
507
+ lies along the bottom edge instead of standing beside the left. */
508
+ _jsxs("div", { style: { display: "flex", gap: 5, alignItems: "stretch", minWidth: 0, ...(byWidth ? { flex: "1 1 auto", alignSelf: "flex-start" } : {}) }, children: [!turned && zones.length > 0 ? (_jsx("div", { "aria-hidden": "true", style: { position: "relative", width: 15, flex: "0 0 auto", alignSelf: "stretch" }, children: zones.map((zone) => (_jsx("span", { title: zone.label, style: {
509
+ position: "absolute",
510
+ top: `${zone.from * 100}%`,
511
+ height: `${(zone.to - zone.from) * 100}%`,
512
+ right: 0,
513
+ writingMode: "vertical-rl",
514
+ transform: "rotate(180deg)",
515
+ display: "flex",
516
+ alignItems: "center",
517
+ justifyContent: "center",
518
+ fontSize: "0.6875rem",
519
+ letterSpacing: "0.14em",
520
+ textTransform: "uppercase",
521
+ whiteSpace: "nowrap",
522
+ // A band shorter than its name shows what fits and says
523
+ // so, rather than the middle of the word.
524
+ overflow: "hidden",
525
+ textOverflow: "ellipsis",
526
+ color: "var(--graview-ink-faint)",
527
+ }, children: zone.label }, zone.label))) })) : null, _jsxs("div", { "data-graview-primitive": "board", "data-graview-arrange": "exact", style: {
528
+ ...field,
529
+ position: "relative",
530
+ aspectRatio: `${turned ? 1 / aspect : aspect}`,
531
+ ...(byWidth
532
+ ? // The room's width, and the aspect gives the height — see `byWidth`.
533
+ // Not stretched: a stretched flex item takes the row's height, and
534
+ // the row's height is the four pixels this is escaping from.
535
+ { width: "100%", height: "auto", flex: "1 1 auto", minWidth: 0, alignSelf: "flex-start" }
536
+ : {
537
+ height: "100%",
538
+ flex: "0 1 auto",
539
+ // The measured width, once the room has spoken — see `tall`.
540
+ // The aspect ratio above stays as the first-paint estimate.
541
+ ...(tall !== null ? { width: Math.round(tall * (turned ? 1 / aspect : aspect)) } : {}),
542
+ }),
543
+ maxWidth: "100%",
544
+ }, children: [zones.map((zone) => (_jsx("div", { "aria-hidden": "true", style: {
545
+ position: "absolute",
546
+ ...(turned
547
+ ? {
548
+ top: 0,
549
+ bottom: 0,
550
+ left: `${(1 - zone.to) * 100}%`,
551
+ width: `${(zone.to - zone.from) * 100}%`,
552
+ borderLeft: "1px dashed var(--graview-edge)",
553
+ }
554
+ : {
555
+ left: 0,
556
+ right: 0,
557
+ top: `${zone.from * 100}%`,
558
+ height: `${(zone.to - zone.from) * 100}%`,
559
+ borderBottom: "1px dashed var(--graview-edge)",
560
+ }),
561
+ } }, zone.label))), turned
562
+ ? zones.map((zone) => (_jsx("span", { "aria-hidden": "true", title: zone.label, style: {
563
+ position: "absolute",
564
+ bottom: 3,
565
+ left: `${(1 - zone.to) * 100}%`,
566
+ width: `${(zone.to - zone.from) * 100}%`,
567
+ textAlign: "center",
568
+ fontSize: "0.6875rem",
569
+ letterSpacing: "0.14em",
570
+ textTransform: "uppercase",
571
+ whiteSpace: "nowrap",
572
+ overflow: "hidden",
573
+ textOverflow: "ellipsis",
574
+ color: "var(--graview-ink-faint)",
575
+ }, children: zone.label }, `label-${zone.label}`)))
576
+ : null, board.slots.map((slot) => renderSlot(slot, {
577
+ position: "absolute",
578
+ left: `${at(slot.x, slot.y).x * 100}%`,
579
+ top: `${at(slot.x, slot.y).y * 100}%`,
580
+ transform: "translate(-50%, -50%)",
581
+ /*
582
+ * AS WIDE AS WHAT IT HOLDS. An absolutely placed box with no
583
+ * width shrinks to the room between its `left` and the
584
+ * field's edge, so a token placed at 82% of a phone-width
585
+ * board was 22 pixels wide with its word broken inside it —
586
+ * and 22 pixels is not a target. A disc never noticed, being
587
+ * 34 pixels by declaration.
588
+ */
589
+ width: "max-content",
590
+ maxWidth: "100%",
591
+ }))] })] })), board.spare.length > 0 ? (_jsxs("div", { style: { display: "grid", gap: 6, alignContent: "start", minWidth: 0 }, children: [_jsx("span", { style: {
592
+ fontSize: "0.75rem",
593
+ letterSpacing: "0.14em",
594
+ textTransform: "uppercase",
595
+ color: "var(--graview-ink-faint)",
596
+ }, children: "Not in" }), _jsx(Roster, { pick: true, max: 10, items: board.spare.map((spare) => ({
597
+ ...spare,
598
+ emphasis: (lit.size === 0 ? "plain" : lit.has(spare.id) ? "lit" : "dimmed"),
599
+ })) })] })) : null] }), page ? null : keyList] }));
600
+ }
601
+ export function createBoardLens(options) {
602
+ /*
603
+ * A real COMPONENT, not a method that happens to call hooks.
604
+ *
605
+ * `View` is rendered as `<lens.View />`, so it is a component — but written
606
+ * as a method on an object literal it looked like one to the hooks lint rule
607
+ * and needed a disable in three files. A disable repeated three times is a
608
+ * rule telling you something, and what it was telling us is that this wanted
609
+ * to be a component.
610
+ */
611
+ function Bound(props) {
612
+ // The SCHEMA comes from the provider: `ViewProps` carries none, so a lens
613
+ // rendered through the registry ran without it and every schema-aware
614
+ // decision inside quietly took its fallback path.
615
+ const { store } = useGraview();
616
+ /*
617
+ * THE OCCUPANTS ARRANGE; THE SLOTS STAY. A filter that hid a slot would
618
+ * redraw the pitch; a sort within a slot's occupants and a filter over
619
+ * who is shown are what a board has room for.
620
+ */
621
+ /*
622
+ * The occupants come from the STORE, not from `props.nodes`: in a scene
623
+ * a board stands for its slots' kind, so the nodes it is handed are the
624
+ * seats, and the people in them are found by the fill edge.
625
+ */
626
+ const state = buildBoard(onTheHorizon(store.graph, store.schema, props.nodes), store.graph.allEdges(), options, store.schema);
627
+ const seated = [...state.slots.flatMap((slot) => slot.occupants.map((occupant) => occupant.id)), ...state.spare.map((spare) => spare.id)];
628
+ const subject = seated.map((id) => store.graph.getNode(id)).filter((node) => node !== undefined);
629
+ const { arranged, bar } = useArranging(props, {
630
+ ...(options.arranging !== undefined ? { allow: options.arranging } : {}),
631
+ lensAllows: { group: false },
632
+ ...(options.arrangedBy ? { arrangedBy: options.arrangedBy } : {}),
633
+ subject,
634
+ });
635
+ const view = (_jsx(BoardView, { schema: store.schema, ...props, options: options, ...(options.arranging === false ? {} : { occupants: arranged.nodes.map((node) => node.id) }) }));
636
+ return withMore(props, store.schema, bar ? (_jsxs("div", { style: { display: "grid", gap: 6, height: "100%", minHeight: 0, gridTemplateRows: "auto 1fr" }, children: [bar, view] })) : (view));
637
+ }
638
+ return {
639
+ name: "board",
640
+ requiredRoles: [...BOARD_REQUIRED_ROLES],
641
+ options,
642
+ /*
643
+ * The SCHEMA comes from the provider, not from the caller.
644
+ *
645
+ * `ViewProps` carries no schema — the registry never passes one — so a
646
+ * lens rendered through the registry ran without it and every
647
+ * schema-aware decision inside quietly took its fallback path. In the
648
+ * timeline that meant an optional field absent on one node looked exactly
649
+ * like a role nobody bound, and a single unplanned task threw for the
650
+ * whole view.
651
+ */
652
+ View: Bound,
653
+ build(nodes, edges, schema) {
654
+ return buildBoard(nodes, edges, options, schema);
655
+ },
656
+ };
657
+ }
658
+ //# sourceMappingURL=board.js.map