@graview/primitives 0.1.65 β†’ 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.
Files changed (128) hide show
  1. package/dist/app-bar.d.ts +88 -15
  2. package/dist/app-bar.d.ts.map +1 -1
  3. package/dist/app-bar.js +176 -130
  4. package/dist/app-bar.js.map +1 -1
  5. package/dist/app-menu.d.ts +9 -0
  6. package/dist/app-menu.d.ts.map +1 -0
  7. package/dist/app-menu.js +60 -0
  8. package/dist/app-menu.js.map +1 -0
  9. package/dist/bar-activity.d.ts +5 -9
  10. package/dist/bar-activity.d.ts.map +1 -1
  11. package/dist/bar-activity.js +24 -7
  12. package/dist/bar-activity.js.map +1 -1
  13. package/dist/bar-panes.d.ts +5 -5
  14. package/dist/bar-panes.d.ts.map +1 -1
  15. package/dist/bar-panes.js +9 -5
  16. package/dist/bar-panes.js.map +1 -1
  17. package/dist/bar-suggested.d.ts +9 -1
  18. package/dist/bar-suggested.d.ts.map +1 -1
  19. package/dist/bar-suggested.js +11 -17
  20. package/dist/bar-suggested.js.map +1 -1
  21. package/dist/better-bar.d.ts +65 -0
  22. package/dist/better-bar.d.ts.map +1 -0
  23. package/dist/better-bar.js +252 -0
  24. package/dist/better-bar.js.map +1 -0
  25. package/dist/better-panel.d.ts +50 -0
  26. package/dist/better-panel.d.ts.map +1 -0
  27. package/dist/better-panel.js +151 -0
  28. package/dist/better-panel.js.map +1 -0
  29. package/dist/editable.d.ts +8 -1
  30. package/dist/editable.d.ts.map +1 -1
  31. package/dist/editable.js +140 -16
  32. package/dist/editable.js.map +1 -1
  33. package/dist/first-look-door.d.ts +2 -2
  34. package/dist/first-look-door.js +2 -2
  35. package/dist/first-look.d.ts.map +1 -1
  36. package/dist/first-look.js +9 -7
  37. package/dist/first-look.js.map +1 -1
  38. package/dist/frame.d.ts +2 -2
  39. package/dist/frame.d.ts.map +1 -1
  40. package/dist/frame.js +1 -1
  41. package/dist/frame.js.map +1 -1
  42. package/dist/inbox-pane.d.ts +31 -0
  43. package/dist/inbox-pane.d.ts.map +1 -0
  44. package/dist/inbox-pane.js +89 -0
  45. package/dist/inbox-pane.js.map +1 -0
  46. package/dist/inbox.d.ts +17 -0
  47. package/dist/inbox.d.ts.map +1 -0
  48. package/dist/inbox.js +84 -0
  49. package/dist/inbox.js.map +1 -0
  50. package/dist/index.d.ts +4 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +3 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/menu-suggested.d.ts +54 -0
  55. package/dist/menu-suggested.d.ts.map +1 -0
  56. package/dist/menu-suggested.js +140 -0
  57. package/dist/menu-suggested.js.map +1 -0
  58. package/dist/menu-told-door.js +1 -1
  59. package/dist/menu-told-door.js.map +1 -1
  60. package/dist/menu-told.d.ts +7 -0
  61. package/dist/menu-told.d.ts.map +1 -1
  62. package/dist/menu-told.js +24 -0
  63. package/dist/menu-told.js.map +1 -1
  64. package/dist/notify-sheet.js +2 -2
  65. package/dist/notify-sheet.js.map +1 -1
  66. package/dist/page-better.d.ts +13 -0
  67. package/dist/page-better.d.ts.map +1 -0
  68. package/dist/page-better.js +714 -0
  69. package/dist/page-better.js.map +1 -0
  70. package/dist/page-parts.d.ts +41 -0
  71. package/dist/page-parts.d.ts.map +1 -0
  72. package/dist/page-parts.js +100 -0
  73. package/dist/page-parts.js.map +1 -0
  74. package/dist/pages.d.ts +2 -3
  75. package/dist/pages.d.ts.map +1 -1
  76. package/dist/pages.js +2 -3
  77. package/dist/pages.js.map +1 -1
  78. package/dist/profile.d.ts +1 -10
  79. package/dist/profile.d.ts.map +1 -1
  80. package/dist/profile.js +2 -3
  81. package/dist/profile.js.map +1 -1
  82. package/dist/scene-bar.d.ts +1 -1
  83. package/dist/scene-bar.d.ts.map +1 -1
  84. package/dist/scene-bar.js +4 -7
  85. package/dist/scene-bar.js.map +1 -1
  86. package/dist/section-better.d.ts +47 -9
  87. package/dist/section-better.d.ts.map +1 -1
  88. package/dist/section-better.js +239 -121
  89. package/dist/section-better.js.map +1 -1
  90. package/dist/section-door.d.ts +7 -0
  91. package/dist/section-door.d.ts.map +1 -1
  92. package/dist/section-door.js +39 -5
  93. package/dist/section-door.js.map +1 -1
  94. package/dist/section-menu.d.ts +23 -9
  95. package/dist/section-menu.d.ts.map +1 -1
  96. package/dist/section-menu.js +176 -23
  97. package/dist/section-menu.js.map +1 -1
  98. package/dist/section-words.d.ts +8 -0
  99. package/dist/section-words.d.ts.map +1 -1
  100. package/dist/section-words.js +25 -0
  101. package/dist/section-words.js.map +1 -1
  102. package/dist/shell.d.ts +5 -1
  103. package/dist/shell.d.ts.map +1 -1
  104. package/dist/shell.js +4 -2
  105. package/dist/shell.js.map +1 -1
  106. package/dist/suggested.d.ts +21 -10
  107. package/dist/suggested.d.ts.map +1 -1
  108. package/dist/suggested.js +43 -64
  109. package/dist/suggested.js.map +1 -1
  110. package/dist/told-ui.d.ts +1 -1
  111. package/dist/told-ui.d.ts.map +1 -1
  112. package/dist/told-ui.js +1 -1
  113. package/dist/told-ui.js.map +1 -1
  114. package/dist/when-words.d.ts +21 -0
  115. package/dist/when-words.d.ts.map +1 -0
  116. package/dist/when-words.js +43 -0
  117. package/dist/when-words.js.map +1 -0
  118. package/dist/workbench/activity.d.ts +44 -0
  119. package/dist/workbench/activity.d.ts.map +1 -1
  120. package/dist/workbench/activity.js +74 -59
  121. package/dist/workbench/activity.js.map +1 -1
  122. package/dist/workbench/inspector.d.ts.map +1 -1
  123. package/dist/workbench/inspector.js +170 -31
  124. package/dist/workbench/inspector.js.map +1 -1
  125. package/dist/workbench/standing.d.ts.map +1 -1
  126. package/dist/workbench/standing.js +1 -1
  127. package/dist/workbench/standing.js.map +1 -1
  128. 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";
