openpencil/packages/op-web-sdk-react/README.md
Kayshen-X 199d9bbec9 feat(sdk): add React + Vue adapters for op-web-sdk
Plan 3 of the web embedding SDK: @zseven-w/op-web-sdk-react and -vue — thin
read-only framework adapters over the TS core. Each: <DesignView> (canvas +
viewer lifecycle), provider/inject of OpViewer, and read-only
useDocument/useViewport/useActivePage. React via useSyncExternalStore
(cache-invalidate-on-event); Vue via an async shallowRef provider so children
inject the viewer after load. Self-contained (no pen-* deps; framework peer);
React 5/5 + Vue 4/4 tests; both tsc clean.
2026-06-19 21:01:27 +08:00

154 lines
4.3 KiB
Markdown

# @zseven-w/op-web-sdk-react
React adapter for the OpenPencil read-only web viewer SDK. Embed a live OpenPencil design canvas in any React 19 application.
> **Read-only boundary:** This package provides a viewer only — no editing. For the full editor experience, use the OpenPencil desktop or web app directly.
## Install
```bash
npm install @zseven-w/op-web-sdk @zseven-w/op-web-sdk-react
# or
bun add @zseven-w/op-web-sdk @zseven-w/op-web-sdk-react
```
## Peer dependencies
| Package | Required version |
|--------------|-----------------|
| `react` | `^19.0.0` |
| `react-dom` | `^19.0.0` |
## WASM note
The core SDK (`@zseven-w/op-web-sdk`) loads a WebAssembly binary at runtime. You must either:
- Pass the wasm URL explicitly via the `wasmUrl` prop, or
- Serve the wasm file from your own static assets and ensure it is reachable.
The wasm file ships alongside the core package at `@zseven-w/op-web-sdk/dist/op_web_sdk_bg.wasm`.
## Usage
### `<DesignView>` component
The simplest way to embed a design viewer:
```tsx
import { DesignView } from '@zseven-w/op-web-sdk-react';
import type { OpViewer } from '@zseven-w/op-web-sdk';
export function MyPage() {
function handleLoad(viewer: OpViewer) {
console.log('viewer ready', viewer.document);
}
return (
<DesignView
doc={docStringOrBytes}
wasmUrl="/assets/op_web_sdk_bg.wasm"
onLoad={handleLoad}
style={{ width: 800, height: 600 }}
/>
);
}
```
**Props (`DesignViewProps`):**
- `doc?: string | Uint8Array` — serialized OpenPencil document (JSON string or binary bytes). Omit to start with an empty document.
- `wasmUrl?: string` — URL to the core WASM binary.
- `onLoad?: (viewer: OpViewer) => void` — called once the viewer is ready.
- `className?: string` — CSS class applied to the wrapper div.
- `children?: ReactNode` — child components that can call viewer hooks (rendered only after the viewer is ready, inside `DesignProvider`).
### Manual provider
For advanced layouts, wrap your subtree with `DesignProvider` and use `useViewer` in descendants:
```tsx
import { useState, useEffect, useRef } from 'react';
import { createViewer } from '@zseven-w/op-web-sdk';
import { DesignProvider } from '@zseven-w/op-web-sdk-react';
export function MyLayout() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [viewer, setViewer] = useState(null);
useEffect(() => {
let cancelled = false;
createViewer({ canvas: canvasRef.current! }).then((v) => {
if (cancelled) { v.destroy(); return; }
setViewer(v);
});
return () => { cancelled = true; viewer?.destroy(); };
}, []);
return (
<>
<canvas ref={canvasRef} />
{viewer && (
<DesignProvider viewer={viewer}>
<MyChildComponents />
</DesignProvider>
)}
</>
);
}
```
## Hooks
These hooks must be called inside a component that is a descendant of `DesignProvider` (or `<DesignView>`).
| Hook | Returns | Reactive event |
|------|---------|----------------|
| `useViewer()` | `OpViewer` | — (stable reference) |
| `useDocument()` | `PenDocument` | `'load'` |
| `useViewport()` | `{ panX: number; panY: number; zoom: number }` | `'viewportchange'` |
| `useActivePage()` | `number` | `'load'` |
Each hook uses `useSyncExternalStore` internally and re-renders only when the relevant viewer event fires.
```tsx
import { useViewport, useDocument } from '@zseven-w/op-web-sdk-react';
export function ZoomDisplay() {
const viewport = useViewport(); // { panX, panY, zoom }
const doc = useDocument(); // PenDocument
return (
<div>
<p>Zoom: {viewport.zoom}</p>
<p>Pages: {doc.pages?.length}</p>
</div>
);
}
```
## API reference
```ts
// Provider / context
function DesignProvider(props: { viewer: OpViewer; children?: ReactNode }): JSX.Element
function useViewer(): OpViewer // throws if no provider above
// Hooks (re-render on event)
function useDocument(): PenDocument
function useViewport(): { panX: number; panY: number; zoom: number }
function useActivePage(): number
// Component
function DesignView(props: DesignViewProps): JSX.Element
interface DesignViewProps {
doc?: string | Uint8Array;
wasmUrl?: string;
onLoad?: (viewer: OpViewer) => void;
className?: string;
children?: ReactNode;
}
```
## License
MIT