import type { CanvasEdge, CanvasGraph, CanvasNode } from "../graph-projection.js"; import { type NestingKind } from "../nesting.js"; import { type FoldMode } from "../fold-tree.js"; import { type LayoutDirection } from "../layout-direction.js"; import { type LayoutMode } from "../layout-mode.js"; import { type StylePresetId } from "./style-presets.js"; import type { DecorationMap } from "./graph-canvas.js"; import type { ContextMenuTarget } from "./context-menu-model.js"; import type { BottomPanelTabId } from "./query-panel.js"; export interface SelectedElement { readonly type: "element"; readonly id: string; readonly title: string; readonly kind: string; readonly description: string | null; } export interface SelectedRelationship { readonly type: "relationship"; readonly id: string; readonly sourceId: string; readonly sourceTitle: string; readonly targetId: string; readonly targetTitle: string; readonly label: string | null; readonly description: string | null; readonly kind: string; } export type SelectedDiagramSubject = SelectedElement | SelectedRelationship; /** * A relationship being drawn. The source is chosen first and the target * second, rather than dragged, so every step is a state transition a test can * make and a keyboard can reach. * * The kinds on offer are NOT held here: they are a function of the two * endpoints and the ArchiMate table, so they are derived on render by * `connectableKinds`. Storing them would let a stale palette outlive the model * frame it was computed from. */ export interface ConnectionDraft { readonly from: string; readonly to: string | null; /** * A question's answer shape, when the tool was armed from one (#515): * the relationship kinds the trigger names, qualified, narrowing what the * panel offers first; and which end the subject is. `incoming` means the * drawn relationship runs from the target TO `from`, because the question * asked what realizes, influences or serves this subject, not what it * reaches. Absent on a tool armed by hand, which offers everything the * table permits, from `from` to the target. */ readonly kinds?: readonly string[]; readonly direction?: "outgoing" | "incoming"; } export declare const normalizeSelectedElement: (node: CanvasNode) => SelectedElement; export declare const normalizeSelectedRelationship: (edge: CanvasEdge, nodeTitles: ReadonlyMap) => SelectedRelationship; /** * What the imperative methods read at the moment they are called (#297, * ADR 0118). A getter rather than a captured value, because the pointer is * handed up once and the graph changes under it with every commit - a method * bound to the model that existed at mount time would answer for a model * that is gone. */ export interface EditorPointerContext { readonly graph: CanvasGraph | null; readonly readOnly: boolean; /** * What the staged changeset pinned: path -> the revision that path held when * the first edit against it was staged (#444). Empty when nothing is staged. * * The pointer only reports these; it never compares them. A revision is * opaque and only the store that minted it may compare two (ADR 0100), so * the staleness question is answered host-side. */ readonly stagedPins: Readonly>; /** * The views the model lists (#523, ADR 0152), so `showView` can refuse an * id that names none of them the way `select` refuses an unknown subject. * Empty until the model arrives. */ readonly views: readonly { readonly id: string; }[]; } /** * The host's way of pointing at the canvas (#297, ADR 0118): three methods, * each the programmatic twin of a gesture the surface already has - a canvas * tap, a palette pick, the inspector's Connect. Every method answers with * whether it acted; false means nothing moved - the id named nothing, the * model has not arrived, or the mount is a viewer (#298) and the gesture * would have reached for the pen. */ export interface EditorPointer { readonly select: (subjectId: string) => boolean; readonly openDraft: (options?: { readonly kind?: string; }) => boolean; readonly startConnection: (fromSubjectId: string) => boolean; /** * Opens the named view exactly as picking it in the rail does (#523, * ADR 0152): the local navigation and the word to the host, together. * Reading, so a viewer accepts it. False for an id the model lists no view * under, and before the model arrives. */ readonly showView: (viewId: string) => boolean; /** * Replaces the host's per-subject marks wholesale (#314, ADR 0119) - the * map is the unit of exchange, never merged into what stood. Unlike its * siblings this one needs no model and no pen: the marks are client state * drawn when the graph (or its successor) is on screen, and decorating is * reading, so a viewer accepts them too. It answers false only where every * method does - a handle before the shell's first render or after disposal. */ readonly setDecorations: (decorations: DecorationMap) => boolean; /** * The staged changeset's pins, for a host about to refresh the canvas * (#444). Reported, never interpreted: the host owns the store that minted * these revisions and is the only party that may compare them (ADR 0100). */ readonly stagedPins: () => Readonly>; } /** * Builds the pointer over the shell's own dispatchers. Nothing here is a * second write path: every method dispatches the same workspace action its * on-screen twin dispatches, so what a host can reach is exactly what a * reviewer can reach, and anything staged still lands through the changeset. */ export declare const editorPointerFor: (context: () => EditorPointerContext, dispatch: (action: VisualWorkspaceAction) => void, seedDraftKind: (kind: string | undefined) => void, replaceDecorations: (decorations: DecorationMap) => void, navigate: (viewId: string) => void) => EditorPointer; /** * The right column's sections, in the order they stack (#249, ADR-free: the * design settles the order and nothing derives it). * * The palette leads (#295): it is the tool that makes subjects, and tools * read above inspection - a reviewer reaches for what to add before what is * selected. Chat is last because it is pinned at the foot and owns the * session's own control - the reviewer ends the conversation beside the * conversation, not from a strip that carries identity and nothing else. */ export declare const RIGHT_SECTIONS: readonly ["palette", "properties", "questions", "changes", "chat"]; export type RightSectionId = (typeof RIGHT_SECTIONS)[number]; /** How short a section may be dragged before it is just a header. */ export declare const SECTION_MIN_HEIGHT = 96; /** Room the sections above one must keep, however far it is dragged. */ export declare const SECTION_HEADROOM = 160; export declare const sectionHeightBounds: (viewportHeight: number) => { min: number; max: number; }; export declare const clampSectionHeight: (height: number, viewportHeight: number) => number; export declare const CONVERSATION_MIN_WIDTH = 320; export declare const CONVERSATION_MAX_WIDTH = 640; export interface VisualWorkspaceState { /** * The right column: one width, whether the reviewer has put the whole * column away, and the sections stacked inside it. * * The sections are collapsible one by one, and a reviewer who wants room * shuts sections rather than the column - shut headers say what is behind * them (#249). `hidden` is the further step for the moments that want the * whole canvas - presenting, a projector, a laptop - and the reopen strip * that stands in for the column carries what a shut header would have * said: the unread count and a waiting choice (#294). */ /** * Whether the reviewer has put the left rail away (#294's shape, applied to * the other side of the workspace). * * A boolean and not a width, because the rail is a FIXED column rather than * a dragged one: it holds names at one size, so there is no dragged width to * restore and nothing for a mode-beside-width to protect. The branches * inside it already collapse one by one; this is the further step for the * moments that want the whole canvas. */ readonly railHidden: boolean; readonly conversation: { readonly width: number; /** * Whether the reviewer has put the whole column away (#294). A mode * beside the width rather than a zero width, so the width stays what it * was and reopening restores it - sections and splitters intact. The * presenting moments are exactly when this is used, and presentation * must not cost the reviewer their layout. */ readonly hidden: boolean; readonly unread: number; /** The sections the reviewer has shut. Held as what is CLOSED so a * section added later arrives open rather than hidden behind a default * nobody chose - the same rule the rail's branches follow. */ readonly collapsed: readonly RightSectionId[]; /** Heights for the two sections that have one; properties takes the rest. */ readonly changesHeight: number; readonly chatHeight: number; }; /** * The viewport this state was last clamped against. Presentation reads its * resize bounds from here rather than from `window`, so every viewport change * reaches the separator's reported minimum and maximum. */ readonly viewportWidth: number; /** The viewport the SECTION heights are clamped against, held for the same * reason the width is: a shorter window changes what a splitter may be * dragged to, and a render that read the live global would only say so by * accident. */ readonly viewportHeight: number; readonly selectedSubject: SelectedDiagramSubject | null; /** The relationship being drawn, or null when the tool is not in use. */ readonly connection: ConnectionDraft | null; /** * Whether the add-a-subject form is open. Only the openness lives here: the * fields belong to the form while it is on screen, and a half-typed name is * not workspace state anyone else needs. */ readonly draftingSubject: boolean; /** * The subject or relationship a deletion has been asked for, held until the * reviewer confirms. Deleting is the one motion that removes authored text, * so it is the one that asks first. */ readonly pendingDeletion: string | null; /** * The view a deletion has been asked for, held until the reviewer confirms. * Separate from `pendingDeletion` because the two ask different questions: a * subject's deletion is described against the graph and takes relationships * with it, a view's removes one projection document and touches no subject. */ readonly pendingViewDeletion: string | null; /** * The view being retitled, held while the reviewer types. Only the id: the * half-typed name belongs to the dialog while it is on screen. */ readonly pendingViewRename: string | null; readonly descriptionExpanded: boolean; readonly detailsOpen: boolean; /** * The canvas column's foot panel: whether it is open, and which tab it shows. * * Collapsed at rest, because the canvas keeps the room until the reviewer * asks for it. The tab is remembered while it is shut, so re-opening returns * to the tab that was last read rather than to the first one. */ readonly bottomPanel: { readonly open: boolean; readonly tab: BottomPanelTabId; }; /** * The left rail's own state. The text narrows the rail and nothing else — * `quickFilterText` still narrows the canvas — and `collapsed` holds only * the branches the reviewer shut, so a folder or layer that appears later * arrives open rather than hidden behind a default nobody chose. */ readonly tree: { readonly filterText: string; readonly collapsed: readonly string[]; }; /** * The open context menu: what was right-clicked and where the pointer was, * or null. The menu's CONTENTS are not held here — `contextMenuFor` derives * them from the model on every render, so a commit that lands while a menu * is open redraws it instead of leaving stale items over a subject that is * no longer there. */ readonly contextMenu: { readonly target: ContextMenuTarget; readonly x: number; readonly y: number; } | null; /** * What draws as nesting in the active view, in precedence order (ADR 0101). * A view that says nothing keeps `DEFAULT_NESTING`, which is composition * alone - the behaviour that shipped before a view could say. */ readonly nesting: readonly NestingKind[]; /** * Whether the active view folds pattern instances by default (#473), and the * two sets that override it: boxes the reader shut beyond the default, and * boxes the reader OPENED against it. * * Two sets rather than one flag, because `fold.set` restates the view's * default on every view switch and a reader who opened a box must not have it * shut again the moment that default is read back. An instance draws folded * when the mode says so and it is not in `unfolded`, or when it is in * `folded` outright. */ readonly foldMode: FoldMode; readonly folded: readonly string[]; readonly unfolded: readonly string[]; /** * Which way the active view runs its layers (#274, ADR 0121). A view that * says nothing runs `DEFAULT_DIRECTION`, the same rule `nesting` follows: * silence restores the default rather than carrying the previous view's * answer across. */ readonly direction: LayoutDirection; /** * How the active view arranges itself (ADR 0147). A view that says nothing * lays out `DEFAULT_LAYOUT`, the rule `direction` follows; a reviewer picks * another on the canvas, and a save writes what is in force. */ readonly layout: LayoutMode; readonly showLifecycle: boolean; readonly showEvidence: boolean; readonly showOwnership: boolean; /** Whether responsibility edges draw on the canvas (#557, ADR 0159). */ readonly showResponsibility: boolean; /** Whether an unnamed relationship is labelled with its reading (ADR 0147). */ readonly showKindLabels: boolean; readonly showNudges: boolean; /** * The dress the canvas wears (ADR 0148). The reviewer's own: remembered by * the browser, never written into a view, untouched by a view switch. */ readonly stylePreset: StylePresetId; } export type VisualWorkspaceAction = { readonly type: "section.toggled"; readonly section: RightSectionId; } | { readonly type: "section.resized"; readonly section: "changes" | "chat"; readonly height: number; } | { readonly type: "conversation.resized"; readonly width: number; } | { readonly type: "conversation.toggled"; } | { readonly type: "rail.toggled"; } | { readonly type: "viewport.resized"; readonly viewportWidth: number; readonly viewportHeight?: number; } | { readonly type: "attention.received"; } | { readonly type: "connection.started"; readonly from: string; readonly kinds?: readonly string[]; readonly direction?: "outgoing" | "incoming"; } | { readonly type: "connection.targeted"; readonly to: string; } | { readonly type: "connection.cancelled"; } | { readonly type: "subject.draft.opened"; } | { readonly type: "subject.draft.closed"; } | { readonly type: "deletion.asked"; readonly id: string; } | { readonly type: "deletion.dismissed"; } | { readonly type: "viewDeletion.asked"; readonly id: string; } | { readonly type: "viewDeletion.dismissed"; } | { readonly type: "viewRename.asked"; readonly id: string; } | { readonly type: "viewRename.dismissed"; } | { readonly type: "subject.selected"; readonly subject: SelectedDiagramSubject; } | { readonly type: "subject.cleared"; } | { readonly type: "description.toggled"; } | { readonly type: "details.toggled"; } | { readonly type: "bottomPanel.toggled"; } | { readonly type: "bottomPanel.tabSelected"; readonly tab: BottomPanelTabId; } | { readonly type: "menu.opened"; readonly target: ContextMenuTarget; readonly x: number; readonly y: number; } | { readonly type: "menu.dismissed"; } | { readonly type: "tree.filtered"; readonly filterText: string; } | { readonly type: "tree.toggled"; readonly key: string; } | { readonly type: "nesting.set"; readonly nesting: readonly NestingKind[]; } | { /** * The view's fold default plus whatever the sidecar saved, restated on * every view switch (#473) — the rule `nesting.set` follows. */ readonly type: "fold.set"; readonly mode: FoldMode; readonly folded: readonly string[]; readonly unfolded: readonly string[]; } | { /** One box opened or shut by a reader. */ readonly type: "fold.toggled"; readonly id: string; } | { /** Every instance at once, from the canvas menu. */ readonly type: "fold.all"; readonly folded: boolean; readonly ids: readonly string[]; } | { readonly type: "direction.set"; readonly direction: LayoutDirection; } | { readonly type: "layout.set"; readonly layout: LayoutMode; } | { readonly type: "style.set"; readonly preset: StylePresetId; } | { readonly type: "presentation.toggled"; readonly flag: "showLifecycle" | "showEvidence" | "showOwnership" | "showResponsibility" | "showKindLabels" | "showNudges"; readonly value: boolean; } | { readonly type: "model.replaced"; /** The graph that replaced it, so a subject that survived the commit can * be re-read from it instead of being dropped along with the old model. */ readonly graph: CanvasGraph | null; }; export declare const presentationActionsFor: (presentation: { readonly layout?: LayoutMode; readonly nesting?: readonly NestingKind[]; readonly fold?: FoldMode; readonly direction?: LayoutDirection; readonly showLifecycle?: boolean; readonly showEvidence?: boolean; readonly showOwnership?: boolean; readonly showResponsibility?: boolean; readonly showKindLabels?: boolean; } | undefined) => readonly VisualWorkspaceAction[]; export declare const viewNeedingApplication: (activeView: string, views: readonly View[], applied: string | null, connected: boolean) => View | null; export declare const conversationWidthBounds: (viewportWidth: number) => { min: number; max: number; }; export declare const createVisualWorkspaceState: (viewportWidth: number, viewportHeight?: number) => VisualWorkspaceState; export declare const visualWorkspaceReducer: (state: VisualWorkspaceState, action: VisualWorkspaceAction) => VisualWorkspaceState; export declare const formatContextualQuestion: (question: string, subject: SelectedDiagramSubject | null) => string;