@ai-matrx/canvas 0.1.1 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - `BrowserBar` keeps its own 40px height in a panel body. 0.2.0 gave it `flex: 1 1 auto`, so in a column body it grew and sat in the middle of the panel (seen in the desktop browser panel). It still fills the width in a header's leading slot.
6
+
7
+ ### Consumer action
8
+
9
+ - None.
10
+
11
+ ## 0.2.0
12
+
13
+ One canvas for the web app and the desktop app: the package now owns the pane chrome everywhere and carries what an Electron host needs (a store in the main process, native views, several windows).
14
+
15
+ - **Chrome.** Panes are drawn with THE canvas chrome, ported from the design-system app shell: rounded cards on the column's frame background separated by gaps that are the split handles, a 36px header (title for a single tab, the tab strip for several), a per-kind leading slot, the kind's own button, "…", Expand, Close. Exported for hosts that frame their own panels: `CanvasPanel`, `CanvasPanelEmpty`, `CanvasHeaderButton`, `CanvasMenuTrigger`, `CanvasTab`, `BrowserTabs`, `BrowserBar` (same props as the design-system components they replace). The design-system copies are deleted (design-system 0.53.0).
16
+ - **Layout rules.** `layoutRules` on `CanvasProvider` / `createCanvasController` (`CANVAS_WEB_LAYOUT_RULES` default, `CANVAS_DESKTOP_LAYOUT_RULES`): default width in px or as a fraction of the region, canvas min, centre min (the canvas shrinks first), the split-down share (desktop 70/30), the split-right share, and `paneMinWidth` under which side-by-side panes stack. `regionWidth` tells the provider the space the canvas shares (desktop: right of the sidebar). Pure helpers `fitCanvasWidth`, `clampDraggedCanvasWidth`, `shouldStackSplit` for a main process.
17
+ - **Native-body kinds.** `body: "native"`: the pane renders a placeholder and reports its frame through `onNativeBodyFrame(itemId, rect | null, { visible, windowId })` in window CSS px with `devicePixelRatio`, in the same frame as any split, resize, expand or tab switch; null when hidden or gone; `visible: false` while package DOM floats over it (a DOM menu, a tab drag). `useCanvasNativeBodies().remeasure()` for host animations.
18
+ - **Floating things are ports.** `showMenu(entries, anchor) → Promise<id | null>` draws the "…" menu (default: the package's DOM menu); `overlay.show(element) → remove` draws tooltips, drop indicators and tab drags (default: a DOM layer). Header tooltips now come from the package (no tap-target tooltip in the pane).
19
+ - **Shortcuts as data.** `CANVAS_SHORTCUTS` (`{ id, accelerator, when, label, web }`), `runCanvasCommand(controller, id)`, `matchesCanvasAccelerator`, `canvasShortcutHint`. The provider binds the web-safe ones; ⌘W / Ctrl+Tab / Ctrl+Shift+Tab are desktop-only. New: Expand (⌘⇧Enter), Split right (⌘⌥\\), Split down (⌘⌥⇧\\), all only while focus is in the canvas.
20
+ - **Remote store.** `createRemoteCanvasStore({ initialSnapshot, dispatch, subscribe, onError })` for a renderer bound to a main-process store: async dispatch, whole snapshots merged with `shareStructure` so unchanged slices keep their identity. A controller over it never hydrates or saves. `createKeyValueCanvasPersistence(kv)` is the main-process memory over any key-value store.
21
+ - **Windows.** `CanvasWindowId`, `CANVAS_MAIN_WINDOW`, `state.windows` (the top-level frame fields stay the main window's). `CanvasProvider windowId`, `CanvasWindowScope`, `controller.forWindow(id)`, window-aware actions and selectors. Windows other than main are not restored: their tabs fold home.
22
+ - **Pop-out.** Host ports `popOut(item) → Promise<{ windowId } | null>` and `dockBack(itemId, fromWindowId)`; `controller.popOut / dockBack / closeWindow`; reducer actions `popOut`, `dockBack`, `closeWindow(windowId, "dock" | "close")`. Without a port a pop-out is refused aloud (`pop-out-unavailable`) and no "Pop out" entry shows; a refusal reports `pop-out-refused`.
23
+ - **Session-keyed restore.** `restore: "session"` with `sessionKey` (default `data.sessionKey`), `onAttach` (`open` · `restore` · `arrive`) and `onDetach` (`close` · `leave` · `unmount`). Hook failures report `session-hook`.
24
+ - **Placeholder kinds.** `unavailable(data) → string | null` shows the honest one-line state in place of the body.
25
+ - **Menu.** "Close tab" is always in "…"; "Dock back" replaces "Pop out" in a pop-out window.
26
+
27
+ ### Consumer action
28
+
29
+ - `state.width` is `number | null` (null until the person drags; the layout rule decides). Read widths through `useCanvasColumnWidth()` / `fitCanvasWidth`, never `state.width` directly.
30
+ - `CanvasState` has a required `windows` map; build states with `createInitialCanvasState()` / the reducer, never by hand.
31
+ - `selectCanvasLayout` and `selectCanvasFocusedPaneId` return `null` for an unknown window; `selectCanvasItemCount` still counts every window (`selectCanvasWindowItemCount` counts one).
32
+ - The `popOut` port now returns `Promise<{ windowId } | null>` and the package moves the item; a host that passed the old `(item) => void` must adopt the new contract.
33
+ - Map the new tokens (or import `@ai-matrx/canvas/tokens.css`, which now follows a design-system theme): `--mxc-frame-bg`, `--mxc-title-fg`, `--mxc-disabled`, `--mxc-handle-hover`, `--mxc-tab-active-shadow`, `--mxc-input-bg`, `--mxc-ring`, `--mxc-tooltip-bg`, `--mxc-tooltip-fg`, `--mxc-float-shadow`, `--mxc-radius`, `--mxc-gap`. `--mxc-header-h` defaults to 2.25rem.
34
+ - `CanvasKindProps` carries `windowId`; a kind that builds the props itself must pass it.
35
+ - Hosts importing `CanvasPanel`, `CanvasPanelEmpty`, `BrowserTabs` or `BrowserBar` from `@ai-matrx/design-system` import them from `@ai-matrx/canvas/react` and add `@ai-matrx/canvas/styles.css`.
36
+
3
37
  ## 0.1.1
4
38
 
5
39
  - Build core and React declarations together so branded pane IDs, stores and controllers retain one type identity across entries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @ai-matrx/canvas
2
2
 
3
- A docked right-hand canvas column with split panes, tabs, fullscreen, identity-based deduplication and persisted layout. Core state is framework-independent; React components ship through `./react`.
3
+ A docked right-hand canvas column with split panes, tabs, expand, identity-based deduplication, persisted layout and ONE pane chrome. Core state is framework-independent (it runs in an Electron main process); React components ship through `./react`.
4
4
 
5
5
  ```tsx
6
6
  import "@ai-matrx/canvas/styles.css";
7
- import "@ai-matrx/canvas/tokens.css";
7
+ import "@ai-matrx/canvas/tokens.css"; // follows a design-system theme; else built-in light/dark values
8
8
  import { CanvasProvider, CanvasFrame, CanvasToggle, registerCanvasKind, defineCanvasKind } from "@ai-matrx/canvas/react";
9
9
 
10
10
  registerCanvasKind(defineCanvasKind({ id: "note", label: "Note", icon: NoteIcon, load: () => import("./NoteView") }));
@@ -14,8 +14,118 @@ registerCanvasKind(defineCanvasKind({ id: "note", label: "Note", icon: NoteIcon,
14
14
  </CanvasProvider>
15
15
  ```
16
16
 
17
- `NoteIcon`, `NoteView` and `App` are your host components. A Redux host passes `store={bindCanvasToReduxStore(store, (s) => s.canvasHost)}` after mounting `canvasReducer`. Hosts own identity, navigation and token values; the package owns layout, pane/tab behavior and restoration.
17
+ `NoteIcon`, `NoteView` and `App` are your host components. A Redux host passes `store={bindCanvasToReduxStore(store, (s) => s.canvasHost)}` after mounting `canvasReducer`. Hosts own identity, navigation, placement and token values; the package owns layout, pane/tab behavior, chrome and restoration. Item data must be plain JSON; failures are reported through `onError`.
18
18
 
19
- Entries: core (`.`), React (`./react`), structural CSS (`./styles.css`) and optional default theme values (`./tokens.css`). Item data must be plain JSON; failures are reported through `onError`.
19
+ Entries: core (`.`), React (`./react`), structural CSS (`./styles.css`) and default theme values (`./tokens.css`).
20
+
21
+ ## A kind
22
+
23
+ ```tsx
24
+ defineCanvasKind<{ sessionKey: string }>({
25
+ id: "terminal",
26
+ label: "Terminal",
27
+ icon: TerminalIcon,
28
+ load: () => import("./TerminalView"),
29
+ restore: "session", // closing detaches; a restore or a move reattaches
30
+ onAttach: ({ sessionKey, reason }) => attach(sessionKey, reason),
31
+ onDetach: ({ sessionKey }) => detach(sessionKey), // never kill here
32
+ keepAlive: true,
33
+ HeaderLeading: NewTerminalButton, // "+ new terminal", a browser bar…
34
+ HeaderAction: ClearButton,
35
+ unavailable: () => (backendUp() ? null : "Terminal · next release"),
36
+ });
37
+ ```
38
+
39
+ `body: "native"` makes the body a placeholder whose frame the host receives (below).
40
+
41
+ ## Chrome for panels a host frames itself
42
+
43
+ `CanvasPanel` (title or `header`, `leading`, `actions`, `renderMenu`, `onToggleExpand`, `onClose`), `CanvasPanelEmpty`, `CanvasHeaderButton`, `CanvasTab`, `BrowserTabs`, `BrowserBar` — the same components every canvas pane draws with. They render outside a provider too.
44
+
45
+ ## Layout rules
46
+
47
+ ```tsx
48
+ <CanvasProvider layoutRules={CANVAS_DESKTOP_LAYOUT_RULES} regionWidth={windowWidth - railAndSidebar}>
49
+ ```
50
+
51
+ | Rule | Web (default) | Desktop |
52
+ |---|---|---|
53
+ | `defaultWidth` | 640 | `{ fraction: 0.36 }` |
54
+ | `minWidth` | 360 | 360 |
55
+ | `centreMinWidth` (the canvas shrinks first) | 420 | 520 |
56
+ | `stackSplit` (split down: first pane's share) | 0.5 | 0.7 |
57
+ | `sideSplit` | 0.5 | 0.5 |
58
+ | `paneMinWidth` (narrower side-by-side panes stack) | 0 (never) | 360 |
59
+
60
+ Expand is one flag per window; it fills whatever the host placed the column in (`useCanvasColumnWidth()` returns `null`).
61
+
62
+ ## Electron
63
+
64
+ One store in the main process; every window's renderer is a client.
65
+
66
+ ```ts
67
+ // main
68
+ import { createCanvasStore, createCanvasController, createKeyValueCanvasPersistence, isCanvasAction,
69
+ CANVAS_DESKTOP_LAYOUT_RULES, CANVAS_SHORTCUTS, runCanvasCommand } from "@ai-matrx/canvas";
70
+
71
+ const store = createCanvasStore();
72
+ const canvas = createCanvasController({
73
+ store,
74
+ persistence: createKeyValueCanvasPersistence({ get: (k) => kv.get(k), set: (k, v) => kv.set(k, v) }),
75
+ layoutRules: CANVAS_DESKTOP_LAYOUT_RULES,
76
+ isRestorable: (kind) => kind !== "browser", // main has no kind registry: name the kinds that never come back
77
+ });
78
+ canvas.start(); // hydrates, then autosaves
79
+
80
+ ipcMain.handle("canvas:get", () => store.getState());
81
+ ipcMain.handle("canvas:dispatch", (_e, action: unknown) => { if (isCanvasAction(action)) store.dispatch(action); });
82
+ store.subscribe(() => { for (const w of BrowserWindow.getAllWindows()) w.webContents.send("canvas:snapshot", store.getState()); });
83
+
84
+ // Shortcuts as data: bind them where keys inside native views still arrive.
85
+ for (const s of CANVAS_SHORTCUTS) { /* menu accelerator or before-input-event → */ runCanvasCommand(canvas.forWindow(windowIdOf(win)), s.id); }
86
+ // A BrowserWindow that closes: its tabs go home.
87
+ win.on("closed", () => canvas.closeWindow(windowIdOf(win)));
88
+ ```
89
+
90
+ ```tsx
91
+ // renderer (one per window)
92
+ import { createRemoteCanvasStore } from "@ai-matrx/canvas";
93
+
94
+ const store = createRemoteCanvasStore({
95
+ initialSnapshot: await ipcRenderer.invoke("canvas:get"),
96
+ dispatch: (action) => ipcRenderer.invoke("canvas:dispatch", action),
97
+ subscribe: (onSnapshot) => {
98
+ const on = (_e: unknown, s: CanvasState) => onSnapshot(s);
99
+ ipcRenderer.on("canvas:snapshot", on);
100
+ return () => ipcRenderer.off("canvas:snapshot", on);
101
+ },
102
+ });
103
+
104
+ <CanvasProvider
105
+ store={store} // remote: the main process hydrates and saves
106
+ windowId={myWindowId} // "main" for the first window
107
+ layoutRules={CANVAS_DESKTOP_LAYOUT_RULES}
108
+ regionWidth={rightOfSidebar}
109
+ hotkeys={false} // main binds CANVAS_SHORTCUTS
110
+ onNativeBodyFrame={(itemId, rect, { visible }) => ipc.send("view:place", itemId, visible ? rect : null)}
111
+ showMenu={(entries, anchor) => ipc.invoke("menu:popup", entries, anchor)} // Menu.popup → chosen id
112
+ overlay={{ show: (el) => { ipc.send("overlay:show", el); return () => ipc.send("overlay:hide", el); } }}
113
+ popOut={(item) => ipc.invoke("canvas:pop-out", item.id)} // → { windowId } of a new BrowserWindow
114
+ >
115
+ <CanvasColumn /> {/* a pop-out window renders <CanvasColumn placement="fill" /> */}
116
+ </CanvasProvider>
117
+ ```
118
+
119
+ Rects are window CSS pixels with `devicePixelRatio`, reported in the frame the layout changed (layout effects + ResizeObserver). Call `useCanvasNativeBodies().remeasure()` per animation frame while the host animates its own layout. `visible: false` means package DOM floats over the view (a DOM menu or a tab drag): hide it so the DOM is seen and receives the drop.
120
+
121
+ ## Web pop-out
122
+
123
+ ```tsx
124
+ popOut={async (item) => { const id = `pop-${item.id}`; openFloatingPanel(id); return { windowId: id }; }}
125
+ // the floating panel:
126
+ <CanvasWindowScope windowId={id}><CanvasColumn placement="fill" /></CanvasWindowScope>
127
+ ```
128
+
129
+ When a pop-out window disappears from `state.windows` (docked back, last tab closed), close its panel.
20
130
 
21
131
  MIT.