@ai-matrx/canvas 0.0.0-bootstrap.0 → 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.
@@ -0,0 +1,380 @@
1
+ /**
2
+ * @ai-matrx/canvas — core types.
3
+ *
4
+ * The canvas is ONE docked column on the right edge of an application. It holds
5
+ * panes; panes hold tabs; every tab is one CanvasItem. A CanvasItem is
6
+ * identified by `kind` + `key`, so opening the same thing twice always lands on
7
+ * the existing tab instead of creating a duplicate.
8
+ *
9
+ * Everything in CanvasState is plain JSON: it lives in the host's Redux store
10
+ * (or the package's own standalone store), it is persisted between sessions,
11
+ * and it can be inspected. No functions, no class instances, no React nodes.
12
+ */
13
+ /** Any value that survives JSON.stringify → JSON.parse unchanged. */
14
+ type CanvasJson = string | number | boolean | null | readonly CanvasJson[] | {
15
+ readonly [key: string]: CanvasJson | undefined;
16
+ };
17
+ declare const itemIdBrand: unique symbol;
18
+ declare const paneIdBrand: unique symbol;
19
+ declare const splitIdBrand: unique symbol;
20
+ /** `${kind}::${key}` — the identity of one thing on the canvas. */
21
+ type CanvasItemId = string & {
22
+ readonly [itemIdBrand]: true;
23
+ };
24
+ type CanvasPaneId = string & {
25
+ readonly [paneIdBrand]: true;
26
+ };
27
+ type CanvasSplitId = string & {
28
+ readonly [splitIdBrand]: true;
29
+ };
30
+ interface CanvasItem {
31
+ readonly id: CanvasItemId;
32
+ /** Registered kind id (see `registerCanvasKind`). */
33
+ readonly kind: string;
34
+ /** Stable key inside the kind — an artifact id, a conversation id, "default". */
35
+ readonly key: string;
36
+ /** Explicit tab title. `null` ⇒ the kind's `title(data)` or its label. */
37
+ readonly title: string | null;
38
+ readonly data: CanvasJson;
39
+ readonly openedAt: number;
40
+ readonly updatedAt: number;
41
+ }
42
+ interface CanvasPane {
43
+ readonly id: CanvasPaneId;
44
+ readonly itemIds: readonly CanvasItemId[];
45
+ readonly activeItemId: CanvasItemId | null;
46
+ }
47
+ /** "horizontal" = children side by side; "vertical" = children stacked. */
48
+ type CanvasOrientation = "horizontal" | "vertical";
49
+ type CanvasLayoutNode = {
50
+ readonly type: "pane";
51
+ readonly paneId: CanvasPaneId;
52
+ } | {
53
+ readonly type: "split";
54
+ readonly id: CanvasSplitId;
55
+ readonly orientation: CanvasOrientation;
56
+ readonly children: readonly CanvasLayoutNode[];
57
+ /** Fractions, one per child, summing to 1. */
58
+ readonly sizes: readonly number[];
59
+ };
60
+ interface CanvasState {
61
+ readonly version: 1;
62
+ readonly isOpen: boolean;
63
+ readonly isFullscreen: boolean;
64
+ /** Column width in CSS pixels (desktop). */
65
+ readonly width: number;
66
+ readonly layout: CanvasLayoutNode;
67
+ readonly panes: {
68
+ readonly [paneId: string]: CanvasPane;
69
+ };
70
+ readonly items: {
71
+ readonly [itemId: string]: CanvasItem;
72
+ };
73
+ readonly focusedPaneId: CanvasPaneId;
74
+ /** Monotonic counter for minting pane/split ids — keeps the reducer pure. */
75
+ readonly seq: number;
76
+ /** True once a persisted snapshot was applied (or there was none). */
77
+ readonly hydrated: boolean;
78
+ }
79
+ /** Where a newly opened item goes. Existing items never move on open. */
80
+ type CanvasOpenTarget = "focused" | "split-right" | "split-down" | {
81
+ readonly paneId: CanvasPaneId;
82
+ };
83
+ interface CanvasOpenInput {
84
+ readonly kind: string;
85
+ readonly key: string;
86
+ readonly title?: string | null | undefined;
87
+ readonly data?: CanvasJson | undefined;
88
+ readonly target?: CanvasOpenTarget | undefined;
89
+ /** Reveal the canvas when it is put away. Default true. */
90
+ readonly reveal?: boolean | undefined;
91
+ /** Make the item the pane's active tab. Default true. */
92
+ readonly activate?: boolean | undefined;
93
+ }
94
+ declare const CANVAS_MIN_WIDTH = 360;
95
+ declare const CANVAS_DEFAULT_WIDTH = 640;
96
+ declare const CANVAS_MIN_SPLIT_FRACTION = 0.12;
97
+
98
+ declare function canvasItemId(kind: string, key: string): CanvasItemId;
99
+ declare function parseCanvasItemId(id: CanvasItemId): {
100
+ kind: string;
101
+ key: string;
102
+ };
103
+ /**
104
+ * Returns the path of the first value that is NOT plain JSON, or null when the
105
+ * whole value is JSON. Functions, class instances, Dates, Maps, NaN and
106
+ * Infinity all fail — they would silently vanish or change on persist.
107
+ */
108
+ declare function findNonJson(value: unknown, path?: string): string | null;
109
+ declare function isCanvasJson(value: unknown): value is CanvasJson;
110
+
111
+ /**
112
+ * Pure helpers over the canvas layout tree. Every function returns a new tree
113
+ * and never mutates its input.
114
+ */
115
+
116
+ declare function listPaneIds(node: CanvasLayoutNode): CanvasPaneId[];
117
+ /** Renormalizes to sum 1; falls back to even sizes for garbage input. */
118
+ declare function normalizeSizes(sizes: readonly number[], count: number): number[];
119
+
120
+ /**
121
+ * THE canvas reducer. Plain Redux-compatible reducer + action creators with no
122
+ * Redux Toolkit dependency, so it mounts in a host's RTK store
123
+ * (`canvasHost: canvasReducer`) or runs inside the package's own standalone
124
+ * store with identical behaviour.
125
+ *
126
+ * Identity law: an item is `kind::key`. Opening an item that is already on the
127
+ * canvas never duplicates it — it refreshes its data/title, activates its tab
128
+ * and focuses its pane, wherever that pane is.
129
+ */
130
+
131
+ declare const P = "matrxCanvas/";
132
+ type CanvasAction = {
133
+ type: `${typeof P}open`;
134
+ payload: CanvasOpenInput & {
135
+ now: number;
136
+ };
137
+ } | {
138
+ type: `${typeof P}update`;
139
+ payload: {
140
+ itemId: CanvasItemId;
141
+ data?: CanvasJson | undefined;
142
+ title?: string | null | undefined;
143
+ now: number;
144
+ };
145
+ } | {
146
+ type: `${typeof P}rekey`;
147
+ payload: {
148
+ itemId: CanvasItemId;
149
+ key: string;
150
+ };
151
+ } | {
152
+ type: `${typeof P}closeItem`;
153
+ payload: {
154
+ itemId: CanvasItemId;
155
+ };
156
+ } | {
157
+ type: `${typeof P}closeOthers`;
158
+ payload: {
159
+ itemId: CanvasItemId;
160
+ };
161
+ } | {
162
+ type: `${typeof P}activate`;
163
+ payload: {
164
+ itemId: CanvasItemId;
165
+ };
166
+ } | {
167
+ type: `${typeof P}focusPane`;
168
+ payload: {
169
+ paneId: CanvasPaneId;
170
+ };
171
+ } | {
172
+ type: `${typeof P}moveItem`;
173
+ payload: {
174
+ itemId: CanvasItemId;
175
+ toPaneId: CanvasPaneId;
176
+ index?: number | undefined;
177
+ };
178
+ } | {
179
+ type: `${typeof P}splitPane`;
180
+ payload: {
181
+ paneId: CanvasPaneId;
182
+ orientation: CanvasOrientation;
183
+ moveItemId?: CanvasItemId | undefined;
184
+ };
185
+ } | {
186
+ type: `${typeof P}closePane`;
187
+ payload: {
188
+ paneId: CanvasPaneId;
189
+ };
190
+ } | {
191
+ type: `${typeof P}resizeSplit`;
192
+ payload: {
193
+ splitId: CanvasSplitId;
194
+ sizes: readonly number[];
195
+ };
196
+ } | {
197
+ type: `${typeof P}setOpen`;
198
+ payload: {
199
+ open: boolean;
200
+ };
201
+ } | {
202
+ type: `${typeof P}toggle`;
203
+ } | {
204
+ type: `${typeof P}setFullscreen`;
205
+ payload: {
206
+ fullscreen: boolean;
207
+ };
208
+ } | {
209
+ type: `${typeof P}setWidth`;
210
+ payload: {
211
+ width: number;
212
+ };
213
+ } | {
214
+ type: `${typeof P}hydrate`;
215
+ payload: {
216
+ snapshot: CanvasState | null;
217
+ };
218
+ } | {
219
+ type: `${typeof P}reset`;
220
+ };
221
+ declare const canvasActions: {
222
+ readonly open: (input: CanvasOpenInput) => CanvasAction;
223
+ readonly update: (itemId: CanvasItemId, patch: {
224
+ data?: CanvasJson | undefined;
225
+ title?: string | null | undefined;
226
+ }) => CanvasAction;
227
+ /** Gives an item a new identity in place (e.g. a draft that was just saved and now has an id). */
228
+ readonly rekey: (itemId: CanvasItemId, key: string) => CanvasAction;
229
+ readonly closeItem: (itemId: CanvasItemId) => CanvasAction;
230
+ readonly closeOthers: (itemId: CanvasItemId) => CanvasAction;
231
+ readonly activate: (itemId: CanvasItemId) => CanvasAction;
232
+ readonly focusPane: (paneId: CanvasPaneId) => CanvasAction;
233
+ readonly moveItem: (itemId: CanvasItemId, toPaneId: CanvasPaneId, index?: number) => CanvasAction;
234
+ readonly splitPane: (paneId: CanvasPaneId, orientation: CanvasOrientation, moveItemId?: CanvasItemId) => CanvasAction;
235
+ readonly closePane: (paneId: CanvasPaneId) => CanvasAction;
236
+ readonly resizeSplit: (splitId: CanvasSplitId, sizes: readonly number[]) => CanvasAction;
237
+ readonly setOpen: (open: boolean) => CanvasAction;
238
+ readonly toggle: () => CanvasAction;
239
+ readonly setFullscreen: (fullscreen: boolean) => CanvasAction;
240
+ readonly setWidth: (width: number) => CanvasAction;
241
+ readonly hydrate: (snapshot: CanvasState | null) => CanvasAction;
242
+ readonly reset: () => CanvasAction;
243
+ };
244
+ declare function isCanvasAction(action: unknown): action is CanvasAction;
245
+ declare function createInitialCanvasState(): CanvasState;
246
+ /**
247
+ * Validates a persisted snapshot. Anything malformed is dropped back to the
248
+ * initial state rather than half-applied — a corrupt layout must never crash
249
+ * the shell.
250
+ */
251
+ declare function sanitizeCanvasSnapshot(raw: unknown): CanvasState | null;
252
+ declare function canvasReducer(state: CanvasState | undefined, action: {
253
+ type: string;
254
+ }): CanvasState;
255
+
256
+ /**
257
+ * The store seam. The canvas never owns a second copy of its state: it reads
258
+ * and writes through a CanvasStoreBinding.
259
+ *
260
+ * - A host WITH Redux mounts `canvasReducer` in its root reducer and binds it:
261
+ * bindCanvasToReduxStore(store, (root) => root.canvasHost)
262
+ * - A host WITHOUT Redux (a Vite tool, an Electron window) calls
263
+ * createCanvasStore()
264
+ * which runs the very same reducer in a tiny standalone store.
265
+ */
266
+
267
+ interface CanvasStoreBinding {
268
+ getState(): CanvasState;
269
+ dispatch(action: CanvasAction): void;
270
+ subscribe(listener: () => void): () => void;
271
+ }
272
+ declare function createCanvasStore(initial?: CanvasState): CanvasStoreBinding;
273
+ /** The minimum of a Redux store the canvas needs. */
274
+ interface ReduxStoreLike<TRoot> {
275
+ getState(): TRoot;
276
+ dispatch(action: CanvasAction): unknown;
277
+ subscribe(listener: () => void): () => void;
278
+ }
279
+ declare function bindCanvasToReduxStore<TRoot>(store: ReduxStoreLike<TRoot>, select: (root: TRoot) => CanvasState): CanvasStoreBinding;
280
+
281
+ /**
282
+ * Remembering the canvas between sessions — a table stake, never optional.
283
+ *
284
+ * The default port writes to localStorage under a versioned key. Items whose
285
+ * kind opts out (`restore: false`, e.g. a live session that cannot come back)
286
+ * are dropped from the snapshot, and panes they leave empty are dropped too.
287
+ */
288
+
289
+ interface CanvasPersistencePort {
290
+ load(): CanvasState | null | Promise<CanvasState | null>;
291
+ save(snapshot: CanvasState): void | Promise<void>;
292
+ }
293
+ declare const CANVAS_STORAGE_KEY = "ai-matrx.canvas.v1";
294
+ declare function createLocalStorageCanvasPersistence(key?: string, storage?: Pick<Storage, "getItem" | "setItem"> | null): CanvasPersistencePort;
295
+ /** Builds the snapshot that is actually written: only restorable items. */
296
+ declare function toPersistableSnapshot(state: CanvasState, isRestorable: (kind: string) => boolean): CanvasState;
297
+
298
+ /**
299
+ * The canvas controller: the ONE imperative API every caller uses. It wraps a
300
+ * store binding, validates what callers hand it (so the reducer stays pure and
301
+ * trusting), and owns hydration + autosave.
302
+ *
303
+ * Nothing fails silently: a refused open (non-JSON data, unknown kind) is
304
+ * reported through the error sink and returns false.
305
+ */
306
+
307
+ interface CanvasErrorReport {
308
+ readonly code: "non-json-data" | "unknown-kind" | "persistence-load" | "persistence-save";
309
+ readonly message: string;
310
+ readonly detail?: unknown;
311
+ }
312
+ type CanvasErrorSink = (report: CanvasErrorReport) => void;
313
+ declare const consoleCanvasErrorSink: CanvasErrorSink;
314
+ interface CanvasControllerOptions {
315
+ readonly store: CanvasStoreBinding;
316
+ readonly persistence?: CanvasPersistencePort | null | undefined;
317
+ /** Kinds that may not come back after a reload. Default: every kind restores. */
318
+ readonly isRestorable?: ((kind: string) => boolean) | undefined;
319
+ /** When given, opens of an unregistered kind are refused and reported. */
320
+ readonly isKnownKind?: ((kind: string) => boolean) | undefined;
321
+ readonly onError?: CanvasErrorSink | undefined;
322
+ readonly saveDelayMs?: number | undefined;
323
+ /**
324
+ * Whether a remembered "open" may be restored right now. A host returns
325
+ * false where an open canvas would take the whole screen (a phone).
326
+ */
327
+ readonly mayRestoreOpen?: (() => boolean) | undefined;
328
+ }
329
+ interface CanvasController {
330
+ readonly store: CanvasStoreBinding;
331
+ getState(): CanvasState;
332
+ open(input: CanvasOpenInput): CanvasItemId | null;
333
+ update(itemId: CanvasItemId, patch: {
334
+ data?: CanvasJson | undefined;
335
+ title?: string | null | undefined;
336
+ }): boolean;
337
+ /** Gives an item a new identity in place (a draft that was saved and now has a durable id). */
338
+ rekey(itemId: CanvasItemId, key: string): CanvasItemId | null;
339
+ close(itemId: CanvasItemId): void;
340
+ closeOthers(itemId: CanvasItemId): void;
341
+ activate(itemId: CanvasItemId): void;
342
+ focusPane(paneId: CanvasPaneId): void;
343
+ moveItem(itemId: CanvasItemId, toPaneId: CanvasPaneId, index?: number): void;
344
+ splitPane(paneId: CanvasPaneId, orientation: CanvasOrientation, moveItemId?: CanvasItemId): void;
345
+ closePane(paneId: CanvasPaneId): void;
346
+ resizeSplit(splitId: CanvasSplitId, sizes: readonly number[]): void;
347
+ show(): void;
348
+ hide(): void;
349
+ toggle(): void;
350
+ setFullscreen(fullscreen: boolean): void;
351
+ setWidth(width: number): void;
352
+ /** Is this exact thing on the canvas right now? */
353
+ has(kind: string, key: string): boolean;
354
+ /**
355
+ * Is a canvas column on screen? A store can exist in a layout that shows no
356
+ * column (a kiosk, a meeting stage); opening there must be refused aloud.
357
+ */
358
+ isPresented(): boolean;
359
+ /** Called by a column on mount; returns the unmount callback. */
360
+ registerPresentation(): () => void;
361
+ subscribePresentation(listener: () => void): () => void;
362
+ /** Loads the persisted snapshot and starts autosave. Returns a disposer. */
363
+ start(): () => void;
364
+ }
365
+ declare function createCanvasController(options: CanvasControllerOptions): CanvasController;
366
+
367
+ declare const selectCanvasIsOpen: (s: CanvasState) => boolean;
368
+ declare const selectCanvasIsFullscreen: (s: CanvasState) => boolean;
369
+ declare const selectCanvasWidth: (s: CanvasState) => number;
370
+ declare const selectCanvasLayout: (s: CanvasState) => CanvasLayoutNode;
371
+ declare const selectCanvasFocusedPaneId: (s: CanvasState) => CanvasPaneId;
372
+ declare const selectCanvasIsHydrated: (s: CanvasState) => boolean;
373
+ declare const selectCanvasItemCount: (s: CanvasState) => number;
374
+ declare const selectCanvasPane: (s: CanvasState, paneId: CanvasPaneId) => CanvasPane | undefined;
375
+ declare const selectCanvasItem: (s: CanvasState, itemId: CanvasItemId) => CanvasItem | undefined;
376
+ declare function selectCanvasPaneCount(s: CanvasState): number;
377
+ /** The item the person is looking at in the focused pane. */
378
+ declare function selectCanvasActiveItem(s: CanvasState): CanvasItem | null;
379
+
380
+ export { CANVAS_DEFAULT_WIDTH, CANVAS_MIN_SPLIT_FRACTION, CANVAS_MIN_WIDTH, CANVAS_STORAGE_KEY, type CanvasAction, type CanvasController, type CanvasControllerOptions, type CanvasErrorReport, type CanvasErrorSink, type CanvasItem, type CanvasItemId, type CanvasJson, type CanvasLayoutNode, type CanvasOpenInput, type CanvasOpenTarget, type CanvasOrientation, type CanvasPane, type CanvasPaneId, type CanvasPersistencePort, type CanvasSplitId, type CanvasState, type CanvasStoreBinding, type ReduxStoreLike, bindCanvasToReduxStore, canvasActions, canvasItemId, canvasReducer, consoleCanvasErrorSink, createCanvasController, createCanvasStore, createInitialCanvasState, createLocalStorageCanvasPersistence, findNonJson, isCanvasAction, isCanvasJson, listPaneIds, normalizeSizes, parseCanvasItemId, sanitizeCanvasSnapshot, selectCanvasActiveItem, selectCanvasFocusedPaneId, selectCanvasIsFullscreen, selectCanvasIsHydrated, selectCanvasIsOpen, selectCanvasItem, selectCanvasItemCount, selectCanvasLayout, selectCanvasPane, selectCanvasPaneCount, selectCanvasWidth, toPersistableSnapshot };