@openpen/module-api 1.0.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/package.json +53 -0
  4. package/src/define-module.ts +55 -0
  5. package/src/host/color-utils.ts +18 -0
  6. package/src/host/event-bus.ts +37 -0
  7. package/src/host/host-commands.ts +9 -0
  8. package/src/host/index.ts +53 -0
  9. package/src/host/module-context-registry.ts +34 -0
  10. package/src/host/passthrough.ts +10 -0
  11. package/src/host/popup-anchor.ts +14 -0
  12. package/src/host/slot-entries.ts +9 -0
  13. package/src/host/stroke-store.ts +20 -0
  14. package/src/host/stroke-style.ts +15 -0
  15. package/src/index.ts +106 -0
  16. package/src/inject-keys.ts +143 -0
  17. package/src/locale.ts +52 -0
  18. package/src/slots.ts +135 -0
  19. package/src/types/contributions.ts +298 -0
  20. package/src/types/control-bar-layout.ts +174 -0
  21. package/src/types/module.ts +225 -0
  22. package/src/types/notification.ts +40 -0
  23. package/src/types/tool.ts +82 -0
  24. package/src/uikit/components/.gitkeep +0 -0
  25. package/src/uikit/components/AppBanner.vue +131 -0
  26. package/src/uikit/components/AppButton.vue +135 -0
  27. package/src/uikit/components/AppDialog.vue +393 -0
  28. package/src/uikit/components/AppPopover.vue +268 -0
  29. package/src/uikit/components/AppSegmented.vue +126 -0
  30. package/src/uikit/components/AppSelect.vue +235 -0
  31. package/src/uikit/components/AppSlider.vue +204 -0
  32. package/src/uikit/components/AppTabs.vue +163 -0
  33. package/src/uikit/components/AppToggle.vue +96 -0
  34. package/src/uikit/components/AppTooltip.vue +118 -0
  35. package/src/uikit/components/DialogHost.vue +260 -0
  36. package/src/uikit/composables/dialog-internal-keys.ts +17 -0
  37. package/src/uikit/composables/useDialog.ts +141 -0
  38. package/src/uikit/composables/useDialogPluginComponent.ts +27 -0
  39. package/src/uikit/index.ts +79 -0
  40. package/src/uikit/internal.ts +12 -0
  41. package/src/uikit/primitives.ts +150 -0
  42. package/src/uikit/tokens.css +205 -0
  43. package/src/use-module-context.ts +50 -0
  44. package/src/validation.ts +31 -0
@@ -0,0 +1,205 @@
1
+ /* packages/module-api/src/uikit/tokens.css
2
+ *
3
+ * OpenPen UIKit design tokens. All tokens use the `--openpen-*` prefix
4
+ * to avoid collision with host or third-party CSS.
5
+ *
6
+ * Plugins MAY consume via var(--openpen-*); MUST NOT hardcode values.
7
+ *
8
+ * Values extracted from:
9
+ * - src/style.css (global CSS variables)
10
+ * - src/styles/control-bar-items.css (control bar item chrome)
11
+ *
12
+ * Token naming follows semantic > visual convention.
13
+ * e.g. --openpen-color-accent (semantic), NOT --openpen-color-indigo-400 (visual)
14
+ */
15
+
16
+ :root {
17
+ /* ── Color (semantic; theme module may override actual values) ─────────── */
18
+
19
+ /* Accent — primary brand / interactive highlight color */
20
+ /* Source: src/style.css --accent: #818CF8 */
21
+ --openpen-color-accent: #818cf8;
22
+
23
+ /* Accent hover — slightly deeper accent for :hover state */
24
+ /* Source: src/style.css --accent-glow used as reference; approximated from #6366f1 */
25
+ --openpen-color-accent-hover: #6366f1;
26
+
27
+ /* Accent background — subtle tinted background for active items */
28
+ /* Source: src/style.css --accent-bg: rgba(129, 140, 248, 0.18) */
29
+ --openpen-color-accent-bg: rgba(129, 140, 248, 0.18);
30
+
31
+ /* Accent glow — used for box-shadow rings on active elements */
32
+ /* Source: src/style.css --accent-glow: rgba(129, 140, 248, 0.35) */
33
+ --openpen-color-accent-glow: rgba(129, 140, 248, 0.35);
34
+
35
+ /* Surface (dark) — main background for floating panels, bars */
36
+ /* Source: src/style.css --surface: rgba(18, 26, 48, 0.88) */
37
+ --openpen-color-surface: rgba(18, 26, 48, 0.88);
38
+
39
+ /* Surface hi — slightly elevated surface (nested panels, hover states) */
40
+ /* Source: src/style.css --surface-hi: rgba(30, 41, 70, 0.92) */
41
+ --openpen-color-surface-hi: rgba(30, 41, 70, 0.92);
42
+
43
+ /* Surface popup — background for popover / dropdown panels */
44
+ /* Source: src/style.css --surface-popup: rgba(20, 28, 50, 0.90) */
45
+ --openpen-color-surface-popup: rgba(20, 28, 50, 0.90);
46
+
47
+ /* Border — default subtle border */
48
+ /* Source: src/style.css --border: rgba(255, 255, 255, 0.10) */
49
+ --openpen-color-border: rgba(255, 255, 255, 0.10);
50
+
51
+ /* Border hi — higher-contrast border for focus rings and popup frames */
52
+ /* Source: src/style.css --border-hi: rgba(255, 255, 255, 0.20) */
53
+ --openpen-color-border-hi: rgba(255, 255, 255, 0.20);
54
+
55
+ /* Popover frame — shared by the popover's border AND arrow fill so they
56
+ * read as a single, continuous edge. Separate from --border-hi because the
57
+ * popover frame in light theme needs more presence (the near-opaque white
58
+ * surface buries 14% black). In dark theme it inherits border-hi unchanged. */
59
+ --openpen-color-popover-frame: var(--openpen-color-border-hi);
60
+
61
+ /* Text primary — main content text */
62
+ /* Source: src/style.css --text-primary: #F1F5F9 */
63
+ --openpen-color-text-primary: #f1f5f9;
64
+
65
+ /* Text dim — secondary / label text */
66
+ /* Source: src/style.css --text-dim: #94A3B8 */
67
+ --openpen-color-text-dim: #94a3b8;
68
+
69
+ /* Text muted — placeholder / disabled text */
70
+ /* Source: src/style.css --text-muted: #64748B */
71
+ --openpen-color-text-muted: #64748b;
72
+
73
+ /* Tooltip background, text, and border — always dark regardless of theme */
74
+ /* Source: src/style.css --tooltip-bg: rgba(15, 23, 42, 0.96) */
75
+ --openpen-color-tooltip-bg: rgba(15, 23, 42, 0.96);
76
+ --openpen-color-tooltip-text: #f1f5f9;
77
+ --openpen-color-tooltip-border: rgba(255, 255, 255, 0.15);
78
+
79
+ /* Control bar hover background */
80
+ /* Source: src/styles/control-bar-items.css --cb-hover-bg: rgba(255, 255, 255, 0.08) */
81
+ --openpen-color-control-hover: rgba(255, 255, 255, 0.08);
82
+
83
+ /* Control bar group background */
84
+ /* Source: src/styles/control-bar-items.css --cb-group-bg: rgba(255, 255, 255, 0.04) */
85
+ --openpen-color-control-group: rgba(255, 255, 255, 0.04);
86
+
87
+ /* ── Radius ────────────────────────────────────────────────────────────── */
88
+
89
+ /* Source: src/styles/control-bar-items.css — tooltip border-radius: 7px,
90
+ * cb-btn border-radius: 10px, cb-group border-radius: 11px,
91
+ * popup panels typically ~10-14px; mapped to sm/md/lg scale */
92
+ --openpen-radius-sm: 6px;
93
+ --openpen-radius-md: 10px;
94
+ --openpen-radius-lg: 14px;
95
+
96
+ /* ── Spacing ───────────────────────────────────────────────────────────── */
97
+
98
+ /* Source: control-bar-items.css uses 4px, 8px, 12px, 16px consistently */
99
+ --openpen-space-xs: 4px;
100
+ --openpen-space-sm: 8px;
101
+ --openpen-space-md: 12px;
102
+ --openpen-space-lg: 16px;
103
+
104
+ /* ── Animation duration / easing ──────────────────────────────────────── */
105
+
106
+ /* Source: src/styles/control-bar-items.css
107
+ * .cb-btn transition: background 150ms, color 150ms, transform 150ms
108
+ * ControlBar.vue expand: 350ms cubic-bezier(0.34, 1.56, 0.64, 1)
109
+ * ControlBar.vue collapse: 250ms same easing */
110
+ --openpen-duration-fast: 150ms;
111
+ --openpen-duration-base: 250ms;
112
+ --openpen-duration-bounce: 400ms;
113
+
114
+ /* Source: ControlBar.vue / src/styles/control-bar-items.css
115
+ * .cb-eraser-caret-icon transition: cubic-bezier(0.34, 1.56, 0.64, 1) */
116
+ --openpen-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
117
+ --openpen-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
118
+
119
+ /* State colors — info / warning / success / error (dark) */
120
+ --openpen-color-state-info-bg: rgba(59, 130, 246, 0.10);
121
+ --openpen-color-state-info-border: rgba(59, 130, 246, 0.26);
122
+ --openpen-color-state-info-text: #93C5FD;
123
+ --openpen-color-state-info-icon: #60A5FA;
124
+ --openpen-color-state-warning-bg: rgba(251, 191, 36, 0.10);
125
+ --openpen-color-state-warning-border: rgba(251, 191, 36, 0.28);
126
+ --openpen-color-state-warning-text: #FDE68A;
127
+ --openpen-color-state-warning-icon: #FBBF24;
128
+ --openpen-color-state-success-bg: rgba(52, 211, 153, 0.10);
129
+ --openpen-color-state-success-border: rgba(52, 211, 153, 0.26);
130
+ --openpen-color-state-success-text: #6EE7B7;
131
+ --openpen-color-state-success-icon: #34D399;
132
+ --openpen-color-state-error-bg: rgba(248, 113, 113, 0.10);
133
+ --openpen-color-state-error-border: rgba(248, 113, 113, 0.26);
134
+ --openpen-color-state-error-text: #FCA5A5;
135
+ --openpen-color-state-error-icon: #F87171;
136
+
137
+ /* Danger — destructive / error button foreground and hover states */
138
+ /* Source: AppButton.vue danger variant; matches red-500 / red-400 scale */
139
+ --openpen-color-danger: #ef4444;
140
+ --openpen-color-danger-hover: #f87171;
141
+ --openpen-color-danger-hover-bg: rgba(248, 113, 113, 0.12);
142
+
143
+ /* Toggle off background — inactive state of AppToggle */
144
+ /* Source: src/style.css --toggle-off-bg: rgba(255, 255, 255, 0.12) */
145
+ --openpen-color-toggle-off: rgba(255, 255, 255, 0.12);
146
+
147
+ /* Input background — background for form inputs */
148
+ /* Source: src/style.css --input-bg: rgba(255, 255, 255, 0.07) */
149
+ --openpen-color-input-bg: rgba(255, 255, 255, 0.07);
150
+
151
+ /* ── Shadow ────────────────────────────────────────────────────────────── */
152
+
153
+ /* Source: src/style.css --shadow + --shadow-sm */
154
+ --openpen-shadow: 0 8px 32px rgba(0, 0, 0, 0.55), 0 1px 0 rgba(255, 255, 255, 0.06) inset;
155
+ --openpen-shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.40);
156
+
157
+ /* ── Backdrop filter ───────────────────────────────────────────────────── */
158
+
159
+ /* Source: src/style.css --blur: blur(18px) saturate(160%) */
160
+ --openpen-blur: blur(18px) saturate(160%);
161
+ }
162
+
163
+ /* ── Light theme overrides ─────────────────────────────────────────────────── */
164
+ /* Source: src/style.css [data-theme="light"] block */
165
+
166
+ [data-theme='light'] {
167
+ --openpen-color-surface: rgba(255, 255, 255, 0.92);
168
+ --openpen-color-surface-hi: rgba(248, 250, 252, 0.98);
169
+ --openpen-color-surface-popup: rgba(255, 255, 255, 0.98);
170
+ --openpen-color-border: rgba(0, 0, 0, 0.08);
171
+ --openpen-color-border-hi: rgba(0, 0, 0, 0.14);
172
+ /* Slate-400 (#94a3b8) at 70% — a light, cool-gray semi-transparent tint that
173
+ * mirrors dark theme's lighter-than-surface frame treatment instead of
174
+ * stamping a hard dark wedge onto the near-white popup. Applied to both
175
+ * the popover border and arrow fill so the edge reads as one continuous tone. */
176
+ --openpen-color-popover-frame: rgba(148, 163, 184, 0.7);
177
+ --openpen-color-text-primary: #0f172a;
178
+ --openpen-color-text-dim: #64748b;
179
+ --openpen-color-text-muted: #94a3b8;
180
+ --openpen-color-control-hover: rgba(0, 0, 0, 0.06);
181
+ --openpen-color-control-group: rgba(0, 0, 0, 0.04);
182
+ --openpen-color-toggle-off: rgba(0, 0, 0, 0.15);
183
+ --openpen-color-input-bg: rgba(0, 0, 0, 0.05);
184
+ --openpen-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 1px 0 rgba(255, 255, 255, 0.80) inset;
185
+ --openpen-shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.08);
186
+ /* State colors — light overrides */
187
+ --openpen-color-state-info-bg: rgba(59, 130, 246, 0.07);
188
+ --openpen-color-state-info-border: rgba(59, 130, 246, 0.22);
189
+ --openpen-color-state-info-text: #1D4ED8;
190
+ --openpen-color-state-info-icon: #2563EB;
191
+ --openpen-color-state-warning-bg: rgba(245, 158, 11, 0.08);
192
+ --openpen-color-state-warning-border: rgba(245, 158, 11, 0.28);
193
+ --openpen-color-state-warning-text: #92400E;
194
+ --openpen-color-state-warning-icon: #B45309;
195
+ --openpen-color-state-success-bg: rgba(16, 185, 129, 0.08);
196
+ --openpen-color-state-success-border: rgba(16, 185, 129, 0.24);
197
+ --openpen-color-state-success-text: #065F46;
198
+ --openpen-color-state-success-icon: #059669;
199
+ --openpen-color-state-error-bg: rgba(239, 68, 68, 0.08);
200
+ --openpen-color-state-error-border: rgba(239, 68, 68, 0.24);
201
+ --openpen-color-state-error-text: #991B1B;
202
+ --openpen-color-state-error-icon: #DC2626;
203
+
204
+ /* accent / radius / spacing / duration / easing unchanged in light mode */
205
+ }
@@ -0,0 +1,50 @@
1
+ import { getModuleContext } from './host/module-context-registry'
2
+ import type { ModuleSetupContext } from './types/module'
3
+
4
+ /**
5
+ * Returns the `ModuleSetupContext` that was constructed for `moduleId` by
6
+ * the host runtime. Call this inside a Vue component's `<script setup>` (or
7
+ * `setup()`) to access `ctx.getSettings`, `ctx.updateSettings`,
8
+ * `ctx.onSettingsChange`, `ctx.notify`, and all other context methods from
9
+ * within a component tree — without having to prop-drill or provide/inject
10
+ * the context object manually.
11
+ *
12
+ * @throws If `moduleId` is not currently registered (module not yet loaded,
13
+ * already rolled back, or the id string doesn't match `defineModule({ id })`).
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // MySettingsPanel.vue
18
+ * import { useModuleContext } from '@openpen/module-api'
19
+ * import { onMounted, onUnmounted, ref } from 'vue'
20
+ * import { z } from 'zod'
21
+ *
22
+ * const MySchema = z.object({ color: z.string().default('#fff') })
23
+ * type MySettings = z.infer<typeof MySchema>
24
+ *
25
+ * const ctx = useModuleContext('my-plugin')
26
+ * const color = ref(ctx.getSettings<MySettings>().color)
27
+ *
28
+ * let unsubscribe: (() => void) | null = null
29
+ * onMounted(() => {
30
+ * unsubscribe = ctx.onSettingsChange<MySettings>((s) => { color.value = s.color })
31
+ * })
32
+ * onUnmounted(() => unsubscribe?.())
33
+ *
34
+ * async function changeColor(next: string) {
35
+ * await ctx.updateSettings<MySettings>({ color: next })
36
+ * }
37
+ * ```
38
+ */
39
+ export function useModuleContext(moduleId: string): ModuleSetupContext {
40
+ const ctx = getModuleContext(moduleId)
41
+ if (!ctx) {
42
+ throw new Error(
43
+ `[module-api] useModuleContext("${moduleId}") was called but no module is currently registered under that id. ` +
44
+ `This usually means: (a) the moduleId string does not match the id passed to defineModule, or ` +
45
+ `(b) the composable was called before the module's setup() ran ` +
46
+ `(do not call useModuleContext at module top-level — call it inside a component's setup() or onMounted()).`
47
+ )
48
+ }
49
+ return ctx
50
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Scoped module / plugin ID format: `@scope/name`
3
+ *
4
+ * Rules:
5
+ * - Must start with `@`
6
+ * - scope: lowercase ASCII alphanumeric + hyphen, starts with letter or digit,
7
+ * max 39 chars (mirrors GitHub username/org limits)
8
+ * - Single `/` separator
9
+ * - name: same charset and length limit as scope
10
+ *
11
+ * The same regex is used by the OpenPen CLI (`openpen-cli`) and the
12
+ * main process plugin loader, so plugin IDs round-trip safely between
13
+ * `plugin.json`, filesystem paths under `~/.openpen/plugins/@scope/name/`,
14
+ * and registry lookups.
15
+ */
16
+ export const MODULE_ID_RE =
17
+ /^@([a-z0-9][a-z0-9-]{0,38})\/([a-z0-9][a-z0-9-]{0,38})$/
18
+
19
+ /**
20
+ * Returns `true` when `id` is a non-empty string that satisfies
21
+ * {@link MODULE_ID_RE}; acts as a type guard narrowing `unknown` → `string`.
22
+ *
23
+ * @example
24
+ * isValidModuleId('@openpen/freehand') // true
25
+ * isValidModuleId('@alice/todo-app') // true
26
+ * isValidModuleId('freehand') // false — must be scoped @scope/name
27
+ * isValidModuleId('') // false — empty string rejected
28
+ */
29
+ export function isValidModuleId(id: unknown): id is string {
30
+ return typeof id === 'string' && id.length > 0 && MODULE_ID_RE.test(id)
31
+ }