@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.
- package/LICENSE +96 -0
- package/README.md +25 -0
- package/dist/arrange-bar.d.ts +49 -0
- package/dist/arrange-bar.d.ts.map +1 -0
- package/dist/arrange-bar.js +264 -0
- package/dist/arrange-bar.js.map +1 -0
- package/dist/chat.d.ts +46 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +343 -0
- package/dist/chat.js.map +1 -0
- package/dist/companion.d.ts +58 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +433 -0
- package/dist/companion.js.map +1 -0
- package/dist/connections.d.ts +23 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +100 -0
- package/dist/connections.js.map +1 -0
- package/dist/default-views.d.ts +35 -0
- package/dist/default-views.d.ts.map +1 -0
- package/dist/default-views.js +605 -0
- package/dist/default-views.js.map +1 -0
- package/dist/editable.d.ts +71 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +225 -0
- package/dist/editable.js.map +1 -0
- package/dist/figure.d.ts +30 -0
- package/dist/figure.d.ts.map +1 -0
- package/dist/figure.js +66 -0
- package/dist/figure.js.map +1 -0
- package/dist/find.d.ts +5 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +252 -0
- package/dist/find.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/installation.d.ts +17 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +37 -0
- package/dist/installation.js.map +1 -0
- package/dist/ladder.d.ts +2 -0
- package/dist/ladder.d.ts.map +1 -0
- package/dist/ladder.js +53 -0
- package/dist/ladder.js.map +1 -0
- package/dist/lens/arranging.d.ts +41 -0
- package/dist/lens/arranging.d.ts.map +1 -0
- package/dist/lens/arranging.js +38 -0
- package/dist/lens/arranging.js.map +1 -0
- package/dist/lens/board.d.ts +146 -0
- package/dist/lens/board.d.ts.map +1 -0
- package/dist/lens/board.js +658 -0
- package/dist/lens/board.js.map +1 -0
- package/dist/lens/calendar-dates.d.ts +19 -0
- package/dist/lens/calendar-dates.d.ts.map +1 -0
- package/dist/lens/calendar-dates.js +55 -0
- package/dist/lens/calendar-dates.js.map +1 -0
- package/dist/lens/calendar-drawing.d.ts +59 -0
- package/dist/lens/calendar-drawing.d.ts.map +1 -0
- package/dist/lens/calendar-drawing.js +229 -0
- package/dist/lens/calendar-drawing.js.map +1 -0
- package/dist/lens/calendar-options.d.ts +107 -0
- package/dist/lens/calendar-options.d.ts.map +1 -0
- package/dist/lens/calendar-options.js +19 -0
- package/dist/lens/calendar-options.js.map +1 -0
- package/dist/lens/calendar-placing.d.ts +35 -0
- package/dist/lens/calendar-placing.d.ts.map +1 -0
- package/dist/lens/calendar-placing.js +113 -0
- package/dist/lens/calendar-placing.js.map +1 -0
- package/dist/lens/calendar-spans.d.ts +48 -0
- package/dist/lens/calendar-spans.d.ts.map +1 -0
- package/dist/lens/calendar-spans.js +146 -0
- package/dist/lens/calendar-spans.js.map +1 -0
- package/dist/lens/calendar-view.d.ts +34 -0
- package/dist/lens/calendar-view.d.ts.map +1 -0
- package/dist/lens/calendar-view.js +228 -0
- package/dist/lens/calendar-view.js.map +1 -0
- package/dist/lens/calendar.d.ts +43 -0
- package/dist/lens/calendar.d.ts.map +1 -0
- package/dist/lens/calendar.js +43 -0
- package/dist/lens/calendar.js.map +1 -0
- package/dist/lens/coverage.d.ts +183 -0
- package/dist/lens/coverage.d.ts.map +1 -0
- package/dist/lens/coverage.js +732 -0
- package/dist/lens/coverage.js.map +1 -0
- package/dist/lens/horizon.d.ts +14 -0
- package/dist/lens/horizon.d.ts.map +1 -0
- package/dist/lens/horizon.js +20 -0
- package/dist/lens/horizon.js.map +1 -0
- package/dist/lens/more.d.ts +16 -0
- package/dist/lens/more.d.ts.map +1 -0
- package/dist/lens/more.js +17 -0
- package/dist/lens/more.js.map +1 -0
- package/dist/lens/plan-lens.d.ts +17 -0
- package/dist/lens/plan-lens.d.ts.map +1 -0
- package/dist/lens/plan-lens.js +24 -0
- package/dist/lens/plan-lens.js.map +1 -0
- package/dist/lens/plan-state.d.ts +122 -0
- package/dist/lens/plan-state.d.ts.map +1 -0
- package/dist/lens/plan-state.js +152 -0
- package/dist/lens/plan-state.js.map +1 -0
- package/dist/lens/plan-view.d.ts +10 -0
- package/dist/lens/plan-view.d.ts.map +1 -0
- package/dist/lens/plan-view.js +576 -0
- package/dist/lens/plan-view.js.map +1 -0
- package/dist/lens/plan.d.ts +25 -0
- package/dist/lens/plan.d.ts.map +1 -0
- package/dist/lens/plan.js +25 -0
- package/dist/lens/plan.js.map +1 -0
- package/dist/lens/reach.d.ts +47 -0
- package/dist/lens/reach.d.ts.map +1 -0
- package/dist/lens/reach.js +67 -0
- package/dist/lens/reach.js.map +1 -0
- package/dist/lens/timeline.d.ts +137 -0
- package/dist/lens/timeline.d.ts.map +1 -0
- package/dist/lens/timeline.js +426 -0
- package/dist/lens/timeline.js.map +1 -0
- package/dist/markup.d.ts +23 -0
- package/dist/markup.d.ts.map +1 -0
- package/dist/markup.js +24 -0
- package/dist/markup.js.map +1 -0
- package/dist/places.d.ts +24 -0
- package/dist/places.d.ts.map +1 -0
- package/dist/places.js +227 -0
- package/dist/places.js.map +1 -0
- package/dist/popover.d.ts +28 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +59 -0
- package/dist/popover.js.map +1 -0
- package/dist/primitives/index.d.ts +214 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +381 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/profile.d.ts +65 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +233 -0
- package/dist/profile.js.map +1 -0
- package/dist/quick-relations.d.ts +16 -0
- package/dist/quick-relations.d.ts.map +1 -0
- package/dist/quick-relations.js +183 -0
- package/dist/quick-relations.js.map +1 -0
- package/dist/relation-key.d.ts +44 -0
- package/dist/relation-key.d.ts.map +1 -0
- package/dist/relation-key.js +196 -0
- package/dist/relation-key.js.map +1 -0
- package/dist/seat.d.ts +137 -0
- package/dist/seat.d.ts.map +1 -0
- package/dist/seat.js +162 -0
- package/dist/seat.js.map +1 -0
- package/dist/seats.d.ts +21 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +62 -0
- package/dist/seats.js.map +1 -0
- package/dist/seeding.d.ts +150 -0
- package/dist/seeding.d.ts.map +1 -0
- package/dist/seeding.js +538 -0
- package/dist/seeding.js.map +1 -0
- package/dist/shell.d.ts +74 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +86 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme.d.ts +50 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +1692 -0
- package/dist/theme.js.map +1 -0
- package/dist/wordmark.d.ts +15 -0
- package/dist/wordmark.d.ts.map +1 -0
- package/dist/wordmark.js +55 -0
- package/dist/wordmark.js.map +1 -0
- package/dist/workbench/activity.d.ts +75 -0
- package/dist/workbench/activity.d.ts.map +1 -0
- package/dist/workbench/activity.js +259 -0
- package/dist/workbench/activity.js.map +1 -0
- package/dist/workbench/agent-seat.d.ts +51 -0
- package/dist/workbench/agent-seat.d.ts.map +1 -0
- package/dist/workbench/agent-seat.js +112 -0
- package/dist/workbench/agent-seat.js.map +1 -0
- package/dist/workbench/answer-args.d.ts +31 -0
- package/dist/workbench/answer-args.d.ts.map +1 -0
- package/dist/workbench/answer-args.js +257 -0
- package/dist/workbench/answer-args.js.map +1 -0
- package/dist/workbench/back-out.d.ts +71 -0
- package/dist/workbench/back-out.d.ts.map +1 -0
- package/dist/workbench/back-out.js +238 -0
- package/dist/workbench/back-out.js.map +1 -0
- package/dist/workbench/following.d.ts +7 -0
- package/dist/workbench/following.d.ts.map +1 -0
- package/dist/workbench/following.js +26 -0
- package/dist/workbench/following.js.map +1 -0
- package/dist/workbench/index.d.ts +23 -0
- package/dist/workbench/index.d.ts.map +1 -0
- package/dist/workbench/index.js +23 -0
- package/dist/workbench/index.js.map +1 -0
- package/dist/workbench/inspector.d.ts +27 -0
- package/dist/workbench/inspector.d.ts.map +1 -0
- package/dist/workbench/inspector.js +745 -0
- package/dist/workbench/inspector.js.map +1 -0
- package/dist/workbench/standing.d.ts +16 -0
- package/dist/workbench/standing.d.ts.map +1 -0
- package/dist/workbench/standing.js +102 -0
- package/dist/workbench/standing.js.map +1 -0
- package/dist/workbench/trail.d.ts +35 -0
- package/dist/workbench/trail.d.ts.map +1 -0
- package/dist/workbench/trail.js +150 -0
- package/dist/workbench/trail.js.map +1 -0
- 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
|