openpencil/packages/vue
Danila Poyarkov 9d2b97e679
fix: protect unsaved documents and defer credential access (#713)
* 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.
2026-09-17 15:25:15 +03:00
..
example chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00
src fix: protect unsaved documents and defer credential access (#713) 2026-09-17 15:25:15 +03:00
ARCHITECTURE.md feat(vue): finalize NumberField API and docs 2026-07-13 10:19:24 +03:00
package.json Release v0.15.0 2026-09-16 17:57:35 +03:00
README.md docs: refresh SDK and workflow guides 2026-09-15 22:52:41 +03:00
tsconfig.json Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
tsdown.config.ts fix(vue): make package types ATTW-safe 2026-07-01 13:30:46 +03:00

@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(), and useTextEdit()
  • selection, command, panel, variables, and i18n composables
  • headless structural primitives like CanvasRoot, LayerTreeRoot, PageListRoot, and ToolbarRoot

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:

  • CanvasRoot
  • LayerTreeRoot
  • PageListRoot
  • PropertyListRoot
  • PropertySectionRoot
  • SegmentedControlRoot
  • ToolbarRoot
  • ColorPickerRoot
  • FontPickerRoot
  • NumberFieldRoot / NumberFieldInput / NumberFieldValue
  • BindableValueRoot / BindableValueTrigger / BindableValuePicker
  • LayoutControlsRoot
  • ConstraintsControlRoot

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

  • CanvasRoot
  • LayerTreeRoot
  • PageListRoot
  • PropertyListRoot
  • PropertyListItem
  • PropertyListAdd / PropertyListRemove / PropertyListVisibility
  • PropertySectionRoot / PropertySectionHeader / PropertySectionTitle
  • PropertySectionActions / PropertySectionContent / PropertySectionEmptyAction
  • SegmentedControlRoot / SegmentedControlItem
  • ToolbarRoot
  • NumberFieldRoot
  • NumberFieldInput
  • NumberFieldValue
  • NumberFieldLeading
  • NumberFieldUnit
  • NumberFieldTrailing
  • NumberFieldMenu
  • BindableValueRoot
  • BindableValueTrigger
  • BindableValuePicker
  • FillRoot / FillSwatch
  • ChannelSliderRoot / 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()
  • locale
  • localeSetting
  • setLocale()
  • AVAILABLE_LOCALES
  • LOCALE_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