@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,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
|