@graview/primitives 0.1.64 → 0.1.66
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/README.md +1 -1
- package/dist/app-bar.d.ts +88 -15
- package/dist/app-bar.d.ts.map +1 -1
- package/dist/app-bar.js +177 -131
- package/dist/app-bar.js.map +1 -1
- package/dist/app-menu.d.ts +9 -0
- package/dist/app-menu.d.ts.map +1 -0
- package/dist/app-menu.js +60 -0
- package/dist/app-menu.js.map +1 -0
- package/dist/bar-activity.d.ts +5 -9
- package/dist/bar-activity.d.ts.map +1 -1
- package/dist/bar-activity.js +24 -7
- package/dist/bar-activity.js.map +1 -1
- package/dist/bar-panes.d.ts +5 -5
- package/dist/bar-panes.d.ts.map +1 -1
- package/dist/bar-panes.js +9 -5
- package/dist/bar-panes.js.map +1 -1
- package/dist/bar-suggested.d.ts +9 -1
- package/dist/bar-suggested.d.ts.map +1 -1
- package/dist/bar-suggested.js +11 -17
- package/dist/bar-suggested.js.map +1 -1
- package/dist/better-bar.d.ts +65 -0
- package/dist/better-bar.d.ts.map +1 -0
- package/dist/better-bar.js +252 -0
- package/dist/better-bar.js.map +1 -0
- package/dist/better-panel.d.ts +50 -0
- package/dist/better-panel.d.ts.map +1 -0
- package/dist/better-panel.js +151 -0
- package/dist/better-panel.js.map +1 -0
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/editable.d.ts +8 -1
- package/dist/editable.d.ts.map +1 -1
- package/dist/editable.js +140 -16
- package/dist/editable.js.map +1 -1
- package/dist/find.js +3 -3
- package/dist/find.js.map +1 -1
- package/dist/first-look-door.d.ts +2 -2
- package/dist/first-look-door.js +2 -2
- package/dist/first-look.d.ts.map +1 -1
- package/dist/first-look.js +10 -8
- package/dist/first-look.js.map +1 -1
- package/dist/frame.d.ts +2 -2
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +1 -1
- package/dist/frame.js.map +1 -1
- package/dist/here.d.ts +1 -1
- package/dist/here.js +2 -2
- package/dist/here.js.map +1 -1
- package/dist/inbox-pane.d.ts +31 -0
- package/dist/inbox-pane.d.ts.map +1 -0
- package/dist/inbox-pane.js +89 -0
- package/dist/inbox-pane.js.map +1 -0
- package/dist/inbox.d.ts +17 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +84 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/inline-ai-menu.d.ts +4 -2
- package/dist/inline-ai-menu.d.ts.map +1 -1
- package/dist/inline-ai-menu.js +34 -8
- package/dist/inline-ai-menu.js.map +1 -1
- package/dist/kind-stop.d.ts.map +1 -1
- package/dist/kind-stop.js +204 -21
- package/dist/kind-stop.js.map +1 -1
- package/dist/lines-key.d.ts.map +1 -1
- package/dist/lines-key.js +22 -2
- package/dist/lines-key.js.map +1 -1
- package/dist/map-panel.js +98 -6
- package/dist/map-panel.js.map +1 -1
- package/dist/menu-rank.d.ts +25 -0
- package/dist/menu-rank.d.ts.map +1 -0
- package/dist/menu-rank.js +56 -0
- package/dist/menu-rank.js.map +1 -0
- package/dist/menu-suggested.d.ts +54 -0
- package/dist/menu-suggested.d.ts.map +1 -0
- package/dist/menu-suggested.js +140 -0
- package/dist/menu-suggested.js.map +1 -0
- package/dist/menu-told-door.d.ts +17 -0
- package/dist/menu-told-door.d.ts.map +1 -0
- package/dist/menu-told-door.js +49 -0
- package/dist/menu-told-door.js.map +1 -0
- package/dist/menu-told.d.ts +44 -0
- package/dist/menu-told.d.ts.map +1 -0
- package/dist/menu-told.js +90 -0
- package/dist/menu-told.js.map +1 -0
- package/dist/menu-words.d.ts +15 -0
- package/dist/menu-words.d.ts.map +1 -0
- package/dist/menu-words.js +28 -0
- package/dist/menu-words.js.map +1 -0
- package/dist/notify-sheet.js +2 -2
- package/dist/notify-sheet.js.map +1 -1
- package/dist/page-better.d.ts +13 -0
- package/dist/page-better.d.ts.map +1 -0
- package/dist/page-better.js +714 -0
- package/dist/page-better.js.map +1 -0
- package/dist/page-parts.d.ts +41 -0
- package/dist/page-parts.d.ts.map +1 -0
- package/dist/page-parts.js +100 -0
- package/dist/page-parts.js.map +1 -0
- package/dist/pages.d.ts +4 -3
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +4 -3
- package/dist/pages.js.map +1 -1
- package/dist/profile.d.ts +1 -10
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +2 -3
- package/dist/profile.js.map +1 -1
- package/dist/quick-ask-desk.d.ts +16 -0
- package/dist/quick-ask-desk.d.ts.map +1 -1
- package/dist/quick-ask-desk.js +20 -0
- package/dist/quick-ask-desk.js.map +1 -1
- package/dist/quick-ask-door.d.ts.map +1 -1
- package/dist/quick-ask-door.js +5 -0
- package/dist/quick-ask-door.js.map +1 -1
- package/dist/quick-ask-plan.d.ts +52 -0
- package/dist/quick-ask-plan.d.ts.map +1 -0
- package/dist/quick-ask-plan.js +112 -0
- package/dist/quick-ask-plan.js.map +1 -0
- package/dist/quick-ask.d.ts.map +1 -1
- package/dist/quick-ask.js +341 -19
- package/dist/quick-ask.js.map +1 -1
- package/dist/scene-bar.d.ts +1 -1
- package/dist/scene-bar.d.ts.map +1 -1
- package/dist/scene-bar.js +4 -7
- package/dist/scene-bar.js.map +1 -1
- package/dist/scene-css.d.ts.map +1 -1
- package/dist/scene-css.js +16 -6
- package/dist/scene-css.js.map +1 -1
- package/dist/scene-places.d.ts +3 -2
- package/dist/scene-places.d.ts.map +1 -1
- package/dist/scene-places.js +47 -8
- package/dist/scene-places.js.map +1 -1
- package/dist/scene.d.ts +2 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +2 -0
- package/dist/scene.js.map +1 -1
- package/dist/section-better.d.ts +76 -0
- package/dist/section-better.d.ts.map +1 -0
- package/dist/section-better.js +529 -0
- package/dist/section-better.js.map +1 -0
- package/dist/section-door.d.ts +39 -0
- package/dist/section-door.d.ts.map +1 -0
- package/dist/section-door.js +66 -0
- package/dist/section-door.js.map +1 -0
- package/dist/section-menu.d.ts +32 -0
- package/dist/section-menu.d.ts.map +1 -0
- package/dist/section-menu.js +229 -0
- package/dist/section-menu.js.map +1 -0
- package/dist/section-outline.d.ts +8 -0
- package/dist/section-outline.d.ts.map +1 -0
- package/dist/section-outline.js +48 -0
- package/dist/section-outline.js.map +1 -0
- package/dist/section-words.d.ts +32 -0
- package/dist/section-words.d.ts.map +1 -0
- package/dist/section-words.js +116 -0
- package/dist/section-words.js.map +1 -0
- package/dist/shell.d.ts +5 -1
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +5 -2
- package/dist/shell.js.map +1 -1
- package/dist/subject.js +2 -2
- package/dist/subject.js.map +1 -1
- package/dist/suggested.d.ts +21 -10
- package/dist/suggested.d.ts.map +1 -1
- package/dist/suggested.js +43 -64
- package/dist/suggested.js.map +1 -1
- package/dist/told-card.d.ts +66 -0
- package/dist/told-card.d.ts.map +1 -0
- package/dist/told-card.js +197 -0
- package/dist/told-card.js.map +1 -0
- package/dist/told-listed.d.ts +14 -0
- package/dist/told-listed.d.ts.map +1 -0
- package/dist/told-listed.js +46 -0
- package/dist/told-listed.js.map +1 -0
- package/dist/told-panel-door.d.ts +17 -0
- package/dist/told-panel-door.d.ts.map +1 -0
- package/dist/told-panel-door.js +6 -0
- package/dist/told-panel-door.js.map +1 -0
- package/dist/told-panel.d.ts +19 -0
- package/dist/told-panel.d.ts.map +1 -0
- package/dist/told-panel.js +83 -0
- package/dist/told-panel.js.map +1 -0
- package/dist/told-ui.d.ts +5 -0
- package/dist/told-ui.d.ts.map +1 -0
- package/dist/told-ui.js +12 -0
- package/dist/told-ui.js.map +1 -0
- package/dist/when-words.d.ts +21 -0
- package/dist/when-words.d.ts.map +1 -0
- package/dist/when-words.js +43 -0
- package/dist/when-words.js.map +1 -0
- package/dist/workbench/activity.d.ts +44 -0
- package/dist/workbench/activity.d.ts.map +1 -1
- package/dist/workbench/activity.js +74 -59
- package/dist/workbench/activity.js.map +1 -1
- package/dist/workbench/ai-entries.d.ts.map +1 -1
- package/dist/workbench/ai-entries.js +23 -2
- package/dist/workbench/ai-entries.js.map +1 -1
- package/dist/workbench/back-out.js +1 -1
- package/dist/workbench/back-out.js.map +1 -1
- package/dist/workbench/inspector.d.ts.map +1 -1
- package/dist/workbench/inspector.js +366 -27
- package/dist/workbench/inspector.js.map +1 -1
- package/dist/workbench/standing.d.ts.map +1 -1
- package/dist/workbench/standing.js +1 -1
- package/dist/workbench/standing.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { InvalidArguments, isLeftAlone, layer, nounOf, signInInstead, withArticle } from "@graview/core";
|
|
2
|
+
import { choiceWords, InvalidArguments, isLeftAlone, layer, nounOf, pluralLabel, signInInstead, withArticle } from "@graview/core";
|
|
3
3
|
import { edgeWords } from "@graview/core/words";
|
|
4
4
|
import { failureWords } from "@graview/core/words";
|
|
5
5
|
import { linkProvenance, provenanceWords } from "@graview/core/provenance";
|
|
@@ -10,6 +10,11 @@ import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState } from
|
|
|
10
10
|
import { AnswerArgs, nameOf } from "./answer-args.js";
|
|
11
11
|
import { VISUALLY_HIDDEN } from "../primitives/index.js";
|
|
12
12
|
import { ROVING_ITEM, useRoving } from "../roving.js";
|
|
13
|
+
import { menuMatches } from "../menu-words.js";
|
|
14
|
+
import { MENU_RANK_FLOOR, MENU_RANK_REST_MS, useFastRank } from "../menu-rank.js";
|
|
15
|
+
import { TOLD_SUGGESTED_MOST, useToldSuggestions } from "../menu-told-door.js";
|
|
16
|
+
import { openQuickAsk, preloadQuickAsk, useQuickAskDrawn, whereMenuStands } from "../quick-ask-desk.js";
|
|
17
|
+
import { filledActs, notARow, useSuggested } from "../menu-suggested.js";
|
|
13
18
|
import "../frame.js"; // A door: tied to what the page drew first (TIED DOORS, packages/primitives/src/frame.ts).
|
|
14
19
|
/**
|
|
15
20
|
* The pane itself (`Inspector` in `./inspector-door.tsx` draws it): what is
|
|
@@ -44,19 +49,29 @@ export function InspectorPane({ placement = "float", chosen = true } = {}) {
|
|
|
44
49
|
* "Acts…", which opens the same menu. Measured from the scene's box, so it
|
|
45
50
|
* stands in the box it is drawn in; under Up and the key, in the picture's
|
|
46
51
|
* column of controls, because at the picture's foot a notice stood over it.
|
|
52
|
+
* On a phone's ground Up and the Key share a row above the picture, and a
|
|
53
|
+
* card under it takes the width: "Acts…" stands in that row too, to the
|
|
54
|
+
* left of them, where it covers no card's "Open".
|
|
47
55
|
*/
|
|
48
56
|
function ChosenActs() {
|
|
49
57
|
const { store, setMenuAt, view } = useGraview();
|
|
50
58
|
const { selection } = useSelection();
|
|
51
59
|
const mark = useRef(null);
|
|
52
60
|
const [width, setWidth] = useState(null);
|
|
61
|
+
// How far from the right the row's leftmost control (the Key, else Up) stands: "Acts…" goes to its left.
|
|
62
|
+
const [rowRight, setRowRight] = useState(null);
|
|
53
63
|
useLayoutEffect(() => {
|
|
54
64
|
const parent = mark.current?.offsetParent ?? mark.current?.parentElement;
|
|
55
65
|
if (!parent)
|
|
56
66
|
return;
|
|
57
|
-
const
|
|
67
|
+
const box = parent.getBoundingClientRect();
|
|
68
|
+
const measured = Math.round(box.width);
|
|
58
69
|
if (measured !== width)
|
|
59
70
|
setWidth(measured);
|
|
71
|
+
const row = [...parent.querySelectorAll('[data-testid="lines-key"][data-graview-up-row], [data-testid="overview"]')].map((one) => one.getBoundingClientRect()).filter((one) => one.width > 0 && one.top - box.top < 40);
|
|
72
|
+
const right = row.length > 0 ? Math.round(box.right - Math.min(...row.map((one) => one.left)) + 8) : null;
|
|
73
|
+
if (right !== rowRight)
|
|
74
|
+
setRowRight(right);
|
|
60
75
|
});
|
|
61
76
|
const one = selection.length === 1 && !view.overview ? store.graph.getNode(selection[0]) : undefined;
|
|
62
77
|
if (!one || width === null || width >= RAIL_NEEDS)
|
|
@@ -64,12 +79,36 @@ function ChosenActs() {
|
|
|
64
79
|
return (_jsx("button", { type: "button", ref: mark, "data-testid": "chosen-acts", "aria-label": `What can be done with ${nameOf(store, one.id)}`, "aria-haspopup": "menu", onClick: (event) => {
|
|
65
80
|
const at = event.currentTarget.getBoundingClientRect();
|
|
66
81
|
setMenuAt({ x: Math.round(at.right), y: Math.round(at.bottom + 4), on: one.id });
|
|
67
|
-
}, style: { position: "absolute", right: "calc(14px + var(--graview-dock, 0px))", top: 98, zIndex: "var(--graview-layer-overview, 10)", minHeight: 30, padding: "0 12px", borderRadius: 8, fontSize: "0.8125rem", background: "var(--graview-float)", boxShadow: "var(--graview-lift-low)" }, children: "Acts\u2026" }));
|
|
82
|
+
}, style: { position: "absolute", ...(rowRight !== null ? { right: rowRight, top: 14, minHeight: 38 } : { right: "calc(14px + var(--graview-dock, 0px))", top: 98 }), zIndex: "var(--graview-layer-overview, 10)", ...(rowRight === null ? { minHeight: 30 } : {}), padding: "0 12px", borderRadius: 8, fontSize: "0.8125rem", background: "var(--graview-float)", boxShadow: "var(--graview-lift-low)" }, children: "Acts\u2026" }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* WHY NOTHING IS OFFERED, IN TRUE WORDS (Roadmap f-make-any-page-better).
|
|
86
|
+
* "Nothing this app declares acts on it" was said of the lists in the
|
|
87
|
+
* scene — whose app renames a list, notes why it is there and adds a task
|
|
88
|
+
* to it — because the menu was on all the lists at once, and every act is
|
|
89
|
+
* done to one. So: what is done to one is said as such ("Right-click one
|
|
90
|
+
* list to see what you can do with it"); only where nothing in the app is
|
|
91
|
+
* done to the kind at all is that said, and never in the declaration's
|
|
92
|
+
* words.
|
|
93
|
+
*/
|
|
94
|
+
function emptyWords(store, kinds, selection) {
|
|
95
|
+
const noun = (kind) => nounOf(store.schema.tryDefinition(kind), kind);
|
|
96
|
+
const plural = (kind) => pluralLabel(store.schema, kind).toLowerCase();
|
|
97
|
+
const doneTo = kinds.filter((kind) => store.allMutations().some((mutation) => {
|
|
98
|
+
const subject = mutation.subject;
|
|
99
|
+
return subject?.kinds === "*" || (subject?.kinds ?? []).includes(kind);
|
|
100
|
+
}));
|
|
101
|
+
const many = selection.length !== 1 || selection.some((id) => id.startsWith("aggregate:"));
|
|
102
|
+
if (doneTo.length > 0 && many)
|
|
103
|
+
return kinds.length === 1 ? `Nothing here is done to all the ${plural(kinds[0])} at once. Right-click one ${noun(kinds[0])} to see what you can do with it.` : "Nothing here is done to all of these at once. Right-click one of them to see what you can do with it.";
|
|
104
|
+
if (doneTo.length > 0)
|
|
105
|
+
return `There is nothing you can do with ${kinds.length === 1 ? withArticle(noun(kinds[0])) : "these"} here right now.`;
|
|
106
|
+
return kinds.length === 1 ? `There is nothing to do with ${withArticle(noun(kinds[0]))} in this app yet.` : "There is nothing to do with these in this app yet.";
|
|
68
107
|
}
|
|
69
108
|
/** Whether the picture has room for a rail beside it: the scene's own box, not the window's. */
|
|
70
109
|
const RAIL_NEEDS = 14 + 236 + 14 + 300;
|
|
71
110
|
function Pane({ placement }) {
|
|
72
|
-
const { store, menuAt, setMenuAt, view, signIn } = useGraview();
|
|
111
|
+
const { store, menuAt, setMenuAt, view, signIn, aiDesk, ai, principal } = useGraview();
|
|
73
112
|
/*
|
|
74
113
|
* The pane is positioned within the SCENE'S BOX, not the window. It was
|
|
75
114
|
* fixed to the viewport, which put it at the page's edge when the scene
|
|
@@ -222,7 +261,7 @@ function Pane({ placement }) {
|
|
|
222
261
|
*/
|
|
223
262
|
const target = menuAt === null
|
|
224
263
|
? null
|
|
225
|
-
: { face: menuAt.face ?? "scene", on: menuAt.on, about: selection, field: menuAt.field, place: menuAt.place, href: menuAt.href, act: menuAt.act, column: menuAt.column, element: menuAt.returnTo };
|
|
264
|
+
: { face: menuAt.face ?? "scene", on: menuAt.on, about: selection, field: menuAt.field, place: menuAt.place, href: menuAt.href, act: menuAt.act, column: menuAt.column, ...(menuAt.section ? { section: menuAt.section } : {}), ...(menuAt.page ? { page: menuAt.page } : {}), element: menuAt.returnTo };
|
|
226
265
|
const beside = useMenuSections(target);
|
|
227
266
|
/*
|
|
228
267
|
* Whether the derivation is withholding a creating act for want of the
|
|
@@ -249,7 +288,88 @@ function Pane({ placement }) {
|
|
|
249
288
|
* and the eleventh act of a menu was twenty-one Tabs from the key that
|
|
250
289
|
* opened it (walks XI and XII).
|
|
251
290
|
*/
|
|
252
|
-
|
|
291
|
+
/*
|
|
292
|
+
* In the menu a letter is typed into the menu's own field instead (Roadmap
|
|
293
|
+
* f-menu-type-anything), so the menu's list takes no letters: only the
|
|
294
|
+
* strip keeps the type-ahead.
|
|
295
|
+
*/
|
|
296
|
+
const roving = useRoving({ letters: menuAt === null });
|
|
297
|
+
/*
|
|
298
|
+
* WHAT IS TYPED IN THE MENU (Roadmap f-menu-type-anything): every row —
|
|
299
|
+
* the acts, the withheld, Open, Copy link, Show, Inspect — narrowed to
|
|
300
|
+
* those whose words begin with it (`menuMatches`), Enter running the one
|
|
301
|
+
* lit; and, where the host lent the app a model, the words asked of it
|
|
302
|
+
* about what the menu was opened on, as the last row. A new menu starts
|
|
303
|
+
* with nothing typed.
|
|
304
|
+
*/
|
|
305
|
+
const [typed, setTyped] = useState("");
|
|
306
|
+
const typeField = useRef(null);
|
|
307
|
+
useEffect(() => setTyped(""), [menuAt]);
|
|
308
|
+
/* Asked where the quick ask is drawn at all: a host's model, AI not off, the app's setting on. */
|
|
309
|
+
const asks = useQuickAskDrawn(aiDesk);
|
|
310
|
+
/* What worked before, like the words typed here (Roadmap f-menu-learns): kept tellings re-aimed at what the menu is on, ranked with the rows. */
|
|
311
|
+
const toldLike = useToldSuggestions(asks ? ai : undefined, store, principal, menuAt === null ? "" : typed, menuAt?.on, menuAt);
|
|
312
|
+
/* The Fast slot's ranking of every row the menu could show, for the words typed, once the typing rests (`useFastRank`). */
|
|
313
|
+
const rankRows = useMemo(() => menuAt === null
|
|
314
|
+
? []
|
|
315
|
+
: [
|
|
316
|
+
...beside.start.flatMap((section) => (section.items ?? []).map((item) => ({ key: `item:${item.id}`, label: item.label }))),
|
|
317
|
+
...affordances.map((affordance) => ({ key: `act:${affordance.id}`, label: affordance.label })),
|
|
318
|
+
...beside.end.flatMap((section) => (section.items ?? []).map((item) => ({ key: `item:${item.id}`, label: item.label }))),
|
|
319
|
+
...toldLike.map((one) => ({ key: one.key, label: one.label })),
|
|
320
|
+
], [menuAt, beside, affordances, toldLike]);
|
|
321
|
+
const fastRanked = useFastRank(ai, menuAt === null ? "" : typed, rankRows);
|
|
322
|
+
/*
|
|
323
|
+
* NOTHING MOVES UNDER A POINTER ON A ROW: while it rests on one, a ranking
|
|
324
|
+
* that arrives is held, and the rows stay as they were drawn; it applies
|
|
325
|
+
* once the pointer is off the rows. What the person types still narrows
|
|
326
|
+
* at once — that is theirs.
|
|
327
|
+
*/
|
|
328
|
+
const [onRow, setOnRow] = useState(false);
|
|
329
|
+
const heldRank = useRef({ words: "", rank: null });
|
|
330
|
+
if (!onRow)
|
|
331
|
+
heldRank.current = { words: typed.trim(), rank: fastRanked };
|
|
332
|
+
const fastRank = onRow ? (heldRank.current.words === typed.trim() ? heldRank.current.rank : null) : fastRanked;
|
|
333
|
+
/*
|
|
334
|
+
* TWO WAYS TO ASK, UNDER THE FIELD (Roadmap f-make-any-page-better): 💬
|
|
335
|
+
* Chat — the records, the app's structure, anything broader, the quick
|
|
336
|
+
* ask's card — and ✨ Better UI where the part under the cursor lends it
|
|
337
|
+
* (a section's `mode`). As words are typed the Fast slot says which they
|
|
338
|
+
* are and lights it; a click, or the arrows, choose the other; with
|
|
339
|
+
* nothing typed each opens its card.
|
|
340
|
+
*/
|
|
341
|
+
const better = useMemo(() => [...beside.start, ...beside.end].find((section) => section.mode)?.mode, [beside]);
|
|
342
|
+
const modes = menuAt === null ? [] : [...(asks ? ["chat"] : []), ...(better ? ["better"] : [])];
|
|
343
|
+
const [modeChosen, setModeChosen] = useState(null);
|
|
344
|
+
const [modeHeard, setModeHeard] = useState(null);
|
|
345
|
+
useEffect(() => setModeChosen(null), [menuAt, typed]);
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const asked = typed.trim();
|
|
348
|
+
if (!better || !asks || asked === "")
|
|
349
|
+
return;
|
|
350
|
+
let live = true;
|
|
351
|
+
const timer = setTimeout(() => void better.claims(asked).then((look) => live && setModeHeard({ words: asked, mode: look ? "better" : "chat" }), () => undefined), MENU_RANK_REST_MS);
|
|
352
|
+
return () => {
|
|
353
|
+
live = false;
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
};
|
|
356
|
+
}, [typed, better, asks]);
|
|
357
|
+
const modeLit = modes.length === 0 ? null : (modeChosen ?? (modes.length === 1 ? modes[0] : modeHeard && modeHeard.words === typed.trim() ? modeHeard.mode : "chat"));
|
|
358
|
+
/* SUGGESTED: two or three things worth doing here, each tagged by its way, picked by the Fast slot once it answers. */
|
|
359
|
+
const suggestCandidates = useMemo(() => {
|
|
360
|
+
if (menuAt === null || modes.length === 0)
|
|
361
|
+
return [];
|
|
362
|
+
const nudges = (better?.nudges?.() ?? []).map((label, at) => ({ key: `nudge:${at}`, label, mode: "better", source: "nudge" }));
|
|
363
|
+
const told = toldLike.slice(0, 2).map((one) => ({ key: one.key, label: one.label, mode: "chat", source: "told" }));
|
|
364
|
+
// An act with its answer filled in, never the bare act a row below already offers.
|
|
365
|
+
const on = menuAt.on ?? (selection.length === 1 ? selection[0] : undefined);
|
|
366
|
+
const record = on !== undefined ? store.graph.getNode(on) : undefined;
|
|
367
|
+
const acts = filledActs(affordances, record, record ? choiceWords(store.schema, [record.kind])[record.kind] : undefined, record ? nameOf(store, record.id) : undefined).slice(0, 2);
|
|
368
|
+
return notARow(asks ? [...nudges, ...told, ...acts] : nudges, [...affordances, ...struck].map((one) => one.label));
|
|
369
|
+
}, [menuAt, modes.length, better, toldLike, affordances, struck, asks, store, selection]);
|
|
370
|
+
/* What the answer depends on: the record the menu is on (a new object whenever it changes), else the graph as it stands. */
|
|
371
|
+
const dependsOn = useMemo(() => (menuAt?.on !== undefined ? store.graph.getNode(menuAt.on) : undefined) ?? graphStamp(store), [menuAt, store, affordances]);
|
|
372
|
+
const suggested = useSuggested(ai, suggestCandidates, { name: menuAt?.title ?? (selection.length === 1 ? nameOf(store, selection[0]) : "this"), opened: menuAt, better: better !== undefined, depends: dependsOn }, onRow);
|
|
253
373
|
/*
|
|
254
374
|
* A REFUSAL IS A RESULT, said where the button was pressed.
|
|
255
375
|
*
|
|
@@ -442,7 +562,7 @@ function Pane({ placement }) {
|
|
|
442
562
|
* tail honest. Everything past nine scrolls inside the pane's own box
|
|
443
563
|
* once shown.
|
|
444
564
|
*/
|
|
445
|
-
const fits = Math.max(1, Math.min(9, affordances.length));
|
|
565
|
+
const fits = atPointer ? 4 : Math.max(1, Math.min(9, affordances.length));
|
|
446
566
|
/*
|
|
447
567
|
* ORDERED FOR READING, not only for ranking: what a broken rule demands,
|
|
448
568
|
* then the acts somebody PINNED (the person's own before the app's — a
|
|
@@ -469,8 +589,125 @@ function Pane({ placement }) {
|
|
|
469
589
|
const matched = searchable && trimmedQuery.length > 0
|
|
470
590
|
? arranged.filter((entry) => `${entry.label} ${entry.why}`.toLowerCase().includes(trimmedQuery))
|
|
471
591
|
: null;
|
|
472
|
-
|
|
473
|
-
const
|
|
592
|
+
/* Typed in the menu: its words, and whether a row's label begins with them. */
|
|
593
|
+
const words = atPointer ? typed.trim() : "";
|
|
594
|
+
/* A row fits what is typed when its words begin with it, or when the Fast slot finds it likely enough. */
|
|
595
|
+
const fitsTyped = (key, label) => words === "" || menuMatches(label, words) || (fastRank?.get(key) ?? 0) >= MENU_RANK_FLOOR;
|
|
596
|
+
const shown = (matched ?? (expanded || atPointer ? arranged : arranged.slice(0, fits))).filter((entry) => fitsTyped(`act:${entry.id}`, entry.label));
|
|
597
|
+
const hidden = matched || words ? 0 : arranged.length - shown.length;
|
|
598
|
+
const withheldShown = words ? withheld.filter((action) => fitsTyped(`act:${action.id}`, action.label)) : withheld;
|
|
599
|
+
/* A section's rows narrowed to what is typed; one drawn in place of rows (the AI's) stands down while words are typed, the Ask row below standing for it. */
|
|
600
|
+
const narrowed = (list) => words === ""
|
|
601
|
+
? list
|
|
602
|
+
: list.flatMap((section) => {
|
|
603
|
+
const items = (section.items ?? []).filter((item) => fitsTyped(`item:${item.id}`, item.label));
|
|
604
|
+
return items.length > 0 ? [{ id: section.id, ...(section.heading ? { heading: section.heading } : {}), items }] : [];
|
|
605
|
+
});
|
|
606
|
+
/* A way to ask draws no rows: its button is under the field. Past the first acts, the rest wait behind More… until it is pressed or words are typed. */
|
|
607
|
+
const folded = atPointer && !expanded && words === "";
|
|
608
|
+
const startSections = narrowed(beside.start).filter((section) => (section.items?.length ?? 0) > 0 || section.render);
|
|
609
|
+
const endSections = narrowed(beside.end).filter((section) => (section.items?.length ?? 0) > 0 || section.render);
|
|
610
|
+
/* Which acts wait behind More…: past the first few, while folded. Drawn, and hidden — no stop for the keyboard, nothing a screen reader is told — until More… is pressed. */
|
|
611
|
+
const behindMore = new Set(folded ? shown.slice(fits).map((entry) => entry.id) : []);
|
|
612
|
+
/* What this seat may not do waits behind More… only where the first acts fill the room; a seat offered little is told at once what is not theirs. */
|
|
613
|
+
const withheldFolded = folded && arranged.length >= fits;
|
|
614
|
+
const moreWaits = folded && (arranged.length > fits || (withheldFolded && withheld.length > 0) || observations.length > 0 || beside.end.some((section) => (section.items?.length ?? 0) > 0 || section.render));
|
|
615
|
+
/* What the words are asked about, said in the Ask row: the menu's title, the one record, else "this". */
|
|
616
|
+
const askedAbout = menuAt?.title ?? (selection.length === 1 ? nameOf(store, selection[0]) : selection.length > 1 ? `${selection.length} selected` : "this");
|
|
617
|
+
const askRow = atPointer && words !== "" && modes.length > 0;
|
|
618
|
+
/* Up to two that worked before, the Fast slot's likeliest first once it has answered. */
|
|
619
|
+
const toldShown = atPointer && words !== "" ? (fastRank === null ? toldLike : [...toldLike].sort((a, b) => (fastRank.get(b.key) ?? 0) - (fastRank.get(a.key) ?? 0))).slice(0, TOLD_SUGGESTED_MOST) : [];
|
|
620
|
+
/*
|
|
621
|
+
* THE ROW ENTER RUNS FROM THE FIELD, lit: the first that fits the words
|
|
622
|
+
* — never one that cannot be taken back, which keeps its aimed press —
|
|
623
|
+
* else the Ask row. Nothing is lit with nothing typed.
|
|
624
|
+
*/
|
|
625
|
+
/* The Fast slot's best among the rows shown, when it has answered for these words. */
|
|
626
|
+
const shownKeys = [
|
|
627
|
+
...startSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`)),
|
|
628
|
+
...shown.filter((entry) => !entry.destructive).map((entry) => `act:${entry.id}`),
|
|
629
|
+
...endSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`)),
|
|
630
|
+
...toldShown.map((one) => one.key),
|
|
631
|
+
];
|
|
632
|
+
const ranked = fastRank === null ? undefined : [...shownKeys].sort((a, b) => (fastRank.get(b) ?? 0) - (fastRank.get(a) ?? 0)).find((key) => (fastRank.get(key) ?? 0) >= MENU_RANK_FLOOR);
|
|
633
|
+
const lit = words === ""
|
|
634
|
+
? null
|
|
635
|
+
: (ranked ??
|
|
636
|
+
startSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`))[0] ??
|
|
637
|
+
shown.filter((entry) => !entry.destructive).map((entry) => `act:${entry.id}`)[0] ??
|
|
638
|
+
endSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`))[0] ??
|
|
639
|
+
(askRow && modeLit ? "mode" : null));
|
|
640
|
+
const ask = (asked = words) => {
|
|
641
|
+
if (!menuAt)
|
|
642
|
+
return;
|
|
643
|
+
const back = menuAt.returnTo ?? null;
|
|
644
|
+
// The card opens where the menu stood, read before the menu goes.
|
|
645
|
+
const from = whereMenuStands(asideRef.current);
|
|
646
|
+
// The quick ask's card, where the menu stood, answering the words at once: its thinking marked on the thing itself.
|
|
647
|
+
setMenuAt(null);
|
|
648
|
+
openQuickAsk(aiDesk, { pointed: { on: menuAt.on, about: selection, field: menuAt.field, place: menuAt.place }, ...(asked ? { asked } : {}), returnTo: back, ...(from ? { from } : {}) });
|
|
649
|
+
};
|
|
650
|
+
/** A way to ask, pressed: Chat asks the quick ask (the words, or nothing yet); Better UI opens its card (the words asked at once, or to choose). */
|
|
651
|
+
const runMode = (mode, said = words) => {
|
|
652
|
+
if (!menuAt)
|
|
653
|
+
return;
|
|
654
|
+
if (mode === "chat")
|
|
655
|
+
return ask(said);
|
|
656
|
+
setMenuAt(null);
|
|
657
|
+
better?.open(said || undefined);
|
|
658
|
+
};
|
|
659
|
+
/** A suggestion taken: through its way's card, previewed there before anything lands. */
|
|
660
|
+
const takeSuggested = (one) => {
|
|
661
|
+
const told = one.source === "told" ? toldLike.find((each) => each.key === one.key) : undefined;
|
|
662
|
+
if (told && one.mode === "chat")
|
|
663
|
+
return takeTold(told);
|
|
664
|
+
runMode(one.mode === "better" && better ? "better" : "chat", one.label);
|
|
665
|
+
};
|
|
666
|
+
/* A suggestion taken: its words asked on the card, about what the menu is on, previewed like any plan — never applied from the menu. */
|
|
667
|
+
const takeTold = (one) => {
|
|
668
|
+
if (!menuAt)
|
|
669
|
+
return;
|
|
670
|
+
const back = menuAt.returnTo ?? null;
|
|
671
|
+
const from = whereMenuStands(asideRef.current);
|
|
672
|
+
setMenuAt(null);
|
|
673
|
+
// Asked as the plain act the row says, where the menu stood: where it came from is the Studio's to say, never the card's.
|
|
674
|
+
openQuickAsk(aiDesk, { pointed: { on: menuAt.on, about: selection, field: menuAt.field, place: menuAt.place }, asked: one.label, suggested: one.telling.id, returnTo: back, ...(from ? { from } : {}) });
|
|
675
|
+
};
|
|
676
|
+
/* The menu's rows the keyboard walks, in the order they are drawn: not a disabled one, nor a field of a form an act opened. */
|
|
677
|
+
/* The ways to ask are walked with the left and right arrows; the rows, up and down from the field. */
|
|
678
|
+
const walkable = () => [...(asideRef.current?.querySelectorAll("[data-affordance]:not([disabled]), [data-menu-item]:not([data-menu-mode])") ?? [])].filter((row) => row.closest("[data-graview-asking], [hidden]") === null);
|
|
679
|
+
const litRow = () => asideRef.current?.querySelector("[data-menu-lit]") ?? null;
|
|
680
|
+
/*
|
|
681
|
+
* A LETTER TYPED ANYWHERE IN THE MENU GOES INTO ITS FIELD, the keyboard
|
|
682
|
+
* with it: the menu opens with the keyboard on its first row, so the
|
|
683
|
+
* arrows walk the rows as they always did, and on a phone no keyboard
|
|
684
|
+
* rises until the field is tapped. Space still presses the row it is on;
|
|
685
|
+
* a form an act opened in the menu types its own answers; the arrow up
|
|
686
|
+
* from the first row goes back to the field.
|
|
687
|
+
*/
|
|
688
|
+
const typeInto = (event) => {
|
|
689
|
+
const target = event.target;
|
|
690
|
+
const field = typeField.current;
|
|
691
|
+
if (!field || target === field || event.metaKey || event.ctrlKey || event.altKey)
|
|
692
|
+
return;
|
|
693
|
+
if (target.closest("[data-graview-asking]") || typingIn(target))
|
|
694
|
+
return;
|
|
695
|
+
const typing = event.key.length === 1 && event.key !== " ";
|
|
696
|
+
if (typing || (event.key === "Backspace" && typed !== "")) {
|
|
697
|
+
event.preventDefault();
|
|
698
|
+
event.stopPropagation();
|
|
699
|
+
if (typing && typed === "" && aiDesk)
|
|
700
|
+
preloadQuickAsk(aiDesk);
|
|
701
|
+
setTyped((was) => (typing ? was + event.key : was.slice(0, -1)));
|
|
702
|
+
field.focus({ preventScroll: true });
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (event.key === "ArrowUp" && walkable()[0] === target) {
|
|
706
|
+
event.preventDefault();
|
|
707
|
+
event.stopPropagation();
|
|
708
|
+
field.focus({ preventScroll: true });
|
|
709
|
+
}
|
|
710
|
+
};
|
|
474
711
|
/*
|
|
475
712
|
* GROUPED BY WHAT THEY ANSWER. A repair arrives carrying the violation
|
|
476
713
|
* that produced it, and without that sentence over it, "Cut X from
|
|
@@ -547,7 +784,13 @@ function Pane({ placement }) {
|
|
|
547
784
|
// Somewhere else to go means they went there on purpose.
|
|
548
785
|
if (event.relatedTarget !== null)
|
|
549
786
|
keptFocus.current = null;
|
|
550
|
-
}, onMouseDown: (event) => event.stopPropagation(),
|
|
787
|
+
}, onMouseDown: (event) => event.stopPropagation(), onKeyDownCapture: atPointer ? (event) => typeInto(event) : undefined, onPointerMove: atPointer
|
|
788
|
+
? (event) => {
|
|
789
|
+
const row = event.target.closest?.("[data-menu-item], [data-affordance]") != null;
|
|
790
|
+
if (row !== onRow)
|
|
791
|
+
setOnRow(row);
|
|
792
|
+
}
|
|
793
|
+
: undefined, onPointerLeave: atPointer && onRow ? () => setOnRow(false) : undefined, style: {
|
|
551
794
|
/*
|
|
552
795
|
* Above the jacked-in page, not only above the scene.
|
|
553
796
|
*
|
|
@@ -658,15 +901,71 @@ function Pane({ placement }) {
|
|
|
658
901
|
fontSize: "0.875rem",
|
|
659
902
|
lineHeight: 1,
|
|
660
903
|
borderRadius: 7,
|
|
661
|
-
}, children: "\u00D7" })),
|
|
904
|
+
}, children: "\u00D7" })), atPointer ? (_jsx("input", { ref: typeField, type: "text", "data-testid": "menu-type", ...(FINE_POINTER() ? {} : { "data-graview-not-first": "" }), value: typed, onChange: (event) => {
|
|
905
|
+
// The first letter fetches what Enter will need; the filter itself never waits on it.
|
|
906
|
+
if (typed === "" && event.target.value !== "" && aiDesk)
|
|
907
|
+
preloadQuickAsk(aiDesk);
|
|
908
|
+
setTyped(event.target.value);
|
|
909
|
+
}, onKeyDown: (event) => {
|
|
910
|
+
if (event.key === "Enter") {
|
|
911
|
+
event.preventDefault();
|
|
912
|
+
// As Enter on the row itself: the keyboard on it first, so a form the act opens takes the keyboard from it.
|
|
913
|
+
const row = litRow();
|
|
914
|
+
row?.focus({ preventScroll: true });
|
|
915
|
+
row?.click();
|
|
916
|
+
}
|
|
917
|
+
else if (event.key === "ArrowDown") {
|
|
918
|
+
event.preventDefault();
|
|
919
|
+
(litRow() ?? walkable()[0])?.focus();
|
|
920
|
+
}
|
|
921
|
+
else if (event.key === "ArrowUp") {
|
|
922
|
+
// Round to the last row, as a menu's arrows go: More… where the rest wait behind it.
|
|
923
|
+
event.preventDefault();
|
|
924
|
+
walkable().at(-1)?.focus();
|
|
925
|
+
}
|
|
926
|
+
else if (event.key === "Enter" && !litRow() && modeLit) {
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
runMode(modeLit);
|
|
929
|
+
}
|
|
930
|
+
else if (event.key === "Escape" && typed !== "") {
|
|
931
|
+
// The words go first; the next Escape puts the menu away.
|
|
932
|
+
event.preventDefault();
|
|
933
|
+
event.stopPropagation();
|
|
934
|
+
setTyped("");
|
|
935
|
+
}
|
|
936
|
+
}, placeholder: modes.length > 0 ? "Tell Graview what you want…" : "Type to filter…", "aria-label": modes.length > 0 ? `Tell Graview what you want with ${askedAbout}, or filter what can be done here` : "Filter what can be done here", autoComplete: "off", autoCorrect: "off", spellCheck: false, enterKeyHint: "go", style: {
|
|
937
|
+
boxSizing: "border-box",
|
|
938
|
+
width: "100%",
|
|
939
|
+
font: "inherit",
|
|
940
|
+
// A phone zooms into a field set under 16px.
|
|
941
|
+
fontSize: "max(1rem, 16px)",
|
|
942
|
+
padding: "5px 9px",
|
|
943
|
+
borderRadius: 8,
|
|
944
|
+
border: "1px solid var(--graview-edge)",
|
|
945
|
+
background: "var(--graview-panel)",
|
|
946
|
+
color: "var(--graview-ink)",
|
|
947
|
+
} })) : null, atPointer && modes.length > 0 ? (_jsx("div", { role: "group", "aria-label": modes.length > 1 ? "Chat or Better UI" : "Chat", "data-testid": "menu-modes", style: { display: "flex", gap: 6 }, children: modes.map((mode) => {
|
|
948
|
+
const on = modeLit === mode;
|
|
949
|
+
const label = mode === "chat" ? "Chat" : (better?.label ?? "Better UI");
|
|
950
|
+
const mark = mode === "chat" ? "💬" : (better?.mark ?? "✨");
|
|
951
|
+
return (_jsxs("button", { type: "button", "data-menu-mode": mode, ...(mode === "chat" ? { "data-menu-item": "ask-typed" } : {}), "data-menu-lit": (lit === "mode" && on) || undefined, "aria-pressed": on, "aria-label": words ? `${label}: ${words}` : label, onClick: () => runMode(mode), onKeyDown: (event) => {
|
|
952
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
|
|
953
|
+
return;
|
|
954
|
+
event.preventDefault();
|
|
955
|
+
event.stopPropagation();
|
|
956
|
+
const next = modes[(modes.indexOf(mode) + (event.key === "ArrowRight" ? 1 : modes.length - 1)) % modes.length];
|
|
957
|
+
setModeChosen(next);
|
|
958
|
+
asideRef.current?.querySelector(`[data-menu-mode="${next}"]`)?.focus();
|
|
959
|
+
}, style: on ? MODE_ON : MODE_OFF, children: [_jsx("span", { "aria-hidden": "true", children: mark }), " ", label, lit === "mode" && on ? ENTER_MARK : null] }, mode));
|
|
960
|
+
}) })) : null, atPointer && words === "" && suggested.length > 0 ? (_jsxs("div", { "data-menu-section": "suggested", role: "group", "aria-label": "Suggested", style: { display: "grid", gap: 2, borderTop: "1px solid var(--graview-edge)", paddingTop: 5, minHeight: suggested.length * 31 + 22 }, children: [_jsx("span", { "aria-hidden": "true", style: GROUP_HEAD, children: "Suggested" }), suggested.map((one, at) => (_jsxs("button", { type: "button", "data-menu-item": `suggested-${at}`, "data-suggested-mode": one.mode, onClick: () => takeSuggested(one), style: { ...ROW_STYLE, display: "flex", gap: 8, alignItems: "baseline", minWidth: 0 }, children: [_jsx("span", { "aria-hidden": "true", style: { color: "var(--graview-accent)" }, children: "\u2726" }), _jsx("span", { title: one.label, style: { flex: "1 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: one.label }), _jsxs("span", { title: one.mode === "better" ? "Better UI" : "Chat", style: { fontSize: "0.8125rem" }, children: [_jsx("span", { "aria-hidden": "true", children: one.mode === "better" ? "✨" : "💬" }), _jsx("span", { style: VISUALLY_HIDDEN, children: one.mode === "better" ? " (Better UI)" : " (Chat)" })] })] }, one.key)))] })) : null, 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, unsure ? (_jsx("p", { "data-testid": "edge-provisional", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: provenanceWords(unsure) })) : null, _jsx("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }, children: [
|
|
662
961
|
["from", edgeEnds.from],
|
|
663
962
|
["to", edgeEnds.to],
|
|
664
|
-
].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: 6 }, children: nameOf(store, end.id) })) : (_jsx("span", { style: { fontSize: "0.8125rem", color: "var(--graview-ink-faint)" }, children: "gone" }))] }, which))) })] })) : null, !edge && selection.length === 1 && isLeftAlone(store, selection[0]) ? (_jsx("p", { "data-testid": "left-alone", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: "Left alone: nothing automated changes it." })) : 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: {
|
|
963
|
+
].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: 6 }, children: nameOf(store, end.id) })) : (_jsx("span", { style: { fontSize: "0.8125rem", color: "var(--graview-ink-faint)" }, children: "gone" }))] }, which))) })] })) : null, !edge && selection.length === 1 && isLeftAlone(store, selection[0]) ? (_jsx("p", { "data-testid": "left-alone", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: "Left alone: nothing automated changes it." })) : null, said.length > 0 ? (_jsx("div", { "data-testid": "observations", title: said.map((observation) => observation.text).join("\n"), hidden: folded, style: { display: folded ? "none" : "grid", gap: 3 }, children: said.slice(0, 2).map((observation) => (_jsxs("p", { style: {
|
|
665
964
|
margin: 0,
|
|
666
965
|
fontSize: "0.8125rem",
|
|
667
966
|
lineHeight: 1.45,
|
|
668
967
|
color: "var(--graview-ink-muted)",
|
|
669
|
-
}, 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, atPointer ?
|
|
968
|
+
}, 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, atPointer ? startSections.map((section) => _jsx(SectionDrawn, { section: section, lit: lit, close: () => setMenuAt(null) }, section.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) => {
|
|
670
969
|
if (event.key === "Escape") {
|
|
671
970
|
// Clearing the filter must not also clear the selection —
|
|
672
971
|
// the document-level Escape stays out of it either way.
|
|
@@ -698,7 +997,7 @@ function Pane({ placement }) {
|
|
|
698
997
|
border: "1px solid var(--graview-edge)",
|
|
699
998
|
background: "var(--graview-panel)",
|
|
700
999
|
color: "var(--graview-ink)",
|
|
701
|
-
} })) : 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, gain && affordances.length === 0 ? null : affordances.length === 0 && !waiting && selection.length === 0 ? null : affordances.length === 0 && !waiting ? (
|
|
1000
|
+
} })) : 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, (words !== "" && shown.length === 0) || (gain && affordances.length === 0) ? null : affordances.length === 0 && !waiting && selection.length === 0 ? null : affordances.length === 0 && !waiting ? (
|
|
702
1001
|
/*
|
|
703
1002
|
* An empty action list is a RESULT, not a blank space. Saying which
|
|
704
1003
|
* kind has no mutations declaring it as a subject is the same honesty
|
|
@@ -718,8 +1017,8 @@ function Pane({ placement }) {
|
|
|
718
1017
|
? withArticle(nounOf(store.schema.tryDefinition(subjectKinds[0]), subjectKinds[0]))
|
|
719
1018
|
: "this mix of kinds"} — ${withheld.length} action${withheld.length === 1 ? "" : "s"} withheld.`
|
|
720
1019
|
: edge
|
|
721
|
-
? `
|
|
722
|
-
:
|
|
1020
|
+
? `There is nothing to do with this line here: nothing in this app makes or breaks “${edgeWords(store.schema, store.graph.getNode(edge.from)?.kind ?? "", edge.kind, "out")}”.`
|
|
1021
|
+
: emptyWords(store, subjectKinds, selection) })) : (_jsxs("ol", { ...roving, "data-testid": "affordances", style: {
|
|
723
1022
|
margin: 0,
|
|
724
1023
|
padding: 0,
|
|
725
1024
|
listStyle: "none",
|
|
@@ -746,7 +1045,7 @@ function Pane({ placement }) {
|
|
|
746
1045
|
: {}),
|
|
747
1046
|
padding: "2px 2px 1px",
|
|
748
1047
|
marginTop: index > 0 ? 6 : 0,
|
|
749
|
-
}, 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" }, onKeyDown: (event) => {
|
|
1048
|
+
}, 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", { hidden: behindMore.has(affordance.id), style: { display: behindMore.has(affordance.id) ? "none" : "flex", gap: 2, alignItems: "stretch" }, onKeyDown: (event) => {
|
|
750
1049
|
// Its pin is beside it: the arrow to the right, and back.
|
|
751
1050
|
if (event.metaKey || event.ctrlKey || event.altKey)
|
|
752
1051
|
return;
|
|
@@ -759,7 +1058,7 @@ function Pane({ placement }) {
|
|
|
759
1058
|
event.preventDefault();
|
|
760
1059
|
event.stopPropagation();
|
|
761
1060
|
to.focus();
|
|
762
|
-
}, children: [_jsxs("button", { type: "button", [ROVING_ITEM]: "", "data-affordance": affordance.id, "data-rank": affordance.rank, "data-graview-destructive": affordance.destructive || undefined, "aria-pressed": pending === affordance.id, title: affordance.why, style: {
|
|
1061
|
+
}, children: [_jsxs("button", { type: "button", [ROVING_ITEM]: "", "data-affordance": affordance.id, "data-rank": affordance.rank, "data-graview-destructive": affordance.destructive || undefined, "data-menu-lit": lit === `act:${affordance.id}` || undefined, "aria-pressed": pending === affordance.id, title: affordance.why, style: {
|
|
763
1062
|
padding: "5px 10px",
|
|
764
1063
|
fontSize: "0.875rem",
|
|
765
1064
|
borderRadius: 8,
|
|
@@ -789,13 +1088,14 @@ function Pane({ placement }) {
|
|
|
789
1088
|
// pressed — and the ranking has already put it last.
|
|
790
1089
|
...(affordance.destructive ? { color: "var(--graview-warn)" } : {}),
|
|
791
1090
|
...(pending === affordance.id ? { color: "var(--graview-accent)" } : {}),
|
|
1091
|
+
...(lit === `act:${affordance.id}` ? LIT_STYLE : {}),
|
|
792
1092
|
}, onClick: () => {
|
|
793
1093
|
if (affordance.open.length > 0) {
|
|
794
1094
|
setPending(pending === affordance.id ? null : affordance.id);
|
|
795
1095
|
return;
|
|
796
1096
|
}
|
|
797
1097
|
act(affordance);
|
|
798
|
-
}, children: [affordance.label, affordance.open.length > 0 ? (_jsx("span", { style: { color: "var(--graview-ink-faint)" }, children: " \u2026" })) : null, matched?.length === 1 &&
|
|
1098
|
+
}, children: [affordance.label, affordance.open.length > 0 ? (_jsx("span", { style: { color: "var(--graview-ink-faint)" }, children: " \u2026" })) : null, lit === `act:${affordance.id}` ? ENTER_MARK : null, matched?.length === 1 &&
|
|
799
1099
|
matched[0]?.id === affordance.id &&
|
|
800
1100
|
!affordance.destructive ? (
|
|
801
1101
|
// The searcher's promise, made visible exactly when
|
|
@@ -831,22 +1131,22 @@ function Pane({ placement }) {
|
|
|
831
1131
|
: affordance.pinned === "declared"
|
|
832
1132
|
? "var(--graview-ink-muted)"
|
|
833
1133
|
: "var(--graview-ink-faint)",
|
|
834
|
-
}, children: affordance.pinned !== undefined ? "★" : "☆" })] }, affordance.id)))] }, `${section.heading ?? "plain"}-${index}`))), hidden > 0 ? (_jsx("li", { children: _jsxs("button", { type: "button", [ROVING_ITEM]: "", onClick: () => setExpanded(true),
|
|
1134
|
+
}, children: affordance.pinned !== undefined ? "★" : "☆" })] }, affordance.id)))] }, `${section.heading ?? "plain"}-${index}`))), hidden > 0 && !atPointer ? (_jsx("li", { children: _jsxs("button", { type: "button", [ROVING_ITEM]: "", onClick: () => setExpanded(true),
|
|
835
1135
|
// What it DOES, not what is behind it. "+9 more" is a label
|
|
836
1136
|
// on a quantity; this is a control, and a control says what
|
|
837
1137
|
// pressing it will do.
|
|
838
1138
|
title: `Show all ${affordances.length} actions`, style: { padding: "4px 10px", fontSize: "0.875rem", borderRadius: 8 }, children: ["Show ", hidden, " more"] }) })) : null] })), gain ? (_jsx("button", { type: "button", "data-testid": "sign-in", onClick: () => {
|
|
839
1139
|
setMenuAt(null);
|
|
840
1140
|
gain.run();
|
|
841
|
-
}, style: { padding: "5px 10px", fontSize: "0.875rem", borderRadius: 8, width: "100%", textAlign: "left" }, children: gain.label })) : null,
|
|
1141
|
+
}, style: { padding: "5px 10px", fontSize: "0.875rem", borderRadius: 8, width: "100%", textAlign: "left" }, children: gain.label })) : null, withheldShown.length > 0 ? (_jsxs("ul", { hidden: withheldFolded, "data-testid": "withheld", style: {
|
|
842
1142
|
margin: 0,
|
|
843
1143
|
padding: 0,
|
|
844
1144
|
listStyle: "none",
|
|
845
|
-
display: "flex",
|
|
1145
|
+
display: withheldFolded ? "none" : "flex",
|
|
846
1146
|
flexDirection: "column",
|
|
847
1147
|
flexWrap: "nowrap",
|
|
848
1148
|
gap: 2,
|
|
849
|
-
}, children: [
|
|
1149
|
+
}, children: [withheldShown.slice(0, atPointer || allWithheld ? withheldShown.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: {
|
|
850
1150
|
padding: "4px 10px",
|
|
851
1151
|
fontSize: "0.875rem",
|
|
852
1152
|
borderRadius: 8,
|
|
@@ -859,7 +1159,19 @@ function Pane({ placement }) {
|
|
|
859
1159
|
lineHeight: 1.4,
|
|
860
1160
|
color: "var(--graview-ink-muted)",
|
|
861
1161
|
padding: "0 10px",
|
|
862
|
-
}, 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,
|
|
1162
|
+
}, 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, moreWaits ? (_jsx("button", { type: "button", "data-menu-item": "more", onClick: (event) => {
|
|
1163
|
+
/* The keyboard goes on to the first of what was waiting, where a person reading down would be next. */
|
|
1164
|
+
const menu = event.currentTarget.parentElement;
|
|
1165
|
+
const waiting = [...(menu?.querySelectorAll("li[hidden], [data-menu-more][hidden]") ?? [])];
|
|
1166
|
+
setExpanded(true);
|
|
1167
|
+
requestAnimationFrame(() => {
|
|
1168
|
+
for (const one of waiting) {
|
|
1169
|
+
const to = one.querySelector("button:not([disabled]), a[href], input");
|
|
1170
|
+
if (to && to.getClientRects().length > 0)
|
|
1171
|
+
return to.focus();
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}, style: { ...ROW_STYLE, color: "var(--graview-ink-muted)" }, children: "More\u2026" })) : null, atPointer && endSections.length > 0 ? (_jsx("div", { "data-menu-more": "", hidden: folded, style: { display: folded ? "none" : "contents" }, children: endSections.filter((section) => !folded || !section.render).map((section) => _jsx(SectionDrawn, { section: section, lit: lit, close: () => setMenuAt(null) }, section.id)) })) : null, toldShown.length > 0 ? (_jsx("div", { "data-menu-section": "told", role: "group", "aria-label": "Suggested", style: { display: "grid", gap: 2, borderTop: "1px solid var(--graview-edge)", paddingTop: 5 }, children: toldShown.map((one) => (_jsxs("button", { type: "button", "data-menu-item": one.key, "data-told-suggestion": one.telling.id, "data-menu-lit": lit === one.key || undefined, onClick: () => takeTold(one), style: { ...ROW_STYLE, ...(lit === one.key ? LIT_STYLE : {}) }, children: [one.label, lit === one.key ? ENTER_MARK : null] }, one.key))) })) : null, atPointer && words !== "" && !askRow && lit === null && shown.length === 0 && withheldShown.length === 0 ? (_jsxs("p", { "data-testid": "menu-no-match", style: { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)" }, children: ["Nothing here matches \u201C", words, "\u201D."] })) : null, open ? (_jsx(AnswerArgs, { affordance: open,
|
|
863
1175
|
// On a refusal the prompt stays put with the reason beside it, so
|
|
864
1176
|
// a rejected answer can be corrected rather than retyped blind.
|
|
865
1177
|
onApply: (args) => {
|
|
@@ -869,6 +1181,33 @@ function Pane({ placement }) {
|
|
|
869
1181
|
return refusedArgs.current;
|
|
870
1182
|
}, onCancel: () => setPending(null) })) : null] }));
|
|
871
1183
|
}
|
|
1184
|
+
/** Whether this is somewhere a person types: a field, a box of text. */
|
|
1185
|
+
function typingIn(element) {
|
|
1186
|
+
if (element.isContentEditable || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)
|
|
1187
|
+
return true;
|
|
1188
|
+
return element instanceof HTMLInputElement && !["button", "submit", "reset", "checkbox", "radio", "image", "file", "color", "range"].includes(element.type);
|
|
1189
|
+
}
|
|
1190
|
+
/** A menu's row, and the one Enter runs from the field, lit, with the key that runs it said. */
|
|
1191
|
+
const ROW_STYLE = { padding: "5px 10px", fontSize: "0.875rem", borderRadius: 8, width: "100%", textAlign: "left", borderWidth: 1, borderStyle: "solid", borderColor: "transparent", background: "none", boxShadow: "none" };
|
|
1192
|
+
const LIT_STYLE = { background: "var(--graview-panel-muted)" };
|
|
1193
|
+
/** One object for the graph as it stands, a new one once anything is applied: what a list's Suggested is asked again by. */
|
|
1194
|
+
const STAMPS = /* @__PURE__ */ new WeakMap();
|
|
1195
|
+
function graphStamp(store) {
|
|
1196
|
+
const length = store.log.all().length;
|
|
1197
|
+
const held = STAMPS.get(store);
|
|
1198
|
+
if (held && held.length === length)
|
|
1199
|
+
return held.stamp;
|
|
1200
|
+
const stamp = { length };
|
|
1201
|
+
STAMPS.set(store, { length, stamp });
|
|
1202
|
+
return stamp;
|
|
1203
|
+
}
|
|
1204
|
+
/** Whether the pointer is a fine one (a mouse, a trackpad): there the menu's field takes the keyboard as it opens. */
|
|
1205
|
+
const FINE_POINTER = () => typeof matchMedia === "function" && matchMedia("(pointer: fine)").matches;
|
|
1206
|
+
const MODE_BASE = { display: "inline-flex", alignItems: "center", gap: 5, minHeight: 30, padding: "0 11px", fontSize: "0.8125rem", borderRadius: 999, cursor: "pointer", borderWidth: 1, borderStyle: "solid", boxShadow: "none" };
|
|
1207
|
+
const MODE_ON = { ...MODE_BASE, borderColor: "var(--graview-accent)", background: "color-mix(in srgb, var(--graview-accent) 14%, transparent)", color: "var(--graview-ink)", fontWeight: 600 };
|
|
1208
|
+
const MODE_OFF = { ...MODE_BASE, borderColor: "var(--graview-edge)", background: "none", color: "var(--graview-ink-muted)" };
|
|
1209
|
+
const GROUP_HEAD = { fontSize: "0.6875rem", letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--graview-ink-faint)", padding: "0 10px" };
|
|
1210
|
+
const ENTER_MARK = (_jsx("span", { "aria-hidden": "true", title: "Enter runs it", style: { float: "right", color: "var(--graview-ink-faint)", fontSize: "0.75rem" }, children: "\u21B5" }));
|
|
872
1211
|
/** One shared empty, for a visitor told the way in instead. */
|
|
873
1212
|
const NOTHING_WITHHELD = [];
|
|
874
1213
|
/**
|
|
@@ -876,10 +1215,10 @@ const NOTHING_WITHHELD = [];
|
|
|
876
1215
|
* each pressed to close the menu and run, or what it draws itself. Rows of
|
|
877
1216
|
* the acts' own look, so the menu reads as one list.
|
|
878
1217
|
*/
|
|
879
|
-
function SectionDrawn({ section, close }) {
|
|
880
|
-
return (_jsxs("div", { "data-menu-section": section.id, ...(section.heading ? { role: "group", "aria-label": section.heading } : {}), style: { display: "grid", gap: 2, borderTop: "1px solid var(--graview-edge)", paddingTop: 5 }, children: [section.heading ? (_jsx("p", { "aria-hidden": "true", style: { margin: 0, padding: "2px 2px 1px", fontSize: "0.75rem", lineHeight: 1.4, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--graview-ink-faint)" }, children: section.heading })) : null, section.items && section.items.length > 0 ? (_jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 2 }, children: section.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-menu-item": item.id, onClick: () => {
|
|
1218
|
+
function SectionDrawn({ section, lit = null, close }) {
|
|
1219
|
+
return (_jsxs("div", { "data-menu-section": section.id, ...(section.heading ? { role: "group", "aria-label": section.heading } : {}), style: { display: "grid", gap: 2, borderTop: "1px solid var(--graview-edge)", paddingTop: 5 }, children: [section.heading ? (_jsx("p", { "aria-hidden": "true", style: { margin: 0, padding: "2px 2px 1px", fontSize: "0.75rem", lineHeight: 1.4, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--graview-ink-faint)" }, children: section.heading })) : null, section.items && section.items.length > 0 ? (_jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 2 }, children: section.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-menu-item": item.id, "data-menu-lit": lit === `item:${item.id}` || undefined, onClick: () => {
|
|
881
1220
|
close();
|
|
882
1221
|
item.run();
|
|
883
|
-
}, style: {
|
|
1222
|
+
}, style: { ...ROW_STYLE, ...(lit === `item:${item.id}` ? LIT_STYLE : {}) }, children: [item.label, item.hint ? _jsxs("span", { style: { color: "var(--graview-ink-faint)" }, children: [" \u00B7 ", item.hint] }) : null, lit === `item:${item.id}` ? ENTER_MARK : null] }) }, item.id))) })) : null, section.render?.(close)] }));
|
|
884
1223
|
}
|
|
885
1224
|
//# sourceMappingURL=inspector.js.map
|