@@ -11,9 +11,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
13
  import { menuMatches } from "../menu-words.js";
14
- import { MENU_RANK_FLOOR, useFastRank } from "../menu-rank.js";
14
+ import { MENU_RANK_FLOOR, MENU_RANK_REST_MS, useFastRank } from "../menu-rank.js";
15
15
  import { TOLD_SUGGESTED_MOST, useToldSuggestions } from "../menu-told-door.js";
16
16
  import { openQuickAsk, preloadQuickAsk, useQuickAskDrawn, whereMenuStands } from "../quick-ask-desk.js";
17
+ import { filledActs, notARow, useSuggested } from "../menu-suggested.js";
17
18
  import "../frame.js"; // A door: tied to what the page drew first (TIED DOORS, packages/primitives/src/frame.ts).
18
19
  /**
19
20
  * The pane itself (`Inspector` in `./inspector-door.tsx` draws it): what is
@@ -80,6 +81,30 @@ function ChosenActs() {
80
81
  setMenuAt({ x: Math.round(at.right), y: Math.round(at.bottom + 4), on: one.id });
81
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" }));
82
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.";
107
+ }
83
108
  /** Whether the picture has room for a rail beside it: the scene's own box, not the window's. */
84
109
  const RAIL_NEEDS = 14 + 236 + 14 + 300;
85
110
  function Pane({ placement }) {
@@ -236,7 +261,7 @@ function Pane({ placement }) {
236
261
  */
237
262
  const target = menuAt === null
238
263
  ? null
239
- : { 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 } : {}), 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 };
240
265
  const beside = useMenuSections(target);
241
266
  /*
242
267
  * Whether the derivation is withholding a creating act for want of the
@@ -305,6 +330,46 @@ function Pane({ placement }) {
305
330
  if (!onRow)
306
331
  heldRank.current = { words: typed.trim(), rank: fastRanked };
307
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);
308
373
  /*
309
374
  * A REFUSAL IS A RESULT, said where the button was pressed.
310
375
  *
@@ -497,7 +562,7 @@ function Pane({ placement }) {
497
562
  * tail honest. Everything past nine scrolls inside the pane's own box
498
563
  * once shown.
499
564
  */
