@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,424 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode, CSSProperties } from 'react';
3
+
4
+ /**
5
+ * @ai-matrx/canvas — core types.
6
+ *
7
+ * The canvas is ONE docked column on the right edge of an application. It holds
8
+ * panes; panes hold tabs; every tab is one CanvasItem. A CanvasItem is
9
+ * identified by `kind` + `key`, so opening the same thing twice always lands on
10
+ * the existing tab instead of creating a duplicate.
11
+ *
12
+ * Everything in CanvasState is plain JSON: it lives in the host's Redux store
13
+ * (or the package's own standalone store), it is persisted between sessions,
14
+ * and it can be inspected. No functions, no class instances, no React nodes.
15
+ */
16
+ /** Any value that survives JSON.stringify → JSON.parse unchanged. */
17
+ type CanvasJson = string | number | boolean | null | readonly CanvasJson[] | {
18
+ readonly [key: string]: CanvasJson | undefined;
19
+ };
20
+ declare const itemIdBrand: unique symbol;
21
+ declare const paneIdBrand: unique symbol;
22
+ declare const splitIdBrand: unique symbol;
23
+ /** `${kind}::${key}` — the identity of one thing on the canvas. */
24
+ type CanvasItemId = string & {
25
+ readonly [itemIdBrand]: true;
26
+ };
27
+ type CanvasPaneId = string & {
28
+ readonly [paneIdBrand]: true;
29
+ };
30
+ type CanvasSplitId = string & {
31
+ readonly [splitIdBrand]: true;
32
+ };
33
+ interface CanvasItem {
34
+ readonly id: CanvasItemId;
35
+ /** Registered kind id (see `registerCanvasKind`). */
36
+ readonly kind: string;
37
+ /** Stable key inside the kind — an artifact id, a conversation id, "default". */
38
+ readonly key: string;
39
+ /** Explicit tab title. `null` ⇒ the kind's `title(data)` or its label. */
40
+ readonly title: string | null;
41
+ readonly data: CanvasJson;
42
+ readonly openedAt: number;
43
+ readonly updatedAt: number;
44
+ }
45
+ interface CanvasPane {
46
+ readonly id: CanvasPaneId;
47
+ readonly itemIds: readonly CanvasItemId[];
48
+ readonly activeItemId: CanvasItemId | null;
49
+ }
50
+ /** "horizontal" = children side by side; "vertical" = children stacked. */
51
+ type CanvasOrientation = "horizontal" | "vertical";
52
+ type CanvasLayoutNode = {
53
+ readonly type: "pane";
54
+ readonly paneId: CanvasPaneId;
55
+ } | {
56
+ readonly type: "split";
57
+ readonly id: CanvasSplitId;
58
+ readonly orientation: CanvasOrientation;
59
+ readonly children: readonly CanvasLayoutNode[];
60
+ /** Fractions, one per child, summing to 1. */
61
+ readonly sizes: readonly number[];
62
+ };
63
+ interface CanvasState {
64
+ readonly version: 1;
65
+ readonly isOpen: boolean;
66
+ readonly isFullscreen: boolean;
67
+ /** Column width in CSS pixels (desktop). */
68
+ readonly width: number;
69
+ readonly layout: CanvasLayoutNode;
70
+ readonly panes: {
71
+ readonly [paneId: string]: CanvasPane;
72
+ };
73
+ readonly items: {
74
+ readonly [itemId: string]: CanvasItem;
75
+ };
76
+ readonly focusedPaneId: CanvasPaneId;
77
+ /** Monotonic counter for minting pane/split ids — keeps the reducer pure. */
78
+ readonly seq: number;
79
+ /** True once a persisted snapshot was applied (or there was none). */
80
+ readonly hydrated: boolean;
81
+ }
82
+ /** Where a newly opened item goes. Existing items never move on open. */
83
+ type CanvasOpenTarget = "focused" | "split-right" | "split-down" | {
84
+ readonly paneId: CanvasPaneId;
85
+ };
86
+ interface CanvasOpenInput {
87
+ readonly kind: string;
88
+ readonly key: string;
89
+ readonly title?: string | null | undefined;
90
+ readonly data?: CanvasJson | undefined;
91
+ readonly target?: CanvasOpenTarget | undefined;
92
+ /** Reveal the canvas when it is put away. Default true. */
93
+ readonly reveal?: boolean | undefined;
94
+ /** Make the item the pane's active tab. Default true. */
95
+ readonly activate?: boolean | undefined;
96
+ }
97
+
98
+ /**
99
+ * THE canvas reducer. Plain Redux-compatible reducer + action creators with no
100
+ * Redux Toolkit dependency, so it mounts in a host's RTK store
101
+ * (`canvasHost: canvasReducer`) or runs inside the package's own standalone
102
+ * store with identical behaviour.
103
+ *
104
+ * Identity law: an item is `kind::key`. Opening an item that is already on the
105
+ * canvas never duplicates it — it refreshes its data/title, activates its tab
106
+ * and focuses its pane, wherever that pane is.
107
+ */
108
+
109
+ declare const P = "matrxCanvas/";
110
+ type CanvasAction = {
111
+ type: `${typeof P}open`;
112
+ payload: CanvasOpenInput & {
113
+ now: number;
114
+ };
115
+ } | {
116
+ type: `${typeof P}update`;
117
+ payload: {
118
+ itemId: CanvasItemId;
119
+ data?: CanvasJson | undefined;
120
+ title?: string | null | undefined;
121
+ now: number;
122
+ };
123
+ } | {
124
+ type: `${typeof P}rekey`;
125
+ payload: {
126
+ itemId: CanvasItemId;
127
+ key: string;
128
+ };
129
+ } | {
130
+ type: `${typeof P}closeItem`;
131
+ payload: {
132
+ itemId: CanvasItemId;
133
+ };
134
+ } | {
135
+ type: `${typeof P}closeOthers`;
136
+ payload: {
137
+ itemId: CanvasItemId;
138
+ };
139
+ } | {
140
+ type: `${typeof P}activate`;
141
+ payload: {
142
+ itemId: CanvasItemId;
143
+ };
144
+ } | {
145
+ type: `${typeof P}focusPane`;
146
+ payload: {
147
+ paneId: CanvasPaneId;
148
+ };
149
+ } | {
150
+ type: `${typeof P}moveItem`;
151
+ payload: {
152
+ itemId: CanvasItemId;
153
+ toPaneId: CanvasPaneId;
154
+ index?: number | undefined;
155
+ };
156
+ } | {
157
+ type: `${typeof P}splitPane`;
158
+ payload: {
159
+ paneId: CanvasPaneId;
160
+ orientation: CanvasOrientation;
161
+ moveItemId?: CanvasItemId | undefined;
162
+ };
163
+ } | {
164
+ type: `${typeof P}closePane`;
165
+ payload: {
166
+ paneId: CanvasPaneId;
167
+ };
168
+ } | {
169
+ type: `${typeof P}resizeSplit`;
170
+ payload: {
171
+ splitId: CanvasSplitId;
172
+ sizes: readonly number[];
173
+ };
174
+ } | {
175
+ type: `${typeof P}setOpen`;
176
+ payload: {
177
+ open: boolean;
178
+ };
179
+ } | {
180
+ type: `${typeof P}toggle`;
181
+ } | {
182
+ type: `${typeof P}setFullscreen`;
183
+ payload: {
184
+ fullscreen: boolean;
185
+ };
186
+ } | {
187
+ type: `${typeof P}setWidth`;
188
+ payload: {
189
+ width: number;
190
+ };
191
+ } | {
192
+ type: `${typeof P}hydrate`;
193
+ payload: {
194
+ snapshot: CanvasState | null;
195
+ };
196
+ } | {
197
+ type: `${typeof P}reset`;
198
+ };
199
+
200
+ /**
201
+ * The store seam. The canvas never owns a second copy of its state: it reads
202
+ * and writes through a CanvasStoreBinding.
203
+ *
204
+ * - A host WITH Redux mounts `canvasReducer` in its root reducer and binds it:
205
+ * bindCanvasToReduxStore(store, (root) => root.canvasHost)
206
+ * - A host WITHOUT Redux (a Vite tool, an Electron window) calls
207
+ * createCanvasStore()
208
+ * which runs the very same reducer in a tiny standalone store.
209
+ */
210
+
211
+ interface CanvasStoreBinding {
212
+ getState(): CanvasState;
213
+ dispatch(action: CanvasAction): void;
214
+ subscribe(listener: () => void): () => void;
215
+ }
216
+
217
+ /**
218
+ * Remembering the canvas between sessions — a table stake, never optional.
219
+ *
220
+ * The default port writes to localStorage under a versioned key. Items whose
221
+ * kind opts out (`restore: false`, e.g. a live session that cannot come back)
222
+ * are dropped from the snapshot, and panes they leave empty are dropped too.
223
+ */
224
+
225
+ interface CanvasPersistencePort {
226
+ load(): CanvasState | null | Promise<CanvasState | null>;
227
+ save(snapshot: CanvasState): void | Promise<void>;
228
+ }
229
+
230
+ /**
231
+ * The canvas controller: the ONE imperative API every caller uses. It wraps a
232
+ * store binding, validates what callers hand it (so the reducer stays pure and
233
+ * trusting), and owns hydration + autosave.
234
+ *
235
+ * Nothing fails silently: a refused open (non-JSON data, unknown kind) is
236
+ * reported through the error sink and returns false.
237
+ */
238
+
239
+ interface CanvasErrorReport {
240
+ readonly code: "non-json-data" | "unknown-kind" | "persistence-load" | "persistence-save";
241
+ readonly message: string;
242
+ readonly detail?: unknown;
243
+ }
244
+ type CanvasErrorSink = (report: CanvasErrorReport) => void;
245
+ interface CanvasController {
246
+ readonly store: CanvasStoreBinding;
247
+ getState(): CanvasState;
248
+ open(input: CanvasOpenInput): CanvasItemId | null;
249
+ update(itemId: CanvasItemId, patch: {
250
+ data?: CanvasJson | undefined;
251
+ title?: string | null | undefined;
252
+ }): boolean;
253
+ /** Gives an item a new identity in place (a draft that was saved and now has a durable id). */
254
+ rekey(itemId: CanvasItemId, key: string): CanvasItemId | null;
255
+ close(itemId: CanvasItemId): void;
256
+ closeOthers(itemId: CanvasItemId): void;
257
+ activate(itemId: CanvasItemId): void;
258
+ focusPane(paneId: CanvasPaneId): void;
259
+ moveItem(itemId: CanvasItemId, toPaneId: CanvasPaneId, index?: number): void;
260
+ splitPane(paneId: CanvasPaneId, orientation: CanvasOrientation, moveItemId?: CanvasItemId): void;
261
+ closePane(paneId: CanvasPaneId): void;
262
+ resizeSplit(splitId: CanvasSplitId, sizes: readonly number[]): void;
263
+ show(): void;
264
+ hide(): void;
265
+ toggle(): void;
266
+ setFullscreen(fullscreen: boolean): void;
267
+ setWidth(width: number): void;
268
+ /** Is this exact thing on the canvas right now? */
269
+ has(kind: string, key: string): boolean;
270
+ /**
271
+ * Is a canvas column on screen? A store can exist in a layout that shows no
272
+ * column (a kiosk, a meeting stage); opening there must be refused aloud.
273
+ */
274
+ isPresented(): boolean;
275
+ /** Called by a column on mount; returns the unmount callback. */
276
+ registerPresentation(): () => void;
277
+ subscribePresentation(listener: () => void): () => void;
278
+ /** Loads the persisted snapshot and starts autosave. Returns a disposer. */
279
+ start(): () => void;
280
+ }
281
+
282
+ /**
283
+ * THE canvas kind registry. A kind is one sort of thing the canvas can show —
284
+ * an artifact, a document, a chat, a notification feed. Any feature adds a
285
+ * kind with ONE call and never touches canvas code:
286
+ *
287
+ * registerCanvasKind(defineCanvasKind<{ noteId: string }>({
288
+ * id: "note",
289
+ * label: "Note",
290
+ * icon: NoteIcon,
291
+ * load: () => import("./NoteCanvasView"),
292
+ * title: (data) => data.noteId,
293
+ * }));
294
+ *
295
+ * The registry lives on globalThis under a Symbol.for key so duplicated
296
+ * bundles (ESM + CJS, two chunks) share ONE registry.
297
+ */
298
+
299
+ interface CanvasKindProps<TData extends CanvasJson = CanvasJson> {
300
+ readonly item: CanvasItem;
301
+ readonly data: TData;
302
+ readonly paneId: CanvasPaneId;
303
+ readonly isFocused: boolean;
304
+ readonly canvas: CanvasController;
305
+ }
306
+ interface CanvasMenuItem {
307
+ readonly id: string;
308
+ readonly label: string;
309
+ readonly icon?: ReactNode;
310
+ readonly onSelect: () => void;
311
+ readonly destructive?: boolean;
312
+ }
313
+ interface CanvasKind<TData extends CanvasJson = CanvasJson> {
314
+ readonly id: string;
315
+ /** Singular noun shown in menus and the empty-pane launcher. */
316
+ readonly label: string;
317
+ readonly icon: ComponentType<{
318
+ className?: string;
319
+ }>;
320
+ /** Eager component. Provide this OR `load`. */
321
+ readonly component?: ComponentType<CanvasKindProps<TData>>;
322
+ /** Lazy component — the kind's code loads only when a tab of it renders. */
323
+ readonly load?: () => Promise<{
324
+ default: ComponentType<CanvasKindProps<TData>>;
325
+ }>;
326
+ /** Tab title from the item's data. Falls back to the item's title, then `label`. */
327
+ readonly title?: (data: TData, item: CanvasItem) => string;
328
+ /** Comes back after a reload. Default true; false for live sessions that cannot resume. */
329
+ readonly restore?: boolean;
330
+ /** Stays mounted while its tab is in the background (live chat, a running tool). */
331
+ readonly keepAlive?: boolean;
332
+ /** When set, the kind is offered in an empty pane's launcher. */
333
+ readonly launcher?: {
334
+ readonly key: string;
335
+ readonly data: TData;
336
+ readonly title?: string;
337
+ };
338
+ /** The kind's own button, rendered left of the pane's "…" menu. */
339
+ readonly HeaderAction?: ComponentType<CanvasKindProps<TData>>;
340
+ /** Kind-specific entries for the pane's "…" menu. */
341
+ readonly menuItems?: (props: CanvasKindProps<TData>) => readonly CanvasMenuItem[];
342
+ }
343
+ /** Erased form stored in the registry. */
344
+ type AnyCanvasKind = CanvasKind<CanvasJson>;
345
+ /**
346
+ * Typed authoring helper. The data type is a promise the kind's opener keeps;
347
+ * the registry stores the erased form (the cast is the one registration seam).
348
+ */
349
+ declare function defineCanvasKind<TData extends CanvasJson>(kind: CanvasKind<TData>): AnyCanvasKind;
350
+ /** Registers (or replaces) a kind. Returns an unregister function. */
351
+ declare function registerCanvasKind(kind: AnyCanvasKind): () => void;
352
+ declare function registerCanvasKinds(kinds: readonly AnyCanvasKind[]): () => void;
353
+ declare function getCanvasKind(id: string): AnyCanvasKind | undefined;
354
+ declare function listCanvasKinds(): AnyCanvasKind[];
355
+
356
+ /**
357
+ * Host capabilities that apply to EVERY kind — this is how saving, sharing and
358
+ * history are "handled globally": the host answers them once, here, and every
359
+ * item that qualifies gets the entries in its "…" menu.
360
+ */
361
+ interface CanvasHostPorts {
362
+ /** Extra "…" menu entries for an item (share, save to cloud, version history…). */
363
+ readonly itemActions?: (item: CanvasItem, kind: AnyCanvasKind | undefined) => readonly CanvasMenuItem[];
364
+ /** Pops an item out into a floating window. Absent ⇒ no "Pop out" entry. */
365
+ readonly popOut?: (item: CanvasItem) => void;
366
+ }
367
+ interface CanvasProviderProps extends CanvasHostPorts {
368
+ readonly children: ReactNode;
369
+ /** A Redux-bound store (bindCanvasToReduxStore). Omit for a standalone store. */
370
+ readonly store?: CanvasStoreBinding;
371
+ /** `null` turns memory off. Default: localStorage. */
372
+ readonly persistence?: CanvasPersistencePort | null;
373
+ readonly onError?: CanvasErrorSink;
374
+ /** ⌘\ / Ctrl+\ toggles the canvas; Escape leaves full screen. Default true. */
375
+ readonly hotkeys?: boolean;
376
+ }
377
+ declare function CanvasProvider({ children, store, persistence, onError, hotkeys, itemActions, popOut, }: CanvasProviderProps): react.JSX.Element;
378
+ /** The controller: open, close, toggle, split… */
379
+ declare function useCanvas(): CanvasController;
380
+ /** Same as useCanvas, but null outside a provider (for components that may render anywhere). */
381
+ declare function useOptionalCanvas(): CanvasController | null;
382
+ declare function useCanvasHostPorts(): CanvasHostPorts;
383
+ /** True when a canvas column is on screen in this tree — the ONE availability answer. */
384
+ declare function useCanvasIsPresented(): boolean;
385
+ /** Subscribes to a slice of canvas state. The selector must return stable values. */
386
+ declare function useCanvasState<T>(selector: (state: CanvasState) => T): T;
387
+ /**
388
+ * Like useCanvasState, but safe outside a provider: returns `fallback` there.
389
+ * For components that may render in a tree with no canvas (a bare test, an
390
+ * embed). `fallback` must be referentially stable (a primitive or null).
391
+ */
392
+ declare function useOptionalCanvasState<T>(selector: (state: CanvasState) => T, fallback: T): T;
393
+ /** Re-renders when kinds register, so late-registered kinds appear. */
394
+ declare function useCanvasKinds(): readonly AnyCanvasKind[];
395
+ declare function useCanvasKind(id: string): AnyCanvasKind | undefined;
396
+
397
+ /** The column's rendered width in px: 0 when put away, null when full screen. */
398
+ declare function useCanvasColumnWidth(): number | null;
399
+ interface CanvasColumnProps {
400
+ readonly className?: string;
401
+ readonly style?: CSSProperties;
402
+ /** Called with the live width while the edge is dragged, then null — lets a host shell reflow in step. */
403
+ readonly onLiveWidth?: (width: number | null) => void;
404
+ }
405
+ declare function CanvasColumn({ className, style, onLiveWidth }: CanvasColumnProps): react.JSX.Element | null;
406
+ /** THE one button that opens and puts away the canvas. */
407
+ declare function CanvasToggle({ className }: {
408
+ className?: string;
409
+ }): react.JSX.Element;
410
+ /**
411
+ * For hosts without their own shell (a Vite app, an Electron window): the app
412
+ * on the left, the canvas column on the right, full screen handled.
413
+ */
414
+ declare function CanvasFrame({ children, className }: {
415
+ children: ReactNode;
416
+ className?: string;
417
+ }): react.JSX.Element;
418
+
419
+ declare function itemTitle(item: CanvasItem, kind: AnyCanvasKind | undefined): string;
420
+ declare function CanvasPaneView({ paneId }: {
421
+ paneId: CanvasPaneId;
422
+ }): react.JSX.Element | null;
423
+
424
+ export { type AnyCanvasKind, CanvasColumn, type CanvasColumnProps, CanvasFrame, type CanvasHostPorts, type CanvasKind, type CanvasKindProps, type CanvasMenuItem, CanvasPaneView, CanvasProvider, type CanvasProviderProps, CanvasToggle, defineCanvasKind, getCanvasKind, itemTitle, listCanvasKinds, registerCanvasKind, registerCanvasKinds, useCanvas, useCanvasColumnWidth, useCanvasHostPorts, useCanvasIsPresented, useCanvasKind, useCanvasKinds, useCanvasState, useOptionalCanvas, useOptionalCanvasState };