@ai-matrx/canvas 0.1.1 → 0.2.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/CHANGELOG.md +26 -0
- package/README.md +114 -4
- package/dist/controller-Dv6J6XZm.d.cts +576 -0
- package/dist/controller-Dv6J6XZm.d.ts +576 -0
- package/dist/index.cjs +743 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -11
- package/dist/index.d.ts +118 -11
- package/dist/index.js +743 -104
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1476 -406
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +242 -22
- package/dist/react.d.ts +242 -22
- package/dist/react.js +1486 -404
- package/dist/react.js.map +1 -1
- package/dist/styles.css +346 -51
- package/dist/tokens.css +58 -36
- package/package.json +1 -1
- package/dist/controller-oY84BN2c.d.cts +0 -345
- package/dist/controller-oY84BN2c.d.ts +0 -345
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
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).
|
|
6
|
+
|
|
7
|
+
- **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).
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **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).
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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`.
|
|
15
|
+
- **Session-keyed restore.** `restore: "session"` with `sessionKey` (default `data.sessionKey`), `onAttach` (`open` · `restore` · `arrive`) and `onDetach` (`close` · `leave` · `unmount`). Hook failures report `session-hook`.
|
|
16
|
+
- **Placeholder kinds.** `unavailable(data) → string | null` shows the honest one-line state in place of the body.
|
|
17
|
+
- **Menu.** "Close tab" is always in "…"; "Dock back" replaces "Pop out" in a pop-out window.
|
|
18
|
+
|
|
19
|
+
### Consumer action
|
|
20
|
+
|
|
21
|
+
- `state.width` is `number | null` (null until the person drags; the layout rule decides). Read widths through `useCanvasColumnWidth()` / `fitCanvasWidth`, never `state.width` directly.
|
|
22
|
+
- `CanvasState` has a required `windows` map; build states with `createInitialCanvasState()` / the reducer, never by hand.
|
|
23
|
+
- `selectCanvasLayout` and `selectCanvasFocusedPaneId` return `null` for an unknown window; `selectCanvasItemCount` still counts every window (`selectCanvasWindowItemCount` counts one).
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- `CanvasKindProps` carries `windowId`; a kind that builds the props itself must pass it.
|
|
27
|
+
- 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`.
|
|
28
|
+
|
|
3
29
|
## 0.1.1
|
|
4
30
|
|
|
5
31
|
- 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,
|
|
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
|
|
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.
|