500
- const fits = Math.max(1, Math.min(9, affordances.length));
565
+ const fits = atPointer ? 4 : Math.max(1, Math.min(9, affordances.length));
501
566
  /*
502
567
  * ORDERED FOR READING, not only for ranking: what a broken rule demands,
503
568
  * then the acts somebody PINNED (the person's own before the app's β€” a
@@ -538,11 +603,18 @@ function Pane({ placement }) {
538
603
  const items = (section.items ?? []).filter((item) => fitsTyped(`item:${item.id}`, item.label));
539
604
  return items.length > 0 ? [{ id: section.id, ...(section.heading ? { heading: section.heading } : {}), items }] : [];
540
605
  });
541
- const startSections = narrowed(beside.start);
542
- const endSections = narrowed(beside.end);
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));
543
615
  /* What the words are asked about, said in the Ask row: the menu's title, the one record, else "this". */
544
616
  const askedAbout = menuAt?.title ?? (selection.length === 1 ? nameOf(store, selection[0]) : selection.length > 1 ? `${selection.length} selected` : "this");
545
- const askRow = atPointer && words !== "" && asks;
617
+ const askRow = atPointer && words !== "" && modes.length > 0;
546
618
  /* Up to two that worked before, the Fast slot's likeliest first once it has answered. */
547
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) : [];
548
620
  /*
@@ -564,17 +636,32 @@ function Pane({ placement }) {
564
636
  startSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`))[0] ??
565
637
  shown.filter((entry) => !entry.destructive).map((entry) => `act:${entry.id}`)[0] ??
566
638
  endSections.flatMap((section) => (section.items ?? []).map((item) => `item:${item.id}`))[0] ??
567
- (askRow ? "ask" : null));
568
- const ask = () => {
639
+ (askRow && modeLit ? "mode" : null));
640
+ const ask = (asked = words) => {
569
641
  if (!menuAt)
570
642
  return;
571
- const asked = words;
572
643
  const back = menuAt.returnTo ?? null;
573
644
  // The card opens where the menu stood, read before the menu goes.
574
645
  const from = whereMenuStands(asideRef.current);
575
- setMenuAt(null);
576
646
  // The quick ask's card, where the menu stood, answering the words at once: its thinking marked on the thing itself.
577
- openQuickAsk(aiDesk, { pointed: { on: menuAt.on, about: selection, field: menuAt.field, place: menuAt.place }, asked, returnTo: back, ...(from ? { from } : {}) });
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);
578
665
  };
579
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. */
580
667
  const takeTold = (one) => {
@@ -587,7 +674,8 @@ function Pane({ placement }) {
587
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 } : {}) });
588
675
  };
589
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. */
590
- const walkable = () => [...(asideRef.current?.querySelectorAll("[data-affordance]:not([disabled]), [data-menu-item]") ?? [])].filter((row) => row.closest("[data-graview-asking], [hidden]") === null);
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);
591
679
  const litRow = () => asideRef.current?.querySelector("[data-menu-lit]") ?? null;
