@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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0](https://github.com/openpen-platform/openpen/compare/v1.1.1...v1.2.0) (2026-05-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * **module-api:** add resolveStrokeColor helper for StrokeColor union ([75e9a9c](https://github.com/openpen-platform/openpen/commit/75e9a9c74d002393134931f9bc933b918c9d0391))
9
+ * **module-api:** ship dist + host services via registry injection ([ccae34f](https://github.com/openpen-platform/openpen/commit/ccae34f3030d9d5973336eb4882d4969d5f1b343))
10
+ * **uikit:** add AppButtonDropdown and migrate built-in modules onto SDK button components ([5aeb7d3](https://github.com/openpen-platform/openpen/commit/5aeb7d34f58d5291546cfeca15cf7f3ee8c9b4ba))
11
+
3
12
  ## [1.1.1](https://github.com/openpen-platform/openpen/compare/v1.1.0...v1.1.1) (2026-05-19)
4
13
 
5
14
 
@@ -0,0 +1,245 @@
1
+ import { defineComponent, inject, onBeforeUnmount, watch, computed, useTemplateRef, ref, openBlock, createBlock, unref, withCtx, createVNode, renderSlot, normalizeClass, createTextVNode, toDisplayString, createElementVNode, createElementBlock, createCommentVNode } from "vue";
2
+ import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
3
+ import { M as MODAL_MANAGER_KEY, c as CONTROL_BAR_ANIMATING_KEY, H as HOST_DIALOG_OPEN_COUNT_KEY } from "./inject-keys-CRXCpN-M.js";
4
+ import "dompurify";
5
+ import "zod";
6
+ import { u as usePassthroughGuard } from "./passthrough-DKU7JYuQ.js";
7
+ const _hoisted_1 = { class: "openpen-modal-body" };
8
+ const _hoisted_2 = {
9
+ key: 0,
10
+ class: "openpen-modal-footer"
11
+ };
12
+ const _sfc_main = /* @__PURE__ */ defineComponent({
13
+ __name: "AppDialog",
14
+ props: {
15
+ modalId: {},
16
+ title: {},
17
+ open: { type: Boolean },
18
+ persistent: { type: Boolean, default: false },
19
+ danger: { type: Boolean, default: false }
20
+ },
21
+ emits: ["update:open"],
22
+ setup(__props, { emit: __emit }) {
23
+ const props = __props;
24
+ const emit = __emit;
25
+ const modalManager = inject(MODAL_MANAGER_KEY);
26
+ const animating = inject(CONTROL_BAR_ANIMATING_KEY);
27
+ const dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, null);
28
+ let counted = false;
29
+ function acquireCount() {
30
+ if (counted || !dialogOpenCount) return;
31
+ dialogOpenCount.value += 1;
32
+ counted = true;
33
+ }
34
+ function releaseCount() {
35
+ if (!counted || !dialogOpenCount) return;
36
+ dialogOpenCount.value -= 1;
37
+ counted = false;
38
+ }
39
+ onBeforeUnmount(releaseCount);
40
+ function openDialog() {
41
+ modalManager == null ? void 0 : modalManager.open(props.modalId);
42
+ emit("update:open", true);
43
+ }
44
+ function closeDialog() {
45
+ modalManager == null ? void 0 : modalManager.close(props.modalId);
46
+ emit("update:open", false);
47
+ }
48
+ function toggleDialog() {
49
+ if (props.open) {
50
+ closeDialog();
51
+ } else {
52
+ openDialog();
53
+ }
54
+ }
55
+ watch(
56
+ () => props.open,
57
+ (newOpen) => {
58
+ if (newOpen) {
59
+ modalManager == null ? void 0 : modalManager.open(props.modalId);
60
+ acquireCount();
61
+ } else {
62
+ modalManager == null ? void 0 : modalManager.close(props.modalId);
63
+ releaseCount();
64
+ }
65
+ },
66
+ { immediate: true }
67
+ );
68
+ const isManagerOpen = computed(() => (modalManager == null ? void 0 : modalManager.isOpen(props.modalId)) ?? props.open);
69
+ watch(isManagerOpen, (managerOpen) => {
70
+ if (!managerOpen && props.open) {
71
+ emit("update:open", false);
72
+ }
73
+ });
74
+ const isOpen = computed(() => props.open && isManagerOpen.value);
75
+ function onUpdateOpen(v) {
76
+ if (!v && props.persistent) {
77
+ return;
78
+ }
79
+ if (v) {
80
+ openDialog();
81
+ } else {
82
+ closeDialog();
83
+ }
84
+ }
85
+ watch(
86
+ () => animating == null ? void 0 : animating.value,
87
+ (isAnimating) => {
88
+ if (isAnimating && props.open) {
89
+ closeDialog();
90
+ }
91
+ }
92
+ );
93
+ const contentInstance = useTemplateRef("content");
94
+ const contentEl = ref(null);
95
+ watch(contentInstance, (instance) => {
96
+ if (!instance) {
97
+ contentEl.value = null;
98
+ } else if (instance instanceof HTMLElement) {
99
+ contentEl.value = instance;
100
+ } else if (typeof instance === "object" && "$el" in instance && instance.$el instanceof HTMLElement) {
101
+ contentEl.value = instance.$el;
102
+ } else {
103
+ contentEl.value = null;
104
+ }
105
+ });
106
+ usePassthroughGuard(contentEl);
107
+ return (_ctx, _cache) => {
108
+ return openBlock(), createBlock(unref(DialogRoot), {
109
+ open: isOpen.value,
110
+ "onUpdate:open": onUpdateOpen
111
+ }, {
112
+ default: withCtx(() => [
113
+ createVNode(unref(DialogTrigger), { "as-child": "" }, {
114
+ default: withCtx(() => [
115
+ renderSlot(_ctx.$slots, "trigger", {
116
+ active: isOpen.value,
117
+ toggle: toggleDialog,
118
+ open: openDialog,
119
+ close: closeDialog
120
+ })
121
+ ]),
122
+ _: 3
123
+ }),
124
+ createVNode(unref(DialogPortal), null, {
125
+ default: withCtx(() => [
126
+ createVNode(unref(DialogOverlay), { class: "openpen-modal-overlay" }),
127
+ createVNode(unref(DialogContent), {
128
+ ref: "content",
129
+ class: normalizeClass(["openpen-modal-content openpen-interactive", { "openpen-modal-danger": __props.danger }]),
130
+ onInteractOutside: _cache[0] || (_cache[0] = (e) => __props.persistent && e.preventDefault()),
131
+ onEscapeKeyDown: _cache[1] || (_cache[1] = (e) => __props.persistent && e.preventDefault())
132
+ }, {
133
+ default: withCtx(() => [
134
+ createVNode(unref(DialogTitle), { class: "openpen-modal-title" }, {
135
+ default: withCtx(() => [
136
+ createTextVNode(toDisplayString(__props.title), 1)
137
+ ]),
138
+ _: 1
139
+ }),
140
+ createVNode(unref(DialogDescription), { class: "openpen-modal-description sr-only" }),
141
+ createElementVNode("div", _hoisted_1, [
142
+ renderSlot(_ctx.$slots, "default")
143
+ ]),
144
+ _ctx.$slots.footer ? (openBlock(), createElementBlock("div", _hoisted_2, [
145
+ renderSlot(_ctx.$slots, "footer")
146
+ ])) : createCommentVNode("", true),
147
+ createVNode(unref(DialogClose), {
148
+ class: "openpen-modal-close",
149
+ "aria-label": "Close"
150
+ }, {
151
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [
152
+ createElementVNode("svg", {
153
+ width: "14",
154
+ height: "14",
155
+ viewBox: "0 0 14 14",
156
+ fill: "none",
157
+ "aria-hidden": "true"
158
+ }, [
159
+ createElementVNode("path", {
160
+ d: "M1 1L13 13M13 1L1 13",
161
+ stroke: "currentColor",
162
+ "stroke-width": "2",
163
+ "stroke-linecap": "round"
164
+ })
165
+ ], -1)
166
+ ])]),
167
+ _: 1
168
+ })
169
+ ]),
170
+ _: 3
171
+ }, 8, ["class"])
172
+ ]),
173
+ _: 3
174
+ })
175
+ ]),
176
+ _: 3
177
+ }, 8, ["open"]);
178
+ };
179
+ }
180
+ });
181
+ const _activeRequest = ref(null);
182
+ const _queue = [];
183
+ let _processing = false;
184
+ function _enqueue(run) {
185
+ if (_processing) {
186
+ _queue.push(run);
187
+ } else {
188
+ _processing = true;
189
+ run();
190
+ }
191
+ }
192
+ function _resolveActive() {
193
+ _activeRequest.value = null;
194
+ const next = _queue.shift();
195
+ if (next) {
196
+ next();
197
+ } else {
198
+ _processing = false;
199
+ }
200
+ }
201
+ function useDialog() {
202
+ return {
203
+ confirm(opts) {
204
+ return new Promise((resolve) => {
205
+ _enqueue(() => {
206
+ _activeRequest.value = { kind: "confirm", opts, resolve };
207
+ });
208
+ });
209
+ },
210
+ alert(opts) {
211
+ return new Promise((resolve) => {
212
+ _enqueue(() => {
213
+ _activeRequest.value = { kind: "alert", opts, resolve };
214
+ });
215
+ });
216
+ },
217
+ prompt(opts) {
218
+ return new Promise((resolve) => {
219
+ _enqueue(() => {
220
+ _activeRequest.value = { kind: "prompt", opts, resolve };
221
+ });
222
+ });
223
+ },
224
+ custom(opts) {
225
+ return new Promise((resolve) => {
226
+ _enqueue(() => {
227
+ _activeRequest.value = {
228
+ kind: "custom",
229
+ opts,
230
+ resolve
231
+ };
232
+ });
233
+ });
234
+ }
235
+ };
236
+ }
237
+ const DIALOG_PLUGIN_HANDLE_KEY = Symbol("openpen:dialog-plugin-handle");
238
+ export {
239
+ DIALOG_PLUGIN_HANDLE_KEY as D,
240
+ _sfc_main as _,
241
+ _activeRequest as a,
242
+ _resolveActive as b,
243
+ useDialog as u
244
+ };
245
+ //# sourceMappingURL=AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppButtonDropdown.vue_vue_type_style_index_0_lang-DBVFUpxc.js","sources":["../../src/uikit/components/AppDialog.vue","../../src/uikit/composables/useDialog.ts","../../src/uikit/composables/dialog-internal-keys.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * AppDialog — High-level wrapper (Layer 1).\n *\n * Plugin authors use this component without needing to know about inject\n * keys, Reka UI, modal manager mutual exclusion, or the animating guard.\n *\n * Public API is stable: switching the underlying headless library in the\n * future does not break the plugin author's code.\n *\n * Implementation notes:\n * - `open` prop is the single source of truth (controlled mode). The\n * component emits `update:open` for two-way binding. Internally it also\n * registers with the modal manager for mutual exclusion (same pattern as\n * AppPopover): when `open` turns true we call manager.open(); when\n * another panel causes the manager to close us we emit update:open(false).\n * - `persistent`: when true, ESC and backdrop clicks are suppressed via\n * @interact-outside.prevent and @escape-key-down.prevent on DialogContent.\n * - `danger`: adds class `openpen-modal-danger` on the content element as a\n * styling hook for destructive-action dialogs (no default style yet).\n * - animating guard: auto-close during ControlBar scaleY animation.\n * - usePassthroughGuard: registered on the dialog content element so the\n * transparent Electron window remains clickable when dialog is open.\n * - Styles are unscoped because DialogContent is teleported outside the\n * component's DOM subtree (same reason as AppPopover).\n *\n * CSS class names (.openpen-modal-*) are intentionally kept as the carried-over\n * internal styling tokens — renaming carries no DX benefit and risks accidental\n * style breakage in any code path still referencing them.\n */\nimport { computed, inject, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport {\n DialogRoot,\n DialogTrigger,\n DialogPortal,\n DialogContent,\n DialogOverlay,\n DialogClose,\n DialogTitle,\n DialogDescription,\n} from '../primitives'\n// Inject keys MUST come from the package barrel — see AppPopover.vue\n// header comment for why relative imports break Symbol identity in prod.\nimport {\n MODAL_MANAGER_KEY,\n CONTROL_BAR_ANIMATING_KEY,\n HOST_DIALOG_OPEN_COUNT_KEY,\n} from '@openpen/module-api'\nimport { usePassthroughGuard } from '../../host/passthrough'\n\n// ── OpenPen-owned types (MUST NOT reference reka-ui types) ───────────────────\n\nexport interface AppDialogProps {\n /** Globally unique modal id; used for modal-stack mutual exclusion. */\n modalId: string\n /** Dialog title displayed in the dialog header. */\n title: string\n /** Controlled open state. The parent must handle update:open to toggle. */\n open: boolean\n /**\n * When true, ESC key and backdrop clicks do not close the dialog.\n * Use for confirmations that require an explicit button press.\n */\n persistent?: boolean\n /**\n * When true, adds the `openpen-modal-danger` CSS class on the content\n * element — a hook for styling destructive-action dialogs.\n */\n danger?: boolean\n}\n\nexport interface AppDialogSlots {\n /**\n * Trigger slot; scope exposes active state + toggle / open / close actions.\n * MUST NOT write @click=\"toggle\" on the trigger — DialogTrigger's as-child\n * handles activation automatically; toggle/open/close are escape hatches for\n * programmatic control only.\n */\n trigger: (scope: {\n active: boolean\n toggle: () => void\n open: () => void\n close: () => void\n }) => unknown\n /** Dialog body content slot. */\n default: () => unknown\n /** Optional footer slot (action buttons etc.). */\n footer?: () => unknown\n}\n\nconst props = withDefaults(defineProps<AppDialogProps>(), {\n persistent: false,\n danger: false,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<AppDialogSlots>()\n\n// ── 1. Inject host context (plugin author never sees these) ───────────────────\n\nconst modalManager = inject(MODAL_MANAGER_KEY)\nconst animating = inject(CONTROL_BAR_ANIMATING_KEY)\nconst dialogOpenCount = inject(HOST_DIALOG_OPEN_COUNT_KEY, null)\n\n// Track whether THIS instance currently holds a count, so we never double\n// increment / decrement (e.g. on rapid open / close cycles or unmount-while-open).\nlet counted = false\n\nfunction acquireCount() {\n if (counted || !dialogOpenCount) return\n dialogOpenCount.value += 1\n counted = true\n}\n\nfunction releaseCount() {\n if (!counted || !dialogOpenCount) return\n dialogOpenCount.value -= 1\n counted = false\n}\n\nonBeforeUnmount(releaseCount)\n\n// ── 2. Modal manager mutual exclusion + controlled open prop ─────────────────\n//\n// The `open` prop is the canonical state. We watch it and push changes into\n// the modal manager. When the manager closes us (e.g. another panel opens),\n// we detect that via the computed isManagerOpen and emit update:open(false).\n\nfunction openDialog() {\n modalManager?.open(props.modalId)\n emit('update:open', true)\n}\n\nfunction closeDialog() {\n modalManager?.close(props.modalId)\n emit('update:open', false)\n}\n\nfunction toggleDialog() {\n if (props.open) {\n closeDialog()\n } else {\n openDialog()\n }\n}\n\n// When the prop turns true: register with the manager + bump host dialog count.\n// When it turns false: unregister + release count. Keeps everything in sync.\nwatch(\n () => props.open,\n (newOpen) => {\n if (newOpen) {\n modalManager?.open(props.modalId)\n acquireCount()\n } else {\n modalManager?.close(props.modalId)\n releaseCount()\n }\n },\n { immediate: true }\n)\n\n// When the manager closes us (another panel opened), emit update:open(false)\n// so the parent's v-model updates.\nconst isManagerOpen = computed(() => modalManager?.isOpen(props.modalId) ?? props.open)\n\nwatch(isManagerOpen, (managerOpen) => {\n if (!managerOpen && props.open) {\n emit('update:open', false)\n }\n})\n\n// Effective open state passed to Reka UI's DialogRoot.\nconst isOpen = computed(() => props.open && isManagerOpen.value)\n\n// Sync Reka UI's internal close events (ESC, overlay click) back to caller\n// unless `persistent` suppresses them.\nfunction onUpdateOpen(v: boolean) {\n if (!v && props.persistent) {\n // Persistent mode: ignore Reka UI close attempts; stay open.\n return\n }\n if (v) {\n openDialog()\n } else {\n closeDialog()\n }\n}\n\n// ── 3. ControlBar animation guard — auto-close during transitions ─────────────\n\nwatch(\n () => animating?.value,\n (isAnimating) => {\n if (isAnimating && props.open) {\n closeDialog()\n }\n }\n)\n\n// ── 4. Passthrough guard integration ─────────────────────────────────────────\n//\n// DialogContent is a Vue component; normalize to HTMLElement via watcher.\n\nconst contentInstance = useTemplateRef<HTMLElement | { $el?: HTMLElement } | null>('content')\nconst contentEl = ref<HTMLElement | null>(null)\n\nwatch(contentInstance, (instance) => {\n if (!instance) {\n contentEl.value = null\n } else if (instance instanceof HTMLElement) {\n contentEl.value = instance\n } else if (typeof instance === 'object' && '$el' in instance && instance.$el instanceof HTMLElement) {\n contentEl.value = instance.$el\n } else {\n contentEl.value = null\n }\n})\n\nusePassthroughGuard(contentEl)\n</script>\n\n<template>\n <DialogRoot :open=\"isOpen\" @update:open=\"onUpdateOpen\">\n <DialogTrigger as-child>\n <slot\n name=\"trigger\"\n :active=\"isOpen\"\n :toggle=\"toggleDialog\"\n :open=\"openDialog\"\n :close=\"closeDialog\"\n />\n </DialogTrigger>\n <DialogPortal>\n <DialogOverlay class=\"openpen-modal-overlay\" />\n <DialogContent\n ref=\"content\"\n class=\"openpen-modal-content openpen-interactive\"\n :class=\"{ 'openpen-modal-danger': danger }\"\n @interact-outside=\"(e: Event) => persistent && e.preventDefault()\"\n @escape-key-down=\"(e: KeyboardEvent) => persistent && e.preventDefault()\"\n >\n <DialogTitle class=\"openpen-modal-title\">{{ title }}</DialogTitle>\n <DialogDescription class=\"openpen-modal-description sr-only\" />\n <div class=\"openpen-modal-body\">\n <slot />\n </div>\n <div v-if=\"$slots.footer\" class=\"openpen-modal-footer\">\n <slot name=\"footer\" />\n </div>\n <DialogClose class=\"openpen-modal-close\" aria-label=\"Close\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1 1L13 13M13 1L1 13\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n </svg>\n </DialogClose>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n</template>\n\n<!-- Styles MUST be unscoped — Vue scoped CSS does not follow Teleport targets,\n so .openpen-modal-* classes would not receive the scoped data-v-* attribute\n when rendered inside the portal. -->\n<style>\n/* ── Overlay (transparent click absorber, NOT a dim backdrop) ─────────────────\n OpenPen's overlay window is transparent + frameless. A traditional dim/blur\n backdrop renders as a small dark patch only over the visible chrome — visually\n jarring. The overlay is therefore kept fully transparent here; its only job\n is to absorb outside clicks (so Reka can dispatch interact-outside → close\n when not persistent) and to protect the canvas from stray clicks while the\n dialog is open. Host containers (e.g. ControlBar) render their own \"locked\"\n visual state via HOST_DIALOG_OPEN_COUNT_KEY. */\n.openpen-modal-overlay {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 200;\n pointer-events: auto;\n}\n\n/* ── Dialog content panel ────────────────────────────────────────────────── */\n.openpen-modal-content {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 201;\n min-width: 320px;\n max-width: min(480px, calc(100vw - 48px));\n max-height: calc(100vh - 96px);\n overflow-y: auto;\n\n background: var(--openpen-color-surface-popup);\n border: 1px solid var(--openpen-color-border-hi);\n border-radius: var(--openpen-radius-lg);\n box-shadow: var(--openpen-shadow);\n backdrop-filter: var(--openpen-blur);\n -webkit-backdrop-filter: var(--openpen-blur);\n\n color: var(--openpen-color-text-primary);\n font-size: 13px;\n padding: var(--openpen-space-lg);\n pointer-events: auto;\n}\n\n/* ── Title ───────────────────────────────────────────────────────────────── */\n.openpen-modal-title {\n font-size: 15px;\n font-weight: 600;\n color: var(--openpen-color-text-primary);\n margin: 0 0 var(--openpen-space-md) 0;\n padding-right: 28px; /* leave room for close button */\n line-height: 1.4;\n}\n\n/* ── Visually hidden description (a11y) ──────────────────────────────────── */\n.openpen-modal-description.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* ── Body ────────────────────────────────────────────────────────────────── */\n.openpen-modal-body {\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n.openpen-modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--openpen-space-sm);\n margin-top: var(--openpen-space-lg);\n padding-top: var(--openpen-space-md);\n border-top: 1px solid var(--openpen-color-border);\n}\n\n/* ── Close button ────────────────────────────────────────────────────────── */\n.openpen-modal-close {\n position: absolute;\n top: var(--openpen-space-md);\n right: var(--openpen-space-md);\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n color: var(--openpen-color-text-dim);\n border-radius: var(--openpen-radius-sm);\n cursor: pointer;\n transition: background var(--openpen-duration-fast), color var(--openpen-duration-fast);\n}\n\n.openpen-modal-close:hover {\n background: var(--openpen-color-control-hover);\n color: var(--openpen-color-text-primary);\n}\n\n/* ── Open / close animations ─────────────────────────────────────────────────\n Reka UI's Presence keeps the element in the DOM until the CSS animation\n ends, so [data-state=\"closed\"] animations DO play on close.\n Pattern matches AppPopover keyframes (200ms bounce enter, 150ms ease leave). */\n\n/* Overlay is always transparent — no fade-in/out animation needed.\n Only the dialog content panel animates. */\n\n.openpen-modal-content[data-state='open'] {\n animation: openpen-modal-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.openpen-modal-content[data-state='closed'] {\n animation: openpen-modal-out 150ms ease forwards;\n}\n\n@keyframes openpen-modal-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n@keyframes openpen-modal-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }\n}\n</style>\n","/**\n * useDialog — imperative dialog API for OpenPen modules.\n *\n * Returns a DialogApi with confirm/alert/prompt/custom methods that each\n * return a Promise. At most one imperative dialog is open at a time; extra\n * calls are queued and executed sequentially.\n *\n * The active request is stored in a module-scoped reactive store. DialogHost\n * (mounted by the host at bootstrap) reads this store and renders the dialog.\n *\n * Usage:\n * const dialog = useDialog()\n * const ok = await dialog.confirm({ title: 'Delete?', message: '...' })\n */\nimport type { Component } from 'vue'\nimport { ref } from 'vue'\n\n// ── Public option types ───────────────────────────────────────────────────────\n\ninterface BaseDialogOptions {\n title: string\n persistent?: boolean\n danger?: boolean\n}\n\nexport interface DialogConfirmOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogAlertOptions extends BaseDialogOptions {\n message: string\n okLabel?: string\n}\n\nexport interface DialogPromptOptions extends BaseDialogOptions {\n message: string\n defaultValue?: string\n placeholder?: string\n okLabel?: string\n cancelLabel?: string\n}\n\nexport interface DialogCustomOptions<T = unknown> extends BaseDialogOptions {\n component: Component\n componentProps?: Record<string, unknown>\n /** Phantom type field for TypeScript to infer the resolution payload type. */\n __payloadType?: T\n}\n\n// ── Internal request types (tagged union) ─────────────────────────────────────\n\ntype DialogRequest =\n | { kind: 'confirm'; opts: DialogConfirmOptions; resolve: (v: boolean) => void }\n | { kind: 'alert'; opts: DialogAlertOptions; resolve: () => void }\n | { kind: 'prompt'; opts: DialogPromptOptions; resolve: (v: string | null) => void }\n | { kind: 'custom'; opts: DialogCustomOptions<unknown>; resolve: (v: unknown) => void }\n\n// ── Module-scoped store ───────────────────────────────────────────────────────\n//\n// Single reactive ref holds the currently rendered request. DialogHost watches\n// this and renders the appropriate dialog. When DialogHost resolves a request,\n// it calls resolveActive() which drains the queue.\n\nexport const _activeRequest = ref<DialogRequest | null>(null)\n\nconst _queue: (() => void)[] = []\nlet _processing = false\n\nfunction _enqueue(run: () => void): void {\n if (_processing) {\n _queue.push(run)\n } else {\n _processing = true\n run()\n }\n}\n\n/**\n * Called by DialogHost when the dialog is resolved (ok/cancel/dismiss).\n * Advances the queue if there are pending requests.\n */\nexport function _resolveActive(): void {\n _activeRequest.value = null\n const next = _queue.shift()\n if (next) {\n next()\n } else {\n _processing = false\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface DialogApi {\n confirm(opts: DialogConfirmOptions): Promise<boolean>\n alert(opts: DialogAlertOptions): Promise<void>\n prompt(opts: DialogPromptOptions): Promise<string | null>\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null>\n}\n\nexport function useDialog(): DialogApi {\n return {\n confirm(opts: DialogConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'confirm', opts, resolve }\n })\n })\n },\n\n alert(opts: DialogAlertOptions): Promise<void> {\n return new Promise<void>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'alert', opts, resolve }\n })\n })\n },\n\n prompt(opts: DialogPromptOptions): Promise<string | null> {\n return new Promise<string | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = { kind: 'prompt', opts, resolve }\n })\n })\n },\n\n custom<T = unknown>(opts: DialogCustomOptions<T>): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n _enqueue(() => {\n _activeRequest.value = {\n kind: 'custom',\n opts: opts as DialogCustomOptions<unknown>,\n resolve: resolve as (v: unknown) => void,\n }\n })\n })\n },\n }\n}\n","/**\n * dialog-internal-keys.ts — Internal injection keys for the imperative dialog system.\n *\n * HOST-INTERNAL: Do not export from any public barrel. Plugin authors must\n * never see or depend on these keys; they exist only so DialogHost can\n * provide the handle that useDialogPluginComponent() reads.\n */\nimport type { InjectionKey } from 'vue'\nimport type { DialogPluginComponentHandle } from './useDialogPluginComponent'\n\n/**\n * Injection key that DialogHost provides to custom dialog components.\n * Components rendered via useDialog().custom() use useDialogPluginComponent()\n * which injects this key to get ok/cancel/dismiss callbacks.\n */\nexport const DIALOG_PLUGIN_HANDLE_KEY: InjectionKey<DialogPluginComponentHandle<unknown>> =\n Symbol('openpen:dialog-plugin-handle')\n"],"names":["_createBlock","_unref","_createVNode","_renderSlot","_normalizeClass","_createElementVNode","$slots","_openBlock","_createElementBlock"],"mappings":"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"}
@@ -0,0 +1,196 @@
1
+ import { z } from "zod";
2
+ const V1_ACTIVE_SLOTS = [
3
+ // Canvas
4
+ { id: "canvas.tools", status: "v1", category: "canvas", description: "Drawing tools driven by pointer events." },
5
+ { id: "canvas.shapes", status: "v1", category: "canvas", description: "Shape primitives consumed by the shape tool." },
6
+ { id: "canvas.stroke.style", status: "v1", category: "canvas", description: "Writers into the shared stroke style store." },
7
+ { id: "canvas.layers.background", status: "v1", category: "canvas", description: "Render below strokes (grids, watermarks)." },
8
+ { id: "canvas.layers.overlay", status: "v1", category: "canvas", description: "Render above strokes (rulers, guides)." },
9
+ { id: "canvas.html.overlay", status: "v1", category: "canvas", description: "Mount HTML elements above the canvas (text, stickers)." },
10
+ // UI
11
+ { id: "ui.control-bar", status: "v1", category: "ui", description: "Control bar item slot (groups and ordering managed by layout config)." },
12
+ { id: "ui.settings.tabs", status: "v1", category: "ui", description: "Tabs in the settings window." },
13
+ { id: "ui.settings.panels", status: "v1", category: "ui", description: "Sections inside the Features settings tab, grouped by module." },
14
+ { id: "ui.cursors", status: "v1", category: "ui", description: "Drawing-mode cursor styles." },
15
+ { id: "ui.status", status: "v1", category: "ui", description: "Ephemeral status badges on the control bar." },
16
+ { id: "ui.modals", status: "v1", category: "ui", description: "Registered modals managed by the global modal stack." },
17
+ // System
18
+ { id: "system.shortcuts", status: "v1", category: "system", description: "Global and drawing-mode keyboard shortcuts." },
19
+ { id: "system.locales", status: "v1", category: "system", description: "i18n dictionary contributions." },
20
+ { id: "system.main.handlers", status: "v1", category: "system", description: "Node-side IPC handlers for main process capabilities." },
21
+ { id: "system.events", status: "v1", category: "system", description: "Subscribers and emitters on the domain event bus." },
22
+ { id: "system.lifecycle", status: "v1", category: "system", description: "App lifecycle hooks (onReady / onSuspend / onQuit)." }
23
+ ];
24
+ const V1_RESERVED_SLOTS = [
25
+ { id: "canvas.history.commands", status: "reserved", category: "canvas", description: "Undo/redo history commands. Adapter not yet active; built-in command types are used internally." },
26
+ { id: "canvas.stroke.transformers", status: "reserved", category: "canvas", description: "Post-process strokes after creation (smooth, simplify)." },
27
+ { id: "ui.tray.menu", status: "reserved", category: "ui", description: "System tray menu items. Adapter not yet active." },
28
+ { id: "ui.context.menu", status: "reserved", category: "ui", description: "Right-click context menu items on canvas / control bar." },
29
+ { id: "ui.theme.tokens", status: "reserved", category: "ui", description: "CSS custom property contributions (colour swatches, spacing tokens)." },
30
+ { id: "system.window.behaviors", status: "reserved", category: "system", description: "Window behavior customizations. Adapter not yet active." },
31
+ { id: "system.storage", status: "reserved", category: "system", description: "Per-plugin isolated storage at ~/.openpen/plugins/<id>/data/. Adapter not yet active; first real consumer will drive the storage backend design." },
32
+ { id: "system.file.drop", status: "reserved", category: "system", description: "Handlers for files dropped onto the canvas." }
33
+ ];
34
+ const ALL_SLOTS = [
35
+ ...V1_ACTIVE_SLOTS,
36
+ ...V1_RESERVED_SLOTS
37
+ ];
38
+ const SLOT_BY_ID = new Map(
39
+ ALL_SLOTS.map((s) => [s.id, s])
40
+ );
41
+ function isKnownSlot(id) {
42
+ return SLOT_BY_ID.has(id);
43
+ }
44
+ function getSlot(id) {
45
+ return SLOT_BY_ID.get(id);
46
+ }
47
+ const CONTRIBUTION_KEY_TO_SLOT_ID = {
48
+ // Canvas
49
+ tools: "canvas.tools",
50
+ shapes: "canvas.shapes",
51
+ strokeStyle: "canvas.stroke.style",
52
+ historyCommands: "canvas.history.commands",
53
+ backgroundLayers: "canvas.layers.background",
54
+ overlayLayers: "canvas.layers.overlay",
55
+ htmlOverlays: "canvas.html.overlay",
56
+ strokeTransformers: "canvas.stroke.transformers",
57
+ // UI
58
+ controlBar: "ui.control-bar",
59
+ settingsTabs: "ui.settings.tabs",
60
+ settingsPanels: "ui.settings.panels",
61
+ cursors: "ui.cursors",
62
+ status: "ui.status",
63
+ modals: "ui.modals",
64
+ trayMenu: "ui.tray.menu",
65
+ contextMenu: "ui.context.menu",
66
+ themeTokens: "ui.theme.tokens",
67
+ // System
68
+ shortcuts: "system.shortcuts",
69
+ windowBehaviors: "system.window.behaviors",
70
+ locales: "system.locales",
71
+ events: "system.events",
72
+ lifecycle: "system.lifecycle",
73
+ storage: "system.storage",
74
+ mainHandlers: "system.main.handlers",
75
+ fileDrop: "system.file.drop"
76
+ };
77
+ const GroupInsetSchema = z.object({
78
+ enabled: z.boolean(),
79
+ /** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */
80
+ color: z.string().optional()
81
+ });
82
+ const LayoutGroupSchema = z.object({
83
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/, "Group id must be kebab-case"),
84
+ items: z.array(z.string()),
85
+ separator: z.enum(["auto", "always", "never"]).optional(),
86
+ /**
87
+ * Optional inset rendering for this group. When `enabled: true`, the group
88
+ * MUST render with a visible bg + border + padding container (the design's
89
+ * "grouped tools" look) AND its inner items MUST shrink so the group's
90
+ * outer height matches an unwrapped 36px button — i.e. enabling inset
91
+ * MUST NOT increase control-bar height.
92
+ */
93
+ inset: GroupInsetSchema.optional()
94
+ });
95
+ const ControlBarLayoutSchema = z.object({
96
+ version: z.literal(1),
97
+ groups: z.array(LayoutGroupSchema).refine((groups) => groups.some((g) => g.id === "default"), {
98
+ message: "Layout must contain a 'default' group"
99
+ }).refine(
100
+ (groups) => {
101
+ const ids = groups.map((g) => g.id);
102
+ return new Set(ids).size === ids.length;
103
+ },
104
+ { message: "Group ids must be unique" }
105
+ ).refine(
106
+ (groups) => {
107
+ const allItems = groups.flatMap((g) => g.items);
108
+ return new Set(allItems).size === allItems.length;
109
+ },
110
+ { message: "Item ids must not appear in multiple groups" }
111
+ )
112
+ });
113
+ const DEFAULT_CONTROL_BAR_LAYOUT = {
114
+ version: 1,
115
+ groups: [{ id: "default", items: [], separator: "auto" }]
116
+ };
117
+ function repairLayoutL3a(layout) {
118
+ let repaired = false;
119
+ const groups = layout.groups.map((g) => {
120
+ if (g.separator !== void 0 && !["auto", "always", "never"].includes(g.separator)) {
121
+ repaired = true;
122
+ return { ...g, separator: "auto" };
123
+ }
124
+ return g;
125
+ });
126
+ if (!groups.some((g) => g.id === "default")) {
127
+ groups.push({ id: "default", items: [], separator: "auto" });
128
+ repaired = true;
129
+ }
130
+ return { layout: { ...layout, groups }, repaired };
131
+ }
132
+ function repairLayoutL3b(layout, knownItemIds) {
133
+ let repaired = false;
134
+ const seen = /* @__PURE__ */ new Set();
135
+ const reversedGroups = [...layout.groups].reverse();
136
+ const deduped = reversedGroups.map((g) => {
137
+ const items = g.items.filter((id) => {
138
+ if (seen.has(id)) {
139
+ repaired = true;
140
+ return false;
141
+ }
142
+ seen.add(id);
143
+ return true;
144
+ });
145
+ return { ...g, items };
146
+ });
147
+ const groups = deduped.reverse();
148
+ const totalItems = groups.flatMap((g) => g.items);
149
+ const ghostCount = totalItems.filter((id) => !knownItemIds.has(id)).length;
150
+ if (ghostCount >= 20) {
151
+ repaired = true;
152
+ const cleaned = groups.map((g) => ({
153
+ ...g,
154
+ items: g.items.filter((id) => knownItemIds.has(id))
155
+ }));
156
+ return { layout: { ...layout, groups: cleaned }, repaired };
157
+ }
158
+ return { layout: { ...layout, groups }, repaired };
159
+ }
160
+ const STROKE_STYLE_CONTEXT_KEY = Symbol("openpen.strokeStyleContext");
161
+ const SNAP_EDGE_KEY = Symbol("openpen.snapEdge");
162
+ const IS_VERTICAL_KEY = Symbol("openpen.isVertical");
163
+ const MODAL_MANAGER_KEY = Symbol("openpen.modalManager");
164
+ const WRAPPER_EL_KEY = Symbol("openpen.wrapperEl");
165
+ const ANCHOR_EL_KEY = Symbol("openpen.anchorEl");
166
+ const CONTROL_BAR_ANIMATING_KEY = Symbol("openpen:control-bar-animating");
167
+ const TOOLTIP_FLIP_DOWN_KEY = Symbol("openpen:tooltipFlipDown");
168
+ const HOST_DIALOG_OPEN_COUNT_KEY = Symbol("openpen:hostDialogOpenCount");
169
+ const POPOVER_PLACEMENT_HINT_KEY = Symbol("openpen:popoverPlacementHint");
170
+ const ACTIVE_TOOL_KEY = Symbol("openpen:activeTool");
171
+ export {
172
+ ACTIVE_TOOL_KEY as A,
173
+ CONTRIBUTION_KEY_TO_SLOT_ID as C,
174
+ DEFAULT_CONTROL_BAR_LAYOUT as D,
175
+ GroupInsetSchema as G,
176
+ HOST_DIALOG_OPEN_COUNT_KEY as H,
177
+ IS_VERTICAL_KEY as I,
178
+ LayoutGroupSchema as L,
179
+ MODAL_MANAGER_KEY as M,
180
+ POPOVER_PLACEMENT_HINT_KEY as P,
181
+ SNAP_EDGE_KEY as S,
182
+ TOOLTIP_FLIP_DOWN_KEY as T,
183
+ V1_ACTIVE_SLOTS as V,
184
+ WRAPPER_EL_KEY as W,
185
+ ALL_SLOTS as a,
186
+ ANCHOR_EL_KEY as b,
187
+ CONTROL_BAR_ANIMATING_KEY as c,
188
+ ControlBarLayoutSchema as d,
189
+ STROKE_STYLE_CONTEXT_KEY as e,
190
+ V1_RESERVED_SLOTS as f,
191
+ getSlot as g,
192
+ repairLayoutL3b as h,
193
+ isKnownSlot as i,
194
+ repairLayoutL3a as r
195
+ };
196
+ //# sourceMappingURL=inject-keys-CRXCpN-M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject-keys-CRXCpN-M.js","sources":["../../src/slots.ts","../../src/types/control-bar-layout.ts","../../src/inject-keys.ts"],"sourcesContent":["/**\n * OpenPen Contribution Slot catalogue.\n *\n * Slots are the only places where modules can affect the host app.\n * Adding a slot is an architectural change — it usually requires a\n * new adapter on the OpenPen side and a minor version bump of this\n * package. Removing a slot is a breaking change.\n *\n * `status: 'v1'` slots are wired through to runtime adapters.\n * `status: 'reserved'` slots accept registrations and pass validation;\n * no runtime adapter is active yet so contributions are stored but\n * inert. Forward-compatible: modules can ship against reserved slots\n * without any plugin-side changes once an adapter lands.\n */\n\nexport type SlotCategory = 'canvas' | 'ui' | 'system'\nexport type SlotStatus = 'v1' | 'reserved'\n\nexport interface SlotDefinition {\n /** Dotted slot id, e.g. `'canvas.tools'`. */\n id: string\n /** Implementation status — see file header. */\n status: SlotStatus\n /** High-level grouping for documentation and UI. */\n category: SlotCategory\n /** One-sentence purpose, shown in `docs/slots/index.md`. */\n description: string\n}\n\n/**\n * v1 active slots (16). Wired through to runtime adapters.\n */\nexport const V1_ACTIVE_SLOTS: readonly SlotDefinition[] = [\n // Canvas\n { id: 'canvas.tools', status: 'v1', category: 'canvas', description: 'Drawing tools driven by pointer events.' },\n { id: 'canvas.shapes', status: 'v1', category: 'canvas', description: 'Shape primitives consumed by the shape tool.' },\n { id: 'canvas.stroke.style', status: 'v1', category: 'canvas', description: 'Writers into the shared stroke style store.' },\n { id: 'canvas.layers.background', status: 'v1', category: 'canvas', description: 'Render below strokes (grids, watermarks).' },\n { id: 'canvas.layers.overlay', status: 'v1', category: 'canvas', description: 'Render above strokes (rulers, guides).' },\n { id: 'canvas.html.overlay', status: 'v1', category: 'canvas', description: 'Mount HTML elements above the canvas (text, stickers).' },\n // UI\n { id: 'ui.control-bar', status: 'v1', category: 'ui', description: 'Control bar item slot (groups and ordering managed by layout config).' },\n { id: 'ui.settings.tabs', status: 'v1', category: 'ui', description: 'Tabs in the settings window.' },\n { id: 'ui.settings.panels', status: 'v1', category: 'ui', description: 'Sections inside the Features settings tab, grouped by module.' },\n { id: 'ui.cursors', status: 'v1', category: 'ui', description: 'Drawing-mode cursor styles.' },\n { id: 'ui.status', status: 'v1', category: 'ui', description: 'Ephemeral status badges on the control bar.' },\n { id: 'ui.modals', status: 'v1', category: 'ui', description: 'Registered modals managed by the global modal stack.' },\n // System\n { id: 'system.shortcuts', status: 'v1', category: 'system', description: 'Global and drawing-mode keyboard shortcuts.' },\n { id: 'system.locales', status: 'v1', category: 'system', description: 'i18n dictionary contributions.' },\n { id: 'system.main.handlers', status: 'v1', category: 'system', description: 'Node-side IPC handlers for main process capabilities.' },\n { id: 'system.events', status: 'v1', category: 'system', description: 'Subscribers and emitters on the domain event bus.' },\n { id: 'system.lifecycle', status: 'v1', category: 'system', description: 'App lifecycle hooks (onReady / onSuspend / onQuit).' },\n] as const\n\n/**\n * v1 reserved slots. Registrations pass validation but no runtime\n * adapter is active yet.\n */\nexport const V1_RESERVED_SLOTS: readonly SlotDefinition[] = [\n { id: 'canvas.history.commands', status: 'reserved', category: 'canvas', description: 'Undo/redo history commands. Adapter not yet active; built-in command types are used internally.' },\n { id: 'canvas.stroke.transformers', status: 'reserved', category: 'canvas', description: 'Post-process strokes after creation (smooth, simplify).' },\n { id: 'ui.tray.menu', status: 'reserved', category: 'ui', description: 'System tray menu items. Adapter not yet active.' },\n { id: 'ui.context.menu', status: 'reserved', category: 'ui', description: 'Right-click context menu items on canvas / control bar.' },\n { id: 'ui.theme.tokens', status: 'reserved', category: 'ui', description: 'CSS custom property contributions (colour swatches, spacing tokens).' },\n { id: 'system.window.behaviors', status: 'reserved', category: 'system', description: 'Window behavior customizations. Adapter not yet active.' },\n { id: 'system.storage', status: 'reserved', category: 'system', description: 'Per-plugin isolated storage at ~/.openpen/plugins/<id>/data/. Adapter not yet active; first real consumer will drive the storage backend design.' },\n { id: 'system.file.drop', status: 'reserved', category: 'system', description: 'Handlers for files dropped onto the canvas.' },\n] as const\n\n/**\n * All slots (active + reserved). Source of truth for validation.\n */\nexport const ALL_SLOTS: readonly SlotDefinition[] = [\n ...V1_ACTIVE_SLOTS,\n ...V1_RESERVED_SLOTS,\n]\n\nconst SLOT_BY_ID = new Map<string, SlotDefinition>(\n ALL_SLOTS.map((s) => [s.id, s])\n)\n\n/** Returns `true` when `id` matches a slot in {@link ALL_SLOTS} (active or reserved). */\nexport function isKnownSlot(id: string): boolean {\n return SLOT_BY_ID.has(id)\n}\n\n/**\n * Returns the {@link SlotDefinition} for a known slot id, or `undefined` if\n * the id is not registered in {@link ALL_SLOTS}.\n */\nexport function getSlot(id: string): SlotDefinition | undefined {\n return SLOT_BY_ID.get(id)\n}\n\n/**\n * Mapping from `contributes` field name (camelCase) to slot id (dotted).\n *\n * Modules express contributions via the ergonomic camelCase keys on\n * `contributes` (e.g. `historyCommands`); the validator maps them\n * back to slot ids (`canvas.history.commands`) before checking against\n * `ALL_SLOTS`.\n *\n * Adding a new field here is part of adding a new slot.\n */\nexport const CONTRIBUTION_KEY_TO_SLOT_ID: Record<string, string> = {\n // Canvas\n tools: 'canvas.tools',\n shapes: 'canvas.shapes',\n strokeStyle: 'canvas.stroke.style',\n historyCommands: 'canvas.history.commands',\n backgroundLayers: 'canvas.layers.background',\n overlayLayers: 'canvas.layers.overlay',\n htmlOverlays: 'canvas.html.overlay',\n strokeTransformers: 'canvas.stroke.transformers',\n // UI\n controlBar: 'ui.control-bar',\n settingsTabs: 'ui.settings.tabs',\n settingsPanels: 'ui.settings.panels',\n cursors: 'ui.cursors',\n status: 'ui.status',\n modals: 'ui.modals',\n trayMenu: 'ui.tray.menu',\n contextMenu: 'ui.context.menu',\n themeTokens: 'ui.theme.tokens',\n // System\n shortcuts: 'system.shortcuts',\n windowBehaviors: 'system.window.behaviors',\n locales: 'system.locales',\n events: 'system.events',\n lifecycle: 'system.lifecycle',\n storage: 'system.storage',\n mainHandlers: 'system.main.handlers',\n fileDrop: 'system.file.drop',\n}\n","import { z } from 'zod'\nimport type { Component } from 'vue'\nimport type { LocaleMap } from '../locale'\n\n// ── Schema ────────────────────────────────────────────────────────────────\n\nexport const GroupInsetSchema = z.object({\n enabled: z.boolean(),\n /** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */\n color: z.string().optional(),\n})\n\nexport const LayoutGroupSchema = z.object({\n id: z.string().regex(/^[a-z][a-z0-9-]*$/, 'Group id must be kebab-case'),\n items: z.array(z.string()),\n separator: z.enum(['auto', 'always', 'never']).optional(),\n /**\n * Optional inset rendering for this group. When `enabled: true`, the group\n * MUST render with a visible bg + border + padding container (the design's\n * \"grouped tools\" look) AND its inner items MUST shrink so the group's\n * outer height matches an unwrapped 36px button — i.e. enabling inset\n * MUST NOT increase control-bar height.\n */\n inset: GroupInsetSchema.optional(),\n})\n\nexport const ControlBarLayoutSchema = z.object({\n version: z.literal(1),\n groups: z\n .array(LayoutGroupSchema)\n .refine((groups) => groups.some((g) => g.id === 'default'), {\n message: \"Layout must contain a 'default' group\",\n })\n .refine(\n (groups) => {\n const ids = groups.map((g) => g.id)\n return new Set(ids).size === ids.length\n },\n { message: 'Group ids must be unique' },\n )\n .refine(\n (groups) => {\n const allItems = groups.flatMap((g) => g.items)\n return new Set(allItems).size === allItems.length\n },\n { message: 'Item ids must not appear in multiple groups' },\n ),\n})\n\n// ── TypeScript types ──────────────────────────────────────────────────────\n\nexport interface GroupInset {\n enabled: boolean\n /** Optional CSS color override for the inset background; defaults to --openpen-color-control-group. */\n color?: string\n}\n\nexport interface LayoutGroup {\n id: string\n items: string[]\n separator?: 'auto' | 'always' | 'never'\n /** When `enabled: true`, render the group with the visible \"grouped tools\" container. */\n inset?: GroupInset\n}\n\nexport interface ControlBarLayout {\n version: 1\n groups: LayoutGroup[]\n}\n\n// ── Contribution type ─────────────────────────────────────────────────────\n\nexport interface ControlBarContribution {\n /** Item id — MUST be globally unique across all modules. */\n id: string\n /** Vue component rendered for this item. */\n component: Component\n /**\n * Preferred group id on first install.\n * MUST NOT be 'default' (reserved).\n * When omitted, item falls into the 'default' group.\n */\n defaultGroup?: string\n /**\n * Hints used when host auto-creates the defaultGroup.\n * Ignored if the group already exists in the layout.\n */\n groupHint?: {\n separator?: 'auto' | 'always' | 'never'\n label?: string | LocaleMap\n }\n}\n\n// ── Default layout ────────────────────────────────────────────────────────\n\n/** Baseline layout used on first launch: a single empty `'default'` group. */\nexport const DEFAULT_CONTROL_BAR_LAYOUT: ControlBarLayout = {\n version: 1,\n groups: [{ id: 'default', items: [], separator: 'auto' }],\n}\n\n// ── Reconcile helpers ─────────────────────────────────────────────────────\n\n/**\n * Apply L3a non-destructive repairs to a parsed layout:\n * - Ensure 'default' group exists\n * - Replace invalid separator values with 'auto'\n * Returns the (possibly mutated copy) and whether any repair was made.\n *\n * @internal Host-only utility; not part of the plugin author API.\n */\nexport function repairLayoutL3a(layout: ControlBarLayout): {\n layout: ControlBarLayout\n repaired: boolean\n} {\n let repaired = false\n const groups = layout.groups.map((g) => {\n if (g.separator !== undefined && !['auto', 'always', 'never'].includes(g.separator)) {\n repaired = true\n return { ...g, separator: 'auto' as const }\n }\n return g\n })\n if (!groups.some((g) => g.id === 'default')) {\n groups.push({ id: 'default', items: [], separator: 'auto' })\n repaired = true\n }\n return { layout: { ...layout, groups }, repaired }\n}\n\n/**\n * Apply L3b potentially-destructive repairs:\n * - De-duplicate item ids across groups (keep last occurrence)\n * - Trim ghost ids when count ≥ 20 (noop — ghost trimming is a UX action)\n * Returns the repaired layout and whether any repair was made.\n *\n * @internal Host-only utility; not part of the plugin author API.\n */\nexport function repairLayoutL3b(\n layout: ControlBarLayout,\n knownItemIds: Set<string>,\n): { layout: ControlBarLayout; repaired: boolean } {\n let repaired = false\n const seen = new Set<string>()\n // Process groups in reverse so the last occurrence wins.\n const reversedGroups = [...layout.groups].reverse()\n const deduped = reversedGroups.map((g) => {\n const items = g.items.filter((id) => {\n if (seen.has(id)) {\n repaired = true\n return false\n }\n seen.add(id)\n return true\n })\n return { ...g, items }\n })\n const groups = deduped.reverse()\n\n // Count ghosts across layout\n const totalItems = groups.flatMap((g) => g.items)\n const ghostCount = totalItems.filter((id) => !knownItemIds.has(id)).length\n if (ghostCount >= 20) {\n repaired = true\n // Remove ghosts from all groups\n const cleaned = groups.map((g) => ({\n ...g,\n items: g.items.filter((id) => knownItemIds.has(id)),\n }))\n return { layout: { ...layout, groups: cleaned }, repaired }\n }\n\n return { layout: { ...layout, groups }, repaired }\n}\n","/**\n * Vue InjectionKey symbols that host code provides and module components inject.\n *\n * Module components MUST NOT import host composables directly (boundary rule).\n * Instead, the host provides these contexts via Vue's provide/inject, and\n * modules reference the keys via this file (part of the public SDK surface).\n */\nimport type { InjectionKey, Ref } from 'vue'\nimport type { StrokeColor } from './types/tool'\n\n// ── Stroke Style ─────────────────────────────────────────────────────────────\n\n/**\n * Provides read + write access to the global stroke style state.\n * Provided by App.vue (root); available in all renderer windows.\n *\n * Module components: inject via `inject(STROKE_STYLE_CONTEXT_KEY)`.\n */\nexport interface StrokeStyleContext {\n lineWidth: Readonly<Ref<number>>\n color: Readonly<Ref<StrokeColor>>\n setLineWidth: (w: number) => void\n setColor: (c: StrokeColor) => void\n}\n\nexport const STROKE_STYLE_CONTEXT_KEY: InjectionKey<StrokeStyleContext> =\n Symbol('openpen.strokeStyleContext')\n\n// ── Snap Edge ────────────────────────────────────────────────────────────────\n\n/**\n * Which side of the screen workArea the floating ball is anchored to.\n *\n * - 'left' → ball anchored on the left edge (vbar-left layout)\n * - 'right' → ball anchored on the right edge (vbar-right layout)\n * - 'top' → ball anchored on the top edge (horizontal bar, top)\n * - 'bottom' → ball anchored on the bottom edge (horizontal bar, bottom)\n * - null → not snapped (free-floating or horizontal default)\n *\n * Consumers that need a ready-to-use popup direction (the opposite of the\n * snap edge) should inject POPOVER_PLACEMENT_HINT_KEY instead — it encodes\n * the two-tier UX rule (snap edge → midline fallback) so individual components\n * do not have to replicate the logic.\n *\n * Use IS_VERTICAL_KEY for orientation checks (vbar vs horizontal); this key\n * is null in vbar-free mode where no edge is snapped.\n */\nexport type SnapEdge = 'left' | 'right' | 'top' | 'bottom' | null\n\nexport const SNAP_EDGE_KEY: InjectionKey<Readonly<Ref<SnapEdge>>> =\n Symbol('openpen.snapEdge')\n\n// ── Vertical Layout ──────────────────────────────────────────────────────────\n\n/**\n * True for any vertical layout (vbar-left / vbar-right / vbar-free). Use\n * this for orientation; SNAP_EDGE_KEY is edge-specific and null in vbar-free.\n */\nexport const IS_VERTICAL_KEY: InjectionKey<Readonly<Ref<boolean>>> =\n Symbol('openpen.isVertical')\n\n// ── Modal Manager ─────────────────────────────────────────────────────────────\n\n/**\n * Programmatic open / close for modals registered via `ui.modals` slot.\n * Provided by ModalStack.vue.\n *\n * Module control-bar buttons call `open(id)` to show their modal.\n * Modal components call `isOpen(id)` to control their own visibility.\n */\nexport interface ModalManager {\n /** Open the modal with the given id. Closes any currently-open modal first. */\n open: (id: string) => void\n /** Close the modal with the given id. No-op if it is not open. */\n close: (id: string) => void\n /** Returns true if the modal with the given id is currently open. */\n isOpen: (id: string) => boolean\n}\n\nexport const MODAL_MANAGER_KEY: InjectionKey<ModalManager> =\n Symbol('openpen.modalManager')\n\n// ── Control Bar Layout Context ───────────────────────────────────────────────\n\n/**\n * Reference to the `.control-bar-wrapper` element. Provided by ControlBar.vue.\n * Module components that open anchor-relative popups teleport into this element.\n */\nexport const WRAPPER_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =\n Symbol('openpen.wrapperEl')\n\n/**\n * Reference to the `.control-bar` element. Provided by ControlBar.vue.\n * Module components use this as the anchor for popup positioning.\n */\nexport const ANCHOR_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =\n Symbol('openpen.anchorEl')\n\n// ── Control Bar Animating ──────────────────────────────────────────────────────\n\n/**\n * Reactive flag that is `true` while the ControlBar is running a scaleY\n * animation (expand 350ms / collapse 250ms / snap 250ms).\n *\n * Provided by ControlBar.vue as `readonly(animating)`.\n * AppPopover (and any other UIKit wrapper) watches this and auto-closes\n * when it becomes true, avoiding floating-ui position drift during animation.\n */\nexport const CONTROL_BAR_ANIMATING_KEY: InjectionKey<Readonly<Ref<boolean>>> =\n Symbol('openpen:control-bar-animating')\n\n// ── Tooltip Flip Down (near workArea top) ────────────────────────────────────\n\n/**\n * `true` when the control bar's ball is sitting close to the workArea top\n * edge, in horizontal mode. Buttons that render a tooltip above themselves\n * (`bottom: calc(100% + 8px)`) would extend off-screen in that state, so they\n * flip below the button instead.\n *\n * Provided by ControlBar.vue. Consumed by AppButton (and any other UIKit\n * wrapper that renders an `::after` tooltip).\n *\n * `false` in vertical mode regardless of position — vertical bars use a\n * sideways tooltip direction (see `SNAP_EDGE_KEY`).\n */\nexport const TOOLTIP_FLIP_DOWN_KEY: InjectionKey<Readonly<Ref<boolean>>> =\n Symbol('openpen:tooltipFlipDown')\n\n// ── Host Dialog Presence ─────────────────────────────────────────────────────\n\n/**\n * Reference-counted open-dialog count. Hosts inject this to render their\n * own \"locked\" state when > 0 — a transparent overlay window can't use a\n * dim/blur dialog backdrop without rendering as an isolated dark patch.\n */\nexport const HOST_DIALOG_OPEN_COUNT_KEY: InjectionKey<Ref<number>> =\n Symbol('openpen:hostDialogOpenCount')\n\n// ── Popover Placement Hint ────────────────────────────────────────────────────\n\n/**\n * Deliberate UX rule for initial popover placement direction.\n *\n * Encodes which side of the trigger the popover should prefer to open on,\n * based on two rules applied in priority order:\n * 1. Snap edge: opening away from the snapped edge (e.g. snap-left → 'right').\n * 2. Screen midline (when no snap edge): opening toward the side with more space\n * on the relevant axis (horizontal bar → top/bottom; vertical bar → left/right).\n *\n * The floating-ui flip middleware handles secondary overflow; this hint is the\n * proactive first choice so the popover starts on the correct side without\n * requiring a layout-measurement round-trip.\n *\n * Provided by ControlBar.vue; injected by AppPopover.vue.\n * Plugin authors do not need to consume this key directly.\n */\nexport type PopoverPlacementHint = 'top' | 'bottom' | 'left' | 'right'\n\nexport const POPOVER_PLACEMENT_HINT_KEY: InjectionKey<Readonly<Ref<PopoverPlacementHint>>> =\n Symbol('openpen:popoverPlacementHint')\n\n// ── Active Tool ───────────────────────────────────────────────────────────────\n\n/**\n * The currently active tool id. Provided by ControlBar.vue as `readonly(activeToolId)`.\n * Module tool-button components inject this for reactive active-state without\n * depending on the event-bus timing (event bus is fire-and-forget; this ref is\n * always current regardless of when the component mounts).\n */\nexport const ACTIVE_TOOL_KEY: InjectionKey<Readonly<Ref<string>>> =\n Symbol('openpen:activeTool')\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ const registry = /* @__PURE__ */ new Map();
2
+ function setModuleContext(moduleId, ctx) {
3
+ registry.set(moduleId, ctx);
4
+ }
5
+ function clearModuleContext(moduleId) {
6
+ registry.delete(moduleId);
7
+ }
8
+ function getModuleContext(moduleId) {
9
+ return registry.get(moduleId);
10
+ }
11
+ export {
12
+ clearModuleContext as c,
13
+ getModuleContext as g,
14
+ setModuleContext as s
15
+ };
16
+ //# sourceMappingURL=module-context-registry-Cn1IWYu9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-context-registry-Cn1IWYu9.js","sources":["../../src/host/module-context-registry.ts"],"sourcesContent":["/**\n * Host-side registry that backs `useModuleContext`.\n *\n * The renderer host (module-loader.ts) writes to this registry via\n * `setModuleContext` / `clearModuleContext` immediately around each\n * module's `setup()` call. Plugin-authored Vue components then read\n * from it via the public `useModuleContext` composable.\n *\n * Module code MUST NOT import from this file directly — use\n * `useModuleContext` from `@openpen/module-api` instead.\n */\nimport type { ModuleSetupContext } from '../types/module'\n\nconst registry = new Map<string, ModuleSetupContext>()\n\n/** Register (or replace) the context for a module. Called by module-loader before setup(). */\nexport function setModuleContext(moduleId: string, ctx: ModuleSetupContext): void {\n registry.set(moduleId, ctx)\n}\n\n/** Remove the context for a module. Called by module-loader in rollbackModule(). */\nexport function clearModuleContext(moduleId: string): void {\n registry.delete(moduleId)\n}\n\n/** Look up the context for a module. Returns undefined if not registered. */\nexport function getModuleContext(moduleId: string): ModuleSetupContext | undefined {\n return registry.get(moduleId)\n}\n\n/** Resets the entire registry. Test-only — do not call in production code. */\nexport function resetModuleContextRegistryForTest(): void {\n registry.clear()\n}\n"],"names":[],"mappings":"AAaA,MAAM,+BAAe,IAAA;AAGd,SAAS,iBAAiB,UAAkB,KAA+B;AAChF,WAAS,IAAI,UAAU,GAAG;AAC5B;AAGO,SAAS,mBAAmB,UAAwB;AACzD,WAAS,OAAO,QAAQ;AAC1B;AAGO,SAAS,iBAAiB,UAAkD;AACjF,SAAO,SAAS,IAAI,QAAQ;AAC9B;"}
@@ -0,0 +1,6 @@
1
+ import { _useHost } from "../host/registry.js";
2
+ const usePassthroughGuard = (target) => _useHost().usePassthroughGuard(target);
3
+ export {
4
+ usePassthroughGuard as u
5
+ };
6
+ //# sourceMappingURL=passthrough-DKU7JYuQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passthrough-DKU7JYuQ.js","sources":["../../src/host/passthrough.ts"],"sourcesContent":["/**\n * @openpen/module-api/host — Passthrough Guard\n *\n * Proxies usePassthroughGuard through the host registry so module\n * components (and UIKit wrappers) can use it without reaching past the\n * package boundary.\n */\nimport { _useHost } from './registry'\nimport type { ModuleHost } from './types'\n\nexport const usePassthroughGuard: ModuleHost['usePassthroughGuard'] = (target) =>\n _useHost().usePassthroughGuard(target)\n"],"names":[],"mappings":";AAUO,MAAM,sBAAyD,CAAC,WACrE,SAAA,EAAW,oBAAoB,MAAM;"}