@lupinum/board-core 0.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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/chunk-5JZXHZWR.js +68 -0
- package/dist/colors.d.ts +9 -0
- package/dist/engine/camera-session.d.ts +14 -0
- package/dist/engine/command-runtime.d.ts +31 -0
- package/dist/engine/events.d.ts +21 -0
- package/dist/engine/interaction-adapter.d.ts +3 -0
- package/dist/engine/node-shape.d.ts +16 -0
- package/dist/engine/options.d.ts +17 -0
- package/dist/engine/persistence.d.ts +18 -0
- package/dist/engine/subscribables.d.ts +34 -0
- package/dist/engine/transaction.d.ts +56 -0
- package/dist/engine.d.ts +17 -0
- package/dist/errors.d.ts +17 -0
- package/dist/helpers/animation.d.ts +9 -0
- package/dist/helpers/clone.d.ts +6 -0
- package/dist/helpers/ids.d.ts +2 -0
- package/dist/helpers/node-shape.d.ts +2 -0
- package/dist/helpers/selection-helpers.d.ts +8 -0
- package/dist/hierarchy.d.ts +16 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4159 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +10 -0
- package/dist/invariants.d.ts +3 -0
- package/dist/math.d.ts +16 -0
- package/dist/resize.d.ts +26 -0
- package/dist/selection.d.ts +7 -0
- package/dist/snap.d.ts +29 -0
- package/dist/state/initial.d.ts +2 -0
- package/dist/state/selectors.d.ts +5 -0
- package/dist/state/types.d.ts +31 -0
- package/dist/subscribable.d.ts +20 -0
- package/dist/types.d.ts +576 -0
- package/package.json +50 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A nominal type helper used to distinguish identifiers that are both strings at runtime.
|
|
3
|
+
*/
|
|
4
|
+
type Brand<T, K extends string> = T & {
|
|
5
|
+
readonly __brand: K;
|
|
6
|
+
};
|
|
7
|
+
/** Unique identifier for a board node. */
|
|
8
|
+
export type NodeId = Brand<string, 'NodeId'>;
|
|
9
|
+
/** Unique identifier for a connection edge. */
|
|
10
|
+
export type EdgeId = Brand<string, 'EdgeId'>;
|
|
11
|
+
/** Compile-time-only cast into a branded node id. This does not validate runtime input. */
|
|
12
|
+
export declare const asNodeId: (value: string) => NodeId;
|
|
13
|
+
/** Compile-time-only cast into a branded edge id. This does not validate runtime input. */
|
|
14
|
+
export declare const asEdgeId: (value: string) => EdgeId;
|
|
15
|
+
/** Axis used by snapping guides and edge-alignment calculations. */
|
|
16
|
+
export type SnapAxis = 'x' | 'y';
|
|
17
|
+
/** Visual guide emitted while a drag or resize operation snaps against nearby edges. */
|
|
18
|
+
export interface SnapGuide {
|
|
19
|
+
axis: SnapAxis;
|
|
20
|
+
position: number;
|
|
21
|
+
from: number;
|
|
22
|
+
to: number;
|
|
23
|
+
}
|
|
24
|
+
/** A 2D coordinate in world or screen space depending on call site. */
|
|
25
|
+
export interface Point {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
}
|
|
29
|
+
/** Axis-aligned rectangle represented by its minimum and maximum corners. */
|
|
30
|
+
export interface Bounds {
|
|
31
|
+
minX: number;
|
|
32
|
+
minY: number;
|
|
33
|
+
maxX: number;
|
|
34
|
+
maxY: number;
|
|
35
|
+
}
|
|
36
|
+
/** View transform for the board surface. */
|
|
37
|
+
export interface Camera {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
z: number;
|
|
41
|
+
}
|
|
42
|
+
/** Zoom limits enforced by the engine camera commands. */
|
|
43
|
+
export interface ZoomSettings {
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
}
|
|
47
|
+
/** Visual style used by the board grid renderer. */
|
|
48
|
+
export type GridPattern = 'dot' | 'line' | 'cross' | 'none';
|
|
49
|
+
/** Persistent grid configuration stored with the board snapshot. */
|
|
50
|
+
export interface GridSettings {
|
|
51
|
+
size: number;
|
|
52
|
+
majorEvery: number;
|
|
53
|
+
snap: boolean;
|
|
54
|
+
edgeSnap: boolean;
|
|
55
|
+
edgeSnapThreshold: number;
|
|
56
|
+
pattern: GridPattern;
|
|
57
|
+
}
|
|
58
|
+
/** Default dimensions used when callers omit node sizing data. */
|
|
59
|
+
export interface NodeConstraints {
|
|
60
|
+
minWidth: number;
|
|
61
|
+
minHeight: number;
|
|
62
|
+
defaultWidth: number;
|
|
63
|
+
defaultHeight: number;
|
|
64
|
+
}
|
|
65
|
+
/** JSON Canvas 1.0 node type. */
|
|
66
|
+
export type JsonCanvasNodeType = 'text' | 'file' | 'link' | 'group';
|
|
67
|
+
/** JSON Canvas side name used by edges. */
|
|
68
|
+
export type JsonCanvasSide = 'top' | 'right' | 'bottom' | 'left';
|
|
69
|
+
/** JSON Canvas endpoint marker. */
|
|
70
|
+
export type JsonCanvasEdgeEnd = 'none' | 'arrow';
|
|
71
|
+
/** JSON Canvas group background rendering style. */
|
|
72
|
+
export type JsonCanvasBackgroundStyle = 'cover' | 'ratio' | 'repeat';
|
|
73
|
+
/** Obsidian-compatible color preset stored on nodes and resolved by renderers. */
|
|
74
|
+
export type BoardColorPreset = '1' | '2' | '3' | '4' | '5' | '6';
|
|
75
|
+
/** JSON Canvas color value: a preset id or a concrete hex color. */
|
|
76
|
+
export type CanvasColor = BoardColorPreset | `#${string}`;
|
|
77
|
+
interface JsonCanvasNodeBase<TType extends JsonCanvasNodeType> {
|
|
78
|
+
readonly id: string;
|
|
79
|
+
readonly type: TType;
|
|
80
|
+
readonly x: number;
|
|
81
|
+
readonly y: number;
|
|
82
|
+
readonly width: number;
|
|
83
|
+
readonly height: number;
|
|
84
|
+
readonly color?: CanvasColor;
|
|
85
|
+
}
|
|
86
|
+
export interface JsonCanvasTextNode extends JsonCanvasNodeBase<'text'> {
|
|
87
|
+
readonly text: string;
|
|
88
|
+
}
|
|
89
|
+
export interface JsonCanvasFileNode extends JsonCanvasNodeBase<'file'> {
|
|
90
|
+
readonly file: string;
|
|
91
|
+
readonly subpath?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface JsonCanvasLinkNode extends JsonCanvasNodeBase<'link'> {
|
|
94
|
+
readonly url: string;
|
|
95
|
+
}
|
|
96
|
+
export interface JsonCanvasGroupNode extends JsonCanvasNodeBase<'group'> {
|
|
97
|
+
readonly label?: string;
|
|
98
|
+
readonly background?: string;
|
|
99
|
+
readonly backgroundStyle?: JsonCanvasBackgroundStyle;
|
|
100
|
+
}
|
|
101
|
+
/** JSON Canvas 1.0 node record. */
|
|
102
|
+
export type JsonCanvasNode = JsonCanvasTextNode | JsonCanvasFileNode | JsonCanvasLinkNode | JsonCanvasGroupNode;
|
|
103
|
+
/** JSON Canvas 1.0 edge record. */
|
|
104
|
+
export interface JsonCanvasEdge {
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly fromNode: string;
|
|
107
|
+
readonly fromSide?: JsonCanvasSide;
|
|
108
|
+
readonly fromEnd?: JsonCanvasEdgeEnd;
|
|
109
|
+
readonly toNode: string;
|
|
110
|
+
readonly toSide?: JsonCanvasSide;
|
|
111
|
+
readonly toEnd?: JsonCanvasEdgeEnd;
|
|
112
|
+
readonly color?: CanvasColor;
|
|
113
|
+
readonly label?: string;
|
|
114
|
+
}
|
|
115
|
+
export interface VueBoardNodeMetadata {
|
|
116
|
+
readonly zIndex?: number;
|
|
117
|
+
readonly locked?: boolean;
|
|
118
|
+
readonly visible?: boolean;
|
|
119
|
+
readonly parentId?: string;
|
|
120
|
+
}
|
|
121
|
+
export interface VueBoardEdgeMetadata {
|
|
122
|
+
readonly zIndex?: number;
|
|
123
|
+
readonly data?: Record<string, unknown>;
|
|
124
|
+
}
|
|
125
|
+
/** Nuxt Board metadata for engine state that JSON Canvas 1.0 does not define. */
|
|
126
|
+
export interface VueBoardDocumentMetadata {
|
|
127
|
+
readonly camera?: Camera;
|
|
128
|
+
readonly grid?: GridSettings;
|
|
129
|
+
readonly selection?: readonly string[];
|
|
130
|
+
readonly nextZIndex?: number;
|
|
131
|
+
readonly nodes?: Readonly<Record<string, VueBoardNodeMetadata>>;
|
|
132
|
+
readonly edges?: Readonly<Record<string, VueBoardEdgeMetadata>>;
|
|
133
|
+
}
|
|
134
|
+
/** Canonical persisted board document. */
|
|
135
|
+
export interface JsonCanvasDocument {
|
|
136
|
+
readonly nodes: readonly JsonCanvasNode[];
|
|
137
|
+
readonly edges?: readonly JsonCanvasEdge[];
|
|
138
|
+
readonly 'x-vue-board'?: VueBoardDocumentMetadata;
|
|
139
|
+
}
|
|
140
|
+
interface BoardNodeBase {
|
|
141
|
+
readonly id: NodeId;
|
|
142
|
+
readonly x: number;
|
|
143
|
+
readonly y: number;
|
|
144
|
+
readonly width: number;
|
|
145
|
+
readonly height: number;
|
|
146
|
+
readonly color?: CanvasColor;
|
|
147
|
+
readonly zIndex: number;
|
|
148
|
+
readonly locked: boolean;
|
|
149
|
+
readonly visible: boolean;
|
|
150
|
+
readonly parentId?: NodeId;
|
|
151
|
+
}
|
|
152
|
+
/** Canonical immutable node shape returned by state, selectors, and commands. */
|
|
153
|
+
export type BoardNode = (BoardNodeBase & {
|
|
154
|
+
readonly type: 'text';
|
|
155
|
+
readonly text: string;
|
|
156
|
+
readonly file?: never;
|
|
157
|
+
readonly subpath?: never;
|
|
158
|
+
readonly url?: never;
|
|
159
|
+
readonly label?: never;
|
|
160
|
+
readonly background?: never;
|
|
161
|
+
readonly backgroundStyle?: never;
|
|
162
|
+
}) | (BoardNodeBase & {
|
|
163
|
+
readonly type: 'file';
|
|
164
|
+
readonly file: string;
|
|
165
|
+
readonly subpath?: string;
|
|
166
|
+
readonly text?: never;
|
|
167
|
+
readonly url?: never;
|
|
168
|
+
readonly label?: never;
|
|
169
|
+
readonly background?: never;
|
|
170
|
+
readonly backgroundStyle?: never;
|
|
171
|
+
}) | (BoardNodeBase & {
|
|
172
|
+
readonly type: 'link';
|
|
173
|
+
readonly url: string;
|
|
174
|
+
readonly text?: never;
|
|
175
|
+
readonly file?: never;
|
|
176
|
+
readonly subpath?: never;
|
|
177
|
+
readonly label?: never;
|
|
178
|
+
readonly background?: never;
|
|
179
|
+
readonly backgroundStyle?: never;
|
|
180
|
+
}) | (BoardNodeBase & {
|
|
181
|
+
readonly type: 'group';
|
|
182
|
+
readonly label?: string;
|
|
183
|
+
readonly background?: string;
|
|
184
|
+
readonly backgroundStyle?: JsonCanvasBackgroundStyle;
|
|
185
|
+
readonly text?: never;
|
|
186
|
+
readonly file?: never;
|
|
187
|
+
readonly subpath?: never;
|
|
188
|
+
readonly url?: never;
|
|
189
|
+
});
|
|
190
|
+
interface NodeInputBase {
|
|
191
|
+
id?: NodeId;
|
|
192
|
+
x?: number;
|
|
193
|
+
y?: number;
|
|
194
|
+
width?: number;
|
|
195
|
+
height?: number;
|
|
196
|
+
color?: CanvasColor;
|
|
197
|
+
locked?: boolean;
|
|
198
|
+
visible?: boolean;
|
|
199
|
+
parentId?: NodeId;
|
|
200
|
+
select?: boolean;
|
|
201
|
+
}
|
|
202
|
+
/** Input accepted by `createNode`, with text defaults and explicit file/link values. */
|
|
203
|
+
export type NodeInput = (NodeInputBase & {
|
|
204
|
+
type?: 'text';
|
|
205
|
+
text?: string;
|
|
206
|
+
file?: never;
|
|
207
|
+
subpath?: never;
|
|
208
|
+
url?: never;
|
|
209
|
+
label?: never;
|
|
210
|
+
background?: never;
|
|
211
|
+
backgroundStyle?: never;
|
|
212
|
+
}) | (NodeInputBase & {
|
|
213
|
+
type: 'file';
|
|
214
|
+
file: string;
|
|
215
|
+
subpath?: string;
|
|
216
|
+
text?: never;
|
|
217
|
+
url?: never;
|
|
218
|
+
label?: never;
|
|
219
|
+
background?: never;
|
|
220
|
+
backgroundStyle?: never;
|
|
221
|
+
}) | (NodeInputBase & {
|
|
222
|
+
type: 'link';
|
|
223
|
+
url: string;
|
|
224
|
+
text?: never;
|
|
225
|
+
file?: never;
|
|
226
|
+
subpath?: never;
|
|
227
|
+
label?: never;
|
|
228
|
+
background?: never;
|
|
229
|
+
backgroundStyle?: never;
|
|
230
|
+
}) | (NodeInputBase & {
|
|
231
|
+
type: 'group';
|
|
232
|
+
label?: string;
|
|
233
|
+
background?: string;
|
|
234
|
+
backgroundStyle?: JsonCanvasBackgroundStyle;
|
|
235
|
+
text?: never;
|
|
236
|
+
file?: never;
|
|
237
|
+
subpath?: never;
|
|
238
|
+
url?: never;
|
|
239
|
+
});
|
|
240
|
+
/** Partial update payload accepted by `updateNode`. */
|
|
241
|
+
export interface NodePatch {
|
|
242
|
+
x?: number;
|
|
243
|
+
y?: number;
|
|
244
|
+
width?: number;
|
|
245
|
+
height?: number;
|
|
246
|
+
text?: string;
|
|
247
|
+
file?: string;
|
|
248
|
+
subpath?: string;
|
|
249
|
+
url?: string;
|
|
250
|
+
label?: string;
|
|
251
|
+
background?: string;
|
|
252
|
+
backgroundStyle?: JsonCanvasBackgroundStyle;
|
|
253
|
+
color?: CanvasColor;
|
|
254
|
+
locked?: boolean;
|
|
255
|
+
visible?: boolean;
|
|
256
|
+
parentId?: NodeId;
|
|
257
|
+
}
|
|
258
|
+
/** Nodes created by duplication plus the canonical source-to-copy identity map. */
|
|
259
|
+
export interface DuplicateNodesResult {
|
|
260
|
+
readonly nodes: readonly BoardNode[];
|
|
261
|
+
readonly idMap: ReadonlyMap<NodeId, NodeId>;
|
|
262
|
+
}
|
|
263
|
+
/** Options controlling how a validated document enters the current board. */
|
|
264
|
+
export interface DocumentLoadOptions {
|
|
265
|
+
mode?: 'replace' | 'merge';
|
|
266
|
+
}
|
|
267
|
+
export type ResizeHandle = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
|
268
|
+
export type SelectionMode = 'replace' | 'append' | 'toggle';
|
|
269
|
+
export type BoxSelectBehavior = 'autocad' | 'contain' | 'intersect';
|
|
270
|
+
export type BoxSelectMode = 'window' | 'crossing';
|
|
271
|
+
export interface BoxSelectSettings {
|
|
272
|
+
behavior: BoxSelectBehavior;
|
|
273
|
+
}
|
|
274
|
+
/** High-level interaction state for the active pointer or keyboard gesture. */
|
|
275
|
+
export type InteractionMode = 'idle' | 'panning' | 'dragging-nodes' | 'resizing-node' | 'box-select' | 'editing-text';
|
|
276
|
+
interface IdleInteractionState {
|
|
277
|
+
mode: 'idle';
|
|
278
|
+
}
|
|
279
|
+
interface PanInteractionState {
|
|
280
|
+
mode: 'panning';
|
|
281
|
+
pointerId: number;
|
|
282
|
+
lastScreenPoint: Point;
|
|
283
|
+
}
|
|
284
|
+
interface DragInteractionState {
|
|
285
|
+
mode: 'dragging-nodes';
|
|
286
|
+
pointerId: number;
|
|
287
|
+
nodeIds: NodeId[];
|
|
288
|
+
startScreenPoint: Point;
|
|
289
|
+
startNodePositions: Record<NodeId, Point>;
|
|
290
|
+
}
|
|
291
|
+
interface ResizeInteractionState {
|
|
292
|
+
mode: 'resizing-node';
|
|
293
|
+
pointerId: number;
|
|
294
|
+
nodeId: NodeId;
|
|
295
|
+
handle: ResizeHandle;
|
|
296
|
+
startScreenPoint: Point;
|
|
297
|
+
startNodeBounds: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>;
|
|
298
|
+
aspectRatio: number;
|
|
299
|
+
}
|
|
300
|
+
interface BoxSelectInteractionState {
|
|
301
|
+
mode: 'box-select';
|
|
302
|
+
pointerId: number;
|
|
303
|
+
selectionMode: BoxSelectMode;
|
|
304
|
+
startScreenPoint: Point;
|
|
305
|
+
currentScreenPoint: Point;
|
|
306
|
+
startWorldPoint: Point;
|
|
307
|
+
currentWorldPoint: Point;
|
|
308
|
+
}
|
|
309
|
+
interface EditingInteractionState {
|
|
310
|
+
mode: 'editing-text';
|
|
311
|
+
nodeId: NodeId;
|
|
312
|
+
}
|
|
313
|
+
export type InteractionState = IdleInteractionState | PanInteractionState | DragInteractionState | ResizeInteractionState | BoxSelectInteractionState | EditingInteractionState;
|
|
314
|
+
/** Immutable effective runtime state exposed through `getState()`. */
|
|
315
|
+
export interface BoardState {
|
|
316
|
+
readonly camera: Camera;
|
|
317
|
+
readonly grid: GridSettings;
|
|
318
|
+
readonly nodes: ReadonlyMap<NodeId, BoardNode>;
|
|
319
|
+
readonly selection: ReadonlySet<NodeId>;
|
|
320
|
+
readonly interaction: InteractionState;
|
|
321
|
+
readonly snapGuides: readonly SnapGuide[];
|
|
322
|
+
}
|
|
323
|
+
/** Internal array form used for document normalization, validation, and encoding. */
|
|
324
|
+
export interface InternalBoardSnapshot {
|
|
325
|
+
readonly camera: Camera;
|
|
326
|
+
readonly grid: GridSettings;
|
|
327
|
+
readonly nodes: readonly BoardNode[];
|
|
328
|
+
readonly selection: readonly NodeId[];
|
|
329
|
+
readonly interaction: InteractionState;
|
|
330
|
+
readonly snapGuides: readonly SnapGuide[];
|
|
331
|
+
readonly nextZIndex: number;
|
|
332
|
+
}
|
|
333
|
+
export interface BoardPluginApis {
|
|
334
|
+
}
|
|
335
|
+
/** Opaque install token carrying the API and events installed by a plugin factory. */
|
|
336
|
+
export interface BoardPlugin<TApis extends BoardPluginApis = BoardPluginApis, TEvents = {}> {
|
|
337
|
+
readonly name: string;
|
|
338
|
+
readonly __boardPluginBrand: never;
|
|
339
|
+
readonly __boardPluginApis: TApis;
|
|
340
|
+
readonly __boardPluginEvents: TEvents;
|
|
341
|
+
}
|
|
342
|
+
type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
|
|
343
|
+
type PluginApi<TPlugin> = TPlugin extends BoardPlugin<infer TApis, infer _TEvents> ? TApis : never;
|
|
344
|
+
type PluginEvents<TPlugin> = TPlugin extends BoardPlugin<infer _TApis, infer TEvents> ? TEvents : never;
|
|
345
|
+
type InstalledPluginApisForTuple<TPlugins extends readonly BoardPlugin[]> = [
|
|
346
|
+
TPlugins[number]
|
|
347
|
+
] extends [never] ? BoardPluginApis : BoardPluginApis & UnionToIntersection<PluginApi<TPlugins[number]>>;
|
|
348
|
+
type InstalledPluginEventsForTuple<TPlugins extends readonly BoardPlugin[]> = [
|
|
349
|
+
TPlugins[number]
|
|
350
|
+
] extends [never] ? {} : UnionToIntersection<PluginEvents<TPlugins[number]>>;
|
|
351
|
+
/**
|
|
352
|
+
* Distribute over conditional plugin tuples so only APIs present in every
|
|
353
|
+
* possible runtime branch can be accessed without narrowing.
|
|
354
|
+
*/
|
|
355
|
+
export type InstalledPluginApis<TPlugins extends readonly BoardPlugin[]> = TPlugins extends readonly BoardPlugin[] ? InstalledPluginApisForTuple<TPlugins> : never;
|
|
356
|
+
export type InstalledPluginEvents<TPlugins extends readonly BoardPlugin[]> = TPlugins extends readonly BoardPlugin[] ? InstalledPluginEventsForTuple<TPlugins> : never;
|
|
357
|
+
/** Context for failures reported after the engine can no longer roll back work. */
|
|
358
|
+
export type BoardUnhandledErrorContext = {
|
|
359
|
+
readonly source: 'event-listener';
|
|
360
|
+
readonly event: string;
|
|
361
|
+
} | {
|
|
362
|
+
readonly source: 'subscriber';
|
|
363
|
+
readonly channel: string;
|
|
364
|
+
} | {
|
|
365
|
+
readonly source: 'commit-effect';
|
|
366
|
+
readonly commit: string;
|
|
367
|
+
};
|
|
368
|
+
/** Engine factory options shared by commands, internal plugins, and renderers. */
|
|
369
|
+
export interface BoardEngineOptions<TPlugins extends readonly BoardPlugin[] = readonly []> {
|
|
370
|
+
camera?: Partial<Camera>;
|
|
371
|
+
zoom?: Partial<ZoomSettings>;
|
|
372
|
+
grid?: Partial<GridSettings>;
|
|
373
|
+
nodes?: Partial<NodeConstraints>;
|
|
374
|
+
boxSelect?: Partial<BoxSelectSettings>;
|
|
375
|
+
plugins?: TPlugins;
|
|
376
|
+
diagnostics?: boolean | {
|
|
377
|
+
traceLimit?: number;
|
|
378
|
+
};
|
|
379
|
+
onUnhandledError?: (error: unknown, context: BoardUnhandledErrorContext) => void;
|
|
380
|
+
initialNodes?: ReadonlyArray<BoardNode>;
|
|
381
|
+
initialDocument?: JsonCanvasDocument;
|
|
382
|
+
}
|
|
383
|
+
/** Structured validation failure emitted when validation fails. */
|
|
384
|
+
export interface ValidationFailure {
|
|
385
|
+
name: string;
|
|
386
|
+
message: string;
|
|
387
|
+
state: BoardState;
|
|
388
|
+
context: string;
|
|
389
|
+
}
|
|
390
|
+
/** Trace row recorded when diagnostics are enabled. */
|
|
391
|
+
export interface TraceEntry {
|
|
392
|
+
readonly event: string;
|
|
393
|
+
readonly timestamp: number;
|
|
394
|
+
readonly args: readonly unknown[];
|
|
395
|
+
}
|
|
396
|
+
/** History capture policy attached to command lifecycle events. */
|
|
397
|
+
export type CommandHistoryPolicy = 'record' | 'ignore';
|
|
398
|
+
/** Explicit command lifecycle metadata consumed by internal plugins. */
|
|
399
|
+
export interface CommandMetadata {
|
|
400
|
+
history: CommandHistoryPolicy;
|
|
401
|
+
}
|
|
402
|
+
/** Base event contract emitted by every board engine. Installed plugin tuples add their own event maps. */
|
|
403
|
+
export interface BoardEventMap {
|
|
404
|
+
destroy: () => void;
|
|
405
|
+
'camera:change': (camera: Camera, prev: Camera) => void;
|
|
406
|
+
'viewport:change': (size: Point, prev: Point) => void;
|
|
407
|
+
'node:created': (node: BoardNode) => void;
|
|
408
|
+
'node:updated': (node: BoardNode, prev: BoardNode) => void;
|
|
409
|
+
'node:deleted': (id: NodeId, prev: BoardNode) => void;
|
|
410
|
+
'node:moved': (node: BoardNode, delta: Point) => void;
|
|
411
|
+
'node:resized': (node: BoardNode, prev: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>) => void;
|
|
412
|
+
'selection:change': (selected: NodeId[], prev: NodeId[]) => void;
|
|
413
|
+
'interaction:start': (state: InteractionState) => void;
|
|
414
|
+
'interaction:update': (state: InteractionState) => void;
|
|
415
|
+
'interaction:end': (state: InteractionState) => void;
|
|
416
|
+
'command:before': (name: string, args: unknown[], metadata: CommandMetadata) => void;
|
|
417
|
+
'command:after': (name: string, args: unknown[], duration: number, metadata: CommandMetadata) => void;
|
|
418
|
+
'command:blocked': (name: string, args: unknown[], metadata: CommandMetadata) => void;
|
|
419
|
+
'validation:failed': (failure: ValidationFailure) => void;
|
|
420
|
+
}
|
|
421
|
+
export type PluginCleanup = () => void;
|
|
422
|
+
export type Unsubscribe = () => void;
|
|
423
|
+
/** Immutable command description evaluated by host policy guards. */
|
|
424
|
+
export interface CommandContext {
|
|
425
|
+
readonly name: string;
|
|
426
|
+
readonly args: readonly unknown[];
|
|
427
|
+
readonly metadata: CommandMetadata;
|
|
428
|
+
}
|
|
429
|
+
/** Allow a command with `true`, or block it with an actionable reason. */
|
|
430
|
+
export type CommandGuard = (command: Readonly<CommandContext>) => true | string;
|
|
431
|
+
/** Minimal observable contract used by the engine and framework adapters. */
|
|
432
|
+
export interface Subscribable<T> {
|
|
433
|
+
get(): T;
|
|
434
|
+
subscribe(callback: (value: T, prev: T) => void): Unsubscribe;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Public board engine interface.
|
|
438
|
+
*
|
|
439
|
+
* Commands mutate persistent board state, subscribables expose reactive state,
|
|
440
|
+
* and events let host applications observe lifecycle changes.
|
|
441
|
+
*/
|
|
442
|
+
export interface BoardEngine<TPluginApis extends BoardPluginApis = BoardPluginApis, TPluginEvents = {}> {
|
|
443
|
+
readonly plugins: TPluginApis;
|
|
444
|
+
readonly $camera: Subscribable<Camera>;
|
|
445
|
+
readonly $grid: Subscribable<GridSettings>;
|
|
446
|
+
readonly $nodes: Subscribable<ReadonlyMap<NodeId, BoardNode>>;
|
|
447
|
+
readonly $selection: Subscribable<ReadonlySet<NodeId>>;
|
|
448
|
+
readonly $interaction: Subscribable<InteractionState>;
|
|
449
|
+
readonly $snapGuides: Subscribable<readonly SnapGuide[]>;
|
|
450
|
+
destroy(): void;
|
|
451
|
+
batch(fn: () => void): void;
|
|
452
|
+
getState(): BoardState;
|
|
453
|
+
getGridSettings(): GridSettings;
|
|
454
|
+
getViewportSize(): Point;
|
|
455
|
+
updateGridSettings(patch: Partial<GridSettings>): GridSettings;
|
|
456
|
+
setViewportSize(size: Point): void;
|
|
457
|
+
on<K extends keyof (BoardEventMap & TPluginEvents)>(event: K, handler: (BoardEventMap & TPluginEvents)[K]): Unsubscribe;
|
|
458
|
+
once<K extends keyof (BoardEventMap & TPluginEvents)>(event: K, handler: (BoardEventMap & TPluginEvents)[K]): Unsubscribe;
|
|
459
|
+
off<K extends keyof (BoardEventMap & TPluginEvents)>(event: K, handler: (BoardEventMap & TPluginEvents)[K]): void;
|
|
460
|
+
exportTrace(): TraceEntry[];
|
|
461
|
+
/**
|
|
462
|
+
* Register a synchronous command gate. Intended for concrete host policy such
|
|
463
|
+
* as read-only mode, not broad application orchestration. Returns an
|
|
464
|
+
* unsubscribe function that removes the guard.
|
|
465
|
+
*/
|
|
466
|
+
addCommandGuard(fn: CommandGuard): Unsubscribe;
|
|
467
|
+
screenToWorld(point: Point): Point;
|
|
468
|
+
worldToScreen(point: Point): Point;
|
|
469
|
+
getVisibleBounds(width: number, height: number): Bounds;
|
|
470
|
+
getNode(id: NodeId): BoardNode;
|
|
471
|
+
findNode(id: NodeId): BoardNode | null;
|
|
472
|
+
hasNode(id: NodeId): boolean;
|
|
473
|
+
getNodeAt(worldPoint: Point): BoardNode | null;
|
|
474
|
+
getNodesInBounds(bounds: Bounds): BoardNode[];
|
|
475
|
+
panBy(dx: number, dy: number): void;
|
|
476
|
+
panTo(worldPoint: Point, animated?: boolean): Promise<void>;
|
|
477
|
+
zoomAt(screenPoint: Point, delta: number): void;
|
|
478
|
+
zoomTo(level: number, animated?: boolean): Promise<void>;
|
|
479
|
+
zoomToFit(padding?: number, animated?: boolean): Promise<void>;
|
|
480
|
+
zoomToNodes(ids: NodeId[], padding?: number, animated?: boolean): Promise<void>;
|
|
481
|
+
createNode(input: NodeInput): BoardNode;
|
|
482
|
+
updateNode(id: NodeId, patch: NodePatch): BoardNode;
|
|
483
|
+
deleteNode(id: NodeId): void;
|
|
484
|
+
moveNode(id: NodeId, dx: number, dy: number): BoardNode;
|
|
485
|
+
translateSelectedNodes(dx: number, dy: number): void;
|
|
486
|
+
resizeNode(id: NodeId, handle: ResizeHandle, dx: number, dy: number): BoardNode;
|
|
487
|
+
bringToFront(id: NodeId): void;
|
|
488
|
+
sendToBack(id: NodeId): void;
|
|
489
|
+
lockNode(id: NodeId): void;
|
|
490
|
+
unlockNode(id: NodeId): void;
|
|
491
|
+
duplicateNodes(ids: NodeId[], offset?: Point): DuplicateNodesResult;
|
|
492
|
+
copySelected(): BoardNode[];
|
|
493
|
+
pasteClipboard(offset?: Point): BoardNode[];
|
|
494
|
+
select(ids: NodeId | NodeId[], mode?: SelectionMode): void;
|
|
495
|
+
selectAll(): void;
|
|
496
|
+
clearSelection(): void;
|
|
497
|
+
deleteSelected(): void;
|
|
498
|
+
getSelection(): NodeId[];
|
|
499
|
+
beginTextEdit(id: NodeId): void;
|
|
500
|
+
commitTextEdit(id: NodeId, text?: string): BoardNode;
|
|
501
|
+
cancelTextEdit(): void;
|
|
502
|
+
exportDocument(): JsonCanvasDocument;
|
|
503
|
+
loadDocument(document: unknown, options?: DocumentLoadOptions): void;
|
|
504
|
+
}
|
|
505
|
+
/** Sealed pointer/session adapter consumed by framework integrations. */
|
|
506
|
+
export interface InternalInteractionAdapter {
|
|
507
|
+
beginPan(pointerId: number, screenPoint: Point): void;
|
|
508
|
+
beginNodeDrag(id: NodeId, pointerId: number, screenPoint: Point): void;
|
|
509
|
+
beginResize(id: NodeId, handle: ResizeHandle, pointerId: number, screenPoint: Point): void;
|
|
510
|
+
beginBoxSelect(pointerId: number, screenPoint: Point): void;
|
|
511
|
+
updatePointer(pointerId: number, screenPoint: Point, modifiers?: {
|
|
512
|
+
shift?: boolean;
|
|
513
|
+
space?: boolean;
|
|
514
|
+
}): void;
|
|
515
|
+
endInteraction(pointerId?: number): void;
|
|
516
|
+
cancelInteraction(pointerId?: number): void;
|
|
517
|
+
getUniformTranslationTargets(seedIds: NodeId[]): NodeId[];
|
|
518
|
+
syncGroupZOrder(groupId: NodeId): void;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Internal plugin surface used by workspace packages such as history and
|
|
522
|
+
* connections. This is internal infrastructure, not a general plugin surface.
|
|
523
|
+
*/
|
|
524
|
+
export interface InternalPluginContext<TPluginApis extends BoardPluginApis = BoardPluginApis, TEvents extends {
|
|
525
|
+
[K in keyof TEvents]: (...args: never[]) => unknown;
|
|
526
|
+
} = BoardEventMap> extends Omit<BoardEngine, 'plugins'> {
|
|
527
|
+
readonly plugins: TPluginApis;
|
|
528
|
+
/** Assert that the owning engine has not been destroyed. */
|
|
529
|
+
assertActive(): void;
|
|
530
|
+
/** Whether the current command has joined an explicit outer batch. */
|
|
531
|
+
isBatching(): boolean;
|
|
532
|
+
emit<K extends keyof TEvents>(event: K, ...args: Parameters<TEvents[K]>): void;
|
|
533
|
+
extend<K extends keyof TPluginApis & string>(key: K, value: TPluginApis[K]): void;
|
|
534
|
+
/**
|
|
535
|
+
* Execute a named mutation through guarded command handling. Successful
|
|
536
|
+
* lifecycle events publish after validation; guards are the pre-execution hook.
|
|
537
|
+
* Use this in internal plugins so edge/connection operations appear in traces,
|
|
538
|
+
* are interceptable by command guards, and are captured by the history plugin.
|
|
539
|
+
*/
|
|
540
|
+
runCommand<T>(name: string, args: unknown[], fn: () => T, metadata: CommandMetadata): T;
|
|
541
|
+
/** Read the immutable persistent slice owned by the current plugin. */
|
|
542
|
+
getPluginState<S>(): S;
|
|
543
|
+
/** Replace the current plugin's persistent slice inside the active command. */
|
|
544
|
+
updatePluginState<S>(update: (current: S) => S): S;
|
|
545
|
+
/** Prepare final bookkeeping/event publication for a validated outer commit. The effect cannot mutate or destroy the board. */
|
|
546
|
+
projectCommit(projector: (commit: import('./state/types.js').InternalBoardCommit) => () => void): Unsubscribe;
|
|
547
|
+
/** Atomically restore a persistent root without recording another history frame. */
|
|
548
|
+
restoreHistoryRoot(root: import('./state/types.js').InternalHistoryRoot): void;
|
|
549
|
+
}
|
|
550
|
+
/** Persistent state owned by an internal plugin. */
|
|
551
|
+
interface InternalPluginSlice {
|
|
552
|
+
initial: unknown;
|
|
553
|
+
}
|
|
554
|
+
/** Optional internal hook for persisted JSON Canvas document data. */
|
|
555
|
+
export interface InternalPluginPersistence<TPluginApis extends BoardPluginApis = BoardPluginApis, TEvents extends {
|
|
556
|
+
[K in keyof TEvents]: (...args: never[]) => unknown;
|
|
557
|
+
} = BoardEventMap> {
|
|
558
|
+
exportDocument?(engine: InternalPluginContext<TPluginApis, TEvents>): Partial<JsonCanvasDocument> | void;
|
|
559
|
+
loadDocument?(engine: InternalPluginContext<TPluginApis, TEvents>, document: JsonCanvasDocument, mode: 'replace' | 'merge', idMap: ReadonlyMap<NodeId, NodeId>): void;
|
|
560
|
+
}
|
|
561
|
+
/** Internal plugin contract for state, commands, and side effects. */
|
|
562
|
+
export interface InternalBoardPlugin<TPluginApis extends BoardPluginApis = BoardPluginApis, TEvents extends {
|
|
563
|
+
[K in keyof TEvents]: (...args: never[]) => unknown;
|
|
564
|
+
} = BoardEventMap> extends BoardPlugin<TPluginApis, TEvents> {
|
|
565
|
+
name: string;
|
|
566
|
+
slice?: InternalPluginSlice;
|
|
567
|
+
persistence?: InternalPluginPersistence<TPluginApis, TEvents>;
|
|
568
|
+
nodeDeleted?(engine: InternalPluginContext<TPluginApis, TEvents>, nodeId: NodeId): void;
|
|
569
|
+
install(engine: InternalPluginContext<TPluginApis, TEvents>, options?: Record<string, unknown>): void | PluginCleanup;
|
|
570
|
+
}
|
|
571
|
+
export type InternalBoardPluginDefinition<TPluginApis extends BoardPluginApis = BoardPluginApis, TEvents extends {
|
|
572
|
+
[K in keyof TEvents]: (...args: never[]) => unknown;
|
|
573
|
+
} = BoardEventMap> = Omit<InternalBoardPlugin<TPluginApis, TEvents>, keyof BoardPlugin> & {
|
|
574
|
+
readonly name: string;
|
|
575
|
+
};
|
|
576
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lupinum/board-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless node-based board engine for spatial editors, diagramming tools, and whiteboard-style interfaces.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lupinum OG <info@lupinum.com> (https://lupinum.com)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.19.0"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"module": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./internal": {
|
|
23
|
+
"types": "./dist/internal.d.ts",
|
|
24
|
+
"import": "./dist/internal.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/lupinum-dev/nuxt-board.git",
|
|
30
|
+
"directory": "packages/board-core"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://nuxt-board.lupinum.com",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/lupinum-dev/nuxt-board/issues"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"board",
|
|
38
|
+
"canvas",
|
|
39
|
+
"diagram",
|
|
40
|
+
"editor",
|
|
41
|
+
"nodes",
|
|
42
|
+
"vue"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup src/index.ts src/internal.ts --format esm --clean && tsc -p tsconfig.build.json"
|
|
49
|
+
}
|
|
50
|
+
}
|