592
680
  /*
593
681
  * A LETTER TYPED ANYWHERE IN THE MENU GOES INTO ITS FIELD, the keyboard
@@ -813,15 +901,7 @@ function Pane({ placement }) {
813
901
  fontSize: "0.875rem",
814
902
  lineHeight: 1,
815
903
  borderRadius: 7,
816
- }, 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, 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: [
817
- ["from", edgeEnds.from],
818
- ["to", edgeEnds.to],
819
- ].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: {
820
- margin: 0,
821
- fontSize: "0.8125rem",
822
- lineHeight: 1.45,
823
- color: "var(--graview-ink-muted)",
824
- }, 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 ? (_jsx("input", { ref: typeField, type: "text", "data-testid": "menu-type", "data-graview-not-first": "", value: typed, onChange: (event) => {
904
+ }, children: "\u00D7" })), atPointer ? (_jsx("input", { ref: typeField, type: "text", "data-testid": "menu-type", ...(FINE_POINTER() ? {} : { "data-graview-not-first": "" }), value: typed, onChange: (event) => {
825
905
  // The first letter fetches what Enter will need; the filter itself never waits on it.
826
906
  if (typed === "" && event.target.value !== "" && aiDesk)
827
907
  preloadQuickAsk(aiDesk);
@@ -838,13 +918,22 @@ function Pane({ placement }) {
838
918
  event.preventDefault();
839
919
  (litRow() ?? walkable()[0])?.focus();
840
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
+ }
841
930
  else if (event.key === "Escape" && typed !== "") {
842
931
  // The words go first; the next Escape puts the menu away.
843
932
  event.preventDefault();
844
933
  event.stopPropagation();
845
934
  setTyped("");
846
935
  }
847
- }, placeholder: asks ? "Type to filter, or tell Graview…" : "Type to filter…", "aria-label": asks ? `Filter what can be done here, or tell Graview what to do about ${askedAbout}` : "Filter what can be done here", autoComplete: "off", autoCorrect: "off", spellCheck: false, enterKeyHint: "go", style: {
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: {
848
937
  boxSizing: "border-box",
849
938
  width: "100%",
850
939
  font: "inherit",
@@ -855,7 +944,28 @@ function Pane({ placement }) {
855
944
  border: "1px solid var(--graview-edge)",
856
945
  background: "var(--graview-panel)",
857
946
  color: "var(--graview-ink)",
858
- } })) : 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) => {
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: [
961
+ ["from", edgeEnds.from],
962
+ ["to", edgeEnds.to],
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: {
964
+ margin: 0,
965
+ fontSize: "0.8125rem",
966
+ lineHeight: 1.45,
967
+ color: "var(--graview-ink-muted)",
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) => {
859
969
  if (event.key === "Escape") {
860
970
  // Clearing the filter must not also clear the selection β€”
861
971
  // the document-level Escape stays out of it either way.
@@ -907,8 +1017,8 @@ function Pane({ placement }) {
907
1017
  ? withArticle(nounOf(store.schema.tryDefinition(subjectKinds[0]), subjectKinds[0]))
908
1018
  : "this mix of kinds"} β€” ${withheld.length} action${withheld.length === 1 ? "" : "s"} withheld.`
909
1019
  : edge
910
- ? `Nothing can be done with this line yet β€” nothing this app declares makes or breaks β€œ${edgeWords(store.schema, store.graph.getNode(edge.from)?.kind ?? "", edge.kind, "out")}”.`
911
- : `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", { ...roving, "data-testid": "affordances", style: {
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: {
912
1022
  margin: 0,
913
1023
  padding: 0,
914
1024
  listStyle: "none",
@@ -935,7 +1045,7 @@ function Pane({ placement }) {
935
1045
  : {}),
936
1046
  padding: "2px 2px 1px",
937
1047
  marginTop: index > 0 ? 6 : 0,
938
- }, 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) => {
939
1049
  // Its pin is beside it: the arrow to the right, and back.
940
1050
  if (event.metaKey || event.ctrlKey || event.altKey)
941
1051
  return;
@@ -1021,18 +1131,18 @@ function Pane({ placement }) {
1021
1131
  : affordance.pinned === "declared"
1022
1132
  ? "var(--graview-ink-muted)"
1023
1133
  : "var(--graview-ink-faint)",
1024
- }, 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),
1025
1135
  // What it DOES, not what is behind it. "+9 more" is a label
1026
1136
  // on a quantity; this is a control, and a control says what
1027
1137
  // pressing it will do.
1028
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: () => {
1029
1139
  setMenuAt(null);
1030
1140
  gain.run();
1031
- }, style: { padding: "5px 10px", fontSize: "0.875rem", borderRadius: 8, width: "100%", textAlign: "left" }, children: gain.label })) : null, withheldShown.length > 0 ? (_jsxs("ul", { "data-testid": "withheld", style: {
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: {
1032
1142
  margin: 0,
1033
1143
  padding: 0,
1034
1144
  listStyle: "none",
1035
- display: "flex",
1145
+ display: withheldFolded ? "none" : "flex",
1036
1146
  flexDirection: "column",
1037
1147
  flexWrap: "nowrap",
1038
1148
  gap: 2,
@@ -1049,7 +1159,19 @@ function Pane({ placement }) {
1049
1159
  lineHeight: 1.4,
1050
1160
  color: "var(--graview-ink-muted)",
1051
1161
  padding: "0 10px",
1052
- }, 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, atPointer ? endSections.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, askRow ? (_jsx("div", { "data-menu-section": "ask", style: { display: "grid", gap: 2, borderTop: "1px solid var(--graview-edge)", paddingTop: 5 }, children: _jsxs("button", { type: "button", "data-menu-item": "ask-typed", "data-menu-lit": lit === "ask" || undefined, onClick: ask, style: { ...ROW_STYLE, ...(lit === "ask" ? LIT_STYLE : {}) }, children: ["Tell Graview ", _jsxs("span", { style: { fontWeight: 600 }, children: ["\u201C", words, "\u201D"] }), lit === "ask" ? ENTER_MARK : null] }) })) : 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,
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,
1053
1175
  // On a refusal the prompt stays put with the reason beside it, so
1054
1176
  // a rejected answer can be corrected rather than retyped blind.
1055
1177
  onApply: (args) => {
@@ -1068,6 +1190,23 @@ function typingIn(element) {
1068
1190
  /** A menu's row, and the one Enter runs from the field, lit, with the key that runs it said. */
1069
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" };
1070
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" };
1071
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" }));
1072
1211
  /** One shared empty, for a visitor told the way in instead. */
1073
1212
  const NOTHING_WITHHELD = [];