@graview/primitives 0.1.57 → 0.1.59

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/dist/find.js CHANGED
@@ -1,32 +1,50 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { counted, numberAsRead } from "@graview/core";
3
- import { actsOn, search, touchWeights } from "@graview/core/search";
2
+ import { counted, nounOf, numberAsRead, pluralLabel, pluralOf } from "@graview/core";
3
+ import { actsOn, fold, search, touchWeights } from "@graview/core/search";
4
4
  import { kindCardId, withFocus, withJackIn, withOverview, withPicture, withSelection } from "@graview/layout/view";
5
5
  import { withoutSearch, withQuery } from "@graview/layout/view-moves";
6
6
  import { useKit } from "@graview/react/drawing";
7
7
  import { matchWith, POPOVER_STYLE, useFound, useGraview, usePopover, useViolations } from "@graview/react/provider";
8
8
  import { hueFor } from "@graview/render";
9
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
9
+ import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
10
10
  import { VISUALLY_HIDDEN } from "./primitives/index.js";
11
11
  import { openQuickAsk, useQuickAskDrawn } from "./quick-ask-desk.js";
12
12
  // The Find box searches: it hands the provider the matcher as it is fetched (FR-179).
13
13
  matchWith({ search, touchWeights });
14
+ const isAct = (row) => row !== undefined && "act" in row;
15
+ const recordOf = (row) => (row !== undefined && "hit" in row && row.hit.about === "node" ? row : undefined);
14
16
  /** The kind a hit belongs with in the strip; rules have a group of their own. */
15
17
  const groupOf = (hit) => (hit.about === "rule" ? "rule:" : hit.about === "act" ? "act:" : hit.kind);
18
+ /** Whether the viewport is a desk's: room for the panel beside the list. */
19
+ function useWide(compact) {
20
+ const measure = () => typeof window === "undefined" || window.innerWidth >= WIDE_FROM;
21
+ const [wide, setWide] = useState(measure);
22
+ useEffect(() => {
23
+ const again = () => setWide(measure());
24
+ window.addEventListener("resize", again);
25
+ return () => window.removeEventListener("resize", again);
26
+ }, []);
27
+ return wide && !compact;
28
+ }
16
29
  export function FindBox({ compact = false }) {
17
30
  const { store, view, setView, principal, setMenuAt, brand, aiDesk, selection } = useGraview();
18
31
  const asks = useQuickAskDrawn(aiDesk);
19
32
  const found = useFound();
20
33
  const violations = useViolations();
21
34
  const flag = useKit().marks.flag;
35
+ const wide = useWide(compact);
22
36
  const input = useRef(null);
37
+ const list = useRef(null);
23
38
  // Where the keyboard waits between Enter and the place standing (see `hold`).
24
39
  const holder = useRef(null);
25
40
  const strip = useId();
26
41
  const [open, setOpen] = useState(false);
42
+ // The row lit: the keys' or the pointer's.
27
43
  const [active, setActive] = useState(null);
28
- // The record whose acts are shown: the one highlighted, kept while the keys move into its acts.
29
- const [anchor, setAnchor] = useState(null);
44
+ // The row the desk's panel says: the lit one, a moment behind a pointer crossing rows on its way to the panel.
45
+ const [shown, setShown] = useState(null);
46
+ // The records whose acts were asked for under their rows (a narrow strip's "⋯", or →).
47
+ const [opened, setOpened] = useState([]);
30
48
  const q = view.q ?? "";
31
49
  /*
32
50
  * `/` OR ⌘K, FROM ANYWHERE. ⌘K is a chord nobody types into a field, so
@@ -74,10 +92,11 @@ export function FindBox({ compact = false }) {
74
92
  return () => window.removeEventListener("keydown", onKey);
75
93
  }, []);
76
94
  /*
77
- * WHAT CAN BE DONE ABOUT IT: the acts on the highlighted record, from the
78
- * same matcher asked with that record as the subject — never subjectless.
95
+ * THE GROUPS: a kind's rows under its heading, the kinds in the order of
96
+ * their best hit. Where several kinds share the strip each shows its best
97
+ * few and a row to all of them, so one kind of a hundred hits does not push
98
+ * every other kind out of sight; one kind alone shows its rows whole.
79
99
  */
80
- const acts = useMemo(() => (anchor && q.trim() ? actsOn(store, anchor, found?.words ?? q, { principal, limit: 4 }) : []), [store, q, found, anchor, principal]);
81
100
  const groups = useMemo(() => {
82
101
  const held = new Map();
83
102
  for (const [index, hit] of (found?.hits ?? []).entries()) {
@@ -87,21 +106,53 @@ export function FindBox({ compact = false }) {
87
106
  const rows = held.get(key) ?? [];
88
107
  rows.push({ id: `${strip}-${index}`, hit });
89
108
  held.set(key, rows);
90
- if (hit.about === "node" && hit.id === anchor) {
91
- for (const [at, act] of acts.entries())
92
- rows.push({ id: `${strip}-${index}-act-${at}`, hit: act });
93
- }
94
109
  }
95
- return [...held.entries()];
96
- }, [found, acts, anchor, strip]);
110
+ const several = [...held.values()].filter((rows) => rows.some((row) => row.hit.about === "node")).length > 1;
111
+ return [...held.entries()].map(([key, entries]) => {
112
+ let records = 0;
113
+ const rows = entries.filter((row) => row.hit.about !== "node" || !several || ++records <= GROUP_CAP);
114
+ const listed = rows.filter((row) => row.hit.about === "node").length;
115
+ const count = key.endsWith(":") ? entries.length : (found?.byKind[key] ?? listed);
116
+ const more = !key.endsWith(":") && listed > 0 && count > listed ? { id: `${strip}-all-${key}`, all: key, count } : null;
117
+ return { key, rows, count, more };
118
+ });
119
+ }, [found, strip]);
97
120
  const asking = asks && q.trim() ? { id: `${strip}-ask`, ask: q.trim() } : null;
98
- const rows = useMemo(() => [...groups.flatMap(([, entries]) => entries), ...(asking ? [asking] : [])], [groups, asking?.ask]);
99
- const current = rows.find((row) => row.id === active) ?? rows[0];
121
+ const rows = useMemo(() => [...groups.flatMap((group) => (group.more ? [...group.rows, group.more] : group.rows)), ...(asking ? [asking] : [])], [groups, asking?.ask]);
100
122
  const manyFound = (found?.hits ?? []).filter((hit) => hit.about === "node").length;
123
+ // The words as the matcher folded them, and what they mark in a row.
124
+ const words = found?.words ?? q;
125
+ const marking = useMemo(() => fold(words).split(" ").filter(Boolean), [words]);
126
+ /*
127
+ * WHAT CAN BE DONE ABOUT IT: the acts on a record, from the same matcher
128
+ * asked with that record as the subject — never subjectless, and only
129
+ * those this seat may run.
130
+ */
131
+ const actsOf = useCallback((row) => row.hit.about === "node" ? actsOn(store, row.hit.id, words, { principal, limit: ACTS_MOST }).map((act, at) => ({ id: `${row.id}-act-${at}`, act, of: row.id })) : [], [store, words, principal]);
132
+ const lit = rows.find((row) => row.id === active);
133
+ const subject = rows.find((row) => row.id === shown) ?? lit ?? rows[0];
134
+ const panelActs = useMemo(() => (wide && recordOf(subject) ? actsOf(subject) : []), [wide, subject?.id, actsOf]);
135
+ const openActs = useMemo(() => {
136
+ if (wide)
137
+ return new Map();
138
+ return new Map(opened.flatMap((id) => {
139
+ const row = recordOf(rows.find((one) => one.id === id));
140
+ return row ? [[id, actsOf(row)]] : [];
141
+ }));
142
+ }, [wide, opened, rows, actsOf]);
143
+ // In a narrow strip, which records have acts to ask for: a "⋯" that opened on nothing would be offered and refused.
144
+ const hasActs = useMemo(() => {
145
+ if (wide)
146
+ return new Set();
147
+ return new Set(rows.flatMap((row) => (recordOf(row) && actsOf(row).length > 0 ? [row.id] : [])));
148
+ }, [wide, rows, actsOf]);
149
+ const actRows = useMemo(() => (wide ? panelActs : [...openActs.values()].flat()), [wide, panelActs, openActs]);
150
+ const current = lit ?? actRows.find((row) => row.id === active) ?? rows[0];
101
151
  // A new answer starts at its first row, as a list does.
102
152
  useEffect(() => {
103
153
  setActive(null);
104
- setAnchor(null);
154
+ setShown(null);
155
+ setOpened([]);
105
156
  }, [q]);
106
157
  /*
107
158
  * WHERE ENTER WENT, THE KEYS GO (the tenth walk). Into a kind or one of
@@ -164,22 +215,63 @@ export function FindBox({ compact = false }) {
164
215
  waiting.setAttribute("aria-label", going);
165
216
  waiting.focus({ preventScroll: true });
166
217
  };
218
+ /*
219
+ * THE POINTER LIGHTS, IT DOES NOT MOVE ANYTHING. A row is lit by the
220
+ * pointer only when the pointer itself moved: a list scrolled, or drawn
221
+ * again, under a still pointer sends a mouse event at the same place, and
222
+ * that lit whatever row slid under it. And the panel follows the pointer a
223
+ * moment behind (`AIM`), so a pointer crossing rows on its way to an act
224
+ * in the panel finds the record it left still there.
225
+ */
226
+ const pointer = useRef({ x: Number.NaN, y: Number.NaN, moved: false });
227
+ useEffect(() => {
228
+ const onMove = (event) => {
229
+ const at = pointer.current;
230
+ at.moved = event.clientX !== at.x || event.clientY !== at.y;
231
+ at.x = event.clientX;
232
+ at.y = event.clientY;
233
+ };
234
+ window.addEventListener("mousemove", onMove, { capture: true, passive: true });
235
+ return () => window.removeEventListener("mousemove", onMove, { capture: true });
236
+ }, []);
237
+ const aim = useRef(undefined);
238
+ useEffect(() => () => clearTimeout(aim.current), []);
239
+ /** Lights a row from the keyboard: the panel says it at once. */
167
240
  const highlight = (row) => {
168
241
  if (!row)
169
242
  return;
243
+ clearTimeout(aim.current);
170
244
  setActive(row.id);
171
- if ("ask" in row) {
172
- setAnchor(null);
245
+ if (!isAct(row))
246
+ setShown(row.id);
247
+ };
248
+ /** Lights the row under a pointer that moved. */
249
+ const point = (event) => {
250
+ if (!pointer.current.moved)
173
251
  return;
174
- }
175
- if (row.hit.about === "node")
176
- setAnchor(row.hit.id);
177
- else if (row.hit.about !== "act")
178
- setAnchor(null);
252
+ const id = event.target.closest?.('[role="option"]')?.id;
253
+ if (!id || id === active)
254
+ return;
255
+ const row = rows.find((one) => one.id === id) ?? actRows.find((one) => one.id === id);
256
+ if (!row)
257
+ return;
258
+ setActive(row.id);
259
+ if (isAct(row))
260
+ return;
261
+ clearTimeout(aim.current);
262
+ aim.current = setTimeout(() => setShown(row.id), AIM);
263
+ };
264
+ /** Into the panel: whatever row the pointer crossed on its way, the record the panel says is the one lit. */
265
+ const settle = () => {
266
+ clearTimeout(aim.current);
267
+ if (subject && !isAct(current))
268
+ setActive(subject.id);
179
269
  };
270
+ /** A record's acts, asked for under its row; asked again, put away. */
271
+ const toggle = (row) => setOpened((now) => (now.includes(row.id) ? now.filter((id) => id !== row.id) : [...now, row.id]));
180
272
  /** How many rows a page of the strip is: what it shows at once, less one kept for context. */
181
273
  const pageOf = (count) => {
182
- const pane = document.getElementById(strip);
274
+ const pane = list.current;
183
275
  const one = pane?.querySelector('[role="option"]')?.getBoundingClientRect().height ?? 0;
184
276
  const room = pane?.clientHeight ?? 0;
185
277
  return Math.min(count, one > 0 && room > 0 ? Math.max(1, Math.floor(room / one) - 1) : FIND_PAGE);
@@ -199,6 +291,23 @@ export function FindBox({ compact = false }) {
199
291
  setOpen(false);
200
292
  return;
201
293
  }
294
+ if ("all" in row) {
295
+ // Every one the words found of the kind: into it, narrowed by the words, as the district's "N match" goes in.
296
+ setView((stop) => withJackIn(withOverview(stop, false), kindCardId(row.all), { ownPicture: true, ...(stop.q ? { carry: stop.q } : {}) }));
297
+ landOn.current = row.all;
298
+ setOpen(false);
299
+ return hold(row.all, `Opening ${pluralLabel(store.schema, row.all)} matching “${q.trim()}”…`);
300
+ }
301
+ if (isAct(row)) {
302
+ // The act lives in the menu on its subject, with its form and its preview.
303
+ const subjectId = row.act.subject;
304
+ setView((stop) => withSelection(stop, [subjectId]));
305
+ const box = input.current?.getBoundingClientRect();
306
+ setMenuAt({ x: box?.left ?? 0, y: (box?.bottom ?? 0) + 6, on: subjectId });
307
+ setOpen(false);
308
+ input.current?.blur();
309
+ return;
310
+ }
202
311
  const hit = row.hit;
203
312
  switch (hit.about) {
204
313
  case "node":
@@ -226,13 +335,8 @@ export function FindBox({ compact = false }) {
226
335
  setView((stop) => withSelection(stop, [...new Set(named)]));
227
336
  break;
228
337
  }
229
- case "act": {
230
- // The act lives in the menu on its subject, with its form and its preview.
231
- setView((stop) => withSelection(stop, [hit.subject]));
232
- const box = input.current?.getBoundingClientRect();
233
- setMenuAt({ x: box?.left ?? 0, y: (box?.bottom ?? 0) + 6, on: hit.subject });
338
+ case "act":
234
339
  break;
235
- }
236
340
  }
237
341
  setOpen(false);
238
342
  input.current?.blur();
@@ -266,76 +370,100 @@ export function FindBox({ compact = false }) {
266
370
  },
267
371
  ...(compact ? { at: underTheBar } : {}),
268
372
  });
373
+ const nothing = rows.every((row) => "ask" in row);
374
+ const actsOfRecord = (row) => (isAct(row) ? actRows.filter((one) => one.of === row.of) : []);
375
+ const onKeyDown = (event) => {
376
+ /*
377
+ * RECORD TO RECORD, ONE KEY EACH (the tenth walk). ↓ and ↑ walk the
378
+ * records; → goes into the acts of the one lit — the desk's panel, or
379
+ * opened under it in a narrow strip — and ← back to it. In the acts ↓
380
+ * and ↑ walk the acts.
381
+ */
382
+ const inActs = isAct(current) && active !== null;
383
+ const from = inActs ? rows.find((row) => row.id === current.of) : active !== null ? lit : undefined;
384
+ // Nothing lit yet: the first ↓ lands on the first row, not the second.
385
+ const at = from ? rows.indexOf(from) : -1;
386
+ if (event.key === "ArrowDown") {
387
+ event.preventDefault();
388
+ setOpen(true);
389
+ if (inActs) {
390
+ const acts = actsOfRecord(current);
391
+ const next = acts.indexOf(current) + 1;
392
+ if (next < acts.length)
393
+ return highlight(acts[next]);
394
+ // Past the last act: under a row, the next record; in the panel, it stays.
395
+ if (wide)
396
+ return;
397
+ }
398
+ highlight(rows[Math.min(rows.length - 1, at + 1)]);
399
+ }
400
+ else if (event.key === "ArrowUp") {
401
+ event.preventDefault();
402
+ if (inActs) {
403
+ const acts = actsOfRecord(current);
404
+ const back = acts.indexOf(current) - 1;
405
+ return highlight(back >= 0 ? acts[back] : from);
406
+ }
407
+ highlight(rows[Math.max(0, at - 1)]);
408
+ }
409
+ else if ((event.key === "PageDown" || event.key === "PageUp") && rows.length > 0) {
410
+ /*
411
+ * A PAGE OF RECORDS AT A TIME (the tenth walk): the strip's own
412
+ * height of them, so a hundred talks are ten keys and not a hundred.
413
+ * Home and End stay the words' own: the box is a field being written in.
414
+ */
415
+ event.preventDefault();
416
+ setOpen(true);
417
+ const step = event.key === "PageDown" ? pageOf(rows.length) : -pageOf(rows.length);
418
+ highlight(rows[Math.min(rows.length - 1, Math.max(0, (at < 0 ? -1 : at) + step))]);
419
+ }
420
+ else if (event.key === "ArrowRight" && !inActs && recordOf(current)) {
421
+ const row = current;
422
+ const acts = wide ? (row.id === subject?.id ? panelActs : actsOf(row)) : (openActs.get(row.id) ?? actsOf(row));
423
+ if (acts.length === 0)
424
+ return;
425
+ event.preventDefault();
426
+ if (!wide && !opened.includes(row.id))
427
+ setOpened((now) => [...now, row.id]);
428
+ setShown(row.id);
429
+ setActive(acts[0].id);
430
+ }
431
+ else if (event.key === "ArrowLeft" && inActs) {
432
+ event.preventDefault();
433
+ const of = current.of;
434
+ if (!wide)
435
+ setOpened((now) => now.filter((id) => id !== of));
436
+ highlight(rows.find((row) => row.id === of));
437
+ }
438
+ else if (event.key === "Enter") {
439
+ event.preventDefault();
440
+ travel(current);
441
+ }
442
+ else if (event.key === "Escape") {
443
+ event.preventDefault();
444
+ // The words first; an empty box lets go of the keys.
445
+ if (q)
446
+ setView((stop) => withoutSearch(stop));
447
+ else
448
+ input.current?.blur();
449
+ }
450
+ };
451
+ const option = (row, body, extra = {}, props = {}) => {
452
+ const on = row.id === current?.id;
453
+ return (_jsx("div", { id: row.id, role: "option", "aria-selected": on, onClick: () => travel(row), style: { ...OPTION, ...(on ? LIT : null), ...extra }, ...props, children: body }, row.id));
454
+ };
455
+ const actOption = (row, under) => {
456
+ const on = row.id === current?.id;
457
+ return (_jsx("div", { id: row.id, role: "option", "aria-selected": on, "data-testid": "find-act", "data-about": "act", onClick: () => travel(row), style: { ...(under ? ACT_UNDER : ACT), ...(on ? ACT_LIT : null), ...(row.act.destructive ? { color: "var(--graview-warn)" } : null) }, children: row.act.title }, row.id));
458
+ };
269
459
  return (_jsxs("div", { style: { position: "relative", minWidth: 0, flex: "1 1 auto" }, "data-testid": "find", children: [_jsx("input", { ref: (element) => {
270
460
  input.current = element;
271
461
  popover.trigger.ref(element);
272
462
  }, "data-graview-popover-trigger": "find", type: "search", role: "combobox", "aria-label": "Find anything", "aria-expanded": expanded, "aria-controls": strip, "aria-autocomplete": "list", ...(expanded && current ? { "aria-activedescendant": current.id } : {}), "data-testid": "find-box", placeholder: "Find\u2026", "aria-keyshortcuts": "/ Meta+K Control+K", value: q, onChange: (event) => {
273
- const words = event.target.value;
463
+ const typed = event.target.value;
274
464
  setOpen(true);
275
- setView((stop) => withQuery(stop, words.length > 0 ? words : null));
276
- }, onFocus: () => setOpen(true), onBlur: () => setOpen(false), onKeyDown: (event) => {
277
- // Nothing highlighted yet: the first ↓ lands on the first row, not the second.
278
- const at = active !== null && current ? rows.indexOf(current) : -1;
279
- /*
280
- * RECORD TO RECORD, ONE KEY EACH (the tenth walk). A record lit
281
- * unfolds its acts under it, and ↓ walked into them: five keys a
282
- * record, and the gauntlet's longest title past the keyboard's
283
- * reach. ↓ goes past the acts of the record lit; → goes into them,
284
- * ← back to their record.
285
- */
286
- const anAct = (row) => row !== undefined && !("ask" in row) && row.hit.about === "act";
287
- const onRecord = at >= 0 && current !== undefined && !("ask" in current) && current.hit.about === "node";
288
- if (event.key === "ArrowDown") {
289
- event.preventDefault();
290
- setOpen(true);
291
- let next = at + 1;
292
- if (onRecord)
293
- while (next < rows.length - 1 && anAct(rows[next]))
294
- next += 1;
295
- highlight(rows[Math.min(rows.length - 1, next)]);
296
- }
297
- else if (event.key === "ArrowUp") {
298
- event.preventDefault();
299
- highlight(rows[Math.max(0, at - 1)]);
300
- }
301
- else if ((event.key === "PageDown" || event.key === "PageUp") && rows.length > 0) {
302
- /*
303
- * A PAGE OF RECORDS AT A TIME (the tenth walk): the strip's own
304
- * height of them, past the acts unfolded under one, so a hundred
305
- * talks are ten keys and not a hundred. Home and End stay the
306
- * words' own: the box is a field being written in.
307
- */
308
- event.preventDefault();
309
- setOpen(true);
310
- const records = rows.flatMap((row, index) => (anAct(row) ? [] : [index]));
311
- const from = Math.max(0, records.findIndex((index) => index >= Math.max(at, 0)));
312
- const step = event.key === "PageDown" ? pageOf(rows.length) : -pageOf(rows.length);
313
- highlight(rows[records[Math.min(records.length - 1, Math.max(0, (at < 0 ? -1 : from) + step))]]);
314
- }
315
- else if (event.key === "ArrowRight" && onRecord && anAct(rows[at + 1])) {
316
- event.preventDefault();
317
- highlight(rows[at + 1]);
318
- }
319
- else if (event.key === "ArrowLeft" && anAct(current)) {
320
- event.preventDefault();
321
- let back = at - 1;
322
- while (back > 0 && anAct(rows[back]))
323
- back -= 1;
324
- highlight(rows[back]);
325
- }
326
- else if (event.key === "Enter") {
327
- event.preventDefault();
328
- travel(current);
329
- }
330
- else if (event.key === "Escape") {
331
- event.preventDefault();
332
- // The words first; an empty box lets go of the keys.
333
- if (q)
334
- setView((stop) => withoutSearch(stop));
335
- else
336
- input.current?.blur();
337
- }
338
- }, style: {
465
+ setView((stop) => withQuery(stop, typed.length > 0 ? typed : null));
466
+ }, onFocus: () => setOpen(true), onBlur: () => setOpen(false), onKeyDown: onKeyDown, style: {
339
467
  width: "100%",
340
468
  boxSizing: "border-box",
341
469
  minWidth: 0,
@@ -348,35 +476,136 @@ export function FindBox({ compact = false }) {
348
476
  background: "var(--graview-panel)",
349
477
  border: `1px solid ${q ? "var(--graview-accent)" : "var(--graview-edge)"}`,
350
478
  borderRadius: 8,
351
- } }), _jsx("span", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, "data-testid": "find-count", children: q.trim() ? announce : "" }), _jsx("span", { ref: holder, tabIndex: -1, role: "status", style: VISUALLY_HIDDEN, "data-testid": "find-landing" }), _jsxs("div", { ...popover.pane, id: strip, role: "listbox", "aria-label": q ? `What “${q}” finds` : "What the words find", "data-testid": "find-strip", hidden: !expanded,
479
+ } }), _jsx("span", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, "data-testid": "find-count", children: q.trim() ? announce : "" }), _jsx("span", { ref: holder, tabIndex: -1, role: "status", style: VISUALLY_HIDDEN, "data-testid": "find-landing" }), _jsxs("div", { ...popover.pane, id: strip, role: "listbox", "aria-label": q ? `What “${q}” finds` : "What the words find", "data-testid": "find-strip", "data-graview-find": wide ? "panel" : "narrow", hidden: !expanded,
352
480
  // Mousedown on a row must not blur the box before the click lands.
353
- onMouseDown: (event) => event.preventDefault(), style: compact ? { ...STRIP, ...SHEET } : STRIP, children: [rows.every((row) => "ask" in row) ? (_jsxs("p", { role: "presentation", style: { margin: 0, padding: "10px 12px", color: "var(--graview-ink-muted)", fontSize: "0.875rem" }, "data-testid": "find-nothing", children: ["Nothing here is called \u201C", found?.words || q, "\u201D."] })) : (groups.map(([key, entries]) => (_jsxs("div", { role: "group", "aria-label": headingOf(key, store.schema), children: [_jsxs("div", { role: "presentation", style: HEADING, children: [key !== "rule:" ? _jsx(Mark, { kind: key, accents: brand?.accents }) : null, headingOf(key, store.schema)] }), entries.map((row) => (_jsx("div", { id: row.id, role: "option", "aria-selected": row === current, "data-testid": "find-hit", "data-about": row.hit.about, ...(row.hit.about === "node" && row.hit.near ? { "data-near": "" } : {}), onMouseEnter: () => highlight(row), onClick: () => travel(row), style: {
354
- ...OPTION,
355
- ...(row.hit.about === "act" ? { paddingLeft: 30 } : {}),
356
- background: row === current ? "var(--graview-panel-muted)" : "transparent",
357
- }, children: _jsx(HitLine, { hit: row.hit, flag: flag }) }, row.id)))] }, key)))), asking ? (_jsx("div", { id: asking.id, role: "option", "aria-selected": asking === current || current?.id === asking.id, "data-testid": "find-ask", onMouseEnter: () => highlight(asking), onClick: () => travel(asking), style: {
358
- ...OPTION,
359
- borderTop: "1px solid var(--graview-edge)",
360
- marginTop: 4,
361
- background: current?.id === asking.id ? "var(--graview-panel-muted)" : "transparent",
362
- }, children: _jsxs("span", { children: ["Ask: ", _jsxs("span", { style: { fontWeight: 600 }, children: ["\u2018", asking.ask, "\u2019"] })] }) })) : null, manyFound > MANY_FOUND ? (_jsxs("p", { role: "presentation", style: { margin: 0, padding: "6px 12px 2px", color: "var(--graview-ink-faint)", fontSize: "0.75rem" }, "data-testid": "find-more-words", children: [numberAsRead(manyFound), " found: another word of the name narrows them."] })) : null, found && found.searched.past === false && found.searched.kinds.some((kind) => store.schema.tryDefinition(kind)?.lifecycle) ? (_jsx("p", { role: "presentation", style: { margin: 0, padding: "6px 12px 8px", color: "var(--graview-ink-faint)", fontSize: "0.75rem" }, children: "Current ones; add is:any for the past." })) : null] })] }));
481
+ onMouseDown: (event) => event.preventDefault(),
482
+ // Shown, it is a column — the list, the panel beside it, the keys under them; put away, the popover's own rule hides it.
483
+ style: { ...STRIP, ...(compact ? SHEET : wide && !nothing ? WIDE : null), ...(expanded ? { display: "flex" } : null) }, children: [_jsxs("div", { style: { ...BODY, gridTemplateColumns: wide && !nothing ? `minmax(0, 1fr) ${PANEL_WIDTH}` : "minmax(0, 1fr)" }, children: [_jsxs("div", { ref: list, role: "presentation", "data-testid": "find-list", onMouseMove: point, style: LIST, children: [nothing ? (_jsxs("p", { role: "presentation", style: { margin: 0, padding: "10px 12px", color: "var(--graview-ink-muted)", fontSize: "0.875rem" }, "data-testid": "find-nothing", children: ["Nothing here is called \u201C", found?.words || q, "\u201D."] })) : (groups.map((group) => (_jsxs("div", { role: "group", "aria-label": headingOf(group.key, store.schema), children: [_jsxs("div", { role: "presentation", style: HEADING, children: [group.key !== "rule:" && group.key !== "act:" ? _jsx(Mark, { kind: group.key, accents: brand?.accents }) : null, _jsx("span", { style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: headingOf(group.key, store.schema) }), _jsx("span", { "aria-hidden": "true", style: COUNT, "data-testid": "find-group-count", children: numberAsRead(group.count) })] }), group.rows.map((row) => {
484
+ const hit = row.hit;
485
+ const record = hit.about === "node" ? hit : null;
486
+ const asks = !wide && record !== null && hasActs.has(row.id);
487
+ const under = openActs.get(row.id);
488
+ return (_jsxs(Fragment, { children: [option(row, _jsxs(_Fragment, { children: [_jsx(HitLine, { hit: hit, flag: flag, words: record?.near && record.why.matched ? [...marking, fold(record.why.matched)] : marking }), asks ? (_jsx("span", { "aria-hidden": "true", "data-testid": "find-acts-open", "data-open": under ? "" : undefined, title: under ? "Put its actions away" : "Actions", onClick: (event) => {
489
+ event.stopPropagation();
490
+ toggle(row);
491
+ }, style: { ...MORE, ...(under ? MORE_OPEN : null) }, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [_jsx("circle", { cx: "3.5", cy: "8", r: "1.4", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "8", r: "1.4", fill: "currentColor" }), _jsx("circle", { cx: "12.5", cy: "8", r: "1.4", fill: "currentColor" })] }) })) : null] }), asks ? { paddingRight: 48 } : {}, {
492
+ "data-testid": "find-hit",
493
+ "data-about": hit.about,
494
+ ...(record?.near ? { "data-near": "" } : {}),
495
+ title: titleOf(hit),
496
+ }), under ? (_jsx("div", { role: "group", "aria-label": `What can be done about ${record?.label ?? "it"}`, style: UNDER, children: under.map((act) => actOption(act, true)) })) : null] }, row.id));
497
+ }), group.more
498
+ ? option(group.more, _jsxs("span", { style: ONE_LINE, children: ["All ", numberAsRead(group.more.count), " ", pluralOf(store.schema, group.more.all).toLowerCase(), " matching \u201C", q.trim(), "\u201D"] }), ALL, { "data-testid": "find-all", "data-about": "all" })
499
+ : null] }, group.key)))), asking ? option(asking, _jsxs("span", { style: ONE_LINE, children: ["Ask: ", _jsxs("span", { style: { fontWeight: 600 }, children: ["\u2018", asking.ask, "\u2019"] })] }), { borderTop: "1px solid var(--graview-edge)", marginTop: 4 }, { "data-testid": "find-ask" }) : null, manyFound > MANY_FOUND ? (_jsxs("p", { role: "presentation", style: NOTE, "data-testid": "find-more-words", children: [numberAsRead(manyFound), " found: another word of the name narrows them."] })) : null, found && found.searched.past === false && found.searched.kinds.some((kind) => store.schema.tryDefinition(kind)?.lifecycle) ? (_jsx("p", { role: "presentation", style: NOTE, children: "Current ones; add is:any for the past." })) : null] }), wide && !nothing && subject ? (_jsxs("div", { role: "group", "aria-label": "The one lit", "data-testid": "find-panel", onMouseEnter: settle, onMouseMove: point, style: PANEL, children: [_jsx(Said, { row: subject, schema: store.schema, accents: brand?.accents, marking: marking, q: q.trim(), flag: flag }), panelActs.length > 0 ? (_jsxs("div", { role: "group", "aria-label": "Actions", style: { display: "grid", gap: 4 }, children: [_jsx("div", { role: "presentation", style: { ...HEADING, padding: "10px 0 4px" }, children: "Actions" }), panelActs.map((act) => actOption(act, false))] })) : null] })) : null] }), compact ? null : (_jsxs("div", { "aria-hidden": "true", style: FOOT, "data-testid": "find-keys", children: [_jsxs("span", { children: [_jsx("kbd", { style: KBD, children: "\u2191" }), _jsx("kbd", { style: KBD, children: "\u2193" }), " move"] }), _jsxs("span", { children: [_jsx("kbd", { style: KBD, children: "\u21B5" }), " open"] }), _jsxs("span", { children: [_jsx("kbd", { style: KBD, children: "\u2192" }), " actions"] }), _jsxs("span", { children: [_jsx("kbd", { style: KBD, children: "esc" }), " close"] })] }))] })] }));
500
+ }
501
+ /** The row lit, said whole in the desk's panel: what it is, its name, and why the words found it. */
502
+ function Said({ row, schema, accents, marking, q, flag }) {
503
+ let kind = null;
504
+ let over = "";
505
+ let name;
506
+ let lines = null;
507
+ if ("ask" in row) {
508
+ over = "Ask";
509
+ name = `‘${row.ask}’`;
510
+ lines = _jsx("p", { style: PANEL_LINE, children: "Asks it of what is selected; the answer comes on a card beside it." });
511
+ }
512
+ else if ("all" in row) {
513
+ kind = row.all;
514
+ over = pluralLabel(schema, row.all);
515
+ name = `All ${numberAsRead(row.count)} matching “${q}”`;
516
+ lines = _jsxs("p", { style: PANEL_LINE, children: ["Opens the ", pluralOf(schema, row.all).toLowerCase(), ", narrowed by these words."] });
517
+ }
518
+ else {
519
+ const hit = row.hit;
520
+ switch (hit.about) {
521
+ case "node": {
522
+ kind = hit.kind;
523
+ over = capitalized(nounOf(schema.tryDefinition(hit.kind), hit.kind));
524
+ name = (_jsxs(_Fragment, { children: [hit.flagged ? _jsxs("span", { style: { color: "var(--graview-warn)" }, children: [flag, " "] }) : null, _jsx(Marked, { text: hit.label, words: marking })] }));
525
+ const notes = [hit.apart, hit.current ? null : "past", hit.archived ? "set aside" : null].filter(Boolean).join(" · ");
526
+ lines = (_jsxs(_Fragment, { children: [notes ? _jsx("p", { style: PANEL_LINE, children: capitalized(notes) }) : null, hit.near ? (_jsxs("p", { style: PANEL_LINE, children: ["Near \u201C", hit.why.asked, "\u201D", hit.why.field !== "label" ? ` — ${hit.why.reading.toLowerCase()}: ${hit.why.fragment}` : ""] })) : hit.why.field !== "label" ? (_jsxs("p", { style: PANEL_LINE, children: [_jsx("span", { style: { color: "var(--graview-ink-faint)" }, children: hit.why.reading }), _jsx("br", {}), _jsx(Marked, { text: hit.why.fragment, words: marking })] })) : null] }));
527
+ break;
528
+ }
529
+ case "kind":
530
+ kind = hit.kind;
531
+ over = "Kind";
532
+ name = hit.label;
533
+ lines = _jsxs("p", { style: PANEL_LINE, children: ["Goes into the ", hit.label.toLowerCase(), ", all of them", hit.count > 0 ? `; ${numberAsRead(hit.count)} match these words` : "", "."] });
534
+ break;
535
+ case "place":
536
+ kind = hit.kind;
537
+ over = "Place";
538
+ name = hit.title;
539
+ lines = _jsxs("p", { style: PANEL_LINE, children: ["A picture of the ", pluralOf(schema, hit.kind).toLowerCase(), "."] });
540
+ break;
541
+ case "rule":
542
+ over = "Rule";
543
+ name = hit.label;
544
+ lines = _jsx("p", { style: PANEL_LINE, children: "Selects what it flags." });
545
+ break;
546
+ case "act":
547
+ over = "Act";
548
+ name = hit.title;
549
+ break;
550
+ }
551
+ }
552
+ return (_jsxs("div", { role: "presentation", "data-testid": "find-said", style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { ...HEADING, padding: 0 }, children: [kind ? _jsx(Mark, { kind: kind, accents: accents }) : null, over] }), _jsx("div", { style: PANEL_NAME, children: name }), lines] }));
363
553
  }
364
- function HitLine({ hit, flag }) {
554
+ function HitLine({ hit, flag, words }) {
365
555
  switch (hit.about) {
366
556
  case "node":
367
- return (_jsxs(_Fragment, { children: [_jsxs("span", { style: { fontWeight: 600, overflowWrap: "anywhere" }, children: [hit.flagged ? _jsxs("span", { style: { color: "var(--graview-warn)" }, children: [flag, " "] }) : null, hit.label, hit.apart ? _jsxs("span", { style: MUTED, children: [" \u00B7 ", hit.apart] }) : null, hit.current ? null : _jsx("span", { style: MUTED, children: " \u00B7 past" })] }), hit.near ? (
557
+ return (_jsxs(_Fragment, { children: [_jsxs("span", { style: TITLE, children: [hit.flagged ? _jsxs("span", { style: { color: "var(--graview-warn)" }, children: [flag, " "] }) : null, _jsx(Marked, { text: hit.label, words: words }), hit.apart ? _jsxs("span", { style: MUTED, children: [" \u00B7 ", hit.apart] }) : null, hit.current ? null : _jsx("span", { style: MUTED, children: " \u00B7 past" })] }), hit.near ? (
368
558
  // Found by a word near the one typed, and said so: "Near “florist” — tags: flowers".
369
- _jsxs("span", { style: MUTED, "data-testid": "find-near", children: ["Near \u201C", hit.why.asked, "\u201D", hit.why.field !== "label" ? ` — ${hit.why.reading.toLowerCase()}: ${hit.why.fragment}` : ""] })) : hit.why.field !== "label" ? (_jsxs("span", { style: MUTED, children: [hit.why.reading.toLowerCase(), ": ", hit.why.fragment] })) : null] }));
559
+ _jsxs("span", { style: { ...ONE_LINE, ...MUTED }, "data-testid": "find-near", children: ["Near \u201C", hit.why.asked, "\u201D", hit.why.field !== "label" ? ` — ${hit.why.reading.toLowerCase()}: ` : "", hit.why.field !== "label" ? _jsx(Marked, { text: hit.why.fragment, words: words }) : null] })) : hit.why.field !== "label" ? (_jsxs("span", { style: { ...ONE_LINE, ...MUTED }, children: [hit.why.reading.toLowerCase(), ": ", _jsx(Marked, { text: hit.why.fragment, words: words })] })) : null] }));
370
560
  case "kind":
371
- return (_jsxs("span", { children: [hit.label, _jsxs("span", { style: MUTED, children: [" \u00B7 ", hit.count, " match"] })] }));
561
+ return (_jsxs("span", { style: ONE_LINE, children: [_jsx(Marked, { text: hit.label, words: words }), _jsxs("span", { style: MUTED, children: [" \u00B7 ", hit.count, " match"] })] }));
372
562
  case "place":
373
- return _jsxs("span", { children: ["Go to ", hit.title] });
563
+ return (_jsxs("span", { style: ONE_LINE, children: ["Go to ", _jsx(Marked, { text: hit.title, words: words })] }));
374
564
  case "rule":
375
- return _jsxs("span", { children: ["The rule \u201C", hit.label, "\u201D"] });
565
+ return (_jsxs("span", { style: ONE_LINE, children: ["The rule \u201C", _jsx(Marked, { text: hit.label, words: words }), "\u201D"] }));
376
566
  case "act":
377
- return (_jsxs("span", { style: hit.destructive ? { color: "var(--graview-warn)" } : undefined, children: [hit.title, _jsxs("span", { style: MUTED, children: [" \u00B7 ", hit.why.field === "title" ? "act" : `on ${hit.why.fragment}`] })] }));
567
+ return _jsx("span", { style: ONE_LINE, children: hit.title });
568
+ }
569
+ }
570
+ /**
571
+ * THE WORDS THAT MATCHED, MARKED: each word of the text that a word typed
572
+ * begins — "track" in "on-site tracking" — as the matcher reads them, case
573
+ * and accents aside, so the mark is where the match was.
574
+ */
575
+ function markedParts(text, words) {
576
+ if (words.length === 0)
577
+ return [{ text }];
578
+ const parts = [];
579
+ let last = 0;
580
+ for (const match of text.matchAll(/[\p{L}\p{N}]+/gu)) {
581
+ const token = match[0];
582
+ const folded = fold(token).replace(/ /g, "");
583
+ const word = words.filter((one) => folded.startsWith(one)).sort((a, b) => b.length - a.length)[0];
584
+ if (!word)
585
+ continue;
586
+ // Where folding kept its length, the letters typed; where it did not (a ligature, a dotted I), the whole word.
587
+ const length = folded.length === token.length ? word.length : token.length;
588
+ const start = match.index ?? 0;
589
+ if (start > last)
590
+ parts.push({ text: text.slice(last, start) });
591
+ parts.push({ text: token.slice(0, length), marked: true });
592
+ last = start + length;
378
593
  }
594
+ if (last < text.length)
595
+ parts.push({ text: text.slice(last) });
596
+ return parts;
597
+ }
598
+ function Marked({ text, words }) {
599
+ return (_jsx(_Fragment, { children: markedParts(text, words).map((part, at) => part.marked ? (_jsx("mark", { style: MARK, children: part.text }, at)) : (_jsx(Fragment, { children: part.text }, at))) }));
600
+ }
601
+ /** A row's whole text, for the pointer that rests on one cut short. */
602
+ function titleOf(hit) {
603
+ if (hit.about !== "node")
604
+ return "";
605
+ const why = hit.near ? `Near “${hit.why.asked}”` : hit.why.field !== "label" ? `${hit.why.reading}: ${hit.why.fragment}` : "";
606
+ return why ? `${hit.label}\n${why}` : hit.label;
379
607
  }
608
+ const capitalized = (text) => (text ? text[0].toUpperCase() + text.slice(1) : text);
380
609
  function Mark({ kind, accents }) {
381
610
  const hue = Math.round(hueFor(kind, accents));
382
611
  return _jsx("span", { "aria-hidden": "true", style: { width: 8, height: 8, borderRadius: 2, flex: "0 0 auto", background: `hsl(${hue} 55% 52%)` } });
@@ -386,8 +615,7 @@ function headingOf(key, schema) {
386
615
  return "Rules";
387
616
  if (key === "act:")
388
617
  return "Acts";
389
- const definition = schema.tryDefinition(key);
390
- return definition?.plural ?? `${key}s`;
618
+ return pluralLabel(schema, key);
391
619
  }
392
620
  /** "3 tasks, 1 list" — what a screen reader hears as the count changes. */
393
621
  function spoken(byKind, hits, schema) {
@@ -404,21 +632,34 @@ function spoken(byKind, hits, schema) {
404
632
  const FIND_PAGE = 10;
405
633
  /** From how many records an answer asks for another word rather than being walked. */
406
634
  const MANY_FOUND = 12;
635
+ /** Where several kinds share the strip, how many of each it shows before "All N …". */
636
+ const GROUP_CAP = 5;
637
+ /** How many acts a record offers in the strip; the rest are in its menu. */
638
+ const ACTS_MOST = 6;
639
+ /** How long the panel waits on a pointer crossing rows before it says the row lit (ms). */
640
+ const AIM = 140;
641
+ /** From what viewport width the strip has room for the panel beside the list. */
642
+ const WIDE_FROM = 760;
643
+ const PANEL_WIDTH = "15.5rem";
407
644
  const STRIP = {
408
645
  ...POPOVER_STYLE,
646
+ flexDirection: "column",
409
647
  width: "min(26rem, calc(100vw - 24px))",
410
- maxHeight: "min(60vh, 28rem)",
411
- overflowY: "auto",
648
+ maxHeight: "min(64vh, 30rem)",
649
+ overflow: "hidden",
412
650
  boxSizing: "border-box",
413
- background: "var(--graview-panel)",
651
+ // The panel's color over the ground's: where the scheme's panel is a glass, the picture behind does not read through the words.
652
+ background: "linear-gradient(var(--graview-panel), var(--graview-panel)) var(--graview-ground, var(--graview-panel))",
414
653
  borderTop: "1px solid var(--graview-edge)",
415
654
  borderRight: "1px solid var(--graview-edge)",
416
655
  borderBottom: "1px solid var(--graview-edge)",
417
656
  borderLeft: "1px solid var(--graview-edge)",
418
657
  borderRadius: 10,
419
658
  boxShadow: "0 10px 28px rgb(0 0 0 / 0.18)",
420
- padding: "4px 0",
659
+ padding: 0,
421
660
  };
661
+ /** On a desk, room for the panel beside the list. */
662
+ const WIDE = { width: "min(42rem, calc(100vw - 24px))" };
422
663
  /** The gap `useTopLayer` leaves under an anchor, taken back so the sheet meets the bar's edge. */
423
664
  const SHEET_GAP = 6;
424
665
  /** On a phone's bar the strip is a sheet under the bar, the bar's own width (`--graview-bar-width`, the screen's where no bar says it). */
@@ -429,26 +670,127 @@ const SHEET = {
429
670
  borderLeft: "none",
430
671
  borderRight: "none",
431
672
  };
673
+ const BODY = { display: "grid", gridTemplateRows: "minmax(0, 1fr)", flex: "1 1 auto", minHeight: 0 };
674
+ const LIST = { overflowY: "auto", overflowX: "hidden", minHeight: 0, padding: "4px 0", overscrollBehavior: "contain" };
675
+ const PANEL = {
676
+ overflowY: "auto",
677
+ minHeight: 0,
678
+ padding: "12px 14px",
679
+ borderLeft: "1px solid var(--graview-edge)",
680
+ background: "var(--graview-panel-muted)",
681
+ display: "flex",
682
+ flexDirection: "column",
683
+ gap: 4,
684
+ };
685
+ const PANEL_NAME = { fontWeight: 600, fontSize: "0.9375rem", lineHeight: 1.35, color: "var(--graview-ink)", overflowWrap: "anywhere" };
686
+ const PANEL_LINE = { margin: 0, fontSize: "0.8125rem", lineHeight: 1.45, color: "var(--graview-ink-muted)", overflowWrap: "anywhere" };
432
687
  const HEADING = {
433
688
  display: "flex",
434
689
  alignItems: "center",
435
690
  gap: 6,
436
- padding: "8px 12px 2px",
691
+ padding: "10px 12px 4px",
437
692
  fontSize: "0.6875rem",
693
+ fontWeight: 600,
438
694
  letterSpacing: "0.06em",
439
695
  textTransform: "uppercase",
440
696
  color: "var(--graview-ink-muted)",
441
697
  };
698
+ const COUNT = { marginLeft: "auto", fontWeight: 400, letterSpacing: 0, fontVariantNumeric: "tabular-nums", color: "var(--graview-ink-faint)" };
442
699
  const OPTION = {
700
+ position: "relative",
443
701
  display: "grid",
444
702
  gap: 1,
445
- minHeight: 32,
703
+ minHeight: 34,
446
704
  padding: "5px 12px",
447
705
  alignContent: "center",
448
706
  cursor: "pointer",
449
707
  fontSize: "0.875rem",
450
708
  lineHeight: 1.35,
451
709
  color: "var(--graview-ink)",
710
+ minWidth: 0,
711
+ };
712
+ /** The row lit: a wash and a bar at its edge, in the app's accent. */
713
+ const LIT = { background: "color-mix(in srgb, var(--graview-accent) 12%, var(--graview-panel))", boxShadow: "inset 3px 0 0 var(--graview-accent)" };
714
+ const ALL = { color: "var(--graview-accent)", fontSize: "0.8125rem", minHeight: 30 };
715
+ const ONE_LINE = { display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" };
716
+ /** A record's name: two lines at most, so a long name still shows the word that found it, and never more, so no row is a paragraph. */
717
+ const TITLE = {
718
+ display: "-webkit-box",
719
+ WebkitBoxOrient: "vertical",
720
+ WebkitLineClamp: 2,
721
+ minWidth: 0,
722
+ overflow: "hidden",
723
+ overflowWrap: "anywhere",
724
+ fontWeight: 600,
452
725
  };
453
726
  const MUTED = { color: "var(--graview-ink-muted)", fontSize: "0.8125rem", fontWeight: 400 };
727
+ const MARK = {
728
+ background: "color-mix(in srgb, var(--graview-accent) 24%, transparent)",
729
+ color: "inherit",
730
+ borderRadius: 2,
731
+ padding: 0,
732
+ boxShadow: "0 1px 0 color-mix(in srgb, var(--graview-accent) 70%, transparent)",
733
+ };
734
+ const ACT = {
735
+ display: "flex",
736
+ alignItems: "center",
737
+ minHeight: 32,
738
+ padding: "4px 10px",
739
+ boxSizing: "border-box",
740
+ borderWidth: 1,
741
+ borderStyle: "solid",
742
+ borderColor: "var(--graview-edge)",
743
+ borderRadius: 6,
744
+ background: "var(--graview-panel)",
745
+ color: "var(--graview-ink)",
746
+ fontSize: "0.8125rem",
747
+ lineHeight: 1.3,
748
+ cursor: "pointer",
749
+ overflowWrap: "anywhere",
750
+ };
751
+ const ACT_UNDER = { ...ACT, minHeight: 32, margin: "0 12px 4px 24px" };
752
+ const ACT_LIT = { borderColor: "var(--graview-accent)", boxShadow: "inset 0 0 0 1px var(--graview-accent)", background: "color-mix(in srgb, var(--graview-accent) 12%, var(--graview-panel))" };
753
+ const UNDER = { display: "grid", padding: "2px 0 6px" };
754
+ /** A narrow strip's "⋯": its acts, asked for. Over the row's end, in room the row keeps for it, so the row's words never run under it. */
755
+ const MORE = {
756
+ position: "absolute",
757
+ top: "50%",
758
+ right: 6,
759
+ transform: "translateY(-50%)",
760
+ width: 36,
761
+ height: 32,
762
+ display: "grid",
763
+ placeItems: "center",
764
+ borderRadius: 6,
765
+ color: "var(--graview-ink-muted)",
766
+ borderWidth: 1,
767
+ borderStyle: "solid",
768
+ borderColor: "transparent",
769
+ };
770
+ const MORE_OPEN = { color: "var(--graview-accent)", borderColor: "var(--graview-edge)", background: "var(--graview-panel)" };
771
+ const NOTE = { margin: 0, padding: "6px 12px 4px", color: "var(--graview-ink-faint)", fontSize: "0.75rem" };
772
+ const FOOT = {
773
+ display: "flex",
774
+ flexWrap: "wrap",
775
+ gap: "4px 14px",
776
+ padding: "7px 12px",
777
+ borderTop: "1px solid var(--graview-edge)",
778
+ color: "var(--graview-ink-faint)",
779
+ fontSize: "0.75rem",
780
+ flex: "0 0 auto",
781
+ };
782
+ const KBD = {
783
+ display: "inline-block",
784
+ minWidth: 16,
785
+ padding: "0 4px",
786
+ marginRight: 3,
787
+ border: "1px solid var(--graview-edge)",
788
+ borderRadius: 4,
789
+ font: "inherit",
790
+ fontSize: "0.6875rem",
791
+ lineHeight: "16px",
792
+ textAlign: "center",
793
+ color: "var(--graview-ink-muted)",
794
+ background: "var(--graview-panel)",
795
+ };
454
796
  //# sourceMappingURL=find.js.map