@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +53 -0
- package/src/define-module.ts +55 -0
- package/src/host/color-utils.ts +18 -0
- package/src/host/event-bus.ts +37 -0
- package/src/host/host-commands.ts +9 -0
- package/src/host/index.ts +53 -0
- package/src/host/module-context-registry.ts +34 -0
- package/src/host/passthrough.ts +10 -0
- package/src/host/popup-anchor.ts +14 -0
- package/src/host/slot-entries.ts +9 -0
- package/src/host/stroke-store.ts +20 -0
- package/src/host/stroke-style.ts +15 -0
- package/src/index.ts +106 -0
- package/src/inject-keys.ts +143 -0
- package/src/locale.ts +52 -0
- package/src/slots.ts +135 -0
- package/src/types/contributions.ts +298 -0
- package/src/types/control-bar-layout.ts +174 -0
- package/src/types/module.ts +225 -0
- package/src/types/notification.ts +40 -0
- package/src/types/tool.ts +82 -0
- package/src/uikit/components/.gitkeep +0 -0
- package/src/uikit/components/AppBanner.vue +131 -0
- package/src/uikit/components/AppButton.vue +135 -0
- package/src/uikit/components/AppDialog.vue +393 -0
- package/src/uikit/components/AppPopover.vue +268 -0
- package/src/uikit/components/AppSegmented.vue +126 -0
- package/src/uikit/components/AppSelect.vue +235 -0
- package/src/uikit/components/AppSlider.vue +204 -0
- package/src/uikit/components/AppTabs.vue +163 -0
- package/src/uikit/components/AppToggle.vue +96 -0
- package/src/uikit/components/AppTooltip.vue +118 -0
- package/src/uikit/components/DialogHost.vue +260 -0
- package/src/uikit/composables/dialog-internal-keys.ts +17 -0
- package/src/uikit/composables/useDialog.ts +141 -0
- package/src/uikit/composables/useDialogPluginComponent.ts +27 -0
- package/src/uikit/index.ts +79 -0
- package/src/uikit/internal.ts +12 -0
- package/src/uikit/primitives.ts +150 -0
- package/src/uikit/tokens.css +205 -0
- package/src/use-module-context.ts +50 -0
- package/src/validation.ts +31 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue InjectionKey symbols that host code provides and module components inject.
|
|
3
|
+
*
|
|
4
|
+
* Module components MUST NOT import host composables directly (boundary rule).
|
|
5
|
+
* Instead, the host provides these contexts via Vue's provide/inject, and
|
|
6
|
+
* modules reference the keys via this file (part of the public SDK surface).
|
|
7
|
+
*/
|
|
8
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
9
|
+
import type { StrokeColor } from './types/tool'
|
|
10
|
+
|
|
11
|
+
// ── Stroke Style ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provides read + write access to the global stroke style state.
|
|
15
|
+
* Provided by App.vue (root); available in all renderer windows.
|
|
16
|
+
*
|
|
17
|
+
* Module components: inject via `inject(STROKE_STYLE_CONTEXT_KEY)`.
|
|
18
|
+
*/
|
|
19
|
+
export interface StrokeStyleContext {
|
|
20
|
+
lineWidth: Readonly<Ref<number>>
|
|
21
|
+
color: Readonly<Ref<StrokeColor>>
|
|
22
|
+
setLineWidth: (w: number) => void
|
|
23
|
+
setColor: (c: StrokeColor) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const STROKE_STYLE_CONTEXT_KEY: InjectionKey<StrokeStyleContext> =
|
|
27
|
+
Symbol('openpen.strokeStyleContext')
|
|
28
|
+
|
|
29
|
+
// ── Snap Edge ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which side of the screen workArea the floating ball is anchored to.
|
|
33
|
+
*
|
|
34
|
+
* - 'left' → ball anchored on the left edge (vbar-left layout)
|
|
35
|
+
* - 'right' → ball anchored on the right edge (vbar-right layout)
|
|
36
|
+
* - 'top' → ball anchored on the top edge (horizontal bar, top)
|
|
37
|
+
* - 'bottom' → ball anchored on the bottom edge (horizontal bar, bottom)
|
|
38
|
+
* - null → not snapped (free-floating or horizontal default)
|
|
39
|
+
*
|
|
40
|
+
* Consumers that need a ready-to-use popup direction (the opposite of the
|
|
41
|
+
* snap edge) should inject POPOVER_PLACEMENT_HINT_KEY instead — it encodes
|
|
42
|
+
* the two-tier UX rule (snap edge → midline fallback) so individual components
|
|
43
|
+
* do not have to replicate the logic.
|
|
44
|
+
*
|
|
45
|
+
* Use IS_VERTICAL_KEY for orientation checks (vbar vs horizontal); this key
|
|
46
|
+
* is null in vbar-free mode where no edge is snapped.
|
|
47
|
+
*/
|
|
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
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True for any vertical layout (vbar-left / vbar-right / vbar-free). Use
|
|
57
|
+
* this for orientation; SNAP_EDGE_KEY is edge-specific and null in vbar-free.
|
|
58
|
+
*/
|
|
59
|
+
export const IS_VERTICAL_KEY: InjectionKey<Readonly<Ref<boolean>>> =
|
|
60
|
+
Symbol('openpen.isVertical')
|
|
61
|
+
|
|
62
|
+
// ── Modal Manager ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Programmatic open / close for modals registered via `ui.modals` slot.
|
|
66
|
+
* Provided by ModalStack.vue.
|
|
67
|
+
*
|
|
68
|
+
* Module control-bar buttons call `open(id)` to show their modal.
|
|
69
|
+
* Modal components call `isOpen(id)` to control their own visibility.
|
|
70
|
+
*/
|
|
71
|
+
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
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const MODAL_MANAGER_KEY: InjectionKey<ModalManager> =
|
|
81
|
+
Symbol('openpen.modalManager')
|
|
82
|
+
|
|
83
|
+
// ── Control Bar Layout Context ───────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reference to the `.control-bar-wrapper` element. Provided by ControlBar.vue.
|
|
87
|
+
* Module components that open anchor-relative popups teleport into this element.
|
|
88
|
+
*/
|
|
89
|
+
export const WRAPPER_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =
|
|
90
|
+
Symbol('openpen.wrapperEl')
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Reference to the `.control-bar` element. Provided by ControlBar.vue.
|
|
94
|
+
* Module components use this as the anchor for popup positioning.
|
|
95
|
+
*/
|
|
96
|
+
export const ANCHOR_EL_KEY: InjectionKey<Readonly<Ref<HTMLElement | null>>> =
|
|
97
|
+
Symbol('openpen.anchorEl')
|
|
98
|
+
|
|
99
|
+
// ── Control Bar Animating ──────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reactive flag that is `true` while the ControlBar is running a scaleY
|
|
103
|
+
* animation (expand 350ms / collapse 250ms / snap 250ms).
|
|
104
|
+
*
|
|
105
|
+
* Provided by ControlBar.vue as `readonly(animating)`.
|
|
106
|
+
* AppPopover (and any other UIKit wrapper) watches this and auto-closes
|
|
107
|
+
* when it becomes true, avoiding floating-ui position drift during animation.
|
|
108
|
+
*/
|
|
109
|
+
export const CONTROL_BAR_ANIMATING_KEY: InjectionKey<Readonly<Ref<boolean>>> =
|
|
110
|
+
Symbol('openpen:control-bar-animating')
|
|
111
|
+
|
|
112
|
+
// ── Host Dialog Presence ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reference-counted open-dialog count. Hosts inject this to render their
|
|
116
|
+
* own "locked" state when > 0 — a transparent overlay window can't use a
|
|
117
|
+
* dim/blur dialog backdrop without rendering as an isolated dark patch.
|
|
118
|
+
*/
|
|
119
|
+
export const HOST_DIALOG_OPEN_COUNT_KEY: InjectionKey<Ref<number>> =
|
|
120
|
+
Symbol('openpen:hostDialogOpenCount')
|
|
121
|
+
|
|
122
|
+
// ── Popover Placement Hint ────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deliberate UX rule for initial popover placement direction.
|
|
126
|
+
*
|
|
127
|
+
* Encodes which side of the trigger the popover should prefer to open on,
|
|
128
|
+
* based on two rules applied in priority order:
|
|
129
|
+
* 1. Snap edge: opening away from the snapped edge (e.g. snap-left → 'right').
|
|
130
|
+
* 2. Screen midline (when no snap edge): opening toward the side with more space
|
|
131
|
+
* on the relevant axis (horizontal bar → top/bottom; vertical bar → left/right).
|
|
132
|
+
*
|
|
133
|
+
* The floating-ui flip middleware handles secondary overflow; this hint is the
|
|
134
|
+
* proactive first choice so the popover starts on the correct side without
|
|
135
|
+
* requiring a layout-measurement round-trip.
|
|
136
|
+
*
|
|
137
|
+
* Provided by ControlBar.vue; injected by AppPopover.vue.
|
|
138
|
+
* Plugin authors do not need to consume this key directly.
|
|
139
|
+
*/
|
|
140
|
+
export type PopoverPlacementHint = 'top' | 'bottom' | 'left' | 'right'
|
|
141
|
+
|
|
142
|
+
export const POPOVER_PLACEMENT_HINT_KEY: InjectionKey<Readonly<Ref<PopoverPlacementHint>>> =
|
|
143
|
+
Symbol('openpen:popoverPlacementHint')
|
package/src/locale.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a scoped module id to a vue-i18n-safe namespace key.
|
|
3
|
+
*
|
|
4
|
+
* vue-i18n uses `.` as path separator, so a raw id like `@openpen/freehand`
|
|
5
|
+
* would be misinterpreted as a three-level path `['@openpen', 'freehand']`.
|
|
6
|
+
* Stripping `@` and replacing `/` with `.` produces `openpen.freehand`,
|
|
7
|
+
* which resolves correctly as a two-level namespace.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* sanitizeIdForI18n('@openpen/freehand') // 'openpen.freehand'
|
|
11
|
+
* sanitizeIdForI18n('@alice/todo') // 'alice.todo'
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizeIdForI18n(moduleId: string): string {
|
|
14
|
+
return moduleId.replace(/^@/, '').replace(/\//g, '.')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maps a BCP-47 locale tag (e.g. `'en'`, `'zh-TW'`) to a localised string.
|
|
19
|
+
*
|
|
20
|
+
* Modules use this to declare i18n display names without coupling
|
|
21
|
+
* themselves to the host app's i18n framework. The OpenPen registry
|
|
22
|
+
* resolves the value at render time based on the active locale.
|
|
23
|
+
*/
|
|
24
|
+
export type LocaleMap = Record<string, string>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a `string | LocaleMap` to a single string for a given locale.
|
|
28
|
+
*
|
|
29
|
+
* Resolution order (first match wins):
|
|
30
|
+
* 1. Plain string → returned as-is
|
|
31
|
+
* 2. Exact tag match (e.g. `'zh-TW'`)
|
|
32
|
+
* 3. Language prefix match (e.g. `'zh'` for `'zh-Hans-CN'`)
|
|
33
|
+
* 4. English fallback (`'en'`)
|
|
34
|
+
* 5. First declared value in the map
|
|
35
|
+
* 6. Empty string (only when the map is `{}`)
|
|
36
|
+
*/
|
|
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
|
+
}
|
package/src/slots.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenPen Contribution Slot catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Slots are the only places where modules can affect the host app.
|
|
5
|
+
* Adding a slot is an architectural change — it usually requires a
|
|
6
|
+
* new adapter on the OpenPen side and a minor version bump of this
|
|
7
|
+
* package. Removing a slot is a breaking change.
|
|
8
|
+
*
|
|
9
|
+
* `status: 'v1'` slots are wired through to runtime adapters.
|
|
10
|
+
* `status: 'reserved'` slots accept registrations and pass validation;
|
|
11
|
+
* no runtime adapter is active yet so contributions are stored but
|
|
12
|
+
* inert. Forward-compatible: modules can ship against reserved slots
|
|
13
|
+
* without any plugin-side changes once an adapter lands.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type SlotCategory = 'canvas' | 'ui' | 'system'
|
|
17
|
+
export type SlotStatus = 'v1' | 'reserved'
|
|
18
|
+
|
|
19
|
+
export interface SlotDefinition {
|
|
20
|
+
/** Dotted slot id, e.g. `'canvas.tools'`. */
|
|
21
|
+
id: string
|
|
22
|
+
/** Implementation status — see file header. */
|
|
23
|
+
status: SlotStatus
|
|
24
|
+
/** High-level grouping for documentation and UI. */
|
|
25
|
+
category: SlotCategory
|
|
26
|
+
/** One-sentence purpose, shown in `docs/reference/slots.md`. */
|
|
27
|
+
description: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* v1 active slots (16). Wired through to runtime adapters.
|
|
32
|
+
*/
|
|
33
|
+
export const V1_ACTIVE_SLOTS: readonly SlotDefinition[] = [
|
|
34
|
+
// Canvas
|
|
35
|
+
{ id: 'canvas.tools', status: 'v1', category: 'canvas', description: 'Drawing tools driven by pointer events.' },
|
|
36
|
+
{ id: 'canvas.shapes', status: 'v1', category: 'canvas', description: 'Shape primitives consumed by the shape tool.' },
|
|
37
|
+
{ id: 'canvas.stroke.style', status: 'v1', category: 'canvas', description: 'Writers into the shared stroke style store.' },
|
|
38
|
+
{ id: 'canvas.layers.background', status: 'v1', category: 'canvas', description: 'Render below strokes (grids, watermarks).' },
|
|
39
|
+
{ id: 'canvas.layers.overlay', status: 'v1', category: 'canvas', description: 'Render above strokes (rulers, guides).' },
|
|
40
|
+
{ id: 'canvas.html.overlay', status: 'v1', category: 'canvas', description: 'Mount HTML elements above the canvas (text, stickers).' },
|
|
41
|
+
// UI
|
|
42
|
+
{ id: 'ui.control-bar', status: 'v1', category: 'ui', description: 'Control bar item slot (groups and ordering managed by layout config).' },
|
|
43
|
+
{ id: 'ui.settings.tabs', status: 'v1', category: 'ui', description: 'Tabs in the settings window.' },
|
|
44
|
+
{ id: 'ui.settings.panels', status: 'v1', category: 'ui', description: 'Sections inside the Features settings tab, grouped by module.' },
|
|
45
|
+
{ id: 'ui.cursors', status: 'v1', category: 'ui', description: 'Drawing-mode cursor styles.' },
|
|
46
|
+
{ id: 'ui.status', status: 'v1', category: 'ui', description: 'Ephemeral status badges on the control bar.' },
|
|
47
|
+
{ id: 'ui.modals', status: 'v1', category: 'ui', description: 'Registered modals managed by the global modal stack.' },
|
|
48
|
+
// System
|
|
49
|
+
{ id: 'system.shortcuts', status: 'v1', category: 'system', description: 'Global and drawing-mode keyboard shortcuts.' },
|
|
50
|
+
{ id: 'system.locales', status: 'v1', category: 'system', description: 'i18n dictionary contributions.' },
|
|
51
|
+
{ id: 'system.main.handlers', status: 'v1', category: 'system', description: 'Node-side IPC handlers for main process capabilities.' },
|
|
52
|
+
{ id: 'system.events', status: 'v1', category: 'system', description: 'Subscribers and emitters on the domain event bus.' },
|
|
53
|
+
{ id: 'system.lifecycle', status: 'v1', category: 'system', description: 'App lifecycle hooks (onReady / onSuspend / onQuit).' },
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* v1 reserved slots. Registrations pass validation but no runtime
|
|
58
|
+
* adapter is active yet.
|
|
59
|
+
*/
|
|
60
|
+
export const V1_RESERVED_SLOTS: readonly SlotDefinition[] = [
|
|
61
|
+
{ id: 'canvas.history.commands', status: 'reserved', category: 'canvas', description: 'Undo/redo history commands. Adapter not yet active; built-in command types are used internally.' },
|
|
62
|
+
{ id: 'canvas.stroke.transformers', status: 'reserved', category: 'canvas', description: 'Post-process strokes after creation (smooth, simplify).' },
|
|
63
|
+
{ id: 'ui.tray.menu', status: 'reserved', category: 'ui', description: 'System tray menu items. Adapter not yet active.' },
|
|
64
|
+
{ id: 'ui.context.menu', status: 'reserved', category: 'ui', description: 'Right-click context menu items on canvas / control bar.' },
|
|
65
|
+
{ id: 'ui.theme.tokens', status: 'reserved', category: 'ui', description: 'CSS custom property contributions (colour swatches, spacing tokens).' },
|
|
66
|
+
{ id: 'system.window.behaviors', status: 'reserved', category: 'system', description: 'Window behavior customizations. Adapter not yet active.' },
|
|
67
|
+
{ 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.' },
|
|
68
|
+
{ id: 'system.file.drop', status: 'reserved', category: 'system', description: 'Handlers for files dropped onto the canvas.' },
|
|
69
|
+
] as const
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* All slots (active + reserved). Source of truth for validation.
|
|
73
|
+
*/
|
|
74
|
+
export const ALL_SLOTS: readonly SlotDefinition[] = [
|
|
75
|
+
...V1_ACTIVE_SLOTS,
|
|
76
|
+
...V1_RESERVED_SLOTS,
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
const SLOT_BY_ID = new Map<string, SlotDefinition>(
|
|
80
|
+
ALL_SLOTS.map((s) => [s.id, s])
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
/** Returns `true` when `id` matches a slot in {@link ALL_SLOTS} (active or reserved). */
|
|
84
|
+
export function isKnownSlot(id: string): boolean {
|
|
85
|
+
return SLOT_BY_ID.has(id)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns the {@link SlotDefinition} for a known slot id, or `undefined` if
|
|
90
|
+
* the id is not registered in {@link ALL_SLOTS}.
|
|
91
|
+
*/
|
|
92
|
+
export function getSlot(id: string): SlotDefinition | undefined {
|
|
93
|
+
return SLOT_BY_ID.get(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Mapping from `contributes` field name (camelCase) to slot id (dotted).
|
|
98
|
+
*
|
|
99
|
+
* Modules express contributions via the ergonomic camelCase keys on
|
|
100
|
+
* `contributes` (e.g. `historyCommands`); the validator maps them
|
|
101
|
+
* back to slot ids (`canvas.history.commands`) before checking against
|
|
102
|
+
* `ALL_SLOTS`.
|
|
103
|
+
*
|
|
104
|
+
* Adding a new field here is part of adding a new slot.
|
|
105
|
+
*/
|
|
106
|
+
export const CONTRIBUTION_KEY_TO_SLOT_ID: Record<string, string> = {
|
|
107
|
+
// Canvas
|
|
108
|
+
tools: 'canvas.tools',
|
|
109
|
+
shapes: 'canvas.shapes',
|
|
110
|
+
strokeStyle: 'canvas.stroke.style',
|
|
111
|
+
historyCommands: 'canvas.history.commands',
|
|
112
|
+
backgroundLayers: 'canvas.layers.background',
|
|
113
|
+
overlayLayers: 'canvas.layers.overlay',
|
|
114
|
+
htmlOverlays: 'canvas.html.overlay',
|
|
115
|
+
strokeTransformers: 'canvas.stroke.transformers',
|
|
116
|
+
// UI
|
|
117
|
+
controlBar: 'ui.control-bar',
|
|
118
|
+
settingsTabs: 'ui.settings.tabs',
|
|
119
|
+
settingsPanels: 'ui.settings.panels',
|
|
120
|
+
cursors: 'ui.cursors',
|
|
121
|
+
status: 'ui.status',
|
|
122
|
+
modals: 'ui.modals',
|
|
123
|
+
trayMenu: 'ui.tray.menu',
|
|
124
|
+
contextMenu: 'ui.context.menu',
|
|
125
|
+
themeTokens: 'ui.theme.tokens',
|
|
126
|
+
// System
|
|
127
|
+
shortcuts: 'system.shortcuts',
|
|
128
|
+
windowBehaviors: 'system.window.behaviors',
|
|
129
|
+
locales: 'system.locales',
|
|
130
|
+
events: 'system.events',
|
|
131
|
+
lifecycle: 'system.lifecycle',
|
|
132
|
+
storage: 'system.storage',
|
|
133
|
+
mainHandlers: 'system.main.handlers',
|
|
134
|
+
fileDrop: 'system.file.drop',
|
|
135
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type { LocaleMap } from '../locale'
|
|
3
|
+
import type { Stroke, Tool } from './tool'
|
|
4
|
+
import type { ControlBarContribution } from './control-bar-layout'
|
|
5
|
+
|
|
6
|
+
export type { ControlBarContribution }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The shape of `OpenPenModule.contributes`. Every field is optional;
|
|
10
|
+
* `defineModule` requires at least one field to be present and rejects
|
|
11
|
+
* any unknown field at module-author boundary time.
|
|
12
|
+
*
|
|
13
|
+
* Each field corresponds to a slot in {@link import('../slots').ALL_SLOTS}.
|
|
14
|
+
* The mapping from camelCase field name to dotted slot id lives in
|
|
15
|
+
* `CONTRIBUTION_KEY_TO_SLOT_ID`.
|
|
16
|
+
*/
|
|
17
|
+
export interface ModuleContributions {
|
|
18
|
+
// ── Canvas ──────────────────────────────────────────────────────────────
|
|
19
|
+
tools?: ToolContribution[]
|
|
20
|
+
shapes?: ShapeContribution[]
|
|
21
|
+
strokeStyle?: StrokeStyleContribution
|
|
22
|
+
historyCommands?: HistoryCommandContribution[]
|
|
23
|
+
backgroundLayers?: CanvasLayerContribution[]
|
|
24
|
+
overlayLayers?: CanvasLayerContribution[]
|
|
25
|
+
htmlOverlays?: HtmlOverlayContribution[]
|
|
26
|
+
/** ⏳ Reserved slot — accepts registrations, adapter not yet wired. */
|
|
27
|
+
strokeTransformers?: StrokeTransformerContribution[]
|
|
28
|
+
|
|
29
|
+
// ── UI ──────────────────────────────────────────────────────────────────
|
|
30
|
+
controlBar?: ControlBarContribution[]
|
|
31
|
+
settingsTabs?: SettingsTabContribution[]
|
|
32
|
+
settingsPanels?: SettingsPanelContribution[]
|
|
33
|
+
cursors?: CursorContribution[]
|
|
34
|
+
status?: StatusContribution[]
|
|
35
|
+
modals?: ModalContribution[]
|
|
36
|
+
trayMenu?: TrayMenuContribution[]
|
|
37
|
+
/** ⏳ Reserved slot — accepts registrations, adapter not yet wired. */
|
|
38
|
+
contextMenu?: ContextMenuContribution[]
|
|
39
|
+
/** ⏳ Reserved slot — accepts registrations, adapter not yet wired. */
|
|
40
|
+
themeTokens?: ThemeTokenContribution
|
|
41
|
+
|
|
42
|
+
// ── System ──────────────────────────────────────────────────────────────
|
|
43
|
+
shortcuts?: ShortcutContribution[]
|
|
44
|
+
windowBehaviors?: WindowBehaviorContribution[]
|
|
45
|
+
locales?: LocaleContribution
|
|
46
|
+
events?: EventSubscriptionContribution[]
|
|
47
|
+
lifecycle?: LifecycleContribution
|
|
48
|
+
storage?: StorageContribution
|
|
49
|
+
mainHandlers?: MainHandlerContribution
|
|
50
|
+
/** ⏳ Reserved slot — accepts registrations, adapter not yet wired. */
|
|
51
|
+
fileDrop?: FileDropContribution[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// Below: per-slot contribution shapes. These are the locked public-facing
|
|
56
|
+
// names referenced by docs and the validator; their internals can extend
|
|
57
|
+
// without breaking modules.
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Drawing tool driven by pointer events. Implements the full `Tool`
|
|
62
|
+
* interface (onPointerDown / Move / Up) plus optional metadata for
|
|
63
|
+
* the toolbar UI and an optional `renderStroke` for tools whose
|
|
64
|
+
* strokes need custom redraw semantics (shapes, erasers, anything
|
|
65
|
+
* beyond a simple polyline).
|
|
66
|
+
*/
|
|
67
|
+
export interface ToolContribution extends Tool {
|
|
68
|
+
id: string
|
|
69
|
+
label?: string | LocaleMap
|
|
70
|
+
/**
|
|
71
|
+
* SVG markup string for this tool's icon in the control-bar button.
|
|
72
|
+
*
|
|
73
|
+
* The host renders tool icons via `v-html`, so only raw SVG strings are
|
|
74
|
+
* accepted (not Vue components or CSS class names). Recommended size is
|
|
75
|
+
* `width="16" height="16"` with `stroke="currentColor"` so the icon
|
|
76
|
+
* inherits the button's colour automatically.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17c3-3 6-6 9-3s6 0 9-3"/></svg>'
|
|
80
|
+
*
|
|
81
|
+
* Note: built-in tools (freehand, line, eraser) use their own Vue button
|
|
82
|
+
* components rather than this field — `icon` is the lightweight path for
|
|
83
|
+
* plugin-contributed tools that do not need a custom button component.
|
|
84
|
+
*/
|
|
85
|
+
icon?: string
|
|
86
|
+
/**
|
|
87
|
+
* Custom redraw renderer. When omitted, canvas-engine falls back
|
|
88
|
+
* to a polyline render through `stroke.points` — sufficient for
|
|
89
|
+
* freehand- and line-style tools.
|
|
90
|
+
*/
|
|
91
|
+
renderStroke?: (canvasCtx: CanvasRenderingContext2D, stroke: Stroke) => void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Shape primitive used by the shape tool. */
|
|
95
|
+
export interface ShapeContribution {
|
|
96
|
+
id: string
|
|
97
|
+
label?: string | LocaleMap
|
|
98
|
+
/**
|
|
99
|
+
* Vue component or SVG string for the shape icon in control-bar / sub-panel.
|
|
100
|
+
* Host renders via `<component :is="icon">`.
|
|
101
|
+
* Component icons MAY accept an optional `filled?: boolean` prop —
|
|
102
|
+
* the host passes the current fill-toggle state so icons can mirror it
|
|
103
|
+
* (e.g. solid vs outline rendering). Plugin icons that ignore the prop
|
|
104
|
+
* still work; they just won't reflect fill state visually.
|
|
105
|
+
*/
|
|
106
|
+
icon?: Component | string
|
|
107
|
+
draw: (...args: unknown[]) => void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Stroke style writer. Modules declare which keys of the shared
|
|
112
|
+
* stroke style store they write to; collision detection happens at
|
|
113
|
+
* module-validator time.
|
|
114
|
+
*/
|
|
115
|
+
export interface StrokeStyleContribution {
|
|
116
|
+
/** Keys this module writes (e.g. `['lineWidth']`, `['color']`). */
|
|
117
|
+
provides: string[]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Undo/redo command handler for the `canvas.history.commands` slot.
|
|
122
|
+
*
|
|
123
|
+
* The host calls `undo` / `redo` with whatever `payload` was stored when the
|
|
124
|
+
* command was pushed onto the history stack. The contributing module decides
|
|
125
|
+
* the payload shape — the host treats it as `unknown` and passes it through
|
|
126
|
+
* without inspection.
|
|
127
|
+
*/
|
|
128
|
+
export interface HistoryCommandContribution {
|
|
129
|
+
type: string
|
|
130
|
+
undo: (payload: unknown) => void
|
|
131
|
+
redo: (payload: unknown) => void
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface CanvasLayerContribution {
|
|
135
|
+
id: string
|
|
136
|
+
/** Render priority within the layer; higher = drawn later (on top). */
|
|
137
|
+
order?: number
|
|
138
|
+
render: (ctx: CanvasRenderingContext2D) => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface HtmlOverlayContribution {
|
|
142
|
+
id: string
|
|
143
|
+
component: Component
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface StrokeTransformerContribution {
|
|
147
|
+
id: string
|
|
148
|
+
/** Receives a stroke, returns a transformed stroke. Slot ⏳ reserved. */
|
|
149
|
+
transform: (stroke: unknown) => unknown
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface SettingsTabContribution {
|
|
153
|
+
id: string
|
|
154
|
+
label: string | LocaleMap
|
|
155
|
+
component: Component
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface SettingsPanelContribution {
|
|
159
|
+
id: string
|
|
160
|
+
label: string | LocaleMap
|
|
161
|
+
component: Component
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface CursorContribution {
|
|
165
|
+
id: string
|
|
166
|
+
/** CSS cursor value or a data URL. */
|
|
167
|
+
cursor: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface StatusContribution {
|
|
171
|
+
id: string
|
|
172
|
+
component: Component
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ModalContribution {
|
|
176
|
+
id: string
|
|
177
|
+
component: Component
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface TrayMenuContribution {
|
|
181
|
+
id: string
|
|
182
|
+
label: string | LocaleMap
|
|
183
|
+
click: () => void
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ContextMenuContribution {
|
|
187
|
+
id: string
|
|
188
|
+
label: string | LocaleMap
|
|
189
|
+
/** Where the menu may appear; runtime filters by context at open time. */
|
|
190
|
+
context: 'canvas' | 'toolbar' | 'tray'
|
|
191
|
+
click: () => void
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* CSS custom property contributions for the `ui.theme.tokens` slot.
|
|
196
|
+
*
|
|
197
|
+
* Keys are CSS custom property names (e.g. `'--my-plugin-accent'`).
|
|
198
|
+
* Values are CSS color or length values (e.g. `'#ff6600'`, `'4px'`).
|
|
199
|
+
* The host merges all contributions into `:root` at startup.
|
|
200
|
+
*/
|
|
201
|
+
export type ThemeTokenContribution = Record<string, string>
|
|
202
|
+
|
|
203
|
+
export interface ShortcutContribution {
|
|
204
|
+
id: string
|
|
205
|
+
/** Accelerator string; tinykeys-style or Electron globalShortcut format. */
|
|
206
|
+
keys: string
|
|
207
|
+
/** `'global'` registers OS-wide; `'drawing'` only when overlay is active. */
|
|
208
|
+
scope: 'global' | 'drawing'
|
|
209
|
+
handler: () => void
|
|
210
|
+
/** Human-readable name. Omit to hide this shortcut from the user-visible shortcuts list. */
|
|
211
|
+
label?: string | LocaleMap
|
|
212
|
+
/** Optional secondary description shown below the label in the shortcuts settings panel. */
|
|
213
|
+
sublabel?: string | LocaleMap
|
|
214
|
+
/** When true, the user can rebind this key. The chosen key is persisted under `config.json → customShortcuts`. */
|
|
215
|
+
userCustomizable?: boolean
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface WindowBehaviorContribution {
|
|
219
|
+
id: string
|
|
220
|
+
/** Reserved — concrete behaviour API not yet defined. */
|
|
221
|
+
[key: string]: unknown
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Leaf or branch in a locale dictionary. Leaves must be strings. */
|
|
225
|
+
type LocaleDictValue = string | LocaleDict
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Recursive locale dictionary.
|
|
229
|
+
* Leaves are translated strings; branches are nested sub-dictionaries.
|
|
230
|
+
* Use the `interface` form (not `type`) to allow self-reference without
|
|
231
|
+
* triggering TypeScript's circular-type restrictions.
|
|
232
|
+
*/
|
|
233
|
+
interface LocaleDict {
|
|
234
|
+
[key: string]: LocaleDictValue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A locale dictionary keyed by BCP-47 tag (e.g. `'en'`, `'zh-Hant'`).
|
|
239
|
+
*
|
|
240
|
+
* Values are per-locale message dictionaries. Supports both flat and nested
|
|
241
|
+
* layouts — vue-i18n treats `.` in ctx.t(key) as a nested path separator,
|
|
242
|
+
* so keys like `'notif.ready'` must use a nested object dict
|
|
243
|
+
* `{ notif: { ready: '...' } }`, not a flat key `{ 'notif.ready': '...' }`.
|
|
244
|
+
*
|
|
245
|
+
* Non-string leaves are rejected at compile time; use ctx.t() to resolve
|
|
246
|
+
* keys to strings before passing them to runtime APIs.
|
|
247
|
+
*/
|
|
248
|
+
export type LocaleContribution = Record<string, LocaleDict>
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Subscription to a named domain event on the `system.events` slot.
|
|
252
|
+
*
|
|
253
|
+
* `event` must match the event name emitted by another module (or the host)
|
|
254
|
+
* via `system.events`. The host routes each emitted event to all registered
|
|
255
|
+
* handlers whose `event` string matches.
|
|
256
|
+
*/
|
|
257
|
+
export interface EventSubscriptionContribution {
|
|
258
|
+
event: string
|
|
259
|
+
handler: (payload: unknown) => void
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* App lifecycle hooks for the `system.lifecycle` slot.
|
|
264
|
+
*
|
|
265
|
+
* Call order: `setup` (from `OpenPenModule.setup`) → `onReady` (host fully
|
|
266
|
+
* booted) → `onSuspend` (overlay hidden) → `onQuit` (app shutting down).
|
|
267
|
+
* All hooks may return a Promise; the host awaits each before advancing.
|
|
268
|
+
*/
|
|
269
|
+
export interface LifecycleContribution {
|
|
270
|
+
onReady?: () => void | Promise<void>
|
|
271
|
+
onSuspend?: () => void | Promise<void>
|
|
272
|
+
onQuit?: () => void | Promise<void>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Marker that this module wants an isolated storage area at
|
|
277
|
+
* `~/.openpen/plugins/<id>/data/`. Capacity / quota policy is defined
|
|
278
|
+
* by the host runtime.
|
|
279
|
+
*/
|
|
280
|
+
export interface StorageContribution {
|
|
281
|
+
/** If true, the host pre-creates the data directory at module load. */
|
|
282
|
+
preCreate?: boolean
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Map of action name to async handler. Routed from the renderer via
|
|
287
|
+
* `window.openPenApi.moduleCall(moduleId, action, payload)`.
|
|
288
|
+
*/
|
|
289
|
+
export type MainHandlerContribution = Record<
|
|
290
|
+
string,
|
|
291
|
+
(payload: unknown) => unknown | Promise<unknown>
|
|
292
|
+
>
|
|
293
|
+
|
|
294
|
+
export interface FileDropContribution {
|
|
295
|
+
/** MIME types this handler accepts (e.g. `['image/png']`). */
|
|
296
|
+
accept: string[]
|
|
297
|
+
handle: (files: File[]) => void | Promise<void>
|
|
298
|
+
}
|