openpencil/packages/docs/programmable/sdk/guides/custom-editor-shell.md
Danila Poyarkov d354c990ee docs: refresh SDK and workflow guides
Document current public contracts and implemented workflows, correct invalid editor and slot examples, and distinguish supported font, recovery, and library behavior from remaining gaps.
2026-09-15 22:52:41 +03:00

117 lines
3.4 KiB
Markdown

---
title: Custom Editor Shell
description: Build your own editor shell with provideEditor, CanvasRoot, menus, panels, and toolbars.
---
# Custom Editor Shell
A typical OpenPencil Vue app has three layers:
1. `@open-pencil/core` creates the editor
2. `@open-pencil/vue` adapts it into Vue composables and headless primitives
3. your app renders the shell, styling, and product UX
## Why this matters
The built-in OpenPencil app is only one possible shell.
You can build a very different one for a focused workflow: an embedded editor inside another product, an internal asset tool, a template editor, an annotation UI, or an AI-assisted editing surface with custom controls.
That is the main reason the SDK exists.
## Recommended composition
A practical shell often looks like this:
- provider at the top with `provideEditor()`
- canvas in the center
- page/layer navigation on one side
- properties on the other side
- menus and toolbars driven by composables
## Example
```vue
<script setup lang="ts">
import { reactive } from 'vue'
import { createDefaultEditorState, createEditor } from '@open-pencil/core/editor'
import { SceneGraph } from '@open-pencil/scene-graph'
import {
provideEditor,
CanvasRoot,
CanvasSurface,
ToolbarRoot,
PageListRoot,
} from '@open-pencil/vue'
const graph = new SceneGraph()
const page = graph.getPages()[0]
if (!page) throw new Error('Expected an initial page')
const editor = createEditor({
graph,
state: reactive(createDefaultEditorState(page.id)),
getViewportSize: () => ({ width: 1440, height: 900 }),
})
provideEditor(editor)
</script>
<template>
<div class="grid h-screen grid-cols-[240px_1fr_320px] grid-rows-[48px_1fr]">
<ToolbarRoot v-slot="{ tools, activeTool, actions }">
<header class="col-span-3 flex items-center gap-2 border-b px-3">
<button
v-for="tool in tools"
:key="tool.key"
:data-active="activeTool === tool.key"
@click="actions.setTool(tool.key)"
>
{{ tool.label }}
</button>
</header>
</ToolbarRoot>
<aside class="border-r">
<PageListRoot v-slot="{ pages, currentPageId, actions }">
<nav>
<button
v-for="page in pages"
:key="page.id"
:data-active="page.id === currentPageId"
@click="actions.switch(page.id)"
>
{{ page.name }}
</button>
</nav>
</PageListRoot>
</aside>
<main>
<CanvasRoot>
<CanvasSurface class="size-full" />
</CanvasRoot>
</main>
<aside class="border-l">
Properties panel here
</aside>
</div>
</template>
```
The fixed viewport dimensions above keep the example short. In a resizable shell, return the actual canvas container size from `getViewportSize`; do not include the surrounding sidebars or toolbar. See [SDK Getting Started](../getting-started) for installation requirements and migration notes.
## Why this split works
- the SDK owns editor integration and reusable headless logic
- your app owns layout, styling, and product-specific actions
- composables can power menus and panels without extra wrapper components
## Related APIs
- [provideEditor](../api/composables/provide-editor)
- [useCanvas](../api/composables/use-canvas)
- [ToolbarRoot](../api/components/toolbar-root)
- [PageListRoot](../api/components/page-list-root)
- [LayerTreeRoot](../api/components/layer-tree-root)