@openpen/module-api 1.2.0 → 1.4.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 +22 -0
- package/README.md +7 -1
- package/dist/_chunks/{AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js → AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js} +3 -2
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js.map +1 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/index.js +2 -0
- package/dist/host/index.js.map +1 -1
- package/dist/host/stroke-store.d.ts +1 -0
- package/dist/host/tools.d.ts +9 -0
- package/dist/host/types.d.ts +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/types/contributions.d.ts +7 -5
- package/dist/types/control-bar-layout.d.ts +19 -148
- package/dist/types/module.d.ts +7 -1
- package/dist/types/tool.d.ts +14 -3
- package/dist/uikit/components/AppButtonDropdown.vue.d.ts +1 -1
- package/dist/uikit/components/AppPopoverPanel.vue.d.ts +28 -0
- package/dist/uikit/components/AppPopoverRow.vue.d.ts +26 -0
- package/dist/uikit/components/AppSegmented.vue.d.ts +6 -1
- package/dist/uikit/components/AppSelect.vue.d.ts +7 -0
- package/dist/uikit/components/AppSlider.vue.d.ts +14 -0
- package/dist/uikit/components/AppSwatchPicker.vue.d.ts +37 -0
- package/dist/uikit/components/AppToggle.vue.d.ts +2 -0
- package/dist/uikit/components/AppToggleButton.vue.d.ts +28 -0
- package/dist/uikit/index.d.ts +9 -3
- package/dist/uikit/index.js +346 -109
- package/dist/uikit/index.js.map +1 -1
- package/dist/uikit/internal.js +1 -1
- package/dist/uikit/module-api.css +287 -86
- package/dist/uikit/primitives.d.ts +4 -2
- package/dist/uikit/tokens.css +7 -6
- package/dist/use-module-context.d.ts +1 -2
- package/package.json +4 -4
- package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.0](https://github.com/openpen-platform/openpen/compare/module-api@1.3.0...module-api@1.4.0) (2026-11-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **drawing:** add semi-transparent highlighter tool ([#123](https://github.com/openpen-platform/openpen/issues/123)) ([68b7e50](https://github.com/openpen-platform/openpen/commit/68b7e50e0d15df482f3d153610177efb49317728))
|
|
9
|
+
* **module-api:** add activateTool and Wayland plugin shortcuts ([#130](https://github.com/openpen-platform/openpen/issues/130)) ([90e3a98](https://github.com/openpen-platform/openpen/commit/90e3a980f704a1c08a5f6453ded3e3ebb25e451c))
|
|
10
|
+
* **module-api:** upgrade zod to 4 ([c25ffab](https://github.com/openpen-platform/openpen/commit/c25ffab8ade237df24e60b5c22f4c61559935a1b))
|
|
11
|
+
* **uikit:** add AppPopoverPanel and AppPopoverRow for plugin popover content ([5879b2c](https://github.com/openpen-platform/openpen/commit/5879b2ccf993dd81ef527fdf4fa46b0768bd2f63))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **uikit:** name slider thumb and select trigger, drop aria-pressed from toggle ([#126](https://github.com/openpen-platform/openpen/issues/126)) ([f08705e](https://github.com/openpen-platform/openpen/commit/f08705eb788ae3ac3f7d4320f6b2758ff247a766))
|
|
17
|
+
|
|
18
|
+
## [1.3.0](https://github.com/openpen-platform/openpen/compare/module-api@1.2.0...module-api@1.3.0) (2026-06-11)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **module-api:** add commitStroke host API for async-placement tools ([c8b1807](https://github.com/openpen-platform/openpen/commit/c8b18072ffff8ecaa14d70e237c069b55b218035))
|
|
24
|
+
|
|
3
25
|
## [1.2.0](https://github.com/openpen-platform/openpen/compare/v1.1.1...v1.2.0) (2026-05-24)
|
|
4
26
|
|
|
5
27
|
|
package/README.md
CHANGED
|
@@ -7,9 +7,11 @@ This is the **only** import path that built-in modules and plugins are allowed t
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @openpen/module-api zod
|
|
10
|
+
npm install @openpen/module-api zod@^4
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
`@openpen/module-api` re-exports Zod 4. The `zod` install only provides type definitions to your editor and must match the major version the host ships; a leftover Zod 3 install makes `settingsSchema` fail to type-check.
|
|
14
|
+
|
|
13
15
|
> **Import `z` from `@openpen/module-api`, not from `'zod'` directly.**
|
|
14
16
|
> The build externalises `'zod'` — a direct `import { z } from 'zod'` in a
|
|
15
17
|
> plugin file will fail at build time with an unresolved-import error. Use
|
|
@@ -40,6 +42,10 @@ export default defineModule({
|
|
|
40
42
|
})
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
### Migrating to Zod 4
|
|
46
|
+
|
|
47
|
+
The re-exported `z` moved from Zod 3 to Zod 4. Three changes affect settings schemas: nested object defaults need `.prefault({})` instead of `.default({})`, the `required_error` / `invalid_type_error` / `errorMap` parameters are ignored, and a rejected `ctx.updateSettings()` error exposes `.issues` but no longer `.errors`. See [Module Settings](../../docs/guides/module-settings.md#zod-4-behaviour-to-watch-for) for examples.
|
|
48
|
+
|
|
43
49
|
### `setup(ctx)` — Module initialisation
|
|
44
50
|
|
|
45
51
|
`defineModule` supports an optional `setup(ctx: ModuleSetupContext)` hook, called once when the module loads. `ctx` provides:
|
|
@@ -127,6 +127,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
127
127
|
createVNode(unref(DialogContent), {
|
|
128
128
|
ref: "content",
|
|
129
129
|
class: normalizeClass(["openpen-modal-content openpen-interactive", { "openpen-modal-danger": __props.danger }]),
|
|
130
|
+
"data-testid": __props.danger ? "modal-dialog-danger" : "modal-dialog",
|
|
130
131
|
onInteractOutside: _cache[0] || (_cache[0] = (e) => __props.persistent && e.preventDefault()),
|
|
131
132
|
onEscapeKeyDown: _cache[1] || (_cache[1] = (e) => __props.persistent && e.preventDefault())
|
|
132
133
|
}, {
|
|
@@ -168,7 +169,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
168
169
|
})
|
|
169
170
|
]),
|
|
170
171
|
_: 3
|
|
171
|
-
}, 8, ["class"])
|
|
172
|
+
}, 8, ["class", "data-testid"])
|
|
172
173
|
]),
|
|
173
174
|
_: 3
|
|
174
175
|
})
|
|
@@ -242,4 +243,4 @@ export {
|
|
|
242
243
|
_resolveActive as b,
|
|
243
244
|
useDialog as u
|
|
244
245
|
};
|
|
245
|
-
//# sourceMappingURL=AppButtonDropdown.vue_vue_type_style_index_0_lang-
|
|
246
|
+
//# sourceMappingURL=AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppButtonDropdown.vue_vue_type_style_index_0_lang-B9dHU-LE.js","sources":["../../src/uikit/components/AppDialog.vue","../../src/uikit/composables/useDialog.ts","../../src/uikit/composables/dialog-internal-keys.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppDialog — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - `open` prop is the single source of truth (controlled mode). The\n * component emits `update:open` for two-way binding. Internally it also\n * registers with the modal manager for mutual exclusion (same pattern as\n * AppPopover): when `open` turns true we call manager.open(); when\n * another panel causes the manager to close us we emit update:open(false).\n * - `persistent`: when true, ESC and backdrop clicks are suppressed via\n * @interact-outside.prevent and @escape-key-down.prevent on DialogContent.\n * - `danger`: adds class `openpen-modal-danger` on the content element as a\n * styling hook for destructive-action dialogs (no default style yet).\n * - animating guard: auto-close during ControlBar scaleY animation.\n * - usePassthroughGuard: registered on the dialog content element so the\n * transparent Electron window remains clickable when dialog is open.\n * - Styles are unscoped because DialogContent is teleported outside the\n * component's DOM subtree (same reason as AppPopover).\n *\n * CSS class names (.openpen-modal-*) are intentionally kept as the carried-over\n * internal styling tokens — renaming carries no DX benefit and risks accidental\n * style breakage in any code path still referencing them.\n */\nimport { computed, inject, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport {\n DialogRoot,\n DialogTrigger,\n DialogPortal,\n DialogContent,\n DialogOverlay,\n DialogClose,\n DialogTitle,\n DialogDescription,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — see AppPopover.vue\n// header comment for why relative imports break Symbol identity in prod.\nimport {\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n HOST_DIALOG_OPEN_COUNT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\nexport interface AppDialogProps {\n /** Globally unique modal id; used for modal-stack mutual exclusion. */\n modalId: string\n /** Dialog title displayed in the dialog header. */\n title: string\n /** Controlled open state. The parent must handle update:open to toggle. */\n open: boolean\n /**\n * When true, ESC key and backdrop clicks do not close the dialog.\n * Use for confirmations that require an explicit button press.\n */\n persistent?: boolean\n /**\n * When true, adds the `openpen-modal-danger` CSS class on the content\n * element — a hook for styling destructive-action dialogs.\n */\n danger?: boolean\n}\n\nexport interface AppDialogSlots {\n /**\n * Trigger slot; scope exposes active state + toggle / open / close actions.\n * MUST NOT write @click=\"toggle\" on the trigger — DialogTrigger's as-child\n * handles activation automatically; toggle/open/close are escape hatches for\n * programmatic control only.\n */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Dialog body content slot. */\n default: () => unknown\n /** Optional footer slot (action buttons etc.). */\n footer?: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppDialogProps>(), {\n persistent: false,\n danger: false,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<AppDialogSlots>()\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, null)\n\n// Track whether THIS instance currently holds a count, so we never double\n// increment / decrement (e.g. on rapid open / close cycles or unmount-while-open).\nlet counted = false\n\nfunction acquireCount() {\n if (counted || !dialogOpenCount) return\n dialogOpenCount.value += 1\n counted = true\n}\n\nfunction releaseCount() {\n if (!counted || !dialogOpenCount) return\n dialogOpenCount.value -= 1\n counted = false\n}\n\nonBeforeUnmount(releaseCount)\n\n// ── 2. Modal manager mutual exclusion + controlled open prop ─────────────────\n//\n// The `open` prop is the canonical state. We watch it and push changes into\n// the modal manager. When the manager closes us (e.g. another panel opens),\n// we detect that via the computed isManagerOpen and emit update:open(false).\n\nfunction openDialog() {\n modalManager?.open(props.modalId)\n emit('update:open', true)\n}\n\nfunction closeDialog() {\n modalManager?.close(props.modalId)\n emit('update:open', false)\n}\n\nfunction toggleDialog() {\n if (props.open) {\n closeDialog()\n } else {\n openDialog()\n }\n}\n\n// When the prop turns true: register with the manager + bump host dialog count.\n// When it turns false: unregister + release count. Keeps everything in sync.\nwatch(\n () => props.open,\n (newOpen) => {\n if (newOpen) {\n modalManager?.open(props.modalId)\n acquireCount()\n } else {\n modalManager?.close(props.modalId)\n releaseCount()\n }\n },\n { immediate: true }\n)\n\n// When the manager closes us (another panel opened), emit update:open(false)\n// so the parent's v-model updates.\nconst isManagerOpen = computed(() => modalManager?.isOpen(props.modalId) ?? props.open)\n\nwatch(isManagerOpen, (managerOpen) => {\n if (!managerOpen && props.open) {\n emit('update:open', false)\n }\n})\n\n// Effective open state passed to Reka UI's DialogRoot.\nconst isOpen = computed(() => props.open && isManagerOpen.value)\n\n// Sync Reka UI's internal close events (ESC, overlay click) back to caller\n// unless `persistent` suppresses them.\nfunction onUpdateOpen(v: boolean) {\n if (!v && props.persistent) {\n // Persistent mode: ignore Reka UI close attempts; stay open.\n return\n }\n if (v) {\n openDialog()\n } else {\n closeDialog()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ─────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && props.open) {\n closeDialog()\n }\n }\n)\n\n// ── 4. Passthrough guard integration ─────────────────────────────────────────\n//\n// DialogContent is a Vue component; normalize to HTMLElement via watcher.\n\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\n\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\n\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <DialogRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <DialogTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggleDialog\"\n :open=\"openDialog\"\n :close=\"closeDialog\"\n />\n </DialogTrigger>\n <DialogPortal>\n <DialogOverlay class=\"openpen-modal-overlay\" />\n <DialogContent\n ref=\"content\"\n class=\"openpen-modal-content openpen-interactive\"\n :class=\"{ 'openpen-modal-danger': danger }\"\n :data-testid=\"danger ? 'modal-dialog-danger' : 'modal-dialog'\"\n @interact-outside=\"(e: Event) => persistent && e.preventDefault()\"\n @escape-key-down=\"(e: KeyboardEvent) => persistent && e.preventDefault()\"\n >\n <DialogTitle class=\"openpen-modal-title\">{{ title }}</DialogTitle>\n <DialogDescription class=\"openpen-modal-description sr-only\" />\n <div class=\"openpen-modal-body\">\n <slot />\n </div>\n <div v-if=\"$slots.footer\" class=\"openpen-modal-footer\">\n <slot name=\"footer\" />\n </div>\n <DialogClose class=\"openpen-modal-close\" aria-label=\"Close\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1 1L13 13M13 1L1 13\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n </svg>\n </DialogClose>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-modal-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Overlay (transparent click absorber, NOT a dim backdrop) ─────────────────\n OpenPen's overlay window is transparent + frameless. A traditional dim/blur\n backdrop renders as a small dark patch only over the visible chrome — visually\n jarring. The overlay is therefore kept fully transparent here; its only job\n is to absorb outside clicks (so Reka can dispatch interact-outside → close\n when not persistent) and to protect the canvas from stray clicks while the\n dialog is open. Host containers (e.g. ControlBar) render their own \"locked\"\n visual state via HOST_DIALOG_OPEN_COUNT_KEY. */\n.openpen-modal-overlay {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 200;\n pointer-events: auto;\n}\n\n/* ── Dialog content panel ────────────────────────────────────────────────── */\n.openpen-modal-content {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 201;\n min-width: 320px;\n max-width: min(480px, calc(100vw - 48px));\n max-height: calc(100vh - 96px);\n overflow-y: auto;\n\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-lg);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n padding: var(--openpen-space-lg);\n pointer-events: auto;\n}\n\n/* ── Title ───────────────────────────────────────────────────────────────── */\n.openpen-modal-title {\n font-size: 15px;\n font-weight: 600;\n color: var(--openpen-color-text-primary);\n margin: 0 0 var(--openpen-space-md) 0;\n padding-right: 28px; /* leave room for close button */\n line-height: 1.4;\n}\n\n/* ── Visually hidden description (a11y) ──────────────────────────────────── */\n.openpen-modal-description.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* ── Body ────────────────────────────────────────────────────────────────── */\n.openpen-modal-body {\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n.openpen-modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--openpen-space-sm);\n margin-top: var(--openpen-space-lg);\n padding-top: var(--openpen-space-md);\n border-top: 1px solid var(--openpen-color-border);\n}\n\n/* ── Close button ────────────────────────────────────────────────────────── */\n.openpen-modal-close {\n position: absolute;\n top: var(--openpen-space-md);\n right: var(--openpen-space-md);\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n border-radius: var(--openpen-radius-sm);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast);\n}\n\n.openpen-modal-close:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the CSS animation\n ends, so [data-state=\"closed\"] animations DO play on close.\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n\n/* Overlay is always transparent — no fade-in/out animation needed.\n Only the dialog content panel animates. */\n\n.openpen-modal-content[data-state='open'] {\n animation: openpen-modal-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-modal-content[data-state='closed'] {\n animation: openpen-modal-out 150ms ease forwards;\n}\n\n@keyframes openpen-modal-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n@keyframes openpen-modal-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n}\n</style>\n","/**\n * useDialog — imperative dialog API for OpenPen modules.\n *\n * Returns a DialogApi with confirm/alert/prompt/custom methods that each\n * return a Promise. At most one imperative dialog is open at a time; extra\n * calls are queued and executed sequentially.\n *\n * The active request is stored in a module-scoped reactive store. DialogHost\n * (mounted by the host at bootstrap) reads this store and renders the dialog.\n *\n * Usage:\n * const dialog = useDialog()\n * const ok = await dialog.confirm({ title: 'Delete?', message: '...' })\n */\nimport type { Component } from 'vue'\nimport { ref } from 'vue'\n\n// ── Public option types ───────────────────────────────────────────────────────\n\ninterface BaseDialogOptions {\n title: string\n persistent?: boolean\n danger?: boolean\n}\n\nexport interface DialogConfirmOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogAlertOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n}\n\nexport interface DialogPromptOptions extends BaseDialogOptions {\n message: string\n defaultValue?: string\n placeholder?: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogCustomOptions<T = unknown> extends BaseDialogOptions {\n component: Component\n componentProps?: Record<string, unknown>\n /** Phantom type field for TypeScript to infer the resolution payload type. */\n __payloadType?: T\n}\n\n// ── Internal request types (tagged union) ─────────────────────────────────────\n\ntype DialogRequest =\n | { kind: 'confirm'; opts: DialogConfirmOptions; resolve: (v: boolean) => void }\n | { kind: 'alert'; opts: DialogAlertOptions; resolve: () => void }\n | { kind: 'prompt'; opts: DialogPromptOptions; resolve: (v: string | null) => void }\n | { kind: 'custom'; opts: DialogCustomOptions<unknown>; resolve: (v: unknown) => void }\n\n// ── Module-scoped store ───────────────────────────────────────────────────────\n//\n// Single reactive ref holds the currently rendered request. DialogHost watches\n// this and renders the appropriate dialog. When DialogHost resolves a request,\n// it calls resolveActive() which drains the queue.\n\nexport const _activeRequest = ref<DialogRequest | null>(null)\n\nconst _queue: (() => void)[] = []\nlet _processing = false\n\nfunction _enqueue(run: () => void): void {\n if (_processing) {\n _queue.push(run)\n } else {\n _processing = true\n run()\n }\n}\n\n/**\n * Called by DialogHost when the dialog is resolved (ok/cancel/dismiss).\n * Advances the queue if there are pending requests.\n */\nexport function _resolveActive(): void {\n _activeRequest.value = null\n const next = _queue.shift()\n if (next) {\n next()\n } else {\n _processing = false\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface DialogApi {\n confirm(opts: DialogConfirmOptions): Promise<boolean>\n alert(opts: DialogAlertOptions): Promise<void>\n prompt(opts: DialogPromptOptions): Promise<string | null>\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null>\n}\n\nexport function useDialog(): DialogApi {\n return {\n confirm(opts: DialogConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'confirm', opts, resolve }\n })\n })\n },\n\n alert(opts: DialogAlertOptions): Promise<void> {\n return new Promise<void>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'alert', opts, resolve }\n })\n })\n },\n\n prompt(opts: DialogPromptOptions): Promise<string | null> {\n return new Promise<string | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'prompt', opts, resolve }\n })\n })\n },\n\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = {\n kind: 'custom',\n opts: opts as DialogCustomOptions<unknown>,\n resolve: resolve as (v: unknown) => void,\n }\n })\n })\n },\n }\n}\n","/**\n * dialog-internal-keys.ts — Internal injection keys for the imperative dialog system.\n *\n * HOST-INTERNAL: Do not export from any public barrel. Plugin authors must\n * never see or depend on these keys; they exist only so DialogHost can\n * provide the handle that useDialogPluginComponent() reads.\n */\nimport type { InjectionKey } from 'vue'\nimport type { DialogPluginComponentHandle } from './useDialogPluginComponent'\n\n/**\n * Injection key that DialogHost provides to custom dialog components.\n * Components rendered via useDialog().custom() use useDialogPluginComponent()\n * which injects this key to get ok/cancel/dismiss callbacks.\n */\nexport const DIALOG_PLUGIN_HANDLE_KEY: InjectionKey<DialogPluginComponentHandle<unknown>> =\n Symbol('openpen:dialog-plugin-handle')\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementVNode","$slots","_openBlock","_createElementBlock"],"mappings":"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"}
|
package/dist/host/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export { useStrokeStyle } from './stroke-style';
|
|
15
15
|
export type { StrokeColor } from './stroke-style';
|
|
16
16
|
export { resolveColorStyle, hexToRgb, rgbToHex, hsvToRgb, rgbToHsv, hexToHsv, hsvToHex, isValidHex, } from './color-utils';
|
|
17
|
-
export { getAllStrokes, removeStrokeById, pushCommand } from './stroke-store';
|
|
17
|
+
export { getAllStrokes, removeStrokeById, pushCommand, commitStroke } from './stroke-store';
|
|
18
18
|
export type { Stroke } from './stroke-store';
|
|
19
19
|
export { usePopupAnchor, calculatePopupAnchor } from './popup-anchor';
|
|
20
20
|
export { usePassthroughGuard } from './passthrough';
|
package/dist/host/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const isValidHex = (hex) => _useHost().colorUtils.isValidHex(hex);
|
|
|
13
13
|
const getAllStrokes = () => _useHost().getAllStrokes();
|
|
14
14
|
const removeStrokeById = (id) => _useHost().removeStrokeById(id);
|
|
15
15
|
const pushCommand = (command) => _useHost().pushCommand(command);
|
|
16
|
+
const commitStroke = (stroke) => _useHost().commitStroke(stroke);
|
|
16
17
|
const usePopupAnchor = (options) => _useHost().usePopupAnchor(options);
|
|
17
18
|
const calculatePopupAnchor = (options) => _useHost().calculatePopupAnchor(options);
|
|
18
19
|
const hostCommands = new Proxy({}, {
|
|
@@ -26,6 +27,7 @@ const getSlotEntries = (slotKey) => _useHost().getSlotEntries(slotKey);
|
|
|
26
27
|
export {
|
|
27
28
|
calculatePopupAnchor,
|
|
28
29
|
c as clearModuleContext,
|
|
30
|
+
commitStroke,
|
|
29
31
|
emit,
|
|
30
32
|
getAllStrokes,
|
|
31
33
|
getSlotEntries,
|
package/dist/host/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/host/stroke-style.ts","../../src/host/color-utils.ts","../../src/host/stroke-store.ts","../../src/host/popup-anchor.ts","../../src/host/host-commands.ts","../../src/host/event-bus.ts","../../src/host/slot-entries.ts"],"sourcesContent":["/**\n * @openpen/module-api/host — Stroke Style\n *\n * Proxies the useStrokeStyle composable through the host registry so\n * module components can read + write the global stroke style without\n * reaching past the package boundary. The reactive refs returned by\n * useStrokeStyle stay live across the proxy because the host registry\n * returns the singleton refs the host owns.\n *\n * initStrokeStyleFromSettings is host-boot-only and intentionally\n * NOT exposed here.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const useStrokeStyle: ModuleHost['useStrokeStyle'] = () =>\n _useHost().useStrokeStyle()\n\n// Re-export the StrokeColor type via the public SDK types (not host internals).\nexport type { StrokeColor } from '../types/tool'\n","/**\n * @openpen/module-api/host — Color Utilities\n *\n * Proxies color manipulation helpers through the host registry. These\n * helpers are pure functions (no host state), so a future refactor MAY\n * lift the implementations into this package itself; for now they flow\n * through injection for consistency with the rest of the host surface.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\ntype Utils = ModuleHost['colorUtils']\n\nexport const resolveColorStyle: Utils['resolveColorStyle'] = (ctx, color, startPoint, endPoint) =>\n _useHost().colorUtils.resolveColorStyle(ctx, color, startPoint, endPoint)\n\nexport const hexToRgb: Utils['hexToRgb'] = (hex) =>\n _useHost().colorUtils.hexToRgb(hex)\n\nexport const rgbToHex: Utils['rgbToHex'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHex(r, g, b)\n\nexport const hsvToRgb: Utils['hsvToRgb'] = (h, s, v) =>\n _useHost().colorUtils.hsvToRgb(h, s, v)\n\nexport const rgbToHsv: Utils['rgbToHsv'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHsv(r, g, b)\n\nexport const hexToHsv: Utils['hexToHsv'] = (hex) =>\n _useHost().colorUtils.hexToHsv(hex)\n\nexport const hsvToHex: Utils['hsvToHex'] = (h, s, v) =>\n _useHost().colorUtils.hsvToHex(h, s, v)\n\nexport const isValidHex: Utils['isValidHex'] = (hex) =>\n _useHost().colorUtils.isValidHex(hex)\n","/**\n * @openpen/module-api/host — Stroke Store\n *\n * Proxies the stroke store read + history APIs through the host registry.\n *
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/host/stroke-style.ts","../../src/host/color-utils.ts","../../src/host/stroke-store.ts","../../src/host/popup-anchor.ts","../../src/host/host-commands.ts","../../src/host/event-bus.ts","../../src/host/slot-entries.ts"],"sourcesContent":["/**\n * @openpen/module-api/host — Stroke Style\n *\n * Proxies the useStrokeStyle composable through the host registry so\n * module components can read + write the global stroke style without\n * reaching past the package boundary. The reactive refs returned by\n * useStrokeStyle stay live across the proxy because the host registry\n * returns the singleton refs the host owns.\n *\n * initStrokeStyleFromSettings is host-boot-only and intentionally\n * NOT exposed here.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const useStrokeStyle: ModuleHost['useStrokeStyle'] = () =>\n _useHost().useStrokeStyle()\n\n// Re-export the StrokeColor type via the public SDK types (not host internals).\nexport type { StrokeColor } from '../types/tool'\n","/**\n * @openpen/module-api/host — Color Utilities\n *\n * Proxies color manipulation helpers through the host registry. These\n * helpers are pure functions (no host state), so a future refactor MAY\n * lift the implementations into this package itself; for now they flow\n * through injection for consistency with the rest of the host surface.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\ntype Utils = ModuleHost['colorUtils']\n\nexport const resolveColorStyle: Utils['resolveColorStyle'] = (ctx, color, startPoint, endPoint) =>\n _useHost().colorUtils.resolveColorStyle(ctx, color, startPoint, endPoint)\n\nexport const hexToRgb: Utils['hexToRgb'] = (hex) =>\n _useHost().colorUtils.hexToRgb(hex)\n\nexport const rgbToHex: Utils['rgbToHex'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHex(r, g, b)\n\nexport const hsvToRgb: Utils['hsvToRgb'] = (h, s, v) =>\n _useHost().colorUtils.hsvToRgb(h, s, v)\n\nexport const rgbToHsv: Utils['rgbToHsv'] = (r, g, b) =>\n _useHost().colorUtils.rgbToHsv(r, g, b)\n\nexport const hexToHsv: Utils['hexToHsv'] = (hex) =>\n _useHost().colorUtils.hexToHsv(hex)\n\nexport const hsvToHex: Utils['hsvToHex'] = (h, s, v) =>\n _useHost().colorUtils.hsvToHex(h, s, v)\n\nexport const isValidHex: Utils['isValidHex'] = (hex) =>\n _useHost().colorUtils.isValidHex(hex)\n","/**\n * @openpen/module-api/host — Stroke Store\n *\n * Proxies the stroke store read + history APIs through the host registry.\n * The exposed surface:\n * - getAllStrokes / removeStrokeById / pushCommand: read + history access\n * required by stroke-eraser (and similar tools).\n * - commitStroke: atomic add + ADD_STROKE history + redraw for async-placement\n * tools (e.g. text) that finalise a stroke outside the synchronous\n * onPointerUp return. Not idempotent — calling it twice appends twice.\n *\n * Host-internal state mutation helpers (clearAll, addStroke, undo, redo)\n * are intentionally NOT exposed here — modules must use hostCommands for\n * undo/redo and canvas clear.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const getAllStrokes: ModuleHost['getAllStrokes'] = () =>\n _useHost().getAllStrokes()\n\nexport const removeStrokeById: ModuleHost['removeStrokeById'] = (id) =>\n _useHost().removeStrokeById(id)\n\nexport const pushCommand: ModuleHost['pushCommand'] = (command) =>\n _useHost().pushCommand(command)\n\nexport const commitStroke: ModuleHost['commitStroke'] = (stroke) =>\n _useHost().commitStroke(stroke)\n\n// Re-export Stroke type via the public SDK types, not host internals.\nexport type { Stroke } from '../types/tool'\n","/**\n * @openpen/module-api/host — Popup Anchor\n *\n * Proxies the `usePopupAnchor` composable and the `calculatePopupAnchor`\n * pure helper through the host registry so module components can position\n * popups relative to anchor elements without reaching past the package\n * boundary.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const usePopupAnchor: ModuleHost['usePopupAnchor'] = (options) =>\n _useHost().usePopupAnchor(options)\n\nexport const calculatePopupAnchor: ModuleHost['calculatePopupAnchor'] = (options) =>\n _useHost().calculatePopupAnchor(options)\n","/**\n * @openpen/module-api/host — Host Commands\n *\n * Proxies hostCommands through the host registry so module components\n * can trigger host infrastructure actions (pin toggle, canvas clear,\n * undo/redo, drawing mode toggle) without reaching past the package\n * boundary. The exported object is a Proxy so every property access\n * fetches the live `hostCommands` from the registered host.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const hostCommands = new Proxy({} as ModuleHost['hostCommands'], {\n get(_target, prop) {\n return Reflect.get(_useHost().hostCommands as object, prop)\n },\n})\n","/**\n * @openpen/module-api/host — Domain Event Bus\n *\n * Proxies the renderer-side event-bus emit/on through the host registry so\n * module components can publish and subscribe without reaching into host\n * runtime internals. The bus is a process-local pub/sub for cross-module\n * coordination.\n *\n * **Host-emitted events (consume via `on(name, cb)`):**\n *\n * | Event name | Payload shape | Fires when |\n * |---------------------------|-------------------------------------------------|----------------------------------------------------------------------|\n * | `'tool-changed'` | `{ tool: string } & Partial<ToolConfig>` | The active drawing tool changes (user click, IPC, or shortcut) |\n * | `'stroke-style-changed'` | `{ color?: StrokeColor; lineWidth?: number }` | Color or stroke width changes |\n * | `'canvas-redraw-requested'` | (no payload) | A module wants the canvas to repaint (eraser, transformer, etc.) |\n *\n * **Module-emitted events:** modules MAY emit any event name they\n * choose; the bus is namespace-free. Use a prefix (e.g. `'my-plugin:'`)\n * to avoid colliding with another module's events.\n *\n * `on(name, cb)` returns an unsubscribe function — register it via\n * `ctx.onDispose(unsubscribe)` so the listener is cleaned up when the\n * module unloads.\n *\n * @example\n * ```ts\n * import { on } from '@openpen/module-api/host'\n *\n * setup(ctx) {\n * const off = on('tool-changed', (payload) => {\n * console.log('tool now', payload.tool)\n * })\n * ctx.onDispose(off)\n * }\n * ```\n */\n\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const emit: ModuleHost['emit'] = (event, payload) =>\n _useHost().emit(event, payload)\n\nexport const on: ModuleHost['on'] = (event, handler) =>\n _useHost().on(event, handler)\n","/**\n * @openpen/module-api/host — Slot Entries Reader\n *\n * Proxies getSlotEntries through the host registry so module components\n * can render UI driven by other modules' contributions to the same slot\n * (for example, the shape-picker rendering all canvas.shapes contributions).\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const getSlotEntries: ModuleHost['getSlotEntries'] = <T = unknown>(slotKey: string) =>\n _useHost().getSlotEntries<T>(slotKey)\n"],"names":["s"],"mappings":";;;AAeO,MAAM,iBAA+C,MAC1D,SAAA,EAAW,eAAA;ACHN,MAAM,oBAAgD,CAAC,KAAK,OAAO,YAAY,aACpF,WAAW,WAAW,kBAAkB,KAAK,OAAO,YAAY,QAAQ;AAEnE,MAAM,WAA8B,CAAC,QAC1C,WAAW,WAAW,SAAS,GAAG;AAE7B,MAAM,WAA8B,CAAC,GAAG,GAAG,MAChD,WAAW,WAAW,SAAS,GAAG,GAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,GAAGA,IAAG,MAChD,WAAW,WAAW,SAAS,GAAGA,IAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,GAAG,GAAG,MAChD,WAAW,WAAW,SAAS,GAAG,GAAG,CAAC;AAEjC,MAAM,WAA8B,CAAC,QAC1C,WAAW,WAAW,SAAS,GAAG;AAE7B,MAAM,WAA8B,CAAC,GAAGA,IAAG,MAChD,WAAW,WAAW,SAAS,GAAGA,IAAG,CAAC;AAEjC,MAAM,aAAkC,CAAC,QAC9C,WAAW,WAAW,WAAW,GAAG;ACjB/B,MAAM,gBAA6C,MACxD,SAAA,EAAW,cAAA;AAEN,MAAM,mBAAmD,CAAC,OAC/D,SAAA,EAAW,iBAAiB,EAAE;AAEzB,MAAM,cAAyC,CAAC,YACrD,SAAA,EAAW,YAAY,OAAO;AAEzB,MAAM,eAA2C,CAAC,WACvD,SAAA,EAAW,aAAa,MAAM;ACjBzB,MAAM,iBAA+C,CAAC,YAC3D,SAAA,EAAW,eAAe,OAAO;AAE5B,MAAM,uBAA2D,CAAC,YACvE,SAAA,EAAW,qBAAqB,OAAO;ACHlC,MAAM,eAAe,IAAI,MAAM,IAAkC;AAAA,EACtE,IAAI,SAAS,MAAM;AACjB,WAAO,QAAQ,IAAI,SAAA,EAAW,cAAwB,IAAI;AAAA,EAC5D;AACF,CAAC;ACwBM,MAAM,OAA2B,CAAC,OAAO,YAC9C,WAAW,KAAK,OAAO,OAAO;AAEzB,MAAM,KAAuB,CAAC,OAAO,YAC1C,WAAW,GAAG,OAAO,OAAO;AClCvB,MAAM,iBAA+C,CAAc,YACxE,SAAA,EAAW,eAAkB,OAAO;"}
|
|
@@ -2,4 +2,5 @@ import type { ModuleHost } from './types';
|
|
|
2
2
|
export declare const getAllStrokes: ModuleHost['getAllStrokes'];
|
|
3
3
|
export declare const removeStrokeById: ModuleHost['removeStrokeById'];
|
|
4
4
|
export declare const pushCommand: ModuleHost['pushCommand'];
|
|
5
|
+
export declare const commitStroke: ModuleHost['commitStroke'];
|
|
5
6
|
export type { Stroke } from '../types/tool';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make `toolId` the active tool. The matching control bar button lights up
|
|
3
|
+
* on every display and every canvas switches to the tool.
|
|
4
|
+
*
|
|
5
|
+
* `toolId` must be the `id` of a `canvas.tools` contribution (built-in or
|
|
6
|
+
* from any loaded module). An unknown id is ignored with a console warning
|
|
7
|
+
* and changes nothing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function activateTool(toolId: string): void;
|
package/dist/host/types.d.ts
CHANGED
|
@@ -81,6 +81,13 @@ export interface HostCommandsAPI {
|
|
|
81
81
|
toggleDrawingMode: () => void;
|
|
82
82
|
setDrawingMode: (enabled: boolean) => void;
|
|
83
83
|
};
|
|
84
|
+
tools: {
|
|
85
|
+
/**
|
|
86
|
+
* Make `toolId` the active tool in every window. Unknown ids are ignored
|
|
87
|
+
* with a console warning.
|
|
88
|
+
*/
|
|
89
|
+
activate: (toolId: string) => void;
|
|
90
|
+
};
|
|
84
91
|
}
|
|
85
92
|
/** Return shape of `useStrokeStyle()`. */
|
|
86
93
|
export interface StrokeStyleAPI {
|
|
@@ -153,6 +160,18 @@ export interface ModuleHost {
|
|
|
153
160
|
getAllStrokes: () => Stroke[];
|
|
154
161
|
removeStrokeById: (id: string) => boolean;
|
|
155
162
|
pushCommand: (command: HistoryCommand) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Atomically commit a finished stroke: append it to the store, record an
|
|
165
|
+
* ADD_STROKE history command, and request a canvas redraw. The commit path
|
|
166
|
+
* for tools that produce strokes outside the synchronous pointer-up return
|
|
167
|
+
* (e.g. async-placement tools that finalise after a deferred interaction).
|
|
168
|
+
*
|
|
169
|
+
* Semantically equivalent to the host's synchronous onPointerUp commit and
|
|
170
|
+
* fully participates in undo/redo. NOT idempotent: calling it twice with the
|
|
171
|
+
* same stroke appends two strokes and two history entries. The stroke shape
|
|
172
|
+
* is the caller's responsibility — v1 is full-trust and performs no sanitize.
|
|
173
|
+
*/
|
|
174
|
+
commitStroke: (stroke: Stroke) => void;
|
|
156
175
|
hostCommands: HostCommandsAPI;
|
|
157
176
|
colorUtils: ColorUtilsAPI;
|
|
158
177
|
useStrokeStyle: () => StrokeStyleAPI;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { useModuleContext } from './use-module-context';
|
|
|
11
11
|
export { isValidModuleId, MODULE_ID_RE } from './validation';
|
|
12
12
|
export { resolveLabel, sanitizeIdForI18n } from './locale';
|
|
13
13
|
export type { LocaleMap } from './locale';
|
|
14
|
+
export { activateTool } from './host/tools';
|
|
14
15
|
export { ALL_SLOTS, V1_ACTIVE_SLOTS, V1_RESERVED_SLOTS, CONTRIBUTION_KEY_TO_SLOT_ID, isKnownSlot, getSlot, } from './slots';
|
|
15
16
|
export type { SlotDefinition, SlotCategory, SlotStatus } from './slots';
|
|
16
17
|
export type { Point, StrokeColor, StrokeStyle, Stroke, PointerModifiers, Tool, } from './types/tool';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { C as CONTRIBUTION_KEY_TO_SLOT_ID } from "./_chunks/inject-keys-CRXCpN-M
|
|
|
2
2
|
import { A, a, b, c, d, D, G, H, I, L, M, P, S, e, T, V, f, W, g, i, r, h } from "./_chunks/inject-keys-CRXCpN-M.js";
|
|
3
3
|
import { g as getModuleContext } from "./_chunks/module-context-registry-Cn1IWYu9.js";
|
|
4
4
|
import { c as c2, s } from "./_chunks/module-context-registry-Cn1IWYu9.js";
|
|
5
|
+
import { _useHost } from "./host/registry.js";
|
|
5
6
|
import DOMPurify from "dompurify";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
const MODULE_ID_RE = /^@([a-z0-9][a-z0-9-]{0,38})\/([a-z0-9][a-z0-9-]{0,38})$/;
|
|
@@ -55,6 +56,9 @@ function resolveLabel(input, locale) {
|
|
|
55
56
|
const first = Object.values(input)[0];
|
|
56
57
|
return first ?? "";
|
|
57
58
|
}
|
|
59
|
+
function activateTool(toolId) {
|
|
60
|
+
_useHost().hostCommands.tools.activate(toolId);
|
|
61
|
+
}
|
|
58
62
|
function resolveStrokeColor(color) {
|
|
59
63
|
return typeof color === "string" ? color : color.from;
|
|
60
64
|
}
|
|
@@ -326,6 +330,7 @@ export {
|
|
|
326
330
|
V as V1_ACTIVE_SLOTS,
|
|
327
331
|
f as V1_RESERVED_SLOTS,
|
|
328
332
|
W as WRAPPER_EL_KEY,
|
|
333
|
+
activateTool,
|
|
329
334
|
c2 as clearModuleContext,
|
|
330
335
|
compileCursor,
|
|
331
336
|
defineModule,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/validation.ts","../src/define-module.ts","../src/use-module-context.ts","../src/locale.ts","../src/types/tool.ts","../src/cursors.ts"],"sourcesContent":["/**\n * Scoped module / plugin ID format: `@scope/name`\n *\n * Rules:\n * - Must start with `@`\n * - scope: lowercase ASCII alphanumeric + hyphen, starts with letter or digit,\n * max 39 chars (mirrors GitHub username/org limits)\n * - Single `/` separator\n * - name: same charset and length limit as scope\n *\n * The same regex is used by the OpenPen CLI (`openpen-cli`) and the\n * main process plugin loader, so plugin IDs round-trip safely between\n * `plugin.json`, filesystem paths under `~/.openpen/plugins/@scope/name/`,\n * and registry lookups.\n */\nexport const MODULE_ID_RE =\n /^@([a-z0-9][a-z0-9-]{0,38})\\/([a-z0-9][a-z0-9-]{0,38})$/\n\n/**\n * Returns `true` when `id` is a non-empty string that satisfies\n * {@link MODULE_ID_RE}; acts as a type guard narrowing `unknown` → `string`.\n *\n * @example\n * isValidModuleId('@openpen/freehand') // true\n * isValidModuleId('@alice/todo-app') // true\n * isValidModuleId('freehand') // false — must be scoped @scope/name\n * isValidModuleId('') // false — empty string rejected\n */\nexport function isValidModuleId(id: unknown): id is string {\n return typeof id === 'string' && id.length > 0 && MODULE_ID_RE.test(id)\n}\n","import { isValidModuleId } from './validation'\nimport { CONTRIBUTION_KEY_TO_SLOT_ID } from './slots'\nimport type { OpenPenModule } from './types/module'\n\n/**\n * Identity helper for declaring an OpenPen module with full type\n * inference and runtime sanity checks. Throws `Error` (preflight) when:\n *\n * - `id` is missing or has invalid characters\n * - `contributes` is missing entirely\n * - `contributes` has no contribution fields\n * - `contributes` references a slot key not in the catalogue\n *\n * This is intentionally a small surface — full validation\n * (id collisions across modules, slot quota, settingsSchema parse, etc.)\n * happens later in the host app's `module-validator`. This helper\n * catches the obvious developer mistakes at the module's own boundary\n * so errors surface in the module's repo, not deep inside OpenPen.\n */\nexport function defineModule<TModule extends OpenPenModule>(\n spec: TModule\n): TModule {\n if (!isValidModuleId(spec.id)) {\n throw new Error(\n `[@openpen/module-api] Invalid module id: ${JSON.stringify(spec.id)}. ` +\n `Module ids must use \\`@scope/name\\` format with lowercase ASCII alphanumeric + hyphen, ` +\n `max 39 chars per segment (e.g. \\`@alice/my-plugin\\`).`\n )\n }\n\n if (!spec.contributes || typeof spec.contributes !== 'object') {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" must declare a non-empty 'contributes' object.`\n )\n }\n\n const keys = Object.keys(spec.contributes)\n if (keys.length === 0) {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" must contribute to at least one slot. ` +\n `Add at least one field to 'contributes' (e.g. 'tools', 'settingsTabs', 'shortcuts').`\n )\n }\n\n for (const key of keys) {\n if (!(key in CONTRIBUTION_KEY_TO_SLOT_ID)) {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" contributes to unknown slot key '${key}'. ` +\n `Known keys: ${Object.keys(CONTRIBUTION_KEY_TO_SLOT_ID).sort().join(', ')}.`\n )\n }\n }\n\n return spec\n}\n","import { getModuleContext } from './host/module-context-registry'\nimport type { ModuleSetupContext } from './types/module'\n\n/**\n * Returns the `ModuleSetupContext` that was constructed for `moduleId` by\n * the host runtime. Call this inside a Vue component's `<script setup>` (or\n * `setup()`) to access `ctx.getSettings`, `ctx.updateSettings`,\n * `ctx.onSettingsChange`, `ctx.notify`, and all other context methods from\n * within a component tree — without having to prop-drill or provide/inject\n * the context object manually.\n *\n * @throws If `moduleId` is not currently registered (module not yet loaded,\n * already rolled back, or the id string doesn't match `defineModule({ id })`).\n *\n * @example\n * ```ts\n * // MySettingsPanel.vue\n * import { useModuleContext } from '@openpen/module-api'\n * import { onMounted, onUnmounted, ref } from 'vue'\n * import { z } from 'zod'\n *\n * const MySchema = z.object({ color: z.string().default('#fff') })\n * type MySettings = z.infer<typeof MySchema>\n *\n * const ctx = useModuleContext('my-plugin')\n * const color = ref(ctx.getSettings<MySettings>().color)\n *\n * let unsubscribe: (() => void) | null = null\n * onMounted(() => {\n * unsubscribe = ctx.onSettingsChange<MySettings>((s) => { color.value = s.color })\n * })\n * onUnmounted(() => unsubscribe?.())\n *\n * async function changeColor(next: string) {\n * await ctx.updateSettings<MySettings>({ color: next })\n * }\n * ```\n */\nexport function useModuleContext(moduleId: string): ModuleSetupContext {\n const ctx = getModuleContext(moduleId)\n if (!ctx) {\n throw new Error(\n `[module-api] useModuleContext(\"${moduleId}\") was called but no module is currently registered under that id. ` +\n `This usually means: (a) the moduleId string does not match the id passed to defineModule, or ` +\n `(b) the composable was called before the module's setup() ran ` +\n `(do not call useModuleContext at module top-level — call it inside a component's setup() or onMounted()).`\n )\n }\n return ctx\n}\n","/**\n * Convert a scoped module id to a vue-i18n-safe namespace key.\n *\n * vue-i18n uses `.` as path separator, so a raw id like `@openpen/freehand`\n * would be misinterpreted as a three-level path `['@openpen', 'freehand']`.\n * Stripping `@` and replacing `/` with `.` produces `openpen.freehand`,\n * which resolves correctly as a two-level namespace.\n *\n * @example\n * sanitizeIdForI18n('@openpen/freehand') // 'openpen.freehand'\n * sanitizeIdForI18n('@alice/todo') // 'alice.todo'\n */\nexport function sanitizeIdForI18n(moduleId: string): string {\n return moduleId.replace(/^@/, '').replace(/\\//g, '.')\n}\n\n/**\n * Maps a BCP-47 locale tag (e.g. `'en'`, `'zh-TW'`) to a localised string.\n *\n * Modules use this to declare i18n display names without coupling\n * themselves to the host app's i18n framework. The OpenPen registry\n * resolves the value at render time based on the active locale.\n */\nexport type LocaleMap = Record<string, string>\n\n/**\n * Resolve a `string | LocaleMap` to a single string for a given locale.\n *\n * Resolution order (first match wins):\n * 1. Plain string → returned as-is\n * 2. Exact tag match (e.g. `'zh-TW'`)\n * 3. Language prefix match (e.g. `'zh'` for `'zh-Hans-CN'`)\n * 4. English fallback (`'en'`)\n * 5. First declared value in the map\n * 6. Empty string (only when the map is `{}`)\n */\nexport function resolveLabel(\n input: string | LocaleMap,\n locale: string\n): string {\n if (typeof input === 'string') return input\n\n if (input[locale] !== undefined) return input[locale]\n\n const lang = locale.split('-')[0]\n if (lang !== locale && input[lang] !== undefined) return input[lang]\n\n if (input.en !== undefined) return input.en\n\n const first = Object.values(input)[0]\n return first ?? ''\n}\n","/**\n * Public Tool / Stroke / Point primitives shared by host code and\n * every module / plugin. Declared inside @openpen/module-api so\n * third parties don't need to import host internals to type their\n * `canvas.tools` contributions.\n */\n\nexport interface Point {\n x: number\n y: number\n}\n\n/** Solid color or linear gradient between two stops. */\nexport type StrokeColor =\n | string\n | { type: 'linear'; from: string; to: string }\n\n/**\n * Reduce a `StrokeColor` to a single CSS colour string.\n *\n * `CanvasRenderingContext2D.strokeStyle` accepts a single colour string but\n * not a structured gradient descriptor; custom `renderStroke` implementations\n * that don't draw the gradient natively need a representative fallback.\n * Returns the string as-is for solid colours, and the `from` stop for linear\n * gradients. Future gradient kinds can pick their own representative without\n * touching callers.\n */\nexport function resolveStrokeColor(color: StrokeColor): string {\n return typeof color === 'string' ? color : color.from\n}\n\nexport interface StrokeStyle {\n color: StrokeColor\n lineWidth: number\n lineCap: CanvasLineCap\n lineJoin: CanvasLineJoin\n}\n\nexport interface Stroke {\n id: string\n /** Id of the tool that produced this stroke (matches a ToolContribution.id). */\n tool: string\n points: Point[]\n style: StrokeStyle\n /**\n * Tool-specific extra state. Tools MAY store arbitrary keys here during\n * `onPointerDown` / `onPointerMove` / `onPointerUp`; those keys survive\n * into `renderStroke(stroke, canvasCtx)` calls so the renderer can read\n * them back.\n *\n * The host treats extra keys as `unknown` — type-cast at the read site is\n * the plugin author's responsibility (TypeScript cannot infer the shape).\n *\n * @example\n * // Store during onPointerUp:\n * const stroke: Stroke = { id, tool: 'my-tool', points, style, opacity: 0.5 }\n *\n * // Read back in renderStroke:\n * const opacity = (stroke.opacity as number) ?? 1\n * canvasCtx.globalAlpha = opacity\n */\n [key: string]: unknown\n}\n\nexport interface PointerModifiers {\n shiftKey?: boolean\n}\n\nexport interface Tool {\n onPointerDown(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n style: StrokeStyle\n ): void\n /**\n * Returns `true` when the engine should perform a full redraw\n * (e.g. stroke-eraser removed an existing stroke). Otherwise `void`.\n */\n onPointerMove(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n modifiers?: PointerModifiers\n ): void | boolean\n /**\n * Returns the finished `Stroke` to push into the store, or `null`\n * to discard (e.g. an eraser that doesn't accumulate state).\n */\n onPointerUp(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n modifiers?: PointerModifiers\n ): Stroke | null\n /** When true, canvas-engine clears + redraws all strokes before each move. */\n needsPreviewRedraw?: boolean\n renderPreview?(canvasCtx: CanvasRenderingContext2D): void\n}\n","/**\n * Cursor compilation + sanitisation helpers shared by host runtime and\n * plugin SDK.\n *\n * Three closure layers gate every cursor entry before it reaches\n * `v-html`: R1–R12 shape validation here, DOMPurify content sanitisation\n * inside compileCursor, and a deep-clone + freeze in the contribution\n * store. Removing any one of them re-opens an attack surface the others\n * cannot close.\n */\n\nimport DOMPurify from 'dompurify'\nimport type {\n CursorContribution,\n CursorSpec,\n Hotspot,\n PngCursorSpec,\n SvgCursorSpec,\n} from './types/contributions'\n\nexport interface CompiledCursor {\n /** Sanitised SVG / HTML fragment to mount as `innerHTML` on the cursor element. */\n svgMarkup: string\n /** Hotspot offset, in CSS pixels relative to the markup's top-left. */\n hotspot: Hotspot\n /** CSS keyword fallback retained for legacy callers. */\n fallbackCssKeyword: string\n}\n\nexport interface CursorResolutionContext {\n /**\n * Hostname-encoded plugin id (`@acme/x` → `acme.x`) used to assemble\n * `openpen-plugin://<hostname>/<path>` URLs for relative-path specs.\n * Undefined for built-in modules — path-form specs without a hostname\n * resolve to `null` (host falls back to its default cursor).\n */\n pluginHostname?: string\n /** Injection point for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * W3C CSS UI Level 3 cursor keywords. The legacy `cursor: string` form\n * MUST match one of these (case-insensitive after trim); everything else\n * is rejected by R2 / R12.\n */\nexport const SAFE_CURSOR_KEYWORDS: ReadonlySet<string> = new Set([\n 'auto', 'default', 'none',\n 'context-menu', 'help', 'pointer', 'progress', 'wait',\n 'cell', 'crosshair', 'text', 'vertical-text',\n 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 'grabbing',\n 'e-resize', 'n-resize', 'ne-resize', 'nw-resize',\n 's-resize', 'se-resize', 'sw-resize', 'w-resize',\n 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize',\n 'col-resize', 'row-resize', 'all-scroll',\n 'zoom-in', 'zoom-out',\n])\n\nconst FORBIDDEN_CSS_LOAD_PATTERN = /url\\s*\\(|image-set\\s*\\(|-webkit-image-set\\s*\\(/i\n\nconst DEFAULT_FALLBACK_KEYWORD = 'crosshair'\n\nexport function isSafeRelativePath(p: unknown): p is string {\n if (typeof p !== 'string' || p.length === 0) return false\n // Bare `:` catches scheme prefixes without `://` (e.g. `data:`).\n if (p.includes(':')) return false\n if (p.startsWith('/')) return false\n for (const seg of p.split('/')) {\n if (seg === '' || seg === '..') return false\n }\n return true\n}\n\nexport function pluginHostname(moduleId: string): string {\n return moduleId.replace(/^@/, '').replace(/\\//g, '.')\n}\n\nexport function pluginAssetUrl(moduleId: string, relativePath: string): string {\n if (!isSafeRelativePath(relativePath)) {\n throw new Error(`Unsafe asset path: ${relativePath}`)\n }\n return `openpen-plugin://${pluginHostname(moduleId)}/${relativePath}`\n}\n\n/**\n * Sanitise SVG markup before it reaches `v-html`. Rationale for each\n * flag: `USE_PROFILES: { svg: true }` covers script tags, on* handlers,\n * and javascript: hrefs; `FORBID_TAGS: ['foreignObject']` closes the\n * mXSS escape hatch the SVG profile alone does not; turning off\n * `ALLOW_UNKNOWN_PROTOCOLS` blocks `<use href=\"data:...\">` and friends.\n */\nexport function sanitizeSvgMarkup(raw: string): string {\n return DOMPurify.sanitize(raw, {\n USE_PROFILES: { svg: true },\n FORBID_TAGS: ['foreignObject'],\n // Cursor SVGs use presentation attributes (fill/stroke/etc.), never\n // inline `style`. Forbidding it strips legacy `expression()` CSS\n // smuggling vectors (S7) without touching anything we draw.\n FORBID_ATTR: ['style'],\n ALLOW_UNKNOWN_PROTOCOLS: false,\n })\n}\n\nfunction resolveHotspot(spec: SvgCursorSpec | PngCursorSpec): Hotspot {\n const h = spec.hotspot\n if (!h || typeof h !== 'object') return { x: 0, y: 0 }\n const x = typeof h.x === 'number' && Number.isFinite(h.x) ? h.x : 0\n const y = typeof h.y === 'number' && Number.isFinite(h.y) ? h.y : 0\n return { x, y }\n}\n\nfunction resolveFallback(spec: SvgCursorSpec | PngCursorSpec): string {\n return typeof spec.fallback === 'string' && spec.fallback.length > 0\n ? spec.fallback\n : DEFAULT_FALLBACK_KEYWORD\n}\n\nfunction escapeHtmlAttr(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/**\n * Convert a frozen, shape-validated CursorContribution.cursor into render\n * data ready for `v-html`. Returns `null` for legacy keyword strings\n * (caller falls back to the host default cursor) and for any spec the\n * sanitiser empties out completely.\n *\n * Async because path-form SVG specs are fetched through `openpen-plugin://`\n * at compile time; inline + PNG branches resolve synchronously inside the\n * returned Promise.\n */\nexport async function compileCursor(\n spec: CursorSpec,\n context: CursorResolutionContext = {},\n): Promise<CompiledCursor | null> {\n if (typeof spec === 'string') return null\n if (!spec || typeof spec !== 'object') return null\n\n if ('svg' in spec && typeof spec.svg === 'string') {\n const hotspot = resolveHotspot(spec)\n const fallbackCssKeyword = resolveFallback(spec)\n const trimmed = spec.svg.trim()\n\n if (trimmed.startsWith('<')) {\n const sanitised = sanitizeSvgMarkup(trimmed)\n if (!sanitised) return null\n return { svgMarkup: sanitised, hotspot, fallbackCssKeyword }\n }\n\n if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null\n const url = `openpen-plugin://${context.pluginHostname}/${trimmed}`\n const fetchImpl = context.fetchImpl\n ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : undefined)\n if (!fetchImpl) return null\n\n try {\n const resp = await fetchImpl(url)\n if (!resp.ok) return null\n const raw = await resp.text()\n const sanitised = sanitizeSvgMarkup(raw)\n if (!sanitised) return null\n return { svgMarkup: sanitised, hotspot, fallbackCssKeyword }\n } catch {\n return null\n }\n }\n\n if ('png' in spec && typeof spec.png === 'string') {\n const hotspot = resolveHotspot(spec)\n const fallbackCssKeyword = resolveFallback(spec)\n const trimmed = spec.png.trim()\n if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null\n const url = escapeHtmlAttr(`openpen-plugin://${context.pluginHostname}/${trimmed}`)\n const svgMarkup =\n `<img src=\"${url}\" alt=\"\" style=\"display:block;max-width:64px;max-height:64px;\" />`\n return { svgMarkup, hotspot, fallbackCssKeyword }\n }\n\n return null\n}\n\nexport type CursorSanitizeRule =\n | 'R1' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'\n | 'R7' | 'R8' | 'R9' | 'R10' | 'R11' | 'R12'\n\nexport interface CursorSanitizeDiagnostic {\n /** Contribution id, or `'<unknown>'` if `id` was missing / invalid. */\n id: string\n rule: CursorSanitizeRule\n reason: string\n}\n\nexport interface CursorSanitizeOptions {\n /**\n * Built-in modules (host bundle) cannot use path forms because they\n * have no `openpen-plugin://<hostname>` namespace. R7 / R10 reject\n * such contributions for built-ins.\n */\n isBuiltin?: boolean\n}\n\nexport interface CursorSanitizeResult {\n cleaned: CursorContribution[]\n diagnostics: CursorSanitizeDiagnostic[]\n}\n\n/**\n * Shape-validate raw cursor contributions before they reach the\n * contribution-store. This is the R1–R12 gate; SVG markup content is\n * sanitised later inside {@link compileCursor} via DOMPurify.\n */\nexport function sanitizeCursorContributions(\n cursors: unknown,\n options: CursorSanitizeOptions = {},\n): CursorSanitizeResult {\n const cleaned: CursorContribution[] = []\n const diagnostics: CursorSanitizeDiagnostic[] = []\n\n if (!Array.isArray(cursors)) return { cleaned, diagnostics }\n\n for (const raw of cursors) {\n if (raw === null || typeof raw !== 'object') {\n diagnostics.push({ id: '<unknown>', rule: 'R1', reason: 'contribution is not an object' })\n continue\n }\n\n const entry = raw as { id?: unknown; cursor?: unknown }\n const id = typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : '<unknown>'\n\n if (id === '<unknown>') {\n diagnostics.push({ id, rule: 'R1', reason: 'missing or empty `id`' })\n continue\n }\n\n const { cursor } = entry\n\n if (typeof cursor === 'string') {\n const trimmedLower = cursor.trim().toLowerCase()\n if (FORBIDDEN_CSS_LOAD_PATTERN.test(trimmedLower)) {\n diagnostics.push({\n id,\n rule: 'R12',\n reason: 'string contains url() / image-set() / -webkit-image-set(); loads external resource',\n })\n continue\n }\n if (!SAFE_CURSOR_KEYWORDS.has(trimmedLower)) {\n diagnostics.push({\n id,\n rule: 'R2',\n reason: `string '${trimmedLower}' is not in the safe CSS cursor keyword whitelist`,\n })\n continue\n }\n cleaned.push({ id, cursor: trimmedLower })\n continue\n }\n\n if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor)) {\n diagnostics.push({ id, rule: 'R1', reason: 'cursor field is neither string nor plain object' })\n continue\n }\n\n const cursorObj = cursor as Record<string, unknown>\n const hasSvg = Object.prototype.hasOwnProperty.call(cursorObj, 'svg')\n const hasPng = Object.prototype.hasOwnProperty.call(cursorObj, 'png')\n\n if (!hasSvg && !hasPng) {\n diagnostics.push({ id, rule: 'R3', reason: 'cursor object has neither `svg` nor `png` key' })\n continue\n }\n if (hasSvg && hasPng) {\n diagnostics.push({ id, rule: 'R4', reason: 'cursor object has both `svg` and `png` keys (ambiguous)' })\n continue\n }\n\n if (hasSvg) {\n const svg = cursorObj.svg\n if (typeof svg !== 'string') {\n diagnostics.push({ id, rule: 'R5', reason: '`svg` value is not a string' })\n continue\n }\n const trimmed = svg.trim()\n const isInline = trimmed.startsWith('<')\n if (!isInline) {\n if (!isSafeRelativePath(trimmed)) {\n diagnostics.push({\n id,\n rule: 'R6',\n reason: '`svg` value is non-inline and not a safe relative path',\n })\n continue\n }\n if (options.isBuiltin) {\n diagnostics.push({\n id,\n rule: 'R7',\n reason: 'built-in modules cannot use path-form `svg` (no openpen-plugin hostname)',\n })\n continue\n }\n }\n }\n\n if (hasPng) {\n const png = cursorObj.png\n if (typeof png !== 'string') {\n diagnostics.push({ id, rule: 'R8', reason: '`png` value is not a string' })\n continue\n }\n if (!isSafeRelativePath(png.trim())) {\n diagnostics.push({\n id,\n rule: 'R9',\n reason: '`png` value is not a safe relative path',\n })\n continue\n }\n if (options.isBuiltin) {\n diagnostics.push({\n id,\n rule: 'R10',\n reason: 'built-in modules cannot use `png` (no openpen-plugin hostname)',\n })\n continue\n }\n }\n\n if (cursorObj.hotspot !== undefined) {\n const h = cursorObj.hotspot as Record<string, unknown> | null\n const valid =\n h !== null &&\n typeof h === 'object' &&\n typeof h.x === 'number' && Number.isFinite(h.x) && h.x >= 0 &&\n typeof h.y === 'number' && Number.isFinite(h.y) && h.y >= 0\n if (!valid) {\n diagnostics.push({\n id,\n rule: 'R11',\n reason: '`hotspot` must be `{ x: number ≥ 0, y: number ≥ 0 }`',\n })\n continue\n }\n }\n\n // Output a strictly-allowlisted clone — only known primitive fields\n // pass through. Anything else (function / symbol / getter / circular\n // reference attached as an extra key) is dropped here so the\n // downstream `structuredClone` in the contribution-store cannot\n // throw and trigger a module-wide rollback.\n const cleanedCursor: SvgCursorSpec | PngCursorSpec = hasSvg\n ? { svg: (cursorObj.svg as string).trim() }\n : { png: (cursorObj.png as string).trim() }\n if (cursorObj.hotspot !== undefined) {\n const h = cursorObj.hotspot as { x: number; y: number }\n cleanedCursor.hotspot = { x: h.x, y: h.y }\n }\n if (typeof cursorObj.fallback === 'string') {\n cleanedCursor.fallback = cursorObj.fallback\n }\n cleaned.push({ id, cursor: cleanedCursor })\n }\n\n return { cleaned, diagnostics }\n}\n"],"names":["h","s"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/validation.ts","../src/define-module.ts","../src/use-module-context.ts","../src/locale.ts","../src/host/tools.ts","../src/types/tool.ts","../src/cursors.ts"],"sourcesContent":["/**\n * Scoped module / plugin ID format: `@scope/name`\n *\n * Rules:\n * - Must start with `@`\n * - scope: lowercase ASCII alphanumeric + hyphen, starts with letter or digit,\n * max 39 chars (mirrors GitHub username/org limits)\n * - Single `/` separator\n * - name: same charset and length limit as scope\n *\n * The same regex is used by the OpenPen CLI (`openpen-cli`) and the\n * main process plugin loader, so plugin IDs round-trip safely between\n * `plugin.json`, filesystem paths under `~/.openpen/plugins/@scope/name/`,\n * and registry lookups.\n */\nexport const MODULE_ID_RE =\n /^@([a-z0-9][a-z0-9-]{0,38})\\/([a-z0-9][a-z0-9-]{0,38})$/\n\n/**\n * Returns `true` when `id` is a non-empty string that satisfies\n * {@link MODULE_ID_RE}; acts as a type guard narrowing `unknown` → `string`.\n *\n * @example\n * isValidModuleId('@openpen/freehand') // true\n * isValidModuleId('@alice/todo-app') // true\n * isValidModuleId('freehand') // false — must be scoped @scope/name\n * isValidModuleId('') // false — empty string rejected\n */\nexport function isValidModuleId(id: unknown): id is string {\n return typeof id === 'string' && id.length > 0 && MODULE_ID_RE.test(id)\n}\n","import { isValidModuleId } from './validation'\nimport { CONTRIBUTION_KEY_TO_SLOT_ID } from './slots'\nimport type { OpenPenModule } from './types/module'\n\n/**\n * Identity helper for declaring an OpenPen module with full type\n * inference and runtime sanity checks. Throws `Error` (preflight) when:\n *\n * - `id` is missing or has invalid characters\n * - `contributes` is missing entirely\n * - `contributes` has no contribution fields\n * - `contributes` references a slot key not in the catalogue\n *\n * This is intentionally a small surface — full validation\n * (id collisions across modules, slot quota, settingsSchema parse, etc.)\n * happens later in the host app's `module-validator`. This helper\n * catches the obvious developer mistakes at the module's own boundary\n * so errors surface in the module's repo, not deep inside OpenPen.\n */\nexport function defineModule<TModule extends OpenPenModule>(\n spec: TModule\n): TModule {\n if (!isValidModuleId(spec.id)) {\n throw new Error(\n `[@openpen/module-api] Invalid module id: ${JSON.stringify(spec.id)}. ` +\n `Module ids must use \\`@scope/name\\` format with lowercase ASCII alphanumeric + hyphen, ` +\n `max 39 chars per segment (e.g. \\`@alice/my-plugin\\`).`\n )\n }\n\n if (!spec.contributes || typeof spec.contributes !== 'object') {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" must declare a non-empty 'contributes' object.`\n )\n }\n\n const keys = Object.keys(spec.contributes)\n if (keys.length === 0) {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" must contribute to at least one slot. ` +\n `Add at least one field to 'contributes' (e.g. 'tools', 'settingsTabs', 'shortcuts').`\n )\n }\n\n for (const key of keys) {\n if (!(key in CONTRIBUTION_KEY_TO_SLOT_ID)) {\n throw new Error(\n `[@openpen/module-api] Module \"${spec.id}\" contributes to unknown slot key '${key}'. ` +\n `Known keys: ${Object.keys(CONTRIBUTION_KEY_TO_SLOT_ID).sort().join(', ')}.`\n )\n }\n }\n\n return spec\n}\n","import { getModuleContext } from './host/module-context-registry'\nimport type { ModuleSetupContext } from './types/module'\n\n/**\n * Returns the `ModuleSetupContext` that was constructed for `moduleId` by\n * the host runtime. Call this inside a Vue component's `<script setup>` (or\n * `setup()`) to access `ctx.getSettings`, `ctx.updateSettings`,\n * `ctx.onSettingsChange`, `ctx.notify`, and all other context methods from\n * within a component tree — without having to prop-drill or provide/inject\n * the context object manually.\n *\n * @throws If `moduleId` is not currently registered (module not yet loaded,\n * already rolled back, or the id string doesn't match `defineModule({ id })`).\n *\n * @example\n * ```ts\n * // MySettingsPanel.vue\n * import { useModuleContext, z } from '@openpen/module-api'\n * import { onMounted, onUnmounted, ref } from 'vue'\n *\n * const MySchema = z.object({ color: z.string().default('#fff') })\n * type MySettings = z.infer<typeof MySchema>\n *\n * const ctx = useModuleContext('my-plugin')\n * const color = ref(ctx.getSettings<MySettings>().color)\n *\n * let unsubscribe: (() => void) | null = null\n * onMounted(() => {\n * unsubscribe = ctx.onSettingsChange<MySettings>((s) => { color.value = s.color })\n * })\n * onUnmounted(() => unsubscribe?.())\n *\n * async function changeColor(next: string) {\n * await ctx.updateSettings<MySettings>({ color: next })\n * }\n * ```\n */\nexport function useModuleContext(moduleId: string): ModuleSetupContext {\n const ctx = getModuleContext(moduleId)\n if (!ctx) {\n throw new Error(\n `[module-api] useModuleContext(\"${moduleId}\") was called but no module is currently registered under that id. ` +\n `This usually means: (a) the moduleId string does not match the id passed to defineModule, or ` +\n `(b) the composable was called before the module's setup() ran ` +\n `(do not call useModuleContext at module top-level — call it inside a component's setup() or onMounted()).`\n )\n }\n return ctx\n}\n","/**\n * Convert a scoped module id to a vue-i18n-safe namespace key.\n *\n * vue-i18n uses `.` as path separator, so a raw id like `@openpen/freehand`\n * would be misinterpreted as a three-level path `['@openpen', 'freehand']`.\n * Stripping `@` and replacing `/` with `.` produces `openpen.freehand`,\n * which resolves correctly as a two-level namespace.\n *\n * @example\n * sanitizeIdForI18n('@openpen/freehand') // 'openpen.freehand'\n * sanitizeIdForI18n('@alice/todo') // 'alice.todo'\n */\nexport function sanitizeIdForI18n(moduleId: string): string {\n return moduleId.replace(/^@/, '').replace(/\\//g, '.')\n}\n\n/**\n * Maps a BCP-47 locale tag (e.g. `'en'`, `'zh-TW'`) to a localised string.\n *\n * Modules use this to declare i18n display names without coupling\n * themselves to the host app's i18n framework. The OpenPen registry\n * resolves the value at render time based on the active locale.\n */\nexport type LocaleMap = Record<string, string>\n\n/**\n * Resolve a `string | LocaleMap` to a single string for a given locale.\n *\n * Resolution order (first match wins):\n * 1. Plain string → returned as-is\n * 2. Exact tag match (e.g. `'zh-TW'`)\n * 3. Language prefix match (e.g. `'zh'` for `'zh-Hans-CN'`)\n * 4. English fallback (`'en'`)\n * 5. First declared value in the map\n * 6. Empty string (only when the map is `{}`)\n */\nexport function resolveLabel(\n input: string | LocaleMap,\n locale: string\n): string {\n if (typeof input === 'string') return input\n\n if (input[locale] !== undefined) return input[locale]\n\n const lang = locale.split('-')[0]\n if (lang !== locale && input[lang] !== undefined) return input[lang]\n\n if (input.en !== undefined) return input.en\n\n const first = Object.values(input)[0]\n return first ?? ''\n}\n","/**\n * @openpen/module-api — public tool activation\n *\n * Re-exported from the package's main entry (not from `./index.ts` of this\n * folder) because the plugin runtime import map only resolves the main entry\n * and `/uikit`.\n */\nimport { _useHost } from './registry'\n\n/**\n * Make `toolId` the active tool. The matching control bar button lights up\n * on every display and every canvas switches to the tool.\n *\n * `toolId` must be the `id` of a `canvas.tools` contribution (built-in or\n * from any loaded module). An unknown id is ignored with a console warning\n * and changes nothing.\n */\nexport function activateTool(toolId: string): void {\n _useHost().hostCommands.tools.activate(toolId)\n}\n","/**\n * Public Tool / Stroke / Point primitives shared by host code and\n * every module / plugin. Declared inside @openpen/module-api so\n * third parties don't need to import host internals to type their\n * `canvas.tools` contributions.\n */\n\nexport interface Point {\n x: number\n y: number\n}\n\n/** Solid color or linear gradient between two stops. */\nexport type StrokeColor =\n | string\n | { type: 'linear'; from: string; to: string }\n\n/**\n * Reduce a `StrokeColor` to a single CSS colour string.\n *\n * `CanvasRenderingContext2D.strokeStyle` accepts a single colour string but\n * not a structured gradient descriptor; custom `renderStroke` implementations\n * that don't draw the gradient natively need a representative fallback.\n * Returns the string as-is for solid colours, and the `from` stop for linear\n * gradients. Future gradient kinds can pick their own representative without\n * touching callers.\n */\nexport function resolveStrokeColor(color: StrokeColor): string {\n return typeof color === 'string' ? color : color.from\n}\n\nexport interface StrokeStyle {\n color: StrokeColor\n lineWidth: number\n lineCap: CanvasLineCap\n lineJoin: CanvasLineJoin\n}\n\nexport interface Stroke {\n id: string\n /** Id of the tool that produced this stroke (matches a ToolContribution.id). */\n tool: string\n points: Point[]\n style: StrokeStyle\n /**\n * Whole-stroke opacity in `0..1`; omitted means fully opaque.\n *\n * The host's default polyline renderer applies it as `globalAlpha` and clamps\n * values outside `0..1` back into range (non-finite values are ignored). A\n * tool that ships its own `renderStroke` owns the drawing and reads this field\n * itself — the host does not wrap custom renderers, so a plugin that already\n * stores and applies its own `opacity` key keeps rendering exactly as before.\n * A tool WITHOUT `renderStroke` that stores an `opacity` key now has it applied\n * by the default renderer; such a tool must store a number in `0..1`.\n */\n opacity?: number\n /**\n * Tool-specific extra state. Tools MAY store arbitrary keys here during\n * `onPointerDown` / `onPointerMove` / `onPointerUp`; those keys survive\n * into `renderStroke(stroke, canvasCtx)` calls so the renderer can read\n * them back.\n *\n * The host treats extra keys as `unknown` — type-cast at the read site is\n * the plugin author's responsibility (TypeScript cannot infer the shape).\n *\n * @example\n * // Store during onPointerUp:\n * const stroke: Stroke = { id, tool: 'my-tool', points, style, taper: 0.5 }\n *\n * // Read back in renderStroke:\n * const taper = (stroke.taper as number) ?? 1\n */\n [key: string]: unknown\n}\n\nexport interface PointerModifiers {\n shiftKey?: boolean\n}\n\nexport interface Tool {\n onPointerDown(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n style: StrokeStyle\n ): void\n /**\n * Returns `true` when the engine should perform a full redraw\n * (e.g. stroke-eraser removed an existing stroke). Otherwise `void`.\n */\n onPointerMove(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n modifiers?: PointerModifiers\n ): void | boolean\n /**\n * Returns the finished `Stroke` to push into the store, or `null`\n * to discard (e.g. an eraser that doesn't accumulate state).\n */\n onPointerUp(\n canvasCtx: CanvasRenderingContext2D,\n point: Point,\n modifiers?: PointerModifiers\n ): Stroke | null\n /** When true, canvas-engine clears + redraws all strokes before each move. */\n needsPreviewRedraw?: boolean\n renderPreview?(canvasCtx: CanvasRenderingContext2D): void\n}\n","/**\n * Cursor compilation + sanitisation helpers shared by host runtime and\n * plugin SDK.\n *\n * Three closure layers gate every cursor entry before it reaches\n * `v-html`: R1–R12 shape validation here, DOMPurify content sanitisation\n * inside compileCursor, and a deep-clone + freeze in the contribution\n * store. Removing any one of them re-opens an attack surface the others\n * cannot close.\n */\n\nimport DOMPurify from 'dompurify'\nimport type {\n CursorContribution,\n CursorSpec,\n Hotspot,\n PngCursorSpec,\n SvgCursorSpec,\n} from './types/contributions'\n\nexport interface CompiledCursor {\n /** Sanitised SVG / HTML fragment to mount as `innerHTML` on the cursor element. */\n svgMarkup: string\n /** Hotspot offset, in CSS pixels relative to the markup's top-left. */\n hotspot: Hotspot\n /** CSS keyword fallback retained for legacy callers. */\n fallbackCssKeyword: string\n}\n\nexport interface CursorResolutionContext {\n /**\n * Hostname-encoded plugin id (`@acme/x` → `acme.x`) used to assemble\n * `openpen-plugin://<hostname>/<path>` URLs for relative-path specs.\n * Undefined for built-in modules — path-form specs without a hostname\n * resolve to `null` (host falls back to its default cursor).\n */\n pluginHostname?: string\n /** Injection point for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * W3C CSS UI Level 3 cursor keywords. The legacy `cursor: string` form\n * MUST match one of these (case-insensitive after trim); everything else\n * is rejected by R2 / R12.\n */\nexport const SAFE_CURSOR_KEYWORDS: ReadonlySet<string> = new Set([\n 'auto', 'default', 'none',\n 'context-menu', 'help', 'pointer', 'progress', 'wait',\n 'cell', 'crosshair', 'text', 'vertical-text',\n 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 'grabbing',\n 'e-resize', 'n-resize', 'ne-resize', 'nw-resize',\n 's-resize', 'se-resize', 'sw-resize', 'w-resize',\n 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize',\n 'col-resize', 'row-resize', 'all-scroll',\n 'zoom-in', 'zoom-out',\n])\n\nconst FORBIDDEN_CSS_LOAD_PATTERN = /url\\s*\\(|image-set\\s*\\(|-webkit-image-set\\s*\\(/i\n\nconst DEFAULT_FALLBACK_KEYWORD = 'crosshair'\n\nexport function isSafeRelativePath(p: unknown): p is string {\n if (typeof p !== 'string' || p.length === 0) return false\n // Bare `:` catches scheme prefixes without `://` (e.g. `data:`).\n if (p.includes(':')) return false\n if (p.startsWith('/')) return false\n for (const seg of p.split('/')) {\n if (seg === '' || seg === '..') return false\n }\n return true\n}\n\nexport function pluginHostname(moduleId: string): string {\n return moduleId.replace(/^@/, '').replace(/\\//g, '.')\n}\n\nexport function pluginAssetUrl(moduleId: string, relativePath: string): string {\n if (!isSafeRelativePath(relativePath)) {\n throw new Error(`Unsafe asset path: ${relativePath}`)\n }\n return `openpen-plugin://${pluginHostname(moduleId)}/${relativePath}`\n}\n\n/**\n * Sanitise SVG markup before it reaches `v-html`. Rationale for each\n * flag: `USE_PROFILES: { svg: true }` covers script tags, on* handlers,\n * and javascript: hrefs; `FORBID_TAGS: ['foreignObject']` closes the\n * mXSS escape hatch the SVG profile alone does not; turning off\n * `ALLOW_UNKNOWN_PROTOCOLS` blocks `<use href=\"data:...\">` and friends.\n */\nexport function sanitizeSvgMarkup(raw: string): string {\n return DOMPurify.sanitize(raw, {\n USE_PROFILES: { svg: true },\n FORBID_TAGS: ['foreignObject'],\n // Cursor SVGs use presentation attributes (fill/stroke/etc.), never\n // inline `style`. Forbidding it strips legacy `expression()` CSS\n // smuggling vectors (S7) without touching anything we draw.\n FORBID_ATTR: ['style'],\n ALLOW_UNKNOWN_PROTOCOLS: false,\n })\n}\n\nfunction resolveHotspot(spec: SvgCursorSpec | PngCursorSpec): Hotspot {\n const h = spec.hotspot\n if (!h || typeof h !== 'object') return { x: 0, y: 0 }\n const x = typeof h.x === 'number' && Number.isFinite(h.x) ? h.x : 0\n const y = typeof h.y === 'number' && Number.isFinite(h.y) ? h.y : 0\n return { x, y }\n}\n\nfunction resolveFallback(spec: SvgCursorSpec | PngCursorSpec): string {\n return typeof spec.fallback === 'string' && spec.fallback.length > 0\n ? spec.fallback\n : DEFAULT_FALLBACK_KEYWORD\n}\n\nfunction escapeHtmlAttr(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/**\n * Convert a frozen, shape-validated CursorContribution.cursor into render\n * data ready for `v-html`. Returns `null` for legacy keyword strings\n * (caller falls back to the host default cursor) and for any spec the\n * sanitiser empties out completely.\n *\n * Async because path-form SVG specs are fetched through `openpen-plugin://`\n * at compile time; inline + PNG branches resolve synchronously inside the\n * returned Promise.\n */\nexport async function compileCursor(\n spec: CursorSpec,\n context: CursorResolutionContext = {},\n): Promise<CompiledCursor | null> {\n if (typeof spec === 'string') return null\n if (!spec || typeof spec !== 'object') return null\n\n if ('svg' in spec && typeof spec.svg === 'string') {\n const hotspot = resolveHotspot(spec)\n const fallbackCssKeyword = resolveFallback(spec)\n const trimmed = spec.svg.trim()\n\n if (trimmed.startsWith('<')) {\n const sanitised = sanitizeSvgMarkup(trimmed)\n if (!sanitised) return null\n return { svgMarkup: sanitised, hotspot, fallbackCssKeyword }\n }\n\n if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null\n const url = `openpen-plugin://${context.pluginHostname}/${trimmed}`\n const fetchImpl = context.fetchImpl\n ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : undefined)\n if (!fetchImpl) return null\n\n try {\n const resp = await fetchImpl(url)\n if (!resp.ok) return null\n const raw = await resp.text()\n const sanitised = sanitizeSvgMarkup(raw)\n if (!sanitised) return null\n return { svgMarkup: sanitised, hotspot, fallbackCssKeyword }\n } catch {\n return null\n }\n }\n\n if ('png' in spec && typeof spec.png === 'string') {\n const hotspot = resolveHotspot(spec)\n const fallbackCssKeyword = resolveFallback(spec)\n const trimmed = spec.png.trim()\n if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null\n const url = escapeHtmlAttr(`openpen-plugin://${context.pluginHostname}/${trimmed}`)\n const svgMarkup =\n `<img src=\"${url}\" alt=\"\" style=\"display:block;max-width:64px;max-height:64px;\" />`\n return { svgMarkup, hotspot, fallbackCssKeyword }\n }\n\n return null\n}\n\nexport type CursorSanitizeRule =\n | 'R1' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'\n | 'R7' | 'R8' | 'R9' | 'R10' | 'R11' | 'R12'\n\nexport interface CursorSanitizeDiagnostic {\n /** Contribution id, or `'<unknown>'` if `id` was missing / invalid. */\n id: string\n rule: CursorSanitizeRule\n reason: string\n}\n\nexport interface CursorSanitizeOptions {\n /**\n * Built-in modules (host bundle) cannot use path forms because they\n * have no `openpen-plugin://<hostname>` namespace. R7 / R10 reject\n * such contributions for built-ins.\n */\n isBuiltin?: boolean\n}\n\nexport interface CursorSanitizeResult {\n cleaned: CursorContribution[]\n diagnostics: CursorSanitizeDiagnostic[]\n}\n\n/**\n * Shape-validate raw cursor contributions before they reach the\n * contribution-store. This is the R1–R12 gate; SVG markup content is\n * sanitised later inside {@link compileCursor} via DOMPurify.\n */\nexport function sanitizeCursorContributions(\n cursors: unknown,\n options: CursorSanitizeOptions = {},\n): CursorSanitizeResult {\n const cleaned: CursorContribution[] = []\n const diagnostics: CursorSanitizeDiagnostic[] = []\n\n if (!Array.isArray(cursors)) return { cleaned, diagnostics }\n\n for (const raw of cursors) {\n if (raw === null || typeof raw !== 'object') {\n diagnostics.push({ id: '<unknown>', rule: 'R1', reason: 'contribution is not an object' })\n continue\n }\n\n const entry = raw as { id?: unknown; cursor?: unknown }\n const id = typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : '<unknown>'\n\n if (id === '<unknown>') {\n diagnostics.push({ id, rule: 'R1', reason: 'missing or empty `id`' })\n continue\n }\n\n const { cursor } = entry\n\n if (typeof cursor === 'string') {\n const trimmedLower = cursor.trim().toLowerCase()\n if (FORBIDDEN_CSS_LOAD_PATTERN.test(trimmedLower)) {\n diagnostics.push({\n id,\n rule: 'R12',\n reason: 'string contains url() / image-set() / -webkit-image-set(); loads external resource',\n })\n continue\n }\n if (!SAFE_CURSOR_KEYWORDS.has(trimmedLower)) {\n diagnostics.push({\n id,\n rule: 'R2',\n reason: `string '${trimmedLower}' is not in the safe CSS cursor keyword whitelist`,\n })\n continue\n }\n cleaned.push({ id, cursor: trimmedLower })\n continue\n }\n\n if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor)) {\n diagnostics.push({ id, rule: 'R1', reason: 'cursor field is neither string nor plain object' })\n continue\n }\n\n const cursorObj = cursor as Record<string, unknown>\n const hasSvg = Object.prototype.hasOwnProperty.call(cursorObj, 'svg')\n const hasPng = Object.prototype.hasOwnProperty.call(cursorObj, 'png')\n\n if (!hasSvg && !hasPng) {\n diagnostics.push({ id, rule: 'R3', reason: 'cursor object has neither `svg` nor `png` key' })\n continue\n }\n if (hasSvg && hasPng) {\n diagnostics.push({ id, rule: 'R4', reason: 'cursor object has both `svg` and `png` keys (ambiguous)' })\n continue\n }\n\n if (hasSvg) {\n const svg = cursorObj.svg\n if (typeof svg !== 'string') {\n diagnostics.push({ id, rule: 'R5', reason: '`svg` value is not a string' })\n continue\n }\n const trimmed = svg.trim()\n const isInline = trimmed.startsWith('<')\n if (!isInline) {\n if (!isSafeRelativePath(trimmed)) {\n diagnostics.push({\n id,\n rule: 'R6',\n reason: '`svg` value is non-inline and not a safe relative path',\n })\n continue\n }\n if (options.isBuiltin) {\n diagnostics.push({\n id,\n rule: 'R7',\n reason: 'built-in modules cannot use path-form `svg` (no openpen-plugin hostname)',\n })\n continue\n }\n }\n }\n\n if (hasPng) {\n const png = cursorObj.png\n if (typeof png !== 'string') {\n diagnostics.push({ id, rule: 'R8', reason: '`png` value is not a string' })\n continue\n }\n if (!isSafeRelativePath(png.trim())) {\n diagnostics.push({\n id,\n rule: 'R9',\n reason: '`png` value is not a safe relative path',\n })\n continue\n }\n if (options.isBuiltin) {\n diagnostics.push({\n id,\n rule: 'R10',\n reason: 'built-in modules cannot use `png` (no openpen-plugin hostname)',\n })\n continue\n }\n }\n\n if (cursorObj.hotspot !== undefined) {\n const h = cursorObj.hotspot as Record<string, unknown> | null\n const valid =\n h !== null &&\n typeof h === 'object' &&\n typeof h.x === 'number' && Number.isFinite(h.x) && h.x >= 0 &&\n typeof h.y === 'number' && Number.isFinite(h.y) && h.y >= 0\n if (!valid) {\n diagnostics.push({\n id,\n rule: 'R11',\n reason: '`hotspot` must be `{ x: number ≥ 0, y: number ≥ 0 }`',\n })\n continue\n }\n }\n\n // Output a strictly-allowlisted clone — only known primitive fields\n // pass through. Anything else (function / symbol / getter / circular\n // reference attached as an extra key) is dropped here so the\n // downstream `structuredClone` in the contribution-store cannot\n // throw and trigger a module-wide rollback.\n const cleanedCursor: SvgCursorSpec | PngCursorSpec = hasSvg\n ? { svg: (cursorObj.svg as string).trim() }\n : { png: (cursorObj.png as string).trim() }\n if (cursorObj.hotspot !== undefined) {\n const h = cursorObj.hotspot as { x: number; y: number }\n cleanedCursor.hotspot = { x: h.x, y: h.y }\n }\n if (typeof cursorObj.fallback === 'string') {\n cleanedCursor.fallback = cursorObj.fallback\n }\n cleaned.push({ id, cursor: cleanedCursor })\n }\n\n return { cleaned, diagnostics }\n}\n"],"names":["h","s"],"mappings":"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"}
|
|
@@ -54,7 +54,8 @@ export interface ToolContribution extends Tool {
|
|
|
54
54
|
id: string;
|
|
55
55
|
label?: string | LocaleMap;
|
|
56
56
|
/**
|
|
57
|
-
* SVG markup string for this tool's icon
|
|
57
|
+
* SVG markup string for this tool's icon, read by host components that
|
|
58
|
+
* render tool pickers (for example the shape tool's variant list).
|
|
58
59
|
*
|
|
59
60
|
* The host renders tool icons via `v-html`, so only raw SVG strings are
|
|
60
61
|
* accepted (not Vue components or CSS class names). Recommended size is
|
|
@@ -64,9 +65,10 @@ export interface ToolContribution extends Tool {
|
|
|
64
65
|
* @example
|
|
65
66
|
* icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17c3-3 6-6 9-3s6 0 9-3"/></svg>'
|
|
66
67
|
*
|
|
67
|
-
* Note:
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* Note: declaring `icon` does not add a control-bar button by itself. To
|
|
69
|
+
* show a button for a plugin tool, contribute one through `controlBar`
|
|
70
|
+
* (the `ui.control-bar` slot); see the `activateTool` example in the
|
|
71
|
+
* canvas slot docs.
|
|
70
72
|
*/
|
|
71
73
|
icon?: string;
|
|
72
74
|
/**
|
|
@@ -213,7 +215,7 @@ export interface ShortcutContribution {
|
|
|
213
215
|
label?: string | LocaleMap;
|
|
214
216
|
/** Optional secondary description shown below the label in the shortcuts settings panel. */
|
|
215
217
|
sublabel?: string | LocaleMap;
|
|
216
|
-
/** When true, the user can rebind this key. The chosen key is persisted under `config.json →
|
|
218
|
+
/** When true, the user can rebind this key. The chosen key is persisted under `config.json → moduleShortcuts["<moduleId>/<shortcutId>"]`. */
|
|
217
219
|
userCustomizable?: boolean;
|
|
218
220
|
}
|
|
219
221
|
export interface WindowBehaviorContribution {
|