@notehub.md/cli 0.1.8 → 0.1.10

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.
@@ -0,0 +1,283 @@
1
+ # Context Menu Integration
2
+
3
+ Add custom items to right-click menus throughout Notehub.
4
+
5
+ ## Overview
6
+
7
+ Context menus are dynamic - providers are called when the menu is triggered and can return different items based on what was clicked.
8
+
9
+ ## Registering a Menu Provider
10
+
11
+ ```typescript
12
+ const unsubscribe = await ctx.invokeApi<() => void>(
13
+ 'context-menu:register',
14
+ contextId, // Where the menu appears
15
+ provider // Function that returns menu items
16
+ );
17
+ ```
18
+
19
+ ## Context IDs
20
+
21
+ | Context ID | Triggered On | Payload |
22
+ |------------|-------------|---------|
23
+ | `explorer-item` | File/folder in explorer | `{ path: string, isDirectory: boolean }` |
24
+
25
+ ## Menu Item Types
26
+
27
+ ### Action
28
+
29
+ A clickable menu item:
30
+
31
+ ```typescript
32
+ {
33
+ type: 'action',
34
+ id: 'my-action', // Unique identifier
35
+ label: 'My Action', // Display text
36
+ icon: 'star', // Lucide icon (optional)
37
+ color: 'var(--nh-danger)', // CSS color (optional)
38
+ disabled: false, // Grey out if true
39
+ onClick: (payload) => {
40
+ // Handle click
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### Separator
46
+
47
+ A visual divider:
48
+
49
+ ```typescript
50
+ {
51
+ type: 'separator'
52
+ }
53
+ ```
54
+
55
+ ### Submenu
56
+
57
+ Nested menu items:
58
+
59
+ ```typescript
60
+ {
61
+ type: 'submenu',
62
+ label: 'More Options',
63
+ icon: 'more-horizontal',
64
+ items: [
65
+ { type: 'action', id: 'sub-1', label: 'Option 1', onClick: () => {} },
66
+ { type: 'action', id: 'sub-2', label: 'Option 2', onClick: () => {} }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Complete Example
74
+
75
+ ```typescript
76
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
77
+
78
+ interface ExplorerPayload {
79
+ path: string;
80
+ isDirectory: boolean;
81
+ }
82
+
83
+ export default class ContextMenuPlugin extends NotehubPlugin {
84
+ private unsubscribe?: () => void;
85
+
86
+ async onload(ctx: PluginContext): Promise<void> {
87
+ // Register menu provider for explorer items
88
+ this.unsubscribe = await ctx.invokeApi(
89
+ 'context-menu:register',
90
+ 'explorer-item',
91
+ (payload: ExplorerPayload) => this.getMenuItems(payload, ctx)
92
+ );
93
+ }
94
+
95
+ private getMenuItems(payload: ExplorerPayload, ctx: PluginContext) {
96
+ const items = [];
97
+
98
+ // Only show for markdown files
99
+ if (payload.path.endsWith('.md')) {
100
+ items.push({
101
+ type: 'action' as const,
102
+ id: 'count-words',
103
+ label: 'Count Words',
104
+ icon: 'hash',
105
+ onClick: async () => {
106
+ const content = await ctx.invokeApi<string>(
107
+ 'fs:read-text-file',
108
+ payload.path
109
+ );
110
+ const wordCount = content.split(/\s+/).length;
111
+ await ctx.invokeApi(
112
+ 'dialog:alert',
113
+ 'Word Count',
114
+ `${wordCount} words`
115
+ );
116
+ }
117
+ });
118
+ }
119
+
120
+ // Separator
121
+ if (items.length > 0) {
122
+ items.push({ type: 'separator' as const });
123
+ }
124
+
125
+ // Add a submenu with export options
126
+ items.push({
127
+ type: 'submenu' as const,
128
+ label: 'Export As',
129
+ icon: 'file-output',
130
+ items: [
131
+ {
132
+ type: 'action' as const,
133
+ id: 'export-txt',
134
+ label: 'Plain Text',
135
+ onClick: () => this.exportAs(payload.path, 'txt', ctx)
136
+ },
137
+ {
138
+ type: 'action' as const,
139
+ id: 'export-html',
140
+ label: 'HTML',
141
+ onClick: () => this.exportAs(payload.path, 'html', ctx)
142
+ }
143
+ ]
144
+ });
145
+
146
+ // Dangerous action (shows in red)
147
+ items.push({
148
+ type: 'action' as const,
149
+ id: 'archive',
150
+ label: 'Move to Archive',
151
+ icon: 'archive',
152
+ color: 'var(--nh-danger)',
153
+ onClick: () => this.archiveFile(payload.path, ctx)
154
+ });
155
+
156
+ return items;
157
+ }
158
+
159
+ private async exportAs(path: string, format: string, ctx: PluginContext) {
160
+ await ctx.invokeApi('logger:info', 'ContextMenu', `Exporting ${path} as ${format}`);
161
+ }
162
+
163
+ private async archiveFile(path: string, ctx: PluginContext) {
164
+ const confirmed = await ctx.invokeApi<boolean>(
165
+ 'dialog:confirm',
166
+ 'Archive File',
167
+ `Move ${path} to archive?`
168
+ );
169
+ if (confirmed) {
170
+ // Move file logic
171
+ }
172
+ }
173
+
174
+ async onunload(): Promise<void> {
175
+ // Manual cleanup (optional - auto-cleaned on unload)
176
+ this.unsubscribe?.();
177
+ }
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Dynamic Menu Items
184
+
185
+ Menu providers are called every time the menu opens. You can return different items based on:
186
+
187
+ ### File Type
188
+
189
+ ```typescript
190
+ (payload: ExplorerPayload) => {
191
+ if (payload.path.endsWith('.md')) {
192
+ return [/* markdown-specific items */];
193
+ } else if (payload.path.endsWith('.png')) {
194
+ return [/* image-specific items */];
195
+ }
196
+ return [/* generic items */];
197
+ }
198
+ ```
199
+
200
+ ### Directory vs File
201
+
202
+ ```typescript
203
+ (payload: ExplorerPayload) => {
204
+ if (payload.isDirectory) {
205
+ return [
206
+ { type: 'action', id: 'new-file', label: 'New File Here', ... }
207
+ ];
208
+ }
209
+ return [
210
+ { type: 'action', id: 'duplicate', label: 'Duplicate File', ... }
211
+ ];
212
+ }
213
+ ```
214
+
215
+ ### Async Providers
216
+
217
+ Providers can be async:
218
+
219
+ ```typescript
220
+ async (payload: ExplorerPayload) => {
221
+ const metadata = await loadMetadata(payload.path);
222
+ return [/* items based on metadata */];
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Icon Reference
229
+
230
+ Icons use [Lucide](https://lucide.dev/icons/) icon names in kebab-case:
231
+
232
+ | Icon Name | Description |
233
+ |-----------|-------------|
234
+ | `file` | Generic file |
235
+ | `folder` | Folder |
236
+ | `star` | Star/favorite |
237
+ | `trash-2` | Delete/trash |
238
+ | `copy` | Copy |
239
+ | `scissors` | Cut |
240
+ | `clipboard` | Paste |
241
+ | `edit` | Edit |
242
+ | `eye` | View |
243
+ | `download` | Download |
244
+ | `upload` | Upload |
245
+ | `archive` | Archive |
246
+ | `more-horizontal` | More options |
247
+
248
+ ---
249
+
250
+ ## Type Definitions
251
+
252
+ ```typescript
253
+ type MenuItem = MenuAction | MenuSeparator | SubMenu;
254
+
255
+ interface MenuAction {
256
+ type: 'action';
257
+ id: string;
258
+ label: string;
259
+ icon?: string;
260
+ color?: string;
261
+ disabled?: boolean;
262
+ onClick: (payload: unknown) => void;
263
+ }
264
+
265
+ interface MenuSeparator {
266
+ type: 'separator';
267
+ }
268
+
269
+ interface SubMenu {
270
+ type: 'submenu';
271
+ label: string;
272
+ icon?: string;
273
+ items: MenuItem[];
274
+ }
275
+
276
+ type MenuProvider = (payload: unknown) => MenuItem[] | Promise<MenuItem[]>;
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Next Steps
282
+
283
+ - See **[Complete Examples](07-examples.md)** for full plugin code