/** * What a right-click offers, as data. * * Four things can be right-clicked — a subject, a relationship, the empty * canvas, a row in the rail — and one rule runs through all four: **operations * that edit the view are separated from operations that edit the model.** * * The rule is not decoration. Removing a subject from a view rewrites one * projection and leaves every other view alone; deleting it from the model * takes every relationship naming it and changes every view that drew it. * Rendered as neighbours in a flat list, the second is one slip away from * someone who meant the first. So the groups are labelled, they are ordered * view-before-model, and anything model-destructive sits last, behind a rule, * in `--failure`. * * Items carry an INTENT rather than a callback. A closure cannot be compared, * so a menu built from closures is a menu no test can read; a discriminated * union can be asserted on exactly, and the shell has nothing left to decide * but which reducer to hand it to. `presentationActionsFor` returns actions * for the same reason. * * Pure, and outside any component, because this repo renders React through * `renderToStaticMarkup` and has no DOM test environment. */ import type { VisualQuestionEntry } from "../adapters/visual/wire.js"; import { type QuestionVerb } from "./question-verbs.js"; import type { CanvasGraph } from "../graph-projection.js"; import type { VisualKindOption } from "../adapters/visual/protocol-contract.js"; import type { ActiveViewMembership } from "./state.js"; /** The id of the "All subjects" row, which is the absence of a view. */ export declare const ALL_SUBJECTS_VIEW = ""; export type ContextMenuTarget = { readonly kind: "subject"; readonly id: string; } | { readonly kind: "relationship"; readonly id: string; } | { readonly kind: "canvas"; } | { readonly kind: "view-row"; readonly id: string; } | { readonly kind: "model-row"; readonly id: string; }; export type ContextMenuIntent = { readonly type: "subject.inspect"; readonly id: string; } | { readonly type: "subject.connect"; readonly from: string; } | { readonly type: "subject.delete"; readonly id: string; } | { readonly type: "relationship.inspect"; readonly id: string; } | { readonly type: "relationship.retype"; readonly id: string; readonly kind: string; } | { readonly type: "relationship.delete"; readonly id: string; } | { readonly type: "canvas.draft-subject"; } | { readonly type: "view.open"; readonly id: string; } /** * Narrow the canvas to this subject and everything one hop from it (#407). * A view operation, so it lives in the view group and survives `readOnly`. * It sets the same narrowed state every other filter sets and is cleared by * the same "Show all subjects" — one narrowing concept, one escape. */ | { readonly type: "subject.focus"; readonly id: string; } /** * Narrow the canvas to what this pattern instance HOLDS (#473 phase 2). * * Distinct from `subject.focus`, which walks one hop of relationships. This * one asks the model what is inside the box, so it answers with the pattern's * own parts rather than with whatever happens to be adjacent. */ | { readonly type: "subject.focus-instance"; readonly id: string; } /** The relationship and its two endpoints. Nothing further (#407). */ | { readonly type: "relationship.focus"; readonly id: string; } /** * Shut a box, open it, or open it and everything inside it (#473). * * `subject.unfold` reveals ONE level: instances nested inside appear folded, * so opening a box is a step rather than a cliff. `unfold-all` is the whole * subtree, for a reader who wants the detail now. */ | { readonly type: "subject.fold"; readonly id: string; } | { readonly type: "subject.unfold"; readonly id: string; } | { readonly type: "subject.unfold-all"; readonly id: string; } /** The same, over whatever the reader has selected. */ | { readonly type: "selection.fold"; readonly ids: readonly string[]; } | { readonly type: "selection.unfold"; readonly ids: readonly string[]; } /** Every instance in the view at once. */ | { readonly type: "canvas.fold-all"; } | { readonly type: "canvas.unfold-all"; } | { readonly type: "view.clear"; } | { readonly type: "view.new"; } /** * A new view in a folder that does not exist yet. A folder is a label on a * document (ADR 0104), so an empty one cannot persist and this is what * "New folder" honestly is: name the folder, then put the first view in it. */ | { readonly type: "view.new-folder"; } /** * A new view in the folder this one occupies. Folders come from projection * paths (#245), so the only way to name one is to point at a view already in * it — which also means the folder is one the manifest demonstrably reaches. */ | { readonly type: "view.new-in-folder"; readonly id: string; } /** Retitles the view. The id and the path do not move — see `viewRowMenu`. */ | { readonly type: "view.rename"; readonly id: string; } | { readonly type: "view.duplicate"; readonly id: string; } | { readonly type: "view.copy-path"; readonly id: string; } | { readonly type: "canvas.export-png"; } | { readonly type: "view.delete"; readonly id: string; } /** * One subject into or out of the ACTIVE view's membership list. Only a view * that enumerates `subjects:` has one — see `ContextMenuContext.membership`. */ | { readonly type: "view.add-subject"; readonly id: string; } | { readonly type: "view.remove-subject"; readonly id: string; } /** * Answer an open question with the gesture its trigger implies (#516, * ADR 0150). The verb is resolved here, in the pure model, so the menu's * reduction is a lookup and a test can read it; `null` means the row has * no gesture and the shell puts the subject's properties in front instead. */ | { readonly type: "question.answer"; readonly questionId: string; readonly subjectId: string | null; readonly verb: QuestionVerb | null; } /** Hand the question to whoever answers questions for this host (ADR 0151). */ | { readonly type: "question.delegate"; readonly questionId: string; readonly subjectId: string | null; readonly question: string; }; /** Which half of the split an operation belongs to. */ export type ContextMenuScope = "view" | "model"; export interface ContextMenuItem { /** Stable across renders and readable in a test. */ readonly key: string; readonly label: string; readonly intent: ContextMenuIntent; /** Marked as the value already in force, for a group that names a choice. */ readonly current?: true; } export interface ContextMenuGroup { readonly key: string; readonly scope: ContextMenuScope; /** The heading, or null for a group that needs no name. */ readonly label: string | null; /** * Whether this group removes authored text. Exactly one group per menu may * say so, it is always last, and it is the group that draws in `--failure`. */ readonly destructive: boolean; readonly items: readonly ContextMenuItem[]; } export interface ContextMenuContext { readonly graph: CanvasGraph | null; /** * The interview overlay (#516): a subject's open questions become a group * in its menu, and the workspace's become a group in the canvas menu. * Absent on a host that ships no overlay, and the menus are as they were. */ readonly questions?: { readonly workspace: readonly VisualQuestionEntry[]; readonly subjects: Readonly>; }; /** * The label of the delegate door for this host (ADR 0151): "Answer via * agent", "Answer via assistant" or "Copy for my assistant". Absent, the * group offers no door. */ readonly delegateLabel?: string; readonly relationshipKinds: readonly VisualKindOption[]; /** The view the canvas is showing, or `ALL_SUBJECTS_VIEW`. */ readonly activeViewId: string; /** Whether anything at all is narrowing the canvas right now. */ readonly filtered: boolean; /** * Where clearing a focus will return to, named for the menu, or absent when * clearing goes to everything (#407). Optional so that every existing * constructor of this context keeps compiling: a required field here would * be free for readers and a typecheck break for constructors, which is the * first rule in CONTRIBUTING.md. */ readonly focusReturnLabel?: string; /** * How the active view can be told what it holds, or `null` when no view is * active and there is nothing to tell. * * A view that enumerates its subjects is told by its list; one that * describes them with facets is told by `exclude` (#267, ADR 0122). A * faceted view used to be `null` here, on the reasoning that an item which * could only ever do nothing is worse than no item (#255) - true of adding a * subject to a rule, and never true of taking one out of it. */ readonly membership: ActiveViewMembership | null; /** * Which subjects draw folded, and which of them contain anything (#473). * * Both optional, so every existing constructor of this context keeps * compiling - the same reason `focusReturnLabel` above is optional, and * CONTRIBUTING's first rule. */ readonly folded?: ReadonlySet; readonly containerIds?: ReadonlySet; /** * Which subjects the model knows as pattern INSTANCES (#473 phase 2). * * Not `containerIds`: a subject contains things when the view's nesting puts * them inside it, which a plain component with a composition does. Only an * instance has parts to focus on, and offering the item on anything else * would compose a query that selects one subject. * * Optional for the reason every field above it is: a required addition here * is free for readers and a typecheck break for constructors. */ readonly instanceIds?: ReadonlySet; /** What the reader has selected, when it is more than one thing. */ readonly selectedIds?: readonly string[]; /** * A viewer, not an author (#298, ADR 0117). A read-only menu keeps the items * that read or navigate and drops every item whose intent stages a change - * absent, never disabled. */ readonly readOnly?: boolean; } /** * The menu for a target, already ordered: view groups first, model groups * after them, and the destructive group last of all. * * Empty when the target has gone — a commit can replace the model between the * right-click and the render, and a menu over a subject that no longer exists * should not be drawn rather than drawn with dead items. */ export declare const contextMenuFor: (target: ContextMenuTarget, context: ContextMenuContext) => readonly ContextMenuGroup[]; export interface MenuPlacement { readonly left: number; readonly top: number; } /** * Where the menu is drawn, given where the pointer was. * * A menu opened near the right or bottom edge would otherwise run off the * viewport with no scrollbar to reach it, which is how a right-click on the * last row of a rail loses its own delete item. Flipping to the other side of * the pointer keeps the menu beside what was clicked rather than over it; * clamping to zero is the last resort for a menu taller than the window. */ export declare const placeMenu: (pointer: { readonly x: number; readonly y: number; }, size: { readonly width: number; readonly height: number; }, viewport: { readonly width: number; readonly height: number; }) => MenuPlacement;