@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,745 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { humaniseField, nounOf, withArticle } from "@graview/core";
3
+ import { useSubject } from "../companion.js";
4
+ import { edgeOfSelection, kindsOf } from "@graview/layout";
5
+ import { useAffordances, useApplyAffordance, useGraview, useSelection } from "@graview/react";
6
+ import { loadPins, togglePin } from "@graview/tools";
7
+ import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
8
+ import { AnswerArgs, nameOf } from "./answer-args.js";
9
+ export function Inspector({ placement = "float" } = {}) {
10
+ const { store, menuAt, setMenuAt, view } = useGraview();
11
+ const subject = useSubject();
12
+ /*
13
+ * The pane is positioned within the SCENE'S BOX, not the window. It was
14
+ * fixed to the viewport, which put it at the page's edge when the scene
15
+ * was a box on a page — an embed's inspector drawn over the host's own
16
+ * navigation. The pointer menu's client coordinates are translated into
17
+ * that box, and clamped to it.
18
+ */
19
+ const asideRef = useRef(null);
20
+ /*
21
+ * THE KEYBOARD KEEPS ITS PLACE.
22
+ *
23
+ * The pane is a live list: an act applies and leaves it, a pin regroups
24
+ * it, an ask closes when it is answered. Every one of those removes the
25
+ * element the keyboard was standing on, and a removed element takes focus
26
+ * to <body> with it — so pressing Enter on "Mark it done" left somebody
27
+ * working from the keyboard at the top of the document, six tabs from
28
+ * where they were, once per act. The routed face never had this: its
29
+ * forms stay mounted, so the two faces disagreed about what pressing a
30
+ * button does.
31
+ *
32
+ * What is remembered is WHICH act, not which element: the ask's controls
33
+ * belong to the act above them, so answering one puts you back on it.
34
+ * Tabbing away is somebody leaving on purpose and is not restored; the
35
+ * element going away underneath them is the only case this is for.
36
+ *
37
+ * And "went away" is asked of the ELEMENT, not of the blur. `relatedTarget`
38
+ * is null when a removed node loses focus — and also when a Tab from the
39
+ * last control in the document wraps round to the first, which Chromium
40
+ * reports the same way. Trusting the blur alone left the pane holding a
41
+ * stale key after an ordinary Tab out of it, and the next time focus fell
42
+ * to <body> anywhere on the page — a rename committed in a card — the pane
43
+ * reclaimed the keyboard from work it had no part in. So the element the
44
+ * keyboard stood on is kept beside the key, and the pane restores only
45
+ * when that element is no longer in the document.
46
+ */
47
+ const keptFocus = useRef(null);
48
+ /** The picture the pane sits on: where the keyboard goes if the pane goes. */
49
+ const scene = useRef(null);
50
+ const remember = (target) => {
51
+ const asking = target.closest("[data-graview-asking]")?.getAttribute("data-graview-asking");
52
+ const pin = target.getAttribute("data-pin-for");
53
+ const key = asking ?? target.getAttribute("data-affordance") ?? (pin === null ? null : `pin:${pin}`);
54
+ keptFocus.current = key === null ? null : { key, element: target };
55
+ };
56
+ useEffect(() => {
57
+ const pane = asideRef.current;
58
+ const kept = keptFocus.current;
59
+ if (kept === null || document.activeElement !== document.body)
60
+ return;
61
+ // Still on the page: nothing was pulled out from under the keyboard, so
62
+ // wherever it went, it went on purpose.
63
+ if (kept.element.isConnected)
64
+ return;
65
+ const key = kept.key;
66
+ /*
67
+ * AND WHEN THE PANE ITSELF GOES — Escape, the ×, or an act that clears
68
+ * the selection — there is nothing inside it left to go back to. Chrome
69
+ * and Firefox pick a new starting point for the next Tab on their own;
70
+ * WEBKIT DOES NOT. Focus went away with a removed node and four presses
71
+ * of Tab moved nothing at all, which is a keyboard that has stopped
72
+ * working and a pointer as the only way out. The picture the pane was
73
+ * about is the honest home.
74
+ */
75
+ if (!pane) {
76
+ keptFocus.current = null;
77
+ scene.current?.focus();
78
+ return;
79
+ }
80
+ const home = (key.startsWith("pin:")
81
+ ? pane.querySelector(`[data-pin-for="${CSS.escape(key.slice(4))}"]`)
82
+ : null) ??
83
+ pane.querySelector(`[data-affordance="${CSS.escape(key)}"]:not([disabled])`) ??
84
+ // The act is gone — finished, severed, no longer offered. The pane
85
+ // itself is then the honest home: the keyboard stays where the work
86
+ // is, and Enter on a container does nothing by accident.
87
+ pane;
88
+ home.focus();
89
+ });
90
+ const [box, setBox] = useState(null);
91
+ useLayoutEffect(() => {
92
+ const parent = asideRef.current?.offsetParent;
93
+ // Held while the pane exists, because the keyboard needs somewhere to
94
+ // land at the moment it stops existing.
95
+ if (parent instanceof HTMLElement)
96
+ scene.current = parent;
97
+ if (!parent)
98
+ return;
99
+ const rect = parent.getBoundingClientRect();
100
+ setBox((current) => current && current.left === rect.left && current.top === rect.top && current.width === rect.width && current.height === rect.height
101
+ ? current
102
+ : { left: rect.left, top: rect.top, width: rect.width, height: rect.height });
103
+ });
104
+ const { selection, set, clear } = useSelection();
105
+ /*
106
+ * THE PERSON'S OWN PINS, loaded once per mount and passed into the same
107
+ * derivation every surface reads — so a pin made here reorders the strip,
108
+ * the pointer menu and nothing else invents a second action system.
109
+ */
110
+ const [pins, setPins] = useState(() => loadPins());
111
+ /*
112
+ * THE THING YOU PRESSED ON leads the list, in the strip as well as the
113
+ * menu — one rank, derived once, so the two surfaces cannot disagree
114
+ * about what comes first.
115
+ *
116
+ * The menu names what its gesture landed on. The strip has no gesture of
117
+ * its own, so it takes the most recent addition to the selection, which
118
+ * is the same thing: the card you just clicked. With a single selection
119
+ * that is simply the selected node, which is what "resolve THIS item's
120
+ * problem" means when only one item is in hand.
121
+ */
122
+ /*
123
+ * WHAT THIS PANE IS ABOUT. The gesture's own target when a right-click
124
+ * opened it, else the most recent addition to the selection — and, in
125
+ * the companion's rail, the rail's own subject when nothing is chosen,
126
+ * so the acts belong to the thing the header names. A rail that said
127
+ * "Pay the deposit" over the acts of nothing was two panels again.
128
+ */
129
+ const focus = menuAt?.on ?? selection[selection.length - 1] ?? (placement === "rail" ? (subject.id ?? undefined) : undefined);
130
+ /*
131
+ * In the rail with nothing chosen, the acts are the SUBJECT's: what the
132
+ * header names is what the buttons under it do. Everywhere else the
133
+ * selection is what the strip is about, as it always was.
134
+ */
135
+ const about = placement === "rail" && selection.length === 0 && subject.id ? [subject.id] : undefined;
136
+ const deriveOptions = useMemo(() => ({ pins, ...(focus === undefined ? {} : { focus }), ...(about ? { about } : {}) }),
137
+ // `about` is a fresh array each render; the one id in it is what changes.
138
+ [pins, focus, about?.[0]]);
139
+ // Which acts the app itself pinned — the star on those demotes rather
140
+ // than doubling up, so pressing it always visibly does something.
141
+ const declaredPins = useMemo(() => new Set(store.allMutations().filter((mutation) => mutation.pinned).map((mutation) => mutation.name)), [store]);
142
+ const { affordances, withheld, observations } = useAffordances(deriveOptions);
143
+ /*
144
+ * Whether the derivation is withholding a creating act for want of the
145
+ * kind it connects to, rather than for want of a mutation. The observation
146
+ * above the list already names it, so the empty-list sentence stands down.
147
+ */
148
+ const waiting = observations.some((observation) => observation.id.startsWith("schema:waits:"));
149
+ const { apply, preview } = useApplyAffordance();
150
+ const [pending, setPending] = useState(null);
151
+ const [expanded, setExpanded] = useState(false);
152
+ /* The withheld acts past the first three, on request — never dropped without a word. */
153
+ const [allWithheld, setAllWithheld] = useState(false);
154
+ /*
155
+ * The searcher's text. Chrome that is mostly not there must not be
156
+ * there: the field only renders once the list outgrows the fold.
157
+ */
158
+ const [query, setQuery] = useState("");
159
+ /*
160
+ * A REFUSAL IS A RESULT, said where the button was pressed.
161
+ *
162
+ * A mutation that throws — a validation failure, a guard's own sentence —
163
+ * used to vanish into the console, and the strip read as a button that
164
+ * does nothing. The seat already surfaces its refusals on itself; this is
165
+ * the same honesty for every action.
166
+ */
167
+ const [failed, setFailed] = useState(null);
168
+ const act = (affordance, args) => {
169
+ try {
170
+ preview(affordance, args);
171
+ apply(affordance, args);
172
+ setFailed(null);
173
+ setMenuAt(null);
174
+ return true;
175
+ }
176
+ catch (error) {
177
+ setFailed(error instanceof Error ? error.message : String(error));
178
+ return false;
179
+ }
180
+ };
181
+ const kinds = [
182
+ ...new Set(selection.flatMap((id) => {
183
+ const node = store.graph.getNode(id);
184
+ return node ? [node.kind] : [];
185
+ })),
186
+ ];
187
+ /*
188
+ * WHAT THE SELECTION IS ABOUT, including a district.
189
+ *
190
+ * `kinds` above reads the selection's NODES, so a selected kind card — a
191
+ * district, which has no node behind it — came out empty, and a pane with
192
+ * every act withheld said "Nothing you may do with this mix of kinds"
193
+ * about one district plainly named Owners. The card's own id knows the
194
+ * kind; the badge stays off, because the title is already the plural.
195
+ */
196
+ const subjectKinds = kinds.length > 0
197
+ ? kinds
198
+ : [...new Set(selection.flatMap((id) => kindsOf(id)))];
199
+ /*
200
+ * A selected LINE. The pane's job flips from "what is this thing" to
201
+ * "what is this relation": the declaration's own sentence for the edge,
202
+ * both ends as pressable names, and the derived actions below — which
203
+ * the schema provider has already pointed at this exact edge.
204
+ */
205
+ const edge = selection.length === 1 ? edgeOfSelection(selection[0]) : null;
206
+ const edgeEnds = edge
207
+ ? { from: store.graph.getNode(edge.from), to: store.graph.getNode(edge.to) }
208
+ : null;
209
+ /*
210
+ * The heading already reads the relation from its declaring end ("Where
211
+ * they work"); what is left to say is how it reads from the other end —
212
+ * "From North lot: who works here" — and nothing when that was not declared.
213
+ */
214
+ const edgeSaid = edge
215
+ ? (() => {
216
+ const declared = store.schema.tryDefinition(store.graph.getNode(edge.from)?.kind ?? "")?.edges?.[edge.kind];
217
+ const far = store.graph.getNode(edge.to);
218
+ return declared?.inverse && far ? `From ${nameOf(store, far.id)}: ${declared.inverse}` : null;
219
+ })()
220
+ : null;
221
+ useEffect(() => {
222
+ setPending(null);
223
+ setExpanded(false);
224
+ setAllWithheld(false);
225
+ setFailed(null);
226
+ setQuery("");
227
+ }, [selection]);
228
+ /*
229
+ * The strip reserves NOTHING. It is a transient elevated surface, and it
230
+ * floats in front of the scene the way a menu floats in front of a page —
231
+ * with the elevation drawn honestly, so covering reads as "nearer", not as
232
+ * a collision. Reserving a permanent band of the scene's height for it
233
+ * squeezed every band on every screen for chrome that mostly is not there,
234
+ * and insetting only while something is selected would reflow the picture
235
+ * under a double-click. Floating is the only answer that moves nothing.
236
+ */
237
+ /*
238
+ * A menu at the pointer closes the way a menu does. The strip does not —
239
+ * it is not covering anything and clearing the selection is what the × is
240
+ * for.
241
+ */
242
+ useEffect(() => {
243
+ if (!menuAt)
244
+ return;
245
+ const away = () => setMenuAt(null);
246
+ const key = (event) => {
247
+ if (event.key === "Escape")
248
+ setMenuAt(null);
249
+ };
250
+ // A frame later, or the click that opened it closes it again.
251
+ const timer = setTimeout(() => document.addEventListener("mousedown", away), 0);
252
+ document.addEventListener("keydown", key);
253
+ return () => {
254
+ clearTimeout(timer);
255
+ document.removeEventListener("mousedown", away);
256
+ document.removeEventListener("keydown", key);
257
+ };
258
+ }, [menuAt, setMenuAt]);
259
+ const atPointer = menuAt !== null;
260
+ /*
261
+ * In the companion's rail the pane is a SECTION, and the pointer popover
262
+ * belongs to the copy mounted for it: drawing both would put the acts on
263
+ * screen twice, once in the rail and once under the pointer.
264
+ */
265
+ const railed = placement === "rail" && !atPointer;
266
+ const standDown = (placement === "rail" && atPointer) || (placement === "menu" && !atPointer);
267
+ /*
268
+ * Whether the picture has room for a rail beside it. Measured from the
269
+ * scene's own box, not the window's: an embed in a column of an article is
270
+ * narrow on the widest monitor there is.
271
+ */
272
+ const RAIL_NEEDS = 14 + 236 + 14 + 300;
273
+ const narrow = (box?.width ?? Number.POSITIVE_INFINITY) < RAIL_NEEDS;
274
+ /*
275
+ * IN A NARROW BOX THE PICTURE MAKES ROOM.
276
+ *
277
+ * Floating is the right answer where there is ground to float over. In a
278
+ * 350-wide Graview — an embed in a column of an article, or a phone —
279
+ * there is none: a sheet along the bottom covers the districts and buries
280
+ * the control that opens one, and a rail down the side covers the very
281
+ * card it is about. So the scene's own box loses the sheet's height while
282
+ * the sheet is open, and the layout re-runs into what is left.
283
+ *
284
+ * The old objection to insetting was that it reflows the picture under a
285
+ * double-click. It does — once, when the sheet appears, and once when it
286
+ * goes. Against chrome permanently over the content, that is the better
287
+ * trade, and it is what every narrow interface already does.
288
+ */
289
+ useLayoutEffect(() => {
290
+ const parent = asideRef.current?.offsetParent;
291
+ // In the companion's rail the companion makes the room, for the whole sheet it is part of.
292
+ if (!parent || !narrow || atPointer || placement === "rail")
293
+ return;
294
+ const height = asideRef.current?.getBoundingClientRect().height ?? 0;
295
+ const room = `${Math.round(height) + 20}px`;
296
+ if (parent.style.paddingBottom === room)
297
+ return;
298
+ const before = parent.style.paddingBottom;
299
+ parent.style.paddingBottom = room;
300
+ return () => {
301
+ parent.style.paddingBottom = before;
302
+ };
303
+ });
304
+ /*
305
+ * NOTHING CHOSEN, NOTHING TO SAY — as a floating strip. In the rail the
306
+ * pane is a section of a panel that is about SOMETHING at all times: the
307
+ * subject is the place you are looking at when you have chosen nothing,
308
+ * and the acts that begin a kind are exactly what belongs under its name
309
+ * there. So the rail draws whatever the derivation offers for the
310
+ * subject, and stands down only when that is empty too.
311
+ */
312
+ if (selection.length === 0 && (placement !== "rail" || focus === undefined))
313
+ return null;
314
+ /*
315
+ * Whether the strip should say what is selected.
316
+ *
317
+ * It should not when the page you are looking at IS that thing: the document
318
+ * has a heading, and the same string twice on one screen reads as a mistake
319
+ * even when both are correct. The same holds after travelling — the focus
320
+ * panel already carries the name at full size, and the strip repeating it
321
+ * from the bottom of the window read as a stale leftover of the previous
322
+ * stop.
323
+ */
324
+ /*
325
+ * And in the companion the header above already names the subject, so a
326
+ * chip saying "0 selected" under it is the same mistake from the other
327
+ * direction: the rail is one panel about one thing, said once.
328
+ */
329
+ const named = !(selection.length === 1 && selection[0] === view.focusId) && placement !== "rail";
330
+ /*
331
+ * Nine rows before "Show N more", and the ranking has already put what
332
+ * answers the current question first — which is what makes hiding the
333
+ * tail honest. Everything past nine scrolls inside the pane's own box
334
+ * once shown.
335
+ */
336
+ const fits = Math.max(1, Math.min(9, affordances.length));
337
+ /*
338
+ * ORDERED FOR READING, not only for ranking: what a broken rule demands,
339
+ * then the acts somebody PINNED (the person's own before the app's — a
340
+ * small fixed head-section), then what you can do to the THING, then its
341
+ * TIES (the connects/severs acts, gathered under one heading instead of
342
+ * shuffled among the rest), then — always last — what cannot be taken
343
+ * back. Repairs first and destructive last are inviolate; pins and usage
344
+ * only ever shuffle inside those walls.
345
+ */
346
+ const arranged = [
347
+ ...affordances.filter((entry) => entry.provider === "invariant" && !entry.destructive),
348
+ ...affordances.filter((entry) => entry.pinned && entry.provider !== "invariant" && !entry.destructive),
349
+ ...affordances.filter((entry) => !entry.ties && !entry.destructive && !entry.pinned && entry.provider !== "invariant"),
350
+ ...affordances.filter((entry) => entry.ties && !entry.destructive && !entry.pinned && entry.provider !== "invariant"),
351
+ ...affordances.filter((entry) => entry.destructive),
352
+ ];
353
+ /*
354
+ * THE SEARCHER filters the same derived list — by label and by why, the
355
+ * two sentences a person actually reads — and appears only when the list
356
+ * outgrows the fold. Enter runs a sole survivor; Escape clears.
357
+ */
358
+ const searchable = arranged.length > fits && !atPointer;
359
+ const trimmedQuery = query.trim().toLowerCase();
360
+ const matched = searchable && trimmedQuery.length > 0
361
+ ? arranged.filter((entry) => `${entry.label} ${entry.why}`.toLowerCase().includes(trimmedQuery))
362
+ : null;
363
+ const shown = matched ?? (expanded || atPointer ? arranged : arranged.slice(0, fits));
364
+ const hidden = matched ? 0 : arranged.length - shown.length;
365
+ /*
366
+ * GROUPED BY WHAT THEY ANSWER. A repair arrives carrying the violation
367
+ * that produced it, and without that sentence over it, "Cut X from
368
+ * Thursday" offered on a selection of Y reads as a non sequitur. The
369
+ * ranked order is untouched — consecutive repairs of one violation
370
+ * gather under its sentence; everything else runs on below.
371
+ */
372
+ const sections = [];
373
+ for (const affordance of shown) {
374
+ const violation = !atPointer && affordance.provider === "invariant";
375
+ const held = !atPointer && !violation && affordance.pinned !== undefined && !affordance.destructive;
376
+ const tie = !atPointer && !violation && !held && affordance.ties === true && !affordance.destructive;
377
+ const heading = violation ? affordance.why : held ? "pinned" : tie ? "its ties" : null;
378
+ const tone = violation ? "violation" : held || tie ? "ties" : null;
379
+ const last = sections[sections.length - 1];
380
+ if (last && last.heading === heading)
381
+ last.items.push(affordance);
382
+ else
383
+ sections.push({ heading, tone, items: [affordance] });
384
+ }
385
+ const open = affordances.find((affordance) => affordance.id === pending);
386
+ /*
387
+ * An observation that OPENS by restating the name is trimmed, not dropped.
388
+ *
389
+ * The strip read "Pay the deposit · task · \"Pay the deposit\" was due
390
+ * 2026-08-28" — the same four words twice in one line, three inches apart,
391
+ * which reads as a rendering fault rather than as two facts. The fact in it
392
+ * is the date, and that is worth keeping; only the restatement goes. A name
393
+ * appearing mid-sentence is left alone, because there it is doing work.
394
+ */
395
+ const title = selection.length === 1 ? nameOf(store, selection[0]) : null;
396
+ const trim = (text) => {
397
+ if (!title)
398
+ return text;
399
+ // Quoted or bare: a sentence that OPENS by restating the selected
400
+ // name says it again three inches under the title.
401
+ const restated = text.startsWith(`"${title}"`)
402
+ ? text.slice(title.length + 2)
403
+ : text.startsWith(title)
404
+ ? text.slice(title.length)
405
+ : null;
406
+ if (restated === null)
407
+ return text;
408
+ const rest = restated.trimStart();
409
+ return rest.length === 0 ? text : rest[0].toUpperCase() + rest.slice(1);
410
+ };
411
+ /*
412
+ * A violation is stated ONCE. The invariant provider hands its message
413
+ * out twice — as an observation, and as the `why` on every repair — so
414
+ * whenever the repairs' own ⚠ heading is on screen, the observation
415
+ * restating it is dropped. Where the heading is not shown (the pointer
416
+ * menu, a violation whose repairs fell past the fold), the observation
417
+ * remains the one statement of the trouble.
418
+ */
419
+ const headed = new Set(atPointer
420
+ ? []
421
+ : shown
422
+ .filter((affordance) => affordance.provider === "invariant")
423
+ .map((affordance) => affordance.why));
424
+ const said = observations
425
+ .filter((observation) => !headed.has(observation.text))
426
+ .map((observation) => ({ ...observation, text: trim(observation.text) }));
427
+ if (standDown)
428
+ return null;
429
+ return (_jsxs("aside", { ref: asideRef, "aria-label": "Inspector", "data-graview-offstage": "", "data-testid": atPointer ? "context-menu" : "inspector-strip",
430
+ // Focusable only programmatically: the fallback home when the act
431
+ // somebody was standing on is no longer offered.
432
+ tabIndex: -1, onFocus: (event) => remember(event.target), onBlur: (event) => {
433
+ // Somewhere else to go means they went there on purpose.
434
+ if (event.relatedTarget !== null)
435
+ keptFocus.current = null;
436
+ }, onMouseDown: (event) => event.stopPropagation(), style: railed ? {
437
+ // In the rail the companion owns the frame: this is a section of it.
438
+ position: "static",
439
+ boxSizing: "border-box",
440
+ display: "flex",
441
+ flexDirection: "column",
442
+ gap: 7,
443
+ } : {
444
+ position: "absolute",
445
+ /*
446
+ * Above the jacked-in page, not only above the scene.
447
+ *
448
+ * Lifting a view out to read it left you with no way to act on it —
449
+ * the strip was behind the full page, so a jacked-in node was
450
+ * read-only by accident. The actions are the same derived ones; only
451
+ * the backdrop changed.
452
+ */
453
+ zIndex: 60,
454
+ boxSizing: "border-box",
455
+ display: "flex",
456
+ flexDirection: "column",
457
+ gap: 7,
458
+ padding: "9px 12px",
459
+ borderRadius: "var(--graview-radius, 12px)",
460
+ border: "1px solid var(--graview-edge)",
461
+ background: "var(--graview-float)",
462
+ // The high lift on both faces: the strip floats in FRONT of the
463
+ // scene now rather than beside it in reserved room, and the shadow
464
+ // is what makes covering read as "nearer" instead of as a collision.
465
+ boxShadow: "var(--graview-lift-high)",
466
+ ...(atPointer
467
+ ? {
468
+ // Clamped so a right click near an edge does not open a menu
469
+ // half off the screen.
470
+ left: Math.min(menuAt.x - (box?.left ?? 0), Math.max(8, (box?.width ?? window.innerWidth) - 320)),
471
+ top: Math.min(menuAt.y - (box?.top ?? 0), Math.max(8, (box?.height ?? window.innerHeight) - 260)),
472
+ width: 300,
473
+ maxHeight: "min(52cqh, 420px)",
474
+ overflow: "auto",
475
+ }
476
+ : narrow
477
+ ? {
478
+ /*
479
+ * A RAIL NEEDS A GUTTER. 14 + 236 + 14 of rail beside a
480
+ * picture that still wants three hundred pixels of its own
481
+ * is six hundred and twenty before anything is drawn — and
482
+ * a Graview in a column of an article, or at 390, has
483
+ * neither. The rail went straight over the focus: measured
484
+ * in a 350-wide embed, the pane covered 85% of the card it
485
+ * was about, and a click meant for the picture landed on
486
+ * "Close it".
487
+ *
488
+ * Along the bottom instead, full width, which is where a
489
+ * narrow interface has always put this. It still floats —
490
+ * the shadow says "nearer" — and it still scrolls inside
491
+ * itself.
492
+ */
493
+ left: 10,
494
+ right: 10,
495
+ width: "auto",
496
+ // Along the bottom, below the picture rather than over it:
497
+ // the scene's box has given up this much room above.
498
+ bottom: 10,
499
+ maxHeight: "min(42cqh, 300px)",
500
+ overflow: "auto",
501
+ }
502
+ : {
503
+ /*
504
+ * The left rail. Under the bar; in the Graview the relation
505
+ * key holds the top of the rail, so the pane starts below it.
506
+ * Everything past the viewport scrolls inside the pane.
507
+ */
508
+ left: 14,
509
+ // Under the relation key in the Graview; under the bar's edge
510
+ // otherwise — measured from the scene's own top, and no lower
511
+ // than a short box can afford.
512
+ top: view.overview ? "min(296px, 38cqh)" : 44,
513
+ // Inside the gutter beside a 1040-wide centred focus at the
514
+ // surveyed width, so the pane sits NEXT to the picture rather
515
+ // than on its title.
516
+ width: 236,
517
+ /*
518
+ * The pane stops ABOVE the raised band (plane 1 begins at 68%
519
+ * of the stage) and scrolls inside itself: a tall list of
520
+ * repairs must not buy its height with the first raised card.
521
+ */
522
+ maxHeight: view.overview ? "calc(100cqh - min(296px, 38cqh) - 20px)" : "calc(68cqh - 94px)",
523
+ overflow: "auto",
524
+ }),
525
+ }, children: [_jsxs("div", { style: {
526
+ display: named || !atPointer ? "flex" : "none",
527
+ alignItems: "baseline",
528
+ flexWrap: "wrap",
529
+ gap: "2px 8px",
530
+ minWidth: 0,
531
+ // Room for the dismiss control pinned to the pane's corner.
532
+ paddingRight: atPointer || railed ? 0 : 24,
533
+ }, children: [!named ? null : (_jsxs(_Fragment, { children: [_jsx("strong", { style: { fontSize: "0.9375rem", whiteSpace: "nowrap" }, children: selection.length === 1
534
+ ? nameOf(store, selection[0])
535
+ : `${selection.length} selected` }), kinds.length > 0 || edge ? (_jsx("span", { style: { fontSize: "0.75rem", color: "var(--graview-ink-faint)", whiteSpace: "nowrap" }, children: edge ? "relation" : kinds.join(" · ") })) : null, selection.length === 1 && !edge ? (_jsx("span", { style: { fontSize: "0.75rem", color: "var(--graview-ink-faint)", whiteSpace: "nowrap" }, children: view.expanded.includes(selection[0]) ? "· double-click closes" : "· double-click opens" })) : null] })), _jsx("span", { style: { flex: "1 1 auto" } })] }), atPointer || railed ? null : (_jsx("button", { type: "button", onClick: () => {
536
+ setMenuAt(null);
537
+ clear();
538
+ }, "aria-label": "Clear selection", title: "Clear selection",
539
+ // Pinned to the pane's corner: a real 24px target that a narrow
540
+ // header cannot push off the edge.
541
+ style: {
542
+ position: "absolute",
543
+ top: 6,
544
+ right: 6,
545
+ flex: "0 0 auto",
546
+ width: 24,
547
+ height: 24,
548
+ display: "grid",
549
+ placeItems: "center",
550
+ padding: 0,
551
+ fontSize: "0.875rem",
552
+ lineHeight: 1,
553
+ borderRadius: 7,
554
+ }, children: "\u00D7" })), edge && edgeEnds ? (_jsxs("div", { style: { display: "grid", gap: 6 }, children: [edgeSaid ? (_jsx("p", { "data-testid": "edge-said", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: edgeSaid })) : null, _jsx("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }, children: [
555
+ ["from", edgeEnds.from],
556
+ ["to", edgeEnds.to],
557
+ ].map(([which, end], index) => (_jsxs(Fragment, { children: [index === 1 ? (_jsx("span", { "aria-hidden": "true", style: { color: "var(--graview-ink-faint)" }, children: "\u2192" })) : null, end ? (_jsx("button", { type: "button", "data-testid": `edge-${which}`, onClick: () => set([end.id]), title: `Select ${nameOf(store, end.id)}`, style: { fontSize: "0.8125rem", padding: "3px 9px", borderRadius: 999 }, children: nameOf(store, end.id) })) : (_jsx("span", { style: { fontSize: "0.8125rem", color: "var(--graview-ink-faint)" }, children: "gone" }))] }, which))) })] })) : null, said.length > 0 ? (_jsx("div", { "data-testid": "observations", title: said.map((observation) => observation.text).join("\n"), style: { display: "grid", gap: 3 }, children: said.slice(0, 2).map((observation) => (_jsxs("p", { style: {
558
+ margin: 0,
559
+ fontSize: "0.8125rem",
560
+ lineHeight: 1.45,
561
+ color: "var(--graview-ink-muted)",
562
+ }, children: [observation.text, observation === said[1] && said.length > 2 ? (_jsxs("span", { style: { color: "var(--graview-ink-faint)" }, children: [" +", said.length - 2] })) : null] }, observation.id))) })) : null, failed ? (_jsx("p", { "data-testid": "refused", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-warn)" }, children: failed })) : null, searchable ? (_jsx("input", { "data-testid": "action-filter", value: query, onChange: (event) => setQuery(event.target.value), placeholder: `Filter ${arranged.length} actions…`, "aria-label": "Filter actions", onKeyDown: (event) => {
563
+ if (event.key === "Escape") {
564
+ // Clearing the filter must not also clear the selection —
565
+ // the document-level Escape stays out of it either way.
566
+ event.stopPropagation();
567
+ if (query.length > 0)
568
+ setQuery("");
569
+ // An empty field releases the key: blurred, the NEXT Escape
570
+ // reaches the product-wide back-out instead of dying here.
571
+ else
572
+ event.currentTarget.blur();
573
+ return;
574
+ }
575
+ if (event.key === "Enter" && matched?.length === 1) {
576
+ const sole = matched[0];
577
+ // What cannot be taken back is never one generic keystroke:
578
+ // the destructive tail keeps requiring the aimed click.
579
+ if (sole.destructive)
580
+ return;
581
+ if (sole.open.length > 0)
582
+ setPending(sole.id);
583
+ else if (act(sole))
584
+ setQuery("");
585
+ }
586
+ }, style: {
587
+ font: "inherit",
588
+ fontSize: "0.875rem",
589
+ padding: "5px 9px",
590
+ borderRadius: 8,
591
+ border: "1px solid var(--graview-edge)",
592
+ background: "var(--graview-panel)",
593
+ color: "var(--graview-ink)",
594
+ } })) : null, matched !== null && matched.length === 0 ? (_jsxs("p", { "data-testid": "no-matches", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: ["Nothing offered here matches \u201C", query.trim(), "\u201D."] })) : null, affordances.length === 0 && !waiting ? (
595
+ /*
596
+ * An empty action list is a RESULT, not a blank space. Saying which
597
+ * kind has no mutations declaring it as a subject is the same honesty
598
+ * the check CLI gives an agent, pointed at a person.
599
+ *
600
+ * Except when the derivation has already said something truer: an
601
+ * act that exists and cannot act YET — "Place a feature" cannot
602
+ * begin until there is a zone — is not "no mutation declares it as a
603
+ * subject", and saying both would be saying one wrong thing loudly.
604
+ */
605
+ _jsx("p", { "data-testid": "no-affordances", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: withheld.length > 0
606
+ ? // "Nothing can be done" would be a lie here: things can be
607
+ // done, by somebody else. Which is a different sentence.
608
+ `Nothing you may do with ${edge
609
+ ? "this relation"
610
+ : subjectKinds.length === 1
611
+ ? withArticle(nounOf(store.schema.tryDefinition(subjectKinds[0]), subjectKinds[0]))
612
+ : "this mix of kinds"} — ${withheld.length} action${withheld.length === 1 ? "" : "s"} withheld.`
613
+ : edge
614
+ ? `Nothing can be done with this line yet — nothing this app declares makes or breaks “${store.schema.edge(edge.kind)?.description ?? humaniseField(edge.kind).toLowerCase()}”.`
615
+ : `Nothing can be done with ${subjectKinds.length === 1 ? withArticle(nounOf(store.schema.tryDefinition(subjectKinds[0]), subjectKinds[0])) : "this mix of kinds"} yet — nothing this app declares acts on ${subjectKinds.length === 1 ? "it" : "them"}.` })) : (_jsxs("ol", { "data-testid": "affordances", style: {
616
+ margin: 0,
617
+ padding: 0,
618
+ listStyle: "none",
619
+ display: "flex",
620
+ // A pane and a menu both read as a column. Same list, same
621
+ // components, same order.
622
+ flexDirection: "column",
623
+ flexWrap: "nowrap",
624
+ gap: 2,
625
+ }, children: [sections.map((section, index) => (_jsxs(Fragment, { children: [section.heading ? (
626
+ // A real list item, not a presentational one: a list whose
627
+ // direct child has no list-item role is invalid to assistive
628
+ // technology (axe's "list" rule), and the heading is content —
629
+ // the finding these repairs answer is exactly what a screen
630
+ // reader should hear before them.
631
+ _jsxs("li", { "data-graview-heading": section.tone, style: {
632
+ fontSize: "0.75rem",
633
+ lineHeight: 1.4,
634
+ color: section.tone === "violation"
635
+ ? "var(--graview-warn)"
636
+ : "var(--graview-ink-faint)",
637
+ ...(section.tone !== "violation"
638
+ ? { letterSpacing: "0.12em", textTransform: "uppercase", fontSize: "0.75rem" }
639
+ : {}),
640
+ padding: "2px 2px 1px",
641
+ marginTop: index > 0 ? 6 : 0,
642
+ }, children: [section.tone === "violation" ? "⚠ " : "", section.tone === "violation" ? trim(section.heading) : section.heading] })) : index > 0 ? (_jsx("li", { "aria-hidden": "true", style: { borderTop: "1px solid var(--graview-edge)", margin: "6px 0 3px" } })) : null, section.items.map((affordance) => (_jsxs("li", { style: { display: "flex", gap: 2, alignItems: "stretch" }, children: [_jsxs("button", { type: "button", "data-affordance": affordance.id, "data-rank": affordance.rank, "data-graview-destructive": affordance.destructive || undefined, "aria-pressed": pending === affordance.id, title: affordance.why, style: {
643
+ padding: "5px 10px",
644
+ fontSize: "0.875rem",
645
+ borderRadius: 8,
646
+ width: "100%",
647
+ textAlign: "left",
648
+ /*
649
+ * In the pane an action looks PRESSABLE — border and
650
+ * ground, like every other button in the product. Rows
651
+ * of bare text read as a list of remarks, and nobody
652
+ * presses a remark. The pointer menu keeps menu rows;
653
+ * a menu's own frame already says "choose one".
654
+ */
655
+ // Longhands, always present: a `border` shorthand with a
656
+ // `borderColor` that comes and goes as the act is
657
+ // opened and applied is a React warning on every
658
+ // rerender, and the colour is the only part that moves.
659
+ borderWidth: 1,
660
+ borderStyle: "solid",
661
+ borderColor: pending === affordance.id
662
+ ? "var(--graview-accent)"
663
+ : atPointer
664
+ ? "transparent"
665
+ : "var(--graview-edge)",
666
+ background: atPointer ? "none" : "var(--graview-panel)",
667
+ boxShadow: "none",
668
+ // What cannot be taken back says so before it is
669
+ // pressed — and the ranking has already put it last.
670
+ ...(affordance.destructive ? { color: "var(--graview-warn)" } : {}),
671
+ ...(pending === affordance.id ? { color: "var(--graview-accent)" } : {}),
672
+ }, onClick: () => {
673
+ if (affordance.open.length > 0) {
674
+ setPending(pending === affordance.id ? null : affordance.id);
675
+ return;
676
+ }
677
+ act(affordance);
678
+ }, children: [affordance.label, affordance.open.length > 0 ? (_jsx("span", { style: { color: "var(--graview-ink-faint)" }, children: " \u2026" })) : null, matched?.length === 1 &&
679
+ matched[0]?.id === affordance.id &&
680
+ !affordance.destructive ? (
681
+ // The searcher's promise, made visible exactly when
682
+ // it holds: Enter runs the one act left standing.
683
+ _jsx("span", { "aria-hidden": "true", title: "Enter runs it", style: { float: "right", color: "var(--graview-ink-faint)", fontSize: "0.75rem" }, children: "\u21B5" })) : null] }), _jsx("button", { type: "button", "data-testid": "pin-toggle", "data-pin-for": affordance.mutation, "aria-pressed": affordance.pinned !== undefined, "aria-label": affordance.pinned !== undefined
684
+ ? `Unpin ${affordance.label}`
685
+ : `Pin ${affordance.label}`, title: affordance.pinned === "declared"
686
+ ? "Pinned by the app — unpin it for yourself"
687
+ : affordance.pinned === "user"
688
+ ? "Unpin"
689
+ : "Pin to the top", onClick: () => setPins(togglePin(pins, affordance.mutation, declaredPins.has(affordance.mutation))), style: {
690
+ flex: "0 0 auto",
691
+ width: 24,
692
+ minHeight: 24,
693
+ display: "grid",
694
+ placeItems: "center",
695
+ padding: 0,
696
+ fontSize: "0.75rem",
697
+ border: "1px solid transparent",
698
+ background: "none",
699
+ boxShadow: "none",
700
+ borderRadius: 7,
701
+ /*
702
+ * WHOSE pin, said in the ink: the person's in the
703
+ * accent, the app's in quiet body ink. Two filled
704
+ * stars in one colour left no way to tell which pin
705
+ * was yours to regret.
706
+ */
707
+ color: affordance.pinned === "user"
708
+ ? "var(--graview-accent)"
709
+ : affordance.pinned === "declared"
710
+ ? "var(--graview-ink-muted)"
711
+ : "var(--graview-ink-faint)",
712
+ }, children: affordance.pinned !== undefined ? "★" : "☆" })] }, affordance.id)))] }, `${section.heading ?? "plain"}-${index}`))), hidden > 0 ? (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => setExpanded(true),
713
+ // What it DOES, not what is behind it. "+9 more" is a label
714
+ // on a quantity; this is a control, and a control says what
715
+ // pressing it will do.
716
+ title: `Show all ${affordances.length} actions`, style: { padding: "4px 10px", fontSize: "0.875rem", borderRadius: 8 }, children: ["Show ", hidden, " more"] }) })) : null] })), withheld.length > 0 ? (_jsxs("ul", { "data-testid": "withheld", style: {
717
+ margin: 0,
718
+ padding: 0,
719
+ listStyle: "none",
720
+ display: "flex",
721
+ flexDirection: "column",
722
+ flexWrap: "nowrap",
723
+ gap: 2,
724
+ }, children: [withheld.slice(0, atPointer || allWithheld ? withheld.length : 3).map((action) => (_jsxs("li", { style: { display: "grid", gap: 2 }, children: [_jsx("button", { type: "button", disabled: true, "data-affordance": action.id, "data-rank": action.rank, "data-withheld": action.refusal.wouldNeed.join(",") || "nobody", style: {
725
+ padding: "4px 10px",
726
+ fontSize: "0.875rem",
727
+ borderRadius: 8,
728
+ borderStyle: "dashed",
729
+ width: "100%",
730
+ textAlign: "left",
731
+ background: "none",
732
+ }, children: _jsx("s", { children: action.label }) }), _jsx("span", { "data-testid": "withheld-why", style: {
733
+ fontSize: "0.8125rem",
734
+ lineHeight: 1.4,
735
+ color: "var(--graview-ink-muted)",
736
+ padding: "0 10px",
737
+ }, children: action.refusal.message })] }, action.id))), !atPointer && !allWithheld && withheld.length > 3 ? (_jsx("li", { children: _jsxs("button", { type: "button", "data-testid": "withheld-more", onClick: () => setAllWithheld(true), title: `Show all ${withheld.length} actions this seat may not take`, style: { padding: "4px 10px", fontSize: "0.875rem", borderRadius: 8 }, children: ["Show ", withheld.length - 3, " more withheld"] }) })) : null] })) : null, open ? (_jsx(AnswerArgs, { affordance: open,
738
+ // On a refusal the prompt stays put with the reason beside it, so
739
+ // a rejected answer can be corrected rather than retyped blind.
740
+ onApply: (args) => {
741
+ if (act(open, args))
742
+ setPending(null);
743
+ }, onCancel: () => setPending(null) })) : null] }));
744
+ }
745
+ //# sourceMappingURL=inspector.js.map