@danxbot/ui 11.1.0 → 12.1.0

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.
@@ -1,10 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
- /** The controlled activation pair every pencil-activated field takes. */
3
- export interface EditActivation {
4
- editing: boolean;
5
- onEditingChange: (editing: boolean) => void;
6
- }
7
- export interface EditGutterProps {
2
+ import { type EditActivation } from "./edit/activation";
3
+ interface EditGutterFieldProps {
8
4
  /**
9
5
  * The pencil's accessible name — say WHAT it edits ("Edit title"), because
10
6
  * a screen reader lists it among every other pencil on the page.
@@ -15,6 +11,20 @@ export interface EditGutterProps {
15
11
  * `{(edit) => <InlineEdit {...edit} … />}`.
16
12
  */
17
13
  children: (activation: EditActivation) => ReactNode;
14
+ /** Render no pencil — the field cannot be edited here. */
15
+ disabled?: boolean;
18
16
  className?: string;
19
17
  }
20
- export declare function EditGutter({ label, children, className }: EditGutterProps): import("react").JSX.Element;
18
+ /**
19
+ * The gutter keeps `editing` itself (omit both), or the caller owns it: pass
20
+ * `editing` AND `onEditingChange` (`EditActivation`) — the pencil then asks
21
+ * through `onEditingChange(true)` and the field's close arrives through
22
+ * `onEditingChange(false)`. One without the other is a type error and throws
23
+ * at render, for the reason `assertActivationPair` gives.
24
+ */
25
+ export type EditGutterProps = EditGutterFieldProps & ({
26
+ editing?: undefined;
27
+ onEditingChange?: (editing: boolean) => void;
28
+ } | EditActivation);
29
+ export declare function EditGutter({ label, children, editing: editingProp, onEditingChange, disabled, className }: EditGutterProps): import("react").JSX.Element;
30
+ export {};
@@ -1,16 +1,19 @@
1
1
  import { type Ref } from "react";
2
2
  import { useRender } from "@base-ui/react/use-render";
3
- import { type InlineEditCommit } from "./edit/outcome";
4
- export type { InlineEditCommit, InlineEditOutcome, InlineEditRefusal } from "./edit/outcome";
5
- export interface InlineEditProps {
3
+ import { type EditActivation } from "./edit/activation";
4
+ import { type EditCommit } from "./edit/outcome";
5
+ interface InlineEditFieldProps {
6
6
  /**
7
7
  * The committed value. Controlled — this component never holds the truth,
8
8
  * only the draft while somebody is typing one.
9
9
  */
10
10
  value: string;
11
11
  /**
12
- * Commit a new value. Called ONLY when the live text actually differs from
13
- * `value`.
12
+ * Commit a new value. Called ONLY when the live text differs both from
13
+ * `value` and from the value editing began from — text the reader did not
14
+ * change is never written back, even if `value` moved underneath the open
15
+ * field (with `autosaveMs`, "unchanged" is measured against the last value
16
+ * this edit committed instead — see "AUTOSAVE").
14
17
  *
15
18
  * `base` is the `value` this control saw at the moment editing BEGAN —
16
19
  * captured once and handed back unchanged regardless of how long the edit or
@@ -23,7 +26,7 @@ export interface InlineEditProps {
23
26
  * it settles. Resolve/return `{ refusal }` to reject the value with a
24
27
  * reason; throw or reject to report that the save itself failed.
25
28
  */
26
- onCommit: InlineEditCommit;
29
+ onCommit: EditCommit;
27
30
  /**
28
31
  * The refusals that are knowable WITHOUT a request. Return the reason, or
29
32
  * `null` to accept. Runs after the unchanged check and before `onCommit`.
@@ -93,7 +96,10 @@ export interface InlineEditProps {
93
96
  /**
94
97
  * This value cannot be edited at all, AND WHY. `{ reason }`, matching
95
98
  * `SettingRow.readOnly` — a control that refuses in silence is
96
- * indistinguishable from one that is broken.
99
+ * indistinguishable from one that is broken. Under controlled activation a
100
+ * request to open (`editing` true) is declined by handing the closed state
101
+ * straight back through `onEditingChange(false)`, so a pencil driving it
102
+ * returns instead of waiting on a field that will never open (DX-3372 H3).
97
103
  */
98
104
  readOnly?: {
99
105
  reason: string;
@@ -102,33 +108,13 @@ export interface InlineEditProps {
102
108
  * What is shown when the commit REJECTS (a thrown error or a rejected
103
109
  * promise — never a `{ refusal }`, which renders its own text).
104
110
  *
105
- * @default "Could not save. Your text is still here — press Enter to try again."
111
+ * @default "Could not save. Your text is still here — press <key> to try again.",
112
+ * where <key> is the key that commits in this configuration: Enter, or
113
+ * Ctrl+Enter for a `multiline` field without `autosaveMs`.
106
114
  */
107
115
  failureMessage?: string;
108
116
  /** The rejected error, handed on so it can be logged or reported. */
109
117
  onError?: (error: unknown) => void;
110
- /**
111
- * CONTROLLED ACTIVATION (DX-3372). Pass it and the control never opens
112
- * ITSELF — no click, Enter or F2 — because something else (the pencil of an
113
- * `EditGutter`, a menu item, a shortcut) is the way in. Setting it `true`
114
- * opens the field, focused, caret at the end; the control asks to close
115
- * through `onEditingChange(false)` once a commit lands or Escape cancels.
116
- * Setting it `false` yourself while it is open abandons the draft exactly as
117
- * Escape would — nothing is written. At rest the element leaves the tab
118
- * order and drops its hover affordance, so the trigger is the one stop.
119
- *
120
- * Omit it for the self-opening control (click, Enter or F2).
121
- */
122
- editing?: boolean;
123
- /**
124
- * Fires exactly when the control itself starts or ends editing — never while
125
- * a read-only control merely declines to open, and never for a change the
126
- * caller made through `editing`. Use it to suspend any re-render-from-server
127
- * logic of your own for as long as someone is actually looking at the
128
- * field; see the module comment. With `editing` controlled, this is how the
129
- * control hands the closed state back.
130
- */
131
- onEditingChange?: (editing: boolean) => void;
132
118
  /** Render as a different element — see `Button`'s own `render` for the shape. */
133
119
  render?: useRender.RenderProp;
134
120
  /**
@@ -143,4 +129,31 @@ export interface InlineEditProps {
143
129
  messageClassName?: string;
144
130
  ref?: Ref<HTMLElement>;
145
131
  }
146
- export declare function InlineEdit({ value, onCommit, validate, liveValidate, label, placeholder, size, multiline, autosaveMs, readOnly, failureMessage, onError, editing: editingProp, onEditingChange, render, className, messageClassName, inputMode, ref, }: InlineEditProps): import("react").JSX.Element;
132
+ /**
133
+ * SELF-ACTIVATED: omit `editing` and the control opens itself on click, Enter
134
+ * or F2. `onEditingChange` then only observes — it fires when the control
135
+ * starts editing and when it stops, so a caller can suspend its own
136
+ * re-render-from-server logic for exactly as long as someone is typing.
137
+ */
138
+ interface InlineEditSelfActivated {
139
+ editing?: undefined;
140
+ onEditingChange?: (editing: boolean) => void;
141
+ }
142
+ /**
143
+ * The props. `editing` + `onEditingChange` are a discriminated pair
144
+ * (DX-3372 review P0): either omit `editing` (self-activated, above) or pass
145
+ * BOTH (`EditActivation` — controlled). Controlled, the control never opens
146
+ * itself; `true` opens it focused with the caret at the end; it hands the
147
+ * closed state back through `onEditingChange(false)` after a landed commit,
148
+ * Escape, or a refused opening (read-only). Setting `editing` false yourself
149
+ * ends the edit exactly as Escape does — nothing more is written. A save that
150
+ * is already out when you do cannot be recalled: it still reaches your
151
+ * `onCommit` promise, but its outcome no longer touches the field — no
152
+ * message, no focus move, no second `onEditingChange(false)` — and a commit
153
+ * from the next opening waits for it before starting (see
154
+ * `edit/use-edit-session.ts`). At rest a controlled control leaves the tab
155
+ * order and drops its hover affordance, so the trigger is the one stop.
156
+ */
157
+ export type InlineEditProps = InlineEditFieldProps & (InlineEditSelfActivated | EditActivation);
158
+ export declare function InlineEdit(props: InlineEditProps): import("react").JSX.Element;
159
+ export {};
@@ -1,7 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { ImageUploader } from "../lib/markdown/image-pipeline";
3
- import { type InlineEditCommit } from "./edit/outcome";
4
- export interface InlineMarkdownEditProps {
3
+ import { type EditActivation } from "./edit/activation";
4
+ import { type EditCommit } from "./edit/outcome";
5
+ export interface InlineMarkdownEditProps extends EditActivation {
5
6
  /** The committed markdown. Controlled — the field only ever holds a draft. */
6
7
  value: string;
7
8
  /**
@@ -10,11 +11,18 @@ export interface InlineMarkdownEditProps {
10
11
  * began from; return/resolve `{ refusal }` to refuse with a reason, throw or
11
12
  * reject to report a failed save.
12
13
  */
13
- onCommit: InlineEditCommit;
14
- /** Whether the editor is open. REQUIRED — see the module comment. */
14
+ onCommit: EditCommit;
15
+ /**
16
+ * Whether the editor is open — REQUIRED, see the module comment. The field
17
+ * hands the closed state back through `onEditingChange(false)` once a commit
18
+ * lands, on Escape, and when asked to open while `readOnly`. Setting it
19
+ * `false` yourself drops the draft exactly as Escape does. A save already
20
+ * out when you do cannot be recalled: your `onCommit` promise still settles,
21
+ * but its outcome no longer touches the field — no message, no focus move,
22
+ * no `onEditingChange(false)` — and a commit from the next opening waits for
23
+ * it before starting.
24
+ */
15
25
  editing: boolean;
16
- /** The field asks to close through this, once a commit lands or Escape cancels. */
17
- onEditingChange: (editing: boolean) => void;
18
26
  /** The accessible name of the field and of its editor. */
19
27
  label: string;
20
28
  /** Shown at rest and in the editor when the value is empty. @default "Empty" */
@@ -25,13 +33,23 @@ export interface InlineMarkdownEditProps {
25
33
  toolbar?: ReactNode;
26
34
  /** Stores a pasted/dropped/picked image — see `MarkdownEditorProps.uploadImage`. */
27
35
  uploadImage?: ImageUploader;
36
+ /**
37
+ * This value cannot be edited at all, AND WHY — `InlineEdit.readOnly`'s
38
+ * shape and rule: asked to open, the field hands `onEditingChange(false)`
39
+ * straight back, and the reason is the rest view's accessible description.
40
+ */
41
+ readOnly?: {
42
+ reason: string;
43
+ };
28
44
  /**
29
45
  * What is shown when the commit REJECTS (never a `{ refusal }`, which
30
46
  * renders its own text).
47
+ *
48
+ * @default "Could not save. Your text is still here — press Ctrl+Enter to try again."
31
49
  */
32
50
  failureMessage?: string;
33
51
  /** The rejected error, handed on so it can be logged or reported. */
34
52
  onError?: (error: unknown) => void;
35
53
  className?: string;
36
54
  }
37
- export declare function InlineMarkdownEdit({ value, onCommit, editing, onEditingChange, label, placeholder, collapsedLines, toolbar, uploadImage, failureMessage, onError, className, }: InlineMarkdownEditProps): import("react").JSX.Element;
55
+ export declare function InlineMarkdownEdit(props: InlineMarkdownEditProps): import("react").JSX.Element;
@@ -15,6 +15,13 @@ export interface MarkdownEditorHandle {
15
15
  * `"end"` (snapped to a real caret stop); omit it to leave the caret wherever
16
16
  * the browser puts it. DX-3372: `InlineMarkdownEdit` opens with the caret at
17
17
  * the end, the same place `InlineEdit` puts it.
18
+ *
19
+ * THROWS rather than returning quietly (DX-3372 review P0) when it cannot do
20
+ * what it was asked: the surface is not built yet (it is created in an
21
+ * effect, so a render or a layout effect is too early), the editor is
22
+ * read-only, focus did not land on the surface, or the caret could not be
23
+ * placed. A caller that goes on to assume focus is inside would otherwise
24
+ * wait for a blur that never comes.
18
25
  */
19
26
  focus: (caret?: "start" | "end") => void;
20
27
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The controlled activation pair every pencil-activated field takes, and the
3
+ * pair `EditGutter` hands out — so wiring a pencil to a field is one spread.
4
+ *
5
+ * `editing` says whether the field is open. The field calls
6
+ * `onEditingChange(false)` when it closes itself (a landed commit, Escape) and
7
+ * when it declines to open (read-only); it never calls `onEditingChange(true)`
8
+ * for an opening the caller made. The pair is all-or-nothing: a controlled
9
+ * `editing` with no way to hand the closed state back would leave the field
10
+ * shut after its first commit while the caller still believes it is open.
11
+ */
12
+ export interface EditActivation {
13
+ editing: boolean;
14
+ onEditingChange: (editing: boolean) => void;
15
+ }
16
+ /**
17
+ * Throws when a caller controls `editing` without `onEditingChange`. The
18
+ * types already forbid it; this covers a caller that got past them (a cast,
19
+ * a spread of `any`, plain JavaScript). DX-3372 review P0.
20
+ */
21
+ export declare function assertActivationPair(component: string, editing: boolean | undefined, onEditingChange: ((editing: boolean) => void) | undefined): void;
@@ -1,16 +1,27 @@
1
1
  /**
2
2
  * The caller's refusal of a value, with the reason that will be shown.
3
3
  */
4
- export interface InlineEditRefusal {
4
+ export interface EditRefusal {
5
5
  refusal: string;
6
6
  }
7
7
  /** What `onCommit` may report. Returning nothing accepts the value. */
8
- export type InlineEditOutcome = void | InlineEditRefusal;
8
+ export type EditOutcome = void | EditRefusal;
9
9
  /** Every in-place editor's `onCommit`: the new value and the value editing began from. */
10
- export type InlineEditCommit = (value: string, base: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
11
- export declare function isRefusal(outcome: InlineEditOutcome): outcome is InlineEditRefusal;
12
- export declare function isPromise(outcome: InlineEditOutcome | Promise<InlineEditOutcome>): outcome is Promise<InlineEditOutcome>;
13
- export declare const DEFAULT_FAILURE = "Could not save. Your text is still here \u2014 press Enter to try again.";
10
+ export type EditCommit = (value: string, base: string) => EditOutcome | Promise<EditOutcome>;
11
+ export declare function isRefusal(outcome: EditOutcome): outcome is EditRefusal;
12
+ export declare function isPromise(outcome: EditOutcome | Promise<EditOutcome>): outcome is Promise<EditOutcome>;
13
+ /**
14
+ * The key that retries a failed save in a given field: Enter where Enter
15
+ * commits, Ctrl+Enter where Enter inserts a newline. (Both fields accept
16
+ * Cmd+Enter as well as Ctrl+Enter, so "Ctrl+Enter" is true on every platform.)
17
+ */
18
+ export type RetryKey = "Enter" | "Ctrl+Enter";
19
+ /**
20
+ * The default failure message. It names the retry key because blur never
21
+ * re-attempts a failed value — DX-3372 review L1: the single shared sentence
22
+ * said "press Enter" in fields where Enter inserts a newline.
23
+ */
24
+ export declare function defaultFailureMessage(retryKey: RetryKey): string;
14
25
  /**
15
26
  * A refusal nobody can read leaves the field open with nothing beside it — the
16
27
  * exact silent failure the outcome contract exists to rule out — so an empty
@@ -0,0 +1,55 @@
1
+ import { type EditCommit, type EditOutcome } from "./outcome";
2
+ export type SaveResult = {
3
+ outcome: EditOutcome;
4
+ } | {
5
+ error: unknown;
6
+ };
7
+ export interface EditSessionOptions {
8
+ /** Named in the thrown message when a refusal carries an empty reason. */
9
+ component: string;
10
+ /** The committed value — "unchanged" is measured against it. Read live. */
11
+ value: string;
12
+ onCommit: EditCommit;
13
+ onError?: (error: unknown) => void;
14
+ failureMessage: string;
15
+ /** End editing after a commit that landed (or had nothing to write). */
16
+ close: () => void;
17
+ /** Put focus back on the field after a refusal or a failure. */
18
+ refocus: () => void;
19
+ }
20
+ export interface EditSession {
21
+ pending: boolean;
22
+ refusal: string | null;
23
+ failure: string | null;
24
+ /** The message on screen, if any — a refusal or a failure. */
25
+ blocked: string | null;
26
+ /** The value this session began from — `onCommit`'s `base`. */
27
+ readonly base: string;
28
+ /** Start a new session. `refusal` seeds the message slot (live validation). */
29
+ begin: (base: string, refusal?: string | null) => void;
30
+ /** Finish the current session: every save still out becomes stale. */
31
+ end: () => void;
32
+ /** Commit `next` — unchanged check (see the module comment), `validate`, `onCommit`, outcome. */
33
+ commit: (next: string, validate?: (next: string) => string | null) => void;
34
+ refuse: (reason: string, source: "validate" | "onCommit") => void;
35
+ fail: (error: unknown) => void;
36
+ setPending: (pending: boolean) => void;
37
+ isPending: () => boolean;
38
+ setRefusal: (refusal: string | null) => void;
39
+ clearFailure: () => void;
40
+ /** The current session's token, for a caller that tracks its own save. */
41
+ token: () => number;
42
+ /** Whether a save from any session is still out. */
43
+ busy: () => boolean;
44
+ /** The in-flight save, settled or not — chain after it to stay in order. */
45
+ inFlight: () => Promise<void> | null;
46
+ /**
47
+ * Track a save sent under `token`. `onResult` runs only if that session is
48
+ * still current when it settles; otherwise `onStale` does (after a stale
49
+ * rejection has reached `onError`).
50
+ */
51
+ track: (save: Promise<EditOutcome>, token: number, onResult: (result: SaveResult) => void, onStale?: (result: SaveResult) => void) => void;
52
+ /** Run `fn` once no save is out — dropped if this session ends first. */
53
+ whenIdle: (fn: () => void) => void;
54
+ }
55
+ export declare function useEditSession(options: EditSessionOptions): EditSession;
@@ -0,0 +1,35 @@
1
+ import type { CanvasNode } from "../node-canvas/types";
2
+ import type { CrossLink, ExplorerNodeRenderer, ExplorerTree, GraphExplorerVariant } from "./types";
3
+ export interface GraphExplorerProps<Extra = unknown> {
4
+ tree: ExplorerTree<Extra>;
5
+ /** Secondary relationships drawn over the fixed layout. Never affects positions. */
6
+ crossLinks?: readonly CrossLink[];
7
+ /** The accessible name of the whole structure. Required — see `NodeCanvasProps.label`. */
8
+ label: string;
9
+ /** The node body. Defaults to the label, wrapped, never truncated. */
10
+ renderNode?: ExplorerNodeRenderer<Extra>;
11
+ /** @default "explorer" */
12
+ variant?: GraphExplorerVariant;
13
+ /** Which node the camera centres on, with a "you are here" highlight. Controlled. */
14
+ focusId?: string | null;
15
+ /** Initial `focusId` for uncontrolled use. */
16
+ defaultFocusId?: string | null;
17
+ /** Fired when the controlled `focusId` would change — a node click, or `flyTo` via the keyboard. Never fired with `null`: nothing in this component ever clears focus, only moves it. */
18
+ onFocusIdChange?: (id: string) => void;
19
+ /** Nodes styled as already seen. */
20
+ visitedIds?: ReadonlySet<string> | readonly string[];
21
+ /** Fired when an ordinary node is clicked or activated by keyboard. */
22
+ onNodeClick?: (node: CanvasNode<Extra>) => void;
23
+ /** `variant="minimap"` only: the expand affordance's handler. Defaults to an in-place full-screen dialog. */
24
+ onExpand?: () => void;
25
+ /** Along-axis extent of a node that does not need more, in px. @default LAYOUT_DEFAULTS.nodeWidth */
26
+ nodeWidth?: number;
27
+ /** Cross-axis extent BEFORE a taller node's own content grows it. @default LAYOUT_DEFAULTS.nodeHeight */
28
+ nodeHeight?: number;
29
+ /** `variant="explorer"` only: height of the viewport. @default "32rem" */
30
+ height?: string;
31
+ /** Nothing pans, zooms or clicks. Reading is unaffected. */
32
+ disabled?: boolean;
33
+ className?: string;
34
+ }
35
+ export declare function GraphExplorer<Extra = unknown>({ tree, crossLinks, label, renderNode, variant, focusId: controlledFocusId, defaultFocusId, onFocusIdChange, visitedIds, onNodeClick, onExpand, nodeWidth, nodeHeight, height, disabled, className, }: GraphExplorerProps<Extra>): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ export interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export interface Box {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export interface Size {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export interface CameraState {
16
+ x: number;
17
+ y: number;
18
+ scale: number;
19
+ }
20
+ export interface ScaleBounds {
21
+ minScale?: number;
22
+ maxScale?: number;
23
+ }
24
+ export declare const CAMERA_DEFAULTS: {
25
+ readonly minScale: 0.15;
26
+ readonly maxScale: 3;
27
+ readonly fitMaxScale: 1;
28
+ readonly fitPadding: 64;
29
+ };
30
+ export declare function clampScale(scale: number, bounds?: ScaleBounds): number;
31
+ /** Screen point → the content point currently sitting under it. */
32
+ export declare function toContentPoint(camera: CameraState, screenPoint: Point): Point;
33
+ /** Content point → where it currently sits on screen. */
34
+ export declare function toScreenPoint(camera: CameraState, contentPoint: Point): Point;
35
+ /**
36
+ * Zoom by `factor`, holding `screenPoint` fixed over the same content point it
37
+ * covered before — the standard "zoom toward the cursor" behaviour a wheel or
38
+ * a pinch both want. `factor` > 1 zooms in.
39
+ */
40
+ export declare function zoomAtPoint(camera: CameraState, screenPoint: Point, factor: number, bounds?: ScaleBounds): CameraState;
41
+ /** A camera that puts `target`'s centre in the middle of `viewport`, at `scale`. */
42
+ export declare function centerOn(target: Box | Point, viewport: Size, scale: number): CameraState;
43
+ export interface FitOptions extends ScaleBounds {
44
+ /** @default CAMERA_DEFAULTS.fitPadding */
45
+ padding?: number;
46
+ /** @default CAMERA_DEFAULTS.fitMaxScale — never zoom IN past this to fit a small box. */
47
+ maxScale?: number;
48
+ }
49
+ /** A camera that fits the whole of `box` inside `viewport`, centred, padded. */
50
+ export declare function fitBox(box: Box, viewport: Size, options?: FitOptions): CameraState;
@@ -0,0 +1,5 @@
1
+ export { GraphExplorer, type GraphExplorerProps } from "./GraphExplorer";
2
+ export { layoutExplorerTree, neighbourhoodTree, edgePath } from "./tree-layout";
3
+ export { CAMERA_DEFAULTS, centerOn, clampScale, fitBox, toContentPoint, toScreenPoint, zoomAtPoint } from "./camera-math";
4
+ export type { Box, CameraState, FitOptions, Point, ScaleBounds, Size } from "./camera-math";
5
+ export type { CrossLink, ExplorerLayout, ExplorerNodeRenderContext, ExplorerNodeRenderer, ExplorerTree, GraphExplorerVariant, TreeNode, } from "./types";
@@ -0,0 +1,29 @@
1
+ import { edgePath } from "../node-canvas/layout";
2
+ import type { LayoutOptions } from "../node-canvas/types";
3
+ import type { CrossLink, ExplorerLayout, ExplorerTree } from "./types";
4
+ /** Re-exported for a consumer drawing `edges`/`crossLinks` into an SVG. */
5
+ export { edgePath };
6
+ /**
7
+ * Lay out a tree, left to right, from a parent backbone alone.
8
+ *
9
+ * `crossLinks` are resolved against the ALREADY-FIXED node positions and
10
+ * returned separately (`ExplorerLayout.crossLinks`) — they never reach
11
+ * `layoutGraph`, so they can never move a node and can never close a cycle
12
+ * for the layout engine to condense. A cross-link naming an id not in the
13
+ * tree, or naming a node as linked to itself, draws nothing — the same
14
+ * "dangling edge draws nothing" rule `node-canvas/layout.ts#resolveScope`
15
+ * uses for a group boundary.
16
+ */
17
+ export declare function layoutExplorerTree<Extra>(tree: ExplorerTree<Extra>, crossLinks?: readonly CrossLink[], options?: LayoutOptions): ExplorerLayout<Extra>;
18
+ /**
19
+ * The subtree a minimap draws: `focusId`'s ancestor chain up to its root,
20
+ * `focusId`'s immediate siblings, and `focusId`'s immediate children — "the
21
+ * focused node's ancestors, siblings and children" the card asks for, kept
22
+ * small enough to read at the minimap's compact size. `focusId` itself is
23
+ * always included. Parent links are REWRITTEN to stay inside this subset: the
24
+ * topmost ancestor kept becomes a root (`parentId: null`) here even if its
25
+ * real parent exists outside the shown set, so `layoutExplorerTree` never
26
+ * sees a dangling parent reference. Returns `null` when `focusId` is not in
27
+ * `tree` at all — nothing sensible to show.
28
+ */
29
+ export declare function neighbourhoodTree<Extra>(tree: ExplorerTree<Extra>, focusId: string): ExplorerTree<Extra> | null;
@@ -0,0 +1,62 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CanvasEdge, CanvasNode, CanvasPlacedEdge, LayoutOptions, PlacedNode } from "../node-canvas/types";
3
+ /**
4
+ * One node of the tree. Same `Extra`-carrying shape as `CanvasNode` — a
5
+ * consumer's own fields come back untouched through `renderNode` — plus the
6
+ * one thing a tree node has that a general graph node does not: its parent.
7
+ */
8
+ export type TreeNode<Extra = unknown> = CanvasNode<Extra> & {
9
+ /** The node this one hangs from. `null` for a root — a tree may have more than one. */
10
+ parentId: string | null;
11
+ };
12
+ /**
13
+ * A secondary relationship drawn OVER the fixed tree layout — never fed to
14
+ * the layout engine, so it can never move a node and can never close a cycle
15
+ * for dagre to condense. `kind` carries no meaning the library assigns, same
16
+ * rule as `CanvasEdge.kind`.
17
+ */
18
+ export interface CrossLink {
19
+ from: string;
20
+ to: string;
21
+ kind?: string;
22
+ }
23
+ /** The whole tree. Never mutated by the component. */
24
+ export interface ExplorerTree<Extra = unknown> {
25
+ nodes: readonly TreeNode<Extra>[];
26
+ }
27
+ /** What `layoutExplorerTree` hands back: the fixed layout, ready to draw. */
28
+ export interface ExplorerLayout<Extra = unknown> {
29
+ nodes: PlacedNode<Extra>[];
30
+ /** The parent → child backbone, drawn as the primary edges. */
31
+ edges: CanvasPlacedEdge[];
32
+ /** `CrossLink`s resolved to drawn curves. Never affects `nodes`' positions. */
33
+ crossLinks: CanvasPlacedEdge[];
34
+ /** Extent of the laid-out tree — the plane the camera's `fitAll` fits to. */
35
+ width: number;
36
+ height: number;
37
+ }
38
+ export type { CanvasEdge, CanvasNode, LayoutOptions, PlacedNode, CanvasPlacedEdge };
39
+ /** Which surface this render is: the full pannable explorer, or the compact strip. */
40
+ export type GraphExplorerVariant = "explorer" | "minimap";
41
+ /**
42
+ * What `renderNode` is told about the node it is drawing — the explorer's own
43
+ * facts (camera focus, visited, hover neighbourhood), analogous to
44
+ * `NodeRenderContext` in `node-canvas/types.ts` but keyed on the camera's
45
+ * `focusId` rather than a drilled-into scope.
46
+ */
47
+ export interface ExplorerNodeRenderContext {
48
+ /** This node carries the canvas's single roving tab stop. */
49
+ isTabStop: boolean;
50
+ /** This IS the controlled `focusId` — the "you are here" node. */
51
+ isFocused: boolean;
52
+ /** In the caller-supplied `visitedIds`. */
53
+ isVisited: boolean;
54
+ /**
55
+ * Hops from whichever node is currently the reference point — the hovered
56
+ * node while one is hovered, else the focused node — along either edge
57
+ * direction (backbone or cross-link). `null` when nothing is being
58
+ * referenced, or this node is further away than the explorer tracks.
59
+ */
60
+ distance: number | null;
61
+ }
62
+ export type ExplorerNodeRenderer<Extra = unknown> = (node: CanvasNode<Extra>, context: ExplorerNodeRenderContext) => ReactNode;
@@ -0,0 +1,31 @@
1
+ import { type Box, type CameraState } from "./camera-math";
2
+ export interface CameraClickEvent {
3
+ /** Client coordinates, for the caller to hit-test against its own elements. */
4
+ target: HTMLElement;
5
+ }
6
+ export interface UseCameraOptions {
7
+ /** Content-space bounding box of the whole drawing. Drives `fitAll`. */
8
+ contentBox: Box;
9
+ /** A node's content-space box, for `flyTo`. `null` for an id not laid out. */
10
+ boxOf: (id: string) => Box | null;
11
+ /** A press that released without travelling past `CLICK_DISTANCE`. */
12
+ onClick?: (event: CameraClickEvent) => void;
13
+ /** Nothing pans, zooms or clicks. Reading the plane still works. */
14
+ disabled?: boolean;
15
+ /** Changing this value re-fits the camera to `contentBox` — a new tree. */
16
+ resetKey?: unknown;
17
+ }
18
+ export interface UseCameraResult {
19
+ camera: CameraState;
20
+ /** A pointer is actively panning or pinching right now. */
21
+ panning: boolean;
22
+ /** True for the duration of a programmatic move — see the module doc. */
23
+ animating: boolean;
24
+ bindViewport: (node: HTMLDivElement | null) => void;
25
+ zoomIn: () => void;
26
+ zoomOut: () => void;
27
+ fitAll: () => void;
28
+ /** Animate the camera to centre node `id`. No-op if `id` isn't laid out yet. */
29
+ flyTo: (id: string, scale?: number) => void;
30
+ }
31
+ export declare function useCamera(options: UseCameraOptions): UseCameraResult;
@@ -630,6 +630,31 @@ export declare const coreIcons: {
630
630
  }], readonly ["path", {
631
631
  readonly d: "M15 3v18";
632
632
  }]];
633
+ readonly crosshair: readonly [readonly ["circle", {
634
+ readonly cx: "12";
635
+ readonly cy: "12";
636
+ readonly r: "10";
637
+ }], readonly ["line", {
638
+ readonly x1: "22";
639
+ readonly x2: "18";
640
+ readonly y1: "12";
641
+ readonly y2: "12";
642
+ }], readonly ["line", {
643
+ readonly x1: "6";
644
+ readonly x2: "2";
645
+ readonly y1: "12";
646
+ readonly y2: "12";
647
+ }], readonly ["line", {
648
+ readonly x1: "12";
649
+ readonly x2: "12";
650
+ readonly y1: "6";
651
+ readonly y2: "2";
652
+ }], readonly ["line", {
653
+ readonly x1: "12";
654
+ readonly x2: "12";
655
+ readonly y1: "22";
656
+ readonly y2: "18";
657
+ }]];
633
658
  readonly "delete-column": readonly [readonly ["path", {
634
659
  readonly d: "M2 12h6";
635
660
  }], readonly ["path", {
@@ -21,9 +21,11 @@ export { Skeleton, SkeletonText, LoadingOverlay, type SkeletonProps, type Skelet
21
21
  export { ErrorBoundary, type ErrorBoundaryProps } from "./components/ErrorBoundary";
22
22
  export { Field, FieldSet, type FieldProps, type FieldSetProps } from "./components/Field";
23
23
  export { Input, Textarea, type InputProps, type TextareaProps } from "./components/Input";
24
- export { InlineEdit, type InlineEditProps, type InlineEditRefusal, type InlineEditOutcome, type InlineEditCommit, } from "./components/InlineEdit";
25
- export { EditGutter, type EditGutterProps, type EditActivation } from "./components/EditGutter";
24
+ export { InlineEdit, type InlineEditProps } from "./components/InlineEdit";
25
+ export { EditGutter, type EditGutterProps } from "./components/EditGutter";
26
26
  export { InlineMarkdownEdit, type InlineMarkdownEditProps } from "./components/InlineMarkdownEdit";
27
+ export type { EditActivation } from "./components/edit/activation";
28
+ export type { EditCommit, EditOutcome, EditRefusal } from "./components/edit/outcome";
27
29
  export { SecretField, type SecretFieldProps, type SecretValue } from "./components/SecretField";
28
30
  export { SettingRow, SettingGroup, type SettingRowProps, type SettingGroupProps, type SettingRowControlRenderProps, } from "./components/SettingRow";
29
31
  export { DescriptionList, DescriptionListItem, type DescriptionListProps, type DescriptionListItemProps, type DescriptionListEntry, } from "./components/DescriptionList";
@@ -127,6 +129,7 @@ export { ProvenanceDisclosure, ProvenanceDetail, ProvenanceBadge, ConfidenceRead
127
129
  export { confidenceLevel, CONFIDENCE_LEVEL_LABELS, type ConfidenceLevel } from "./lib/confidence";
128
130
  export { createSimStore, resetSimStores, useSimSettings, setSimSettings, getNetworkSpeed, getDataSource, simulateLatency, SimNetworkError, type SimStore, type SimStoreOptions, type SimSettings, type NetworkSpeed, type DataSource, type SimNamespace, } from "./lib/sim";
129
131
  export { NodeCanvas, LAYOUT_DEFAULTS, edgePath, layoutGraph, neighbourhood, type NodeCanvasProps, type CanvasCycleScope, type CanvasEdge, type CanvasGraph, type CanvasGroup, type CanvasLayout, type CanvasNode, type CanvasOrientation, type CanvasPlacedEdge, type CanvasScope, type LayoutOptions, type NodeRenderContext, type NodeRenderer, type PlacedNode, } from "./components/node-canvas";
132
+ export { GraphExplorer, layoutExplorerTree, neighbourhoodTree, CAMERA_DEFAULTS, centerOn, clampScale, fitBox, toContentPoint, toScreenPoint, zoomAtPoint, type GraphExplorerProps, type Box, type CameraState, type FitOptions, type Point, type ScaleBounds, type Size, type CrossLink, type ExplorerLayout, type ExplorerNodeRenderContext, type ExplorerNodeRenderer, type ExplorerTree, type GraphExplorerVariant, type TreeNode as ExplorerTreeNode, } from "./components/graph-explorer";
130
133
  export { defineRecordType, resetRecordTypes, useRecord, useRecords, useStaleness, write, writeAll, peekRecord, readState, readStaleness, subscribeToKey, subscribeToStaleness, retainKeys, sweepRecords, setRecordStoreTenant, getRecordStoreTenant, flushRecordNotifications, resetRecordStore, recordStoreStats, UNKNOWN_STATE, normalizeTypeName, normalizeTimestamp, recordKey, isRecordRef, type RecordRef, type ReadOptions, type RecordPayload, type RecordState, type RecordTypeOptions, type RecordTypeToken, type StalenessEntry, type WriteAllResult, type WriteOptions, type WriteResult, type WriteSource, type WriteVerdict, } from "./lib/records";
131
134
  export { RealtimeProvider, useRealtime, useRealtimeEpoch, useRealtimeHealth, useRealtimeRegistry, SubscriptionRegistry, normalizeScope, scopeKey, subscriptionKey, readRealtimeEpoch, subscribeToRealtimeEpoch, subscribeToRealtimeEvents, resetRealtimeSignals, type RealtimeChannel, type RealtimeConnectionState, type RealtimeDiagnostic, type RealtimeEvent, type RealtimeHealth, type RealtimeProviderProps, type RealtimeScope, type RealtimeScopeInput, type RealtimeStatus, type RealtimeTransport, type RegistryOptions, type SubscriptionHealth, type SubscriptionLease, type SubscriptionRequest, } from "./lib/realtime";
132
135
  export { useRecordRead, useRecordListRead, useRecordStoreSweep, applyMutationResult, optimisticWrite, beginOptimisticWrite, resetOptimisticWrites, type OptimisticHandle, type OptimisticWriteOptions, type OptimisticWriteSpec, type SlotDescriptor, acquireSlot, appendCreated, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type ListFetch, type ListFetchResult, type ListReadOptions, type ListReadResult, type ReadError, type RecordFetch, type RecordReadOptions, type RecordReadResult, type RunnerOptions, type SlotSnapshot, } from "./lib/reads";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "11.1.0",
3
+ "version": "12.1.0",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",