@openpen/module-api 1.1.1 → 1.2.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js +245 -0
  3. package/dist/_chunks/AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map +1 -0
  4. package/dist/_chunks/inject-keys-CRXCpN-M.js +196 -0
  5. package/dist/_chunks/inject-keys-CRXCpN-M.js.map +1 -0
  6. package/dist/_chunks/module-context-registry-Cn1IWYu9.js +16 -0
  7. package/dist/_chunks/module-context-registry-Cn1IWYu9.js.map +1 -0
  8. package/dist/_chunks/passthrough-DKU7JYuQ.js +6 -0
  9. package/dist/_chunks/passthrough-DKU7JYuQ.js.map +1 -0
  10. package/dist/cursors.d.ts +83 -0
  11. package/dist/define-module.d.ts +17 -0
  12. package/dist/host/color-utils.d.ts +11 -0
  13. package/{src/host/event-bus.ts → dist/host/event-bus.d.ts} +7 -5
  14. package/dist/host/host-commands.d.ts +1 -0
  15. package/dist/host/index.d.ts +24 -0
  16. package/dist/host/index.js +49 -0
  17. package/dist/host/index.js.map +1 -0
  18. package/{src/host/module-context-registry.ts → dist/host/module-context-registry.d.ts} +5 -19
  19. package/dist/host/passthrough.d.ts +2 -0
  20. package/dist/host/popup-anchor.d.ts +3 -0
  21. package/dist/host/registry.d.ts +39 -0
  22. package/dist/host/registry.js +37 -0
  23. package/dist/host/registry.js.map +1 -0
  24. package/dist/host/slot-entries.d.ts +2 -0
  25. package/dist/host/stroke-store.d.ts +5 -0
  26. package/dist/host/stroke-style.d.ts +3 -0
  27. package/dist/host/types.d.ts +164 -0
  28. package/dist/index.d.ts +28 -0
  29. package/dist/index.js +349 -0
  30. package/dist/index.js.map +1 -0
  31. package/{src/inject-keys.ts → dist/inject-keys.d.ts} +37 -66
  32. package/{src/locale.ts → dist/locale.d.ts} +3 -22
  33. package/dist/slots.d.ts +57 -0
  34. package/dist/types/contributions.d.ts +288 -0
  35. package/dist/types/control-bar-layout.d.ts +227 -0
  36. package/dist/types/module.d.ts +208 -0
  37. package/{src/types/notification.ts → dist/types/notification.d.ts} +12 -14
  38. package/dist/types/tool.d.ts +77 -0
  39. package/dist/uikit/components/AppBanner.vue.d.ts +34 -0
  40. package/dist/uikit/components/AppButton.vue.d.ts +30 -0
  41. package/dist/uikit/components/AppButtonDropdown.vue.d.ts +52 -0
  42. package/dist/uikit/components/AppDialog.vue.d.ts +53 -0
  43. package/dist/uikit/components/AppPopover.vue.d.ts +43 -0
  44. package/dist/uikit/components/AppSegmented.vue.d.ts +19 -0
  45. package/dist/uikit/components/AppSelect.vue.d.ts +26 -0
  46. package/dist/uikit/components/AppSlider.vue.d.ts +43 -0
  47. package/dist/uikit/components/AppTabs.vue.d.ts +35 -0
  48. package/dist/uikit/components/AppToggle.vue.d.ts +13 -0
  49. package/dist/uikit/components/AppTooltip.vue.d.ts +27 -0
  50. package/dist/uikit/components/DialogHost.vue.d.ts +3 -0
  51. package/{src/uikit/composables/dialog-internal-keys.ts → dist/uikit/composables/dialog-internal-keys.d.ts} +3 -5
  52. package/dist/uikit/composables/useDialog.d.ts +399 -0
  53. package/dist/uikit/composables/useDialogPluginComponent.d.ts +9 -0
  54. package/dist/uikit/index.d.ts +42 -0
  55. package/dist/uikit/index.js +884 -0
  56. package/dist/uikit/index.js.map +1 -0
  57. package/{src/uikit/internal.ts → dist/uikit/internal.d.ts} +1 -1
  58. package/dist/uikit/internal.js +157 -0
  59. package/dist/uikit/internal.js.map +1 -0
  60. package/dist/uikit/module-api.css +851 -0
  61. package/dist/uikit/primitives.d.ts +42 -0
  62. package/{src/use-module-context.ts → dist/use-module-context.d.ts} +2 -15
  63. package/{src/validation.ts → dist/validation.d.ts} +2 -6
  64. package/package.json +26 -12
  65. package/src/cursors.ts +0 -369
  66. package/src/define-module.ts +0 -55
  67. package/src/host/color-utils.ts +0 -18
  68. package/src/host/host-commands.ts +0 -9
  69. package/src/host/index.ts +0 -53
  70. package/src/host/passthrough.ts +0 -10
  71. package/src/host/popup-anchor.ts +0 -14
  72. package/src/host/slot-entries.ts +0 -9
  73. package/src/host/stroke-store.ts +0 -20
  74. package/src/host/stroke-style.ts +0 -15
  75. package/src/index.ts +0 -131
  76. package/src/slots.ts +0 -135
  77. package/src/types/contributions.ts +0 -333
  78. package/src/types/control-bar-layout.ts +0 -174
  79. package/src/types/module.ts +0 -225
  80. package/src/types/tool.ts +0 -82
  81. package/src/uikit/components/.gitkeep +0 -0
  82. package/src/uikit/components/AppBanner.vue +0 -131
  83. package/src/uikit/components/AppButton.vue +0 -135
  84. package/src/uikit/components/AppDialog.vue +0 -393
  85. package/src/uikit/components/AppPopover.vue +0 -268
  86. package/src/uikit/components/AppSegmented.vue +0 -128
  87. package/src/uikit/components/AppSelect.vue +0 -235
  88. package/src/uikit/components/AppSlider.vue +0 -204
  89. package/src/uikit/components/AppTabs.vue +0 -163
  90. package/src/uikit/components/AppToggle.vue +0 -96
  91. package/src/uikit/components/AppTooltip.vue +0 -118
  92. package/src/uikit/components/DialogHost.vue +0 -272
  93. package/src/uikit/composables/useDialog.ts +0 -141
  94. package/src/uikit/composables/useDialogPluginComponent.ts +0 -27
  95. package/src/uikit/index.ts +0 -79
  96. package/src/uikit/primitives.ts +0 -150
  97. /package/{src → dist}/uikit/tokens.css +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,349 @@
1
+ import { C as CONTRIBUTION_KEY_TO_SLOT_ID } from "./_chunks/inject-keys-CRXCpN-M.js";
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
+ import { g as getModuleContext } from "./_chunks/module-context-registry-Cn1IWYu9.js";
4
+ import { c as c2, s } from "./_chunks/module-context-registry-Cn1IWYu9.js";
5
+ import DOMPurify from "dompurify";
6
+ import { z } from "zod";
7
+ const MODULE_ID_RE = /^@([a-z0-9][a-z0-9-]{0,38})\/([a-z0-9][a-z0-9-]{0,38})$/;
8
+ function isValidModuleId(id) {
9
+ return typeof id === "string" && id.length > 0 && MODULE_ID_RE.test(id);
10
+ }
11
+ function defineModule(spec) {
12
+ if (!isValidModuleId(spec.id)) {
13
+ throw new Error(
14
+ `[@openpen/module-api] Invalid module id: ${JSON.stringify(spec.id)}. Module ids must use \`@scope/name\` format with lowercase ASCII alphanumeric + hyphen, max 39 chars per segment (e.g. \`@alice/my-plugin\`).`
15
+ );
16
+ }
17
+ if (!spec.contributes || typeof spec.contributes !== "object") {
18
+ throw new Error(
19
+ `[@openpen/module-api] Module "${spec.id}" must declare a non-empty 'contributes' object.`
20
+ );
21
+ }
22
+ const keys = Object.keys(spec.contributes);
23
+ if (keys.length === 0) {
24
+ throw new Error(
25
+ `[@openpen/module-api] Module "${spec.id}" must contribute to at least one slot. Add at least one field to 'contributes' (e.g. 'tools', 'settingsTabs', 'shortcuts').`
26
+ );
27
+ }
28
+ for (const key of keys) {
29
+ if (!(key in CONTRIBUTION_KEY_TO_SLOT_ID)) {
30
+ throw new Error(
31
+ `[@openpen/module-api] Module "${spec.id}" contributes to unknown slot key '${key}'. Known keys: ${Object.keys(CONTRIBUTION_KEY_TO_SLOT_ID).sort().join(", ")}.`
32
+ );
33
+ }
34
+ }
35
+ return spec;
36
+ }
37
+ function useModuleContext(moduleId) {
38
+ const ctx = getModuleContext(moduleId);
39
+ if (!ctx) {
40
+ throw new Error(
41
+ `[module-api] useModuleContext("${moduleId}") was called but no module is currently registered under that id. This usually means: (a) the moduleId string does not match the id passed to defineModule, or (b) the composable was called before the module's setup() ran (do not call useModuleContext at module top-level — call it inside a component's setup() or onMounted()).`
42
+ );
43
+ }
44
+ return ctx;
45
+ }
46
+ function sanitizeIdForI18n(moduleId) {
47
+ return moduleId.replace(/^@/, "").replace(/\//g, ".");
48
+ }
49
+ function resolveLabel(input, locale) {
50
+ if (typeof input === "string") return input;
51
+ if (input[locale] !== void 0) return input[locale];
52
+ const lang = locale.split("-")[0];
53
+ if (lang !== locale && input[lang] !== void 0) return input[lang];
54
+ if (input.en !== void 0) return input.en;
55
+ const first = Object.values(input)[0];
56
+ return first ?? "";
57
+ }
58
+ function resolveStrokeColor(color) {
59
+ return typeof color === "string" ? color : color.from;
60
+ }
61
+ const SAFE_CURSOR_KEYWORDS = /* @__PURE__ */ new Set([
62
+ "auto",
63
+ "default",
64
+ "none",
65
+ "context-menu",
66
+ "help",
67
+ "pointer",
68
+ "progress",
69
+ "wait",
70
+ "cell",
71
+ "crosshair",
72
+ "text",
73
+ "vertical-text",
74
+ "alias",
75
+ "copy",
76
+ "move",
77
+ "no-drop",
78
+ "not-allowed",
79
+ "grab",
80
+ "grabbing",
81
+ "e-resize",
82
+ "n-resize",
83
+ "ne-resize",
84
+ "nw-resize",
85
+ "s-resize",
86
+ "se-resize",
87
+ "sw-resize",
88
+ "w-resize",
89
+ "ew-resize",
90
+ "ns-resize",
91
+ "nesw-resize",
92
+ "nwse-resize",
93
+ "col-resize",
94
+ "row-resize",
95
+ "all-scroll",
96
+ "zoom-in",
97
+ "zoom-out"
98
+ ]);
99
+ const FORBIDDEN_CSS_LOAD_PATTERN = /url\s*\(|image-set\s*\(|-webkit-image-set\s*\(/i;
100
+ const DEFAULT_FALLBACK_KEYWORD = "crosshair";
101
+ function isSafeRelativePath(p) {
102
+ if (typeof p !== "string" || p.length === 0) return false;
103
+ if (p.includes(":")) return false;
104
+ if (p.startsWith("/")) return false;
105
+ for (const seg of p.split("/")) {
106
+ if (seg === "" || seg === "..") return false;
107
+ }
108
+ return true;
109
+ }
110
+ function pluginHostname(moduleId) {
111
+ return moduleId.replace(/^@/, "").replace(/\//g, ".");
112
+ }
113
+ function pluginAssetUrl(moduleId, relativePath) {
114
+ if (!isSafeRelativePath(relativePath)) {
115
+ throw new Error(`Unsafe asset path: ${relativePath}`);
116
+ }
117
+ return `openpen-plugin://${pluginHostname(moduleId)}/${relativePath}`;
118
+ }
119
+ function sanitizeSvgMarkup(raw) {
120
+ return DOMPurify.sanitize(raw, {
121
+ USE_PROFILES: { svg: true },
122
+ FORBID_TAGS: ["foreignObject"],
123
+ // Cursor SVGs use presentation attributes (fill/stroke/etc.), never
124
+ // inline `style`. Forbidding it strips legacy `expression()` CSS
125
+ // smuggling vectors (S7) without touching anything we draw.
126
+ FORBID_ATTR: ["style"],
127
+ ALLOW_UNKNOWN_PROTOCOLS: false
128
+ });
129
+ }
130
+ function resolveHotspot(spec) {
131
+ const h2 = spec.hotspot;
132
+ if (!h2 || typeof h2 !== "object") return { x: 0, y: 0 };
133
+ const x = typeof h2.x === "number" && Number.isFinite(h2.x) ? h2.x : 0;
134
+ const y = typeof h2.y === "number" && Number.isFinite(h2.y) ? h2.y : 0;
135
+ return { x, y };
136
+ }
137
+ function resolveFallback(spec) {
138
+ return typeof spec.fallback === "string" && spec.fallback.length > 0 ? spec.fallback : DEFAULT_FALLBACK_KEYWORD;
139
+ }
140
+ function escapeHtmlAttr(s2) {
141
+ return s2.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
142
+ }
143
+ async function compileCursor(spec, context = {}) {
144
+ if (typeof spec === "string") return null;
145
+ if (!spec || typeof spec !== "object") return null;
146
+ if ("svg" in spec && typeof spec.svg === "string") {
147
+ const hotspot = resolveHotspot(spec);
148
+ const fallbackCssKeyword = resolveFallback(spec);
149
+ const trimmed = spec.svg.trim();
150
+ if (trimmed.startsWith("<")) {
151
+ const sanitised = sanitizeSvgMarkup(trimmed);
152
+ if (!sanitised) return null;
153
+ return { svgMarkup: sanitised, hotspot, fallbackCssKeyword };
154
+ }
155
+ if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null;
156
+ const url = `openpen-plugin://${context.pluginHostname}/${trimmed}`;
157
+ const fetchImpl = context.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
158
+ if (!fetchImpl) return null;
159
+ try {
160
+ const resp = await fetchImpl(url);
161
+ if (!resp.ok) return null;
162
+ const raw = await resp.text();
163
+ const sanitised = sanitizeSvgMarkup(raw);
164
+ if (!sanitised) return null;
165
+ return { svgMarkup: sanitised, hotspot, fallbackCssKeyword };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ if ("png" in spec && typeof spec.png === "string") {
171
+ const hotspot = resolveHotspot(spec);
172
+ const fallbackCssKeyword = resolveFallback(spec);
173
+ const trimmed = spec.png.trim();
174
+ if (!context.pluginHostname || !isSafeRelativePath(trimmed)) return null;
175
+ const url = escapeHtmlAttr(`openpen-plugin://${context.pluginHostname}/${trimmed}`);
176
+ const svgMarkup = `<img src="${url}" alt="" style="display:block;max-width:64px;max-height:64px;" />`;
177
+ return { svgMarkup, hotspot, fallbackCssKeyword };
178
+ }
179
+ return null;
180
+ }
181
+ function sanitizeCursorContributions(cursors, options = {}) {
182
+ const cleaned = [];
183
+ const diagnostics = [];
184
+ if (!Array.isArray(cursors)) return { cleaned, diagnostics };
185
+ for (const raw of cursors) {
186
+ if (raw === null || typeof raw !== "object") {
187
+ diagnostics.push({ id: "<unknown>", rule: "R1", reason: "contribution is not an object" });
188
+ continue;
189
+ }
190
+ const entry = raw;
191
+ const id = typeof entry.id === "string" && entry.id.length > 0 ? entry.id : "<unknown>";
192
+ if (id === "<unknown>") {
193
+ diagnostics.push({ id, rule: "R1", reason: "missing or empty `id`" });
194
+ continue;
195
+ }
196
+ const { cursor } = entry;
197
+ if (typeof cursor === "string") {
198
+ const trimmedLower = cursor.trim().toLowerCase();
199
+ if (FORBIDDEN_CSS_LOAD_PATTERN.test(trimmedLower)) {
200
+ diagnostics.push({
201
+ id,
202
+ rule: "R12",
203
+ reason: "string contains url() / image-set() / -webkit-image-set(); loads external resource"
204
+ });
205
+ continue;
206
+ }
207
+ if (!SAFE_CURSOR_KEYWORDS.has(trimmedLower)) {
208
+ diagnostics.push({
209
+ id,
210
+ rule: "R2",
211
+ reason: `string '${trimmedLower}' is not in the safe CSS cursor keyword whitelist`
212
+ });
213
+ continue;
214
+ }
215
+ cleaned.push({ id, cursor: trimmedLower });
216
+ continue;
217
+ }
218
+ if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
219
+ diagnostics.push({ id, rule: "R1", reason: "cursor field is neither string nor plain object" });
220
+ continue;
221
+ }
222
+ const cursorObj = cursor;
223
+ const hasSvg = Object.prototype.hasOwnProperty.call(cursorObj, "svg");
224
+ const hasPng = Object.prototype.hasOwnProperty.call(cursorObj, "png");
225
+ if (!hasSvg && !hasPng) {
226
+ diagnostics.push({ id, rule: "R3", reason: "cursor object has neither `svg` nor `png` key" });
227
+ continue;
228
+ }
229
+ if (hasSvg && hasPng) {
230
+ diagnostics.push({ id, rule: "R4", reason: "cursor object has both `svg` and `png` keys (ambiguous)" });
231
+ continue;
232
+ }
233
+ if (hasSvg) {
234
+ const svg = cursorObj.svg;
235
+ if (typeof svg !== "string") {
236
+ diagnostics.push({ id, rule: "R5", reason: "`svg` value is not a string" });
237
+ continue;
238
+ }
239
+ const trimmed = svg.trim();
240
+ const isInline = trimmed.startsWith("<");
241
+ if (!isInline) {
242
+ if (!isSafeRelativePath(trimmed)) {
243
+ diagnostics.push({
244
+ id,
245
+ rule: "R6",
246
+ reason: "`svg` value is non-inline and not a safe relative path"
247
+ });
248
+ continue;
249
+ }
250
+ if (options.isBuiltin) {
251
+ diagnostics.push({
252
+ id,
253
+ rule: "R7",
254
+ reason: "built-in modules cannot use path-form `svg` (no openpen-plugin hostname)"
255
+ });
256
+ continue;
257
+ }
258
+ }
259
+ }
260
+ if (hasPng) {
261
+ const png = cursorObj.png;
262
+ if (typeof png !== "string") {
263
+ diagnostics.push({ id, rule: "R8", reason: "`png` value is not a string" });
264
+ continue;
265
+ }
266
+ if (!isSafeRelativePath(png.trim())) {
267
+ diagnostics.push({
268
+ id,
269
+ rule: "R9",
270
+ reason: "`png` value is not a safe relative path"
271
+ });
272
+ continue;
273
+ }
274
+ if (options.isBuiltin) {
275
+ diagnostics.push({
276
+ id,
277
+ rule: "R10",
278
+ reason: "built-in modules cannot use `png` (no openpen-plugin hostname)"
279
+ });
280
+ continue;
281
+ }
282
+ }
283
+ if (cursorObj.hotspot !== void 0) {
284
+ const h2 = cursorObj.hotspot;
285
+ const valid = h2 !== null && typeof h2 === "object" && typeof h2.x === "number" && Number.isFinite(h2.x) && h2.x >= 0 && typeof h2.y === "number" && Number.isFinite(h2.y) && h2.y >= 0;
286
+ if (!valid) {
287
+ diagnostics.push({
288
+ id,
289
+ rule: "R11",
290
+ reason: "`hotspot` must be `{ x: number ≥ 0, y: number ≥ 0 }`"
291
+ });
292
+ continue;
293
+ }
294
+ }
295
+ const cleanedCursor = hasSvg ? { svg: cursorObj.svg.trim() } : { png: cursorObj.png.trim() };
296
+ if (cursorObj.hotspot !== void 0) {
297
+ const h2 = cursorObj.hotspot;
298
+ cleanedCursor.hotspot = { x: h2.x, y: h2.y };
299
+ }
300
+ if (typeof cursorObj.fallback === "string") {
301
+ cleanedCursor.fallback = cursorObj.fallback;
302
+ }
303
+ cleaned.push({ id, cursor: cleanedCursor });
304
+ }
305
+ return { cleaned, diagnostics };
306
+ }
307
+ export {
308
+ A as ACTIVE_TOOL_KEY,
309
+ a as ALL_SLOTS,
310
+ b as ANCHOR_EL_KEY,
311
+ CONTRIBUTION_KEY_TO_SLOT_ID,
312
+ c as CONTROL_BAR_ANIMATING_KEY,
313
+ d as ControlBarLayoutSchema,
314
+ D as DEFAULT_CONTROL_BAR_LAYOUT,
315
+ G as GroupInsetSchema,
316
+ H as HOST_DIALOG_OPEN_COUNT_KEY,
317
+ I as IS_VERTICAL_KEY,
318
+ L as LayoutGroupSchema,
319
+ M as MODAL_MANAGER_KEY,
320
+ MODULE_ID_RE,
321
+ P as POPOVER_PLACEMENT_HINT_KEY,
322
+ SAFE_CURSOR_KEYWORDS,
323
+ S as SNAP_EDGE_KEY,
324
+ e as STROKE_STYLE_CONTEXT_KEY,
325
+ T as TOOLTIP_FLIP_DOWN_KEY,
326
+ V as V1_ACTIVE_SLOTS,
327
+ f as V1_RESERVED_SLOTS,
328
+ W as WRAPPER_EL_KEY,
329
+ c2 as clearModuleContext,
330
+ compileCursor,
331
+ defineModule,
332
+ g as getSlot,
333
+ i as isKnownSlot,
334
+ isSafeRelativePath,
335
+ isValidModuleId,
336
+ pluginAssetUrl,
337
+ pluginHostname,
338
+ r as repairLayoutL3a,
339
+ h as repairLayoutL3b,
340
+ resolveLabel,
341
+ resolveStrokeColor,
342
+ sanitizeCursorContributions,
343
+ sanitizeIdForI18n,
344
+ sanitizeSvgMarkup,
345
+ s as setModuleContext,
346
+ useModuleContext,
347
+ z
348
+ };
349
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\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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"}
@@ -5,11 +5,8 @@
5
5
  * Instead, the host provides these contexts via Vue's provide/inject, and
6
6
  * modules reference the keys via this file (part of the public SDK surface).
7
7
  */
8
- import type { InjectionKey, Ref } from 'vue'
9
- import type { StrokeColor } from './types/tool'
10
-
11
- // ── Stroke Style ─────────────────────────────────────────────────────────────
12
-
8
+ import type { InjectionKey, Ref } from 'vue';
9
+ import type { StrokeColor } from './types/tool';
13
10
  /**
14
11
  * Provides read + write access to the global stroke style state.
15
12
  * Provided by App.vue (root); available in all renderer windows.
@@ -17,17 +14,12 @@ import type { StrokeColor } from './types/tool'
17
14
  * Module components: inject via `inject(STROKE_STYLE_CONTEXT_KEY)`.
18
15
  */
19
16
  export interface StrokeStyleContext {
20
- lineWidth: Readonly<Ref<number>>
21
- color: Readonly<Ref<StrokeColor>>
22
- setLineWidth: (w: number) => void
23
- setColor: (c: StrokeColor) => void
17
+ lineWidth: Readonly<Ref<number>>;
18
+ color: Readonly<Ref<StrokeColor>>;
19
+ setLineWidth: (w: number) => void;
20
+ setColor: (c: StrokeColor) => void;
24
21
  }
25
-
26
- export const STROKE_STYLE_CONTEXT_KEY: InjectionKey<StrokeStyleContext> =
27
- Symbol('openpen.strokeStyleContext')
28
-
29
- // ── Snap Edge ────────────────────────────────────────────────────────────────
30
-
22
+ export declare const STROKE_STYLE_CONTEXT_KEY: InjectionKey<StrokeStyleContext>;
31
23
  /**
32
24
  * Which side of the screen workArea the floating ball is anchored to.
33
25
  *
@@ -45,22 +37,13 @@ export const STROKE_STYLE_CONTEXT_KEY: InjectionKey<StrokeStyleContext> =
45
37
  * Use IS_VERTICAL_KEY for orientation checks (vbar vs horizontal); this key
46
38
  * is null in vbar-free mode where no edge is snapped.
47
39
  */
48
- export type SnapEdge = 'left' | 'right' | 'top' | 'bottom' | null
49
-
50
- export const SNAP_EDGE_KEY: InjectionKey<Readonly<Ref<SnapEdge>>> =
51
- Symbol('openpen.snapEdge')
52
-
53
- // ── Vertical Layout ──────────────────────────────────────────────────────────
54
-
40
+ export type SnapEdge = 'left' | 'right' | 'top' | 'bottom' | null;
41
+ export declare const SNAP_EDGE_KEY: InjectionKey<Readonly<Ref<SnapEdge>>>;
55
42
  /**
56
43
  * True for any vertical layout (vbar-left / vbar-right / vbar-free). Use
57
44
  * this for orientation; SNAP_EDGE_KEY is edge-specific and null in vbar-free.
58
45
  */
59
- export const IS_VERTICAL_KEY: InjectionKey<Readonly<Ref<boolean>>> =
60
- Symbol('openpen.isVertical')
61
-
62
- // ── Modal Manager ─────────────────────────────────────────────────────────────
63
-
46
+ export declare const IS_VERTICAL_KEY: InjectionKey<Readonly<Ref<boolean>>>;
64
47
  /**
65
48
  * Programmatic open / close for modals registered via `ui.modals` slot.
66
49
  * Provided by ModalStack.vue.
@@ -69,35 +52,24 @@ export const IS_VERTICAL_KEY: InjectionKey<Readonly<Ref<boolean>>> =
69
52
  * Modal components call `isOpen(id)` to control their own visibility.
70
53
  */
71
54
  export interface ModalManager {
72
- /** Open the modal with the given id. Closes any currently-open modal first. */
73
- open: (id: string) => void
74
- /** Close the modal with the given id. No-op if it is not open. */
75
- close: (id: string) => void
76
- /** Returns true if the modal with the given id is currently open. */
77
- isOpen: (id: string) => boolean
55
+ /** Open the modal with the given id. Closes any currently-open modal first. */
56
+ open: (id: string) => void;
57
+ /** Close the modal with the given id. No-op if it is not open. */
58
+ close: (id: string) => void;
59
+ /** Returns true if the modal with the given id is currently open. */
60
+ isOpen: (id: string) => boolean;
78
61
  }
79
-
80
- export const MODAL_MANAGER_KEY: InjectionKey<ModalManager> =
81
- Symbol('openpen.modalManager')
82
-
83
- // ── Control Bar Layout Context ───────────────────────────────────────────────
84
-
62
+ export declare const MODAL_MANAGER_KEY: InjectionKey<ModalManager>;
85
63
  /**
86
64
  * Reference to the `.control-bar-wrapper` element. Provided by ControlBar.vue.
87
65
  * Module components that open anchor-relative popups teleport into this element.
88
66
  */
89
- export const WRAPPER_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =
90
- Symbol('openpen.wrapperEl')
91
-
67
+ export declare const WRAPPER_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>>;
92
68
  /**
93
69
  * Reference to the `.control-bar` element. Provided by ControlBar.vue.
94
70
  * Module components use this as the anchor for popup positioning.
95
71
  */
96
- export const ANCHOR_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =
97
- Symbol('openpen.anchorEl')
98
-
99
- // ── Control Bar Animating ──────────────────────────────────────────────────────
100
-
72
+ export declare const ANCHOR_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>>;
101
73
  /**
102
74
  * Reactive flag that is `true` while the ControlBar is running a scaleY
103
75
  * animation (expand 350ms / collapse 250ms / snap 250ms).
@@ -106,21 +78,26 @@ export const ANCHOR_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =
106
78
  * AppPopover (and any other UIKit wrapper) watches this and auto-closes
107
79
  * when it becomes true, avoiding floating-ui position drift during animation.
108
80
  */
109
- export const CONTROL_BAR_ANIMATING_KEY: InjectionKey<Readonly<Ref<boolean>>> =
110
- Symbol('openpen:control-bar-animating')
111
-
112
- // ── Host Dialog Presence ─────────────────────────────────────────────────────
113
-
81
+ export declare const CONTROL_BAR_ANIMATING_KEY: InjectionKey<Readonly<Ref<boolean>>>;
82
+ /**
83
+ * `true` when the control bar's ball is sitting close to the workArea top
84
+ * edge, in horizontal mode. Buttons that render a tooltip above themselves
85
+ * (`bottom: calc(100% + 8px)`) would extend off-screen in that state, so they
86
+ * flip below the button instead.
87
+ *
88
+ * Provided by ControlBar.vue. Consumed by AppButton (and any other UIKit
89
+ * wrapper that renders an `::after` tooltip).
90
+ *
91
+ * `false` in vertical mode regardless of position — vertical bars use a
92
+ * sideways tooltip direction (see `SNAP_EDGE_KEY`).
93
+ */
94
+ export declare const TOOLTIP_FLIP_DOWN_KEY: InjectionKey<Readonly<Ref<boolean>>>;
114
95
  /**
115
96
  * Reference-counted open-dialog count. Hosts inject this to render their
116
97
  * own "locked" state when > 0 — a transparent overlay window can't use a
117
98
  * dim/blur dialog backdrop without rendering as an isolated dark patch.
118
99
  */
119
- export const HOST_DIALOG_OPEN_COUNT_KEY: InjectionKey<Ref<number>> =
120
- Symbol('openpen:hostDialogOpenCount')
121
-
122
- // ── Popover Placement Hint ────────────────────────────────────────────────────
123
-
100
+ export declare const HOST_DIALOG_OPEN_COUNT_KEY: InjectionKey<Ref<number>>;
124
101
  /**
125
102
  * Deliberate UX rule for initial popover placement direction.
126
103
  *
@@ -137,18 +114,12 @@ export const HOST_DIALOG_OPEN_COUNT_KEY: InjectionKey<Ref<number>> =
137
114
  * Provided by ControlBar.vue; injected by AppPopover.vue.
138
115
  * Plugin authors do not need to consume this key directly.
139
116
  */
140
- export type PopoverPlacementHint = 'top' | 'bottom' | 'left' | 'right'
141
-
142
- export const POPOVER_PLACEMENT_HINT_KEY: InjectionKey<Readonly<Ref<PopoverPlacementHint>>> =
143
- Symbol('openpen:popoverPlacementHint')
144
-
145
- // ── Active Tool ───────────────────────────────────────────────────────────────
146
-
117
+ export type PopoverPlacementHint = 'top' | 'bottom' | 'left' | 'right';
118
+ export declare const POPOVER_PLACEMENT_HINT_KEY: InjectionKey<Readonly<Ref<PopoverPlacementHint>>>;
147
119
  /**
148
120
  * The currently active tool id. Provided by ControlBar.vue as `readonly(activeToolId)`.
149
121
  * Module tool-button components inject this for reactive active-state without
150
122
  * depending on the event-bus timing (event bus is fire-and-forget; this ref is
151
123
  * always current regardless of when the component mounts).
152
124
  */
153
- export const ACTIVE_TOOL_KEY: InjectionKey<Readonly<Ref<string>>> =
154
- Symbol('openpen:activeTool')
125
+ export declare const ACTIVE_TOOL_KEY: InjectionKey<Readonly<Ref<string>>>;
@@ -10,10 +10,7 @@
10
10
  * sanitizeIdForI18n('@openpen/freehand') // 'openpen.freehand'
11
11
  * sanitizeIdForI18n('@alice/todo') // 'alice.todo'
12
12
  */
13
- export function sanitizeIdForI18n(moduleId: string): string {
14
- return moduleId.replace(/^@/, '').replace(/\//g, '.')
15
- }
16
-
13
+ export declare function sanitizeIdForI18n(moduleId: string): string;
17
14
  /**
18
15
  * Maps a BCP-47 locale tag (e.g. `'en'`, `'zh-TW'`) to a localised string.
19
16
  *
@@ -21,8 +18,7 @@ export function sanitizeIdForI18n(moduleId: string): string {
21
18
  * themselves to the host app's i18n framework. The OpenPen registry
22
19
  * resolves the value at render time based on the active locale.
23
20
  */
24
- export type LocaleMap = Record<string, string>
25
-
21
+ export type LocaleMap = Record<string, string>;
26
22
  /**
27
23
  * Resolve a `string | LocaleMap` to a single string for a given locale.
28
24
  *
@@ -34,19 +30,4 @@ export type LocaleMap = Record<string, string>
34
30
  * 5. First declared value in the map
35
31
  * 6. Empty string (only when the map is `{}`)
36
32
  */
37
- export function resolveLabel(
38
- input: string | LocaleMap,
39
- locale: string
40
- ): string {
41
- if (typeof input === 'string') return input
42
-
43
- if (input[locale] !== undefined) return input[locale]
44
-
45
- const lang = locale.split('-')[0]
46
- if (lang !== locale && input[lang] !== undefined) return input[lang]
47
-
48
- if (input.en !== undefined) return input.en
49
-
50
- const first = Object.values(input)[0]
51
- return first ?? ''
52
- }
33
+ export declare function resolveLabel(input: string | LocaleMap, locale: string): string;