* fix(app): protect unsaved documents when closing
Mark tabs with unsaved content updates and ask whether to save before
closing them. The prompt now covers tab closes, the desktop window close
button, and the application Quit action, which previously discarded work
when autosave had no writable target.
Track a content revision separately from scene and recovery versions so a
save only clears the indicator when it wrote the revision it captured.
Cancelled pickers, failed writes, and edits made during a save keep the
document open. Desktop uses the platform alert; the browser keeps the
styled dialog.
The desktop menu replaces the predefined Quit item so the accelerator and
Dock-independent quit path request confirmation instead of exiting.
* fix(ai): resolve credentials only when used
Opening a document, creating a chat, or browsing chat history connected
the provider and read saved secrets, which triggered system credential
prompts without user intent.
Startup now reads credential status only, migration runs inside the first
explicit resolution, and the chat panel initializes local history without
creating a transport. Stock-photo keys resolve per search instead of at
settings refresh, and credentials still marked legacy count as configured
so upgrading does not appear to lose them.
* refactor(ai): export diagnostics from Settings only
Chat kept its own debug log, copied mixed app-wide usage into a
conversation export, and reported a missing cache rate as zero. Remove
that surface and record AI requests, model steps, and tool activity as
correlated diagnostic events instead.
Settings remains the single export location, usage summaries can now
distinguish unreported telemetry from zero, and transcript or tool
payloads are no longer part of the export.
* fix(ai): clear legacy credentials for real
Clearing a Pexels, Unsplash, or provider key only removed the current
store entry. A value that still lived in legacy storage kept the key
configured, so a later search migrated and used the credential the user
had just removed.
Migrate before mutating so clearing also removes the legacy value, and
share one in-flight migration so the media and provider paths cannot
migrate the same plaintext twice.
* fix(ai): scope credential migration per source
Sharing one migration promise process-wide let a second storage return
the first migration's result, leaving its own legacy keys unmigrated
while reporting success. Track in-flight migrations per storage and
serialize them, because every migration writes to the same store and
concurrent runs could overwrite each other.
* fix(app): destroy the window after a confirmed close
Tauri's onCloseRequested helper destroys the window itself when a handler
returns without preventing the event. Approving a close therefore invoked
plugin:window|destroy, which the capability set did not grant, so the
window stayed open with a permission error after saving.
Always intercept the request and destroy the window explicitly once the
choice is confirmed, and grant core🪟allow-destroy in place of the
now-unused close permission.
* fix(app): show a filled dot for unsaved tabs
The unsaved indicator used a stroked Lucide circle whose fill attribute
kept it an empty outline, reading as a disabled control. Draw the
indicator as a filled accent dot matching the status dots used elsewhere
in the app.
* refactor(app): focus the unsaved prompt with VueUse
Replace the manual watcher, nextTick, and component $el focus with
useFocus, which focuses the Save button when the dialog mounts. Assert the
focus in the close-protection test so the Return-saves behavior stays
covered.
* refactor(app): route Quit through the shared menu channel
The Quit item emitted a bespoke app:request-exit event and the close
module listened for it, while every other native item travels as a
menu-event id dispatched by the shell and editor menu composables.
Emit menu-event "quit" for both the Quit item and the platform exit
request, handle it in useShellMenu beside check-updates, and share one
confirmAppExit so window closes and app exits agree on a single approval.
* refactor(app): generate the macOS app menu entries
The application menu hardcoded its labels and the Quit accelerator in
Rust while every other menu entry is generated from APP_MENU_SCHEMA.
Move the custom app entries (About, Check for Updates, Quit) into
APP_MENU_APP_ITEMS and emit desktop/generated/app-menu.json, keyed by id
so the native builder cannot silently drop a label.
Placement stays in Rust because the OS-predefined items sit between them,
and the menu title now comes from the packaged product name.
* build(tauri-menu): check generated menus against the schema
The generated menu files are committed but nothing verified them, so a
schema edit could silently leave desktop/generated stale until the next
release build regenerated it.
Split the renderers from the write step, register the tool as a workspace
so its dependencies resolve, and compare the committed files with the
schema in a test that runs with the other tool checks.
* fix(app): serialize exit confirmations
The window close handler and the Quit item both call confirmAppExit, and
the per-handler closing flag does not cover the two paths. Both could run
close preparation, so an unsaved document could be prompted twice.
Share one in-flight confirmation and clear it when it settles, so a
cancelled or failed attempt still prompts again on the next request.
|
||
|---|---|---|
| .. | ||
| example | ||
| src | ||
| ARCHITECTURE.md | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsdown.config.ts | ||
@open-pencil/vue
Headless Vue 3 SDK for building OpenPencil-powered editors.
@open-pencil/vue sits on top of @open-pencil/core and provides:
- Vue editor injection via
provideEditor()/useEditor() - canvas integration via
useCanvas(),useCanvasInput(), anduseTextEdit() - selection, command, panel, variables, and i18n composables
- headless structural primitives like
CanvasRoot,LayerTreeRoot,PageListRoot, andToolbarRoot
The SDK is headless by design: it provides logic and structure, while your app owns styling and product-specific UI.
Install
bun add @open-pencil/vue @open-pencil/core @open-pencil/scene-graph canvaskit-wasm
The current development version requires Vue ^3.5.41 and, when supplying the optional CanvasKit peer, canvaskit-wasm >=0.41.1. See SDK Getting Started for migration guidance; older releases may have different peer requirements.
Quick start
<script setup lang="ts">
import { reactive } from 'vue'
import { createDefaultEditorState, createEditor } from '@open-pencil/core/editor'
import { SceneGraph } from '@open-pencil/scene-graph'
import { CanvasRoot, CanvasSurface, provideEditor } 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: 1200, height: 800 }),
})
editor.createShape('RECTANGLE', 100, 100, 200, 150)
editor.zoomToFit()
provideEditor(editor)
</script>
<template>
<div class="h-screen">
<CanvasRoot>
<CanvasSurface class="size-full" />
</CanvasRoot>
</div>
</template>
The fixed viewport size above is illustrative; a resizable shell should return its actual canvas container dimensions from getViewportSize. Pass reactive state for Vue controls, while keeping the Scene Graph itself framework-neutral.
Core concepts
Editor context
Use provideEditor(editor) once near the top of your subtree.
import { provideEditor } from '@open-pencil/vue'
provideEditor(editor)
Read it anywhere below with useEditor().
import { useEditor } from '@open-pencil/vue'
const editor = useEditor()
Canvas wiring
At the composable level, the main canvas APIs are:
useCanvas()useCanvasInput()useTextEdit()
If you want SDK-provided structure, use headless primitives like CanvasRoot and CanvasSurface.
Headless primitives
Main structural primitives include:
CanvasRootLayerTreeRootPageListRootPropertyListRootPropertySectionRootSegmentedControlRootToolbarRootColorPickerRootFontPickerRootNumberFieldRoot/NumberFieldInput/NumberFieldValueBindableValueRoot/BindableValueTrigger/BindableValuePickerLayoutControlsRootConstraintsControlRoot
These components coordinate structure and state, but do not impose app styling. NumberField
adds pointer scrubbing, Arrow-key stepping, mixed/bound state attributes, and safe arithmetic
expressions such as +10, *2, 50%, and 12*8+4. BindableValue composes fields with a
generic BindingProvider and supports detach-on-edit, read-only, and edit-variable policies.
Focusing a bound NumberField is non-destructive; the configured policy begins only on the first
value mutation. LayoutControlsRoot exposes axis-oriented sizing actions; editing a Hug or Fill
dimension can switch that axis to Fixed inside the same provider transaction.
ConstraintsControlRoot exposes eligible frame-child constraints, mixed axis values, pin actions,
and undo-batched multi-selection updates. AppearanceControlsRoot
exposes selection-derived independent-corner presentation state so consumers do not need parallel
expansion heuristics. PropertyListRoot is controlled and
editor-agnostic; OpenPencil panels connect it to selection and undo through
useEditorPropertyList(). useColorModel() provides precise scene-color/Reka bridges, reactive
RGB/HSL/HSB/OkHCL channels, extensible format state, and shared slider presentation data.
FillRoot and FillSwatch separate fill behavior and binding-aware previews from popover
composition; ChannelSlider provides accessible scalar OkHCL controls until Reka supports them.
Public API tiers
Core API
These are the main APIs most SDK consumers should start with.
Context and canvas
provideEditor()useEditor()useCanvas()useCanvasInput()useTextEdit()
Selection and commands
useSelectionState()useSelectionCapabilities()useEditorCommands()useMenuModel()
Property panels
usePosition()useLayout()useConstraints()useComponentProperties()useAppearance()useSharedStyleBinding()useColorModel()useMask()useTypography()useExport()useFillControls()useStrokeControls()useEffectsControls()
Variables, navigation, and localization
useVariablesEditor()usePageList()useI18n()
Headless primitives
CanvasRootLayerTreeRootPageListRootPropertyListRootPropertyListItemPropertyListAdd/PropertyListRemove/PropertyListVisibilityPropertySectionRoot/PropertySectionHeader/PropertySectionTitlePropertySectionActions/PropertySectionContent/PropertySectionEmptyActionSegmentedControlRoot/SegmentedControlItemToolbarRootNumberFieldRootNumberFieldInputNumberFieldValueNumberFieldLeadingNumberFieldUnitNumberFieldTrailingNumberFieldMenuBindableValueRootBindableValueTriggerBindableValuePickerFillRoot/FillSwatchChannelSliderRoot/ChannelSliderTrack/ChannelSliderThumb
Advanced API
These exports are intentionally public, but they are lower-level or more specialized.
useNodeProps()useEditorPropertyList()useSceneComputed()useColorBindingProvider()useColorVariableBinding()provideBindingProvider()useBindingProvider()useNumberBindingProvider()useFill()useGradientStops()useFontPicker()useOkHCL()useVariables()useVariablesDialogState()useVariablesTable()usePropScrub()useLayerDrag()useInlineRename()useToolbarState()useNodeFontStatus()useCanvasDrop()extractImageFilesFromClipboard()useViewportKind()toolCursor()
Primitive context helpers and low-level stores
These are mostly useful when extending SDK primitives rather than building from top-level composables.
useCanvasContext()useLayerTree()useToolbar()usePropertyList()useNumberField()localelocaleSettingsetLocale()AVAILABLE_LOCALESLOCALE_LABELS
Example patterns
Minimal provider component
<script setup lang="ts">
import { provideEditor } from '@open-pencil/vue'
import type { Editor } from '@open-pencil/core/editor'
const props = defineProps<{
editor: Editor
}>()
provideEditor(props.editor)
</script>
<template>
<slot />
</template>
Read selection state
import { useSelectionState } from '@open-pencil/vue'
const { hasSelection, selectedCount, selectedNode } = useSelectionState()
Build a menu
import { useMenuModel } from '@open-pencil/vue'
const { appMenu, canvasMenu } = useMenuModel()
Build a page list
<PageListRoot v-slot="{ pages, currentPageId, switchPage }">
<ul>
<li v-for="page in pages" :key="page.id">
<button :data-active="page.id === currentPageId" @click="switchPage(page.id)">
{{ page.name }}
</button>
</li>
</ul>
</PageListRoot>
Documentation
For fuller guides and API docs, see the documentation site:
packages/docs/programmable/sdk/
Example app
Run the included example:
cd packages/vue/example
bun install
bun run dev