@openpen/module-api 1.3.0 → 1.5.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 +32 -0
- package/README.md +7 -1
- package/dist/host/tools.d.ts +9 -0
- package/dist/host/types.d.ts +7 -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/AppToggleButton.vue.d.ts +28 -0
- package/dist/uikit/index.d.ts +9 -3
- package/dist/uikit/index.js +327 -104
- package/dist/uikit/index.js.map +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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/openpen-platform/openpen/compare/module-api@1.4.0...module-api@1.5.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
|
+
* **uikit:** add AppSwatchPicker and AppToggleButton ([d6c4bf8](https://github.com/openpen-platform/openpen/commit/d6c4bf8a7ffefeb4c2d39aeb3a52084819b03a4e))
|
|
13
|
+
* **uikit:** align popover rows, add slider bubble, restyle segmented ([98c1b6f](https://github.com/openpen-platform/openpen/commit/98c1b6f0f75e140da31386ef87f376240da28611))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **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))
|
|
19
|
+
|
|
20
|
+
## [1.4.0](https://github.com/openpen-platform/openpen/compare/module-api@1.3.0...module-api@1.4.0) (2026-11-14)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **drawing:** add semi-transparent highlighter tool ([#123](https://github.com/openpen-platform/openpen/issues/123)) ([68b7e50](https://github.com/openpen-platform/openpen/commit/68b7e50e0d15df482f3d153610177efb49317728))
|
|
26
|
+
* **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))
|
|
27
|
+
* **module-api:** upgrade zod to 4 ([c25ffab](https://github.com/openpen-platform/openpen/commit/c25ffab8ade237df24e60b5c22f4c61559935a1b))
|
|
28
|
+
* **uikit:** add AppPopoverPanel and AppPopoverRow for plugin popover content ([5879b2c](https://github.com/openpen-platform/openpen/commit/5879b2ccf993dd81ef527fdf4fa46b0768bd2f63))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* **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))
|
|
34
|
+
|
|
3
35
|
## [1.3.0](https://github.com/openpen-platform/openpen/compare/module-api@1.2.0...module-api@1.3.0) (2026-06-11)
|
|
4
36
|
|
|
5
37
|
|
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:
|
|
@@ -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 {
|
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 {
|
|
@@ -3,166 +3,37 @@ import type { Component } from 'vue';
|
|
|
3
3
|
import type { LocaleMap } from '../locale';
|
|
4
4
|
export declare const GroupInsetSchema: z.ZodObject<{
|
|
5
5
|
enabled: z.ZodBoolean;
|
|
6
|
-
/** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */
|
|
7
6
|
color: z.ZodOptional<z.ZodString>;
|
|
8
|
-
},
|
|
9
|
-
enabled: boolean;
|
|
10
|
-
color?: string | undefined;
|
|
11
|
-
}, {
|
|
12
|
-
enabled: boolean;
|
|
13
|
-
color?: string | undefined;
|
|
14
|
-
}>;
|
|
7
|
+
}, z.core.$strip>;
|
|
15
8
|
export declare const LayoutGroupSchema: z.ZodObject<{
|
|
16
9
|
id: z.ZodString;
|
|
17
|
-
items: z.ZodArray<z.ZodString
|
|
18
|
-
separator: z.ZodOptional<z.ZodEnum<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* outer height matches an unwrapped 36px button — i.e. enabling inset
|
|
24
|
-
* MUST NOT increase control-bar height.
|
|
25
|
-
*/
|
|
10
|
+
items: z.ZodArray<z.ZodString>;
|
|
11
|
+
separator: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
never: "never";
|
|
13
|
+
auto: "auto";
|
|
14
|
+
always: "always";
|
|
15
|
+
}>>;
|
|
26
16
|
inset: z.ZodOptional<z.ZodObject<{
|
|
27
17
|
enabled: z.ZodBoolean;
|
|
28
|
-
/** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */
|
|
29
18
|
color: z.ZodOptional<z.ZodString>;
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
color?: string | undefined;
|
|
33
|
-
}, {
|
|
34
|
-
enabled: boolean;
|
|
35
|
-
color?: string | undefined;
|
|
36
|
-
}>>;
|
|
37
|
-
}, "strip", z.ZodTypeAny, {
|
|
38
|
-
id: string;
|
|
39
|
-
items: string[];
|
|
40
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
41
|
-
inset?: {
|
|
42
|
-
enabled: boolean;
|
|
43
|
-
color?: string | undefined;
|
|
44
|
-
} | undefined;
|
|
45
|
-
}, {
|
|
46
|
-
id: string;
|
|
47
|
-
items: string[];
|
|
48
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
49
|
-
inset?: {
|
|
50
|
-
enabled: boolean;
|
|
51
|
-
color?: string | undefined;
|
|
52
|
-
} | undefined;
|
|
53
|
-
}>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
54
21
|
export declare const ControlBarLayoutSchema: z.ZodObject<{
|
|
55
22
|
version: z.ZodLiteral<1>;
|
|
56
|
-
groups: z.
|
|
23
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
57
24
|
id: z.ZodString;
|
|
58
|
-
items: z.ZodArray<z.ZodString
|
|
59
|
-
separator: z.ZodOptional<z.ZodEnum<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* outer height matches an unwrapped 36px button — i.e. enabling inset
|
|
65
|
-
* MUST NOT increase control-bar height.
|
|
66
|
-
*/
|
|
25
|
+
items: z.ZodArray<z.ZodString>;
|
|
26
|
+
separator: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
never: "never";
|
|
28
|
+
auto: "auto";
|
|
29
|
+
always: "always";
|
|
30
|
+
}>>;
|
|
67
31
|
inset: z.ZodOptional<z.ZodObject<{
|
|
68
32
|
enabled: z.ZodBoolean;
|
|
69
|
-
/** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */
|
|
70
33
|
color: z.ZodOptional<z.ZodString>;
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}, {
|
|
75
|
-
enabled: boolean;
|
|
76
|
-
color?: string | undefined;
|
|
77
|
-
}>>;
|
|
78
|
-
}, "strip", z.ZodTypeAny, {
|
|
79
|
-
id: string;
|
|
80
|
-
items: string[];
|
|
81
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
82
|
-
inset?: {
|
|
83
|
-
enabled: boolean;
|
|
84
|
-
color?: string | undefined;
|
|
85
|
-
} | undefined;
|
|
86
|
-
}, {
|
|
87
|
-
id: string;
|
|
88
|
-
items: string[];
|
|
89
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
90
|
-
inset?: {
|
|
91
|
-
enabled: boolean;
|
|
92
|
-
color?: string | undefined;
|
|
93
|
-
} | undefined;
|
|
94
|
-
}>, "many">, {
|
|
95
|
-
id: string;
|
|
96
|
-
items: string[];
|
|
97
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
98
|
-
inset?: {
|
|
99
|
-
enabled: boolean;
|
|
100
|
-
color?: string | undefined;
|
|
101
|
-
} | undefined;
|
|
102
|
-
}[], {
|
|
103
|
-
id: string;
|
|
104
|
-
items: string[];
|
|
105
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
106
|
-
inset?: {
|
|
107
|
-
enabled: boolean;
|
|
108
|
-
color?: string | undefined;
|
|
109
|
-
} | undefined;
|
|
110
|
-
}[]>, {
|
|
111
|
-
id: string;
|
|
112
|
-
items: string[];
|
|
113
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
114
|
-
inset?: {
|
|
115
|
-
enabled: boolean;
|
|
116
|
-
color?: string | undefined;
|
|
117
|
-
} | undefined;
|
|
118
|
-
}[], {
|
|
119
|
-
id: string;
|
|
120
|
-
items: string[];
|
|
121
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
122
|
-
inset?: {
|
|
123
|
-
enabled: boolean;
|
|
124
|
-
color?: string | undefined;
|
|
125
|
-
} | undefined;
|
|
126
|
-
}[]>, {
|
|
127
|
-
id: string;
|
|
128
|
-
items: string[];
|
|
129
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
130
|
-
inset?: {
|
|
131
|
-
enabled: boolean;
|
|
132
|
-
color?: string | undefined;
|
|
133
|
-
} | undefined;
|
|
134
|
-
}[], {
|
|
135
|
-
id: string;
|
|
136
|
-
items: string[];
|
|
137
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
138
|
-
inset?: {
|
|
139
|
-
enabled: boolean;
|
|
140
|
-
color?: string | undefined;
|
|
141
|
-
} | undefined;
|
|
142
|
-
}[]>;
|
|
143
|
-
}, "strip", z.ZodTypeAny, {
|
|
144
|
-
version: 1;
|
|
145
|
-
groups: {
|
|
146
|
-
id: string;
|
|
147
|
-
items: string[];
|
|
148
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
149
|
-
inset?: {
|
|
150
|
-
enabled: boolean;
|
|
151
|
-
color?: string | undefined;
|
|
152
|
-
} | undefined;
|
|
153
|
-
}[];
|
|
154
|
-
}, {
|
|
155
|
-
version: 1;
|
|
156
|
-
groups: {
|
|
157
|
-
id: string;
|
|
158
|
-
items: string[];
|
|
159
|
-
separator?: "never" | "auto" | "always" | undefined;
|
|
160
|
-
inset?: {
|
|
161
|
-
enabled: boolean;
|
|
162
|
-
color?: string | undefined;
|
|
163
|
-
} | undefined;
|
|
164
|
-
}[];
|
|
165
|
-
}>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
}, z.core.$strip>;
|
|
166
37
|
export interface GroupInset {
|
|
167
38
|
enabled: boolean;
|
|
168
39
|
/** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */
|
package/dist/types/module.d.ts
CHANGED
|
@@ -64,6 +64,11 @@ export interface OpenPenModule {
|
|
|
64
64
|
* made via `ctx.updateSettings()`; writes that fail validation reject
|
|
65
65
|
* with the underlying `ZodError`.
|
|
66
66
|
*
|
|
67
|
+
* Nested object fields that need their inner defaults filled from an
|
|
68
|
+
* empty or invalid stored value MUST use `.prefault({})`, not
|
|
69
|
+
* `.default({})`: the host rebuilds invalid settings from `{}`, and
|
|
70
|
+
* `.default()` returns its value without parsing it.
|
|
71
|
+
*
|
|
67
72
|
* Modules that intend to call `ctx.updateSettings()` MUST declare
|
|
68
73
|
* this field; calling `updateSettings` without a schema throws at
|
|
69
74
|
* runtime so the missing contract surfaces during development.
|
|
@@ -130,7 +135,8 @@ export interface ModuleSetupContext {
|
|
|
130
135
|
* result against `settingsSchema` (Zod), and persists to `config.json`.
|
|
131
136
|
* Resolves once the write is durable; rejects with the underlying
|
|
132
137
|
* `ZodError` if the merged object fails validation (no I/O happens
|
|
133
|
-
* on validation failure).
|
|
138
|
+
* on validation failure). Read the failures from `error.issues`;
|
|
139
|
+
* `ZodError` has no `.errors` property.
|
|
134
140
|
*
|
|
135
141
|
* The in-memory snapshot returned by `getSettings()` is updated
|
|
136
142
|
* before the Promise resolves; subscribers registered via
|
package/dist/types/tool.d.ts
CHANGED
|
@@ -37,6 +37,18 @@ export interface Stroke {
|
|
|
37
37
|
tool: string;
|
|
38
38
|
points: Point[];
|
|
39
39
|
style: StrokeStyle;
|
|
40
|
+
/**
|
|
41
|
+
* Whole-stroke opacity in `0..1`; omitted means fully opaque.
|
|
42
|
+
*
|
|
43
|
+
* The host's default polyline renderer applies it as `globalAlpha` and clamps
|
|
44
|
+
* values outside `0..1` back into range (non-finite values are ignored). A
|
|
45
|
+
* tool that ships its own `renderStroke` owns the drawing and reads this field
|
|
46
|
+
* itself — the host does not wrap custom renderers, so a plugin that already
|
|
47
|
+
* stores and applies its own `opacity` key keeps rendering exactly as before.
|
|
48
|
+
* A tool WITHOUT `renderStroke` that stores an `opacity` key now has it applied
|
|
49
|
+
* by the default renderer; such a tool must store a number in `0..1`.
|
|
50
|
+
*/
|
|
51
|
+
opacity?: number;
|
|
40
52
|
/**
|
|
41
53
|
* Tool-specific extra state. Tools MAY store arbitrary keys here during
|
|
42
54
|
* `onPointerDown` / `onPointerMove` / `onPointerUp`; those keys survive
|
|
@@ -48,11 +60,10 @@ export interface Stroke {
|
|
|
48
60
|
*
|
|
49
61
|
* @example
|
|
50
62
|
* // Store during onPointerUp:
|
|
51
|
-
* const stroke: Stroke = { id, tool: 'my-tool', points, style,
|
|
63
|
+
* const stroke: Stroke = { id, tool: 'my-tool', points, style, taper: 0.5 }
|
|
52
64
|
*
|
|
53
65
|
* // Read back in renderStroke:
|
|
54
|
-
* const
|
|
55
|
-
* canvasCtx.globalAlpha = opacity
|
|
66
|
+
* const taper = (stroke.taper as number) ?? 1
|
|
56
67
|
*/
|
|
57
68
|
[key: string]: unknown;
|
|
58
69
|
}
|
|
@@ -35,7 +35,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
35
35
|
disabled: boolean;
|
|
36
36
|
active: boolean;
|
|
37
37
|
variant: "default" | "danger";
|
|
38
|
-
popoverPlacement: "auto" | "
|
|
38
|
+
popoverPlacement: "auto" | "left" | "right" | "top" | "bottom";
|
|
39
39
|
mainTooltip: string;
|
|
40
40
|
mainAriaLabel: string;
|
|
41
41
|
caretAriaLabel: string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppPopoverPanel - High-level wrapper (Layer 1).
|
|
3
|
+
*
|
|
4
|
+
* Inner layout for the `content` slot of AppPopover: a vertical stack with the
|
|
5
|
+
* same 10px spacing the host's own popup panels use, plus an optional title.
|
|
6
|
+
* Rows share three columns (label, control, value) so controls start at the
|
|
7
|
+
* same x in every row without any plugin-side CSS.
|
|
8
|
+
* The frame (background, border, radius, padding, shadow) comes from AppPopover.
|
|
9
|
+
*
|
|
10
|
+
* Styles use --openpen-* design tokens only.
|
|
11
|
+
*/
|
|
12
|
+
type __VLS_Props = {
|
|
13
|
+
/** Optional small-caps heading rendered above the panel content. */
|
|
14
|
+
title?: string;
|
|
15
|
+
};
|
|
16
|
+
declare var __VLS_1: {};
|
|
17
|
+
type __VLS_Slots = {} & {
|
|
18
|
+
default?: (props: typeof __VLS_1) => any;
|
|
19
|
+
};
|
|
20
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
24
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
25
|
+
new (): {
|
|
26
|
+
$slots: S;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppPopoverRow - High-level wrapper (Layer 1).
|
|
3
|
+
*
|
|
4
|
+
* One form row inside an AppPopoverPanel: an optional label followed by a
|
|
5
|
+
* control (toggle, slider, segmented control, ...) in the default slot, and an
|
|
6
|
+
* optional third element (value text such as "150px") that is right-aligned in
|
|
7
|
+
* the panel's shared value column.
|
|
8
|
+
* Spacing matches the host's own popup rows (10px gap, vertically centred).
|
|
9
|
+
*/
|
|
10
|
+
type __VLS_Props = {
|
|
11
|
+
/** Optional label rendered before the control. */
|
|
12
|
+
label?: string;
|
|
13
|
+
};
|
|
14
|
+
declare var __VLS_1: {};
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
default?: (props: typeof __VLS_1) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|