@notehub.md/cli 0.1.6
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/README.md +129 -0
- package/dist/commands/build.d.ts +30 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +202 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create.d.ts +33 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +236 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/templates/PLUGIN_GUIDE.md +599 -0
- package/templates/PLUGIN_GUIDE_RU.md +599 -0
- package/templates/docs.html +534 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# 🔌 Notehub Plugin Developer Guide
|
|
2
|
+
|
|
3
|
+
> Complete documentation for creating plugins for Notehub.md
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Getting Started](#getting-started)
|
|
10
|
+
2. [Plugin Architecture](#plugin-architecture)
|
|
11
|
+
3. [API Reference](#api-reference)
|
|
12
|
+
4. [Widgets (Portals)](#widgets-portals)
|
|
13
|
+
5. [Settings Integration](#settings-integration)
|
|
14
|
+
6. [Context Menu](#context-menu)
|
|
15
|
+
7. [Plugin Examples](#plugin-examples)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Getting Started
|
|
20
|
+
|
|
21
|
+
This guide will walk you through creating your first Notehub.md plugin.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Node.js** v18+ with npm/pnpm
|
|
26
|
+
- **TypeScript** (recommended, but JavaScript works too)
|
|
27
|
+
- **Bundler**: esbuild, Vite, or Rollup
|
|
28
|
+
- A Notehub.md vault to test in
|
|
29
|
+
|
|
30
|
+
## Plugin Structure
|
|
31
|
+
|
|
32
|
+
Every plugin needs at minimum:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-plugin/
|
|
36
|
+
├── manifest.json # Plugin metadata (required)
|
|
37
|
+
├── main.js # Entry point (compiled)
|
|
38
|
+
└── src/ # Source files (optional)
|
|
39
|
+
└── index.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## manifest.json
|
|
43
|
+
|
|
44
|
+
The manifest describes your plugin to Notehub:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"id": "my-awesome-plugin",
|
|
49
|
+
"name": "My Awesome Plugin",
|
|
50
|
+
"version": "1.0.0",
|
|
51
|
+
"main": "main.js",
|
|
52
|
+
"dependencies": []
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Field | Required | Description |
|
|
57
|
+
|-------|----------|-------------|
|
|
58
|
+
| `id` | ✅ | Unique identifier (lowercase, hyphens allowed) |
|
|
59
|
+
| `name` | ✅ | Human-readable display name |
|
|
60
|
+
| `version` | ✅ | Semantic version (e.g., `1.0.0`) |
|
|
61
|
+
| `main` | ❌ | Entry point file, defaults to `main.js` |
|
|
62
|
+
| `dependencies` | ❌ | Array of internal plugin IDs this plugin requires |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick Start with CLI
|
|
67
|
+
|
|
68
|
+
The fastest way to create a new plugin:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx @notehub.md/cli create ext.my-plugin --name "My Plugin"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This generates the complete plugin structure with all necessary files.
|
|
75
|
+
|
|
76
|
+
## Manual Setup
|
|
77
|
+
|
|
78
|
+
### Step 1: Create the folder structure
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mkdir my-plugin
|
|
82
|
+
cd my-plugin
|
|
83
|
+
npm init -y
|
|
84
|
+
npm install @notehub/api typescript esbuild --save-dev
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 2: Create src/index.ts
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { NotehubPlugin, PluginContext } from '@notehub/api';
|
|
91
|
+
|
|
92
|
+
export default class HelloWorldPlugin extends NotehubPlugin {
|
|
93
|
+
async onload(ctx: PluginContext): Promise<void> {
|
|
94
|
+
await ctx.invokeApi('logger:info', 'HelloWorld', 'Hello from my plugin!');
|
|
95
|
+
|
|
96
|
+
ctx.registerApi('hello:say', (message: string) => {
|
|
97
|
+
console.log(`[HelloWorld] ${message}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
ctx.subscribe<{ path: string }>('explorer:file-selected', (payload) => {
|
|
101
|
+
console.log('File selected:', payload.path);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async onunload(): Promise<void> {
|
|
106
|
+
console.log('HelloWorld plugin unloaded');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Step 3: Build and Install
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm run build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Copy to your vault:
|
|
118
|
+
```
|
|
119
|
+
MyVault/.notehub/plugins/hello-world/
|
|
120
|
+
├── manifest.json
|
|
121
|
+
└── main.js
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Hot Reload
|
|
125
|
+
|
|
126
|
+
Notehub watches the `.notehub/plugins/` directory. When you update your plugin:
|
|
127
|
+
|
|
128
|
+
1. The old version is automatically unloaded
|
|
129
|
+
2. The new version is loaded
|
|
130
|
+
3. All your API registrations are cleaned up automatically!
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# Plugin Architecture
|
|
135
|
+
|
|
136
|
+
## Microkernel Architecture
|
|
137
|
+
|
|
138
|
+
Notehub.md follows a **microkernel** design where the core is minimal and all functionality comes from plugins:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
142
|
+
│ NotehubCore │
|
|
143
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
|
144
|
+
│ │ EventBus │ │ ApiBus │ │ Plugin Registry │ │
|
|
145
|
+
│ │ (pub/sub) │ │ (RPC calls) │ │ (lifecycle mgmt) │ │
|
|
146
|
+
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
|
147
|
+
└─────────────────────────────────────────────────────────────┘
|
|
148
|
+
▲ ▲ ▲
|
|
149
|
+
│ │ │
|
|
150
|
+
┌────┴───┐ ┌─────┴────┐ ┌─────┴────┐
|
|
151
|
+
│ Logger │ │ Editor │ │ Explorer │
|
|
152
|
+
│ Plugin │ │ Plugin │ │ Plugin │
|
|
153
|
+
└────────┘ └──────────┘ └──────────┘
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## PluginContext
|
|
157
|
+
|
|
158
|
+
Your gateway to the Notehub ecosystem:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface PluginContext {
|
|
162
|
+
registerApi(name: string, handler: (...args: unknown[]) => unknown): void;
|
|
163
|
+
invokeApi<T>(name: string, ...args: unknown[]): Promise<T>;
|
|
164
|
+
subscribe<T>(event: string, handler: (payload: T) => void): void;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Auto-Cleanup Magic
|
|
169
|
+
|
|
170
|
+
When your plugin is unloaded, `PluginContext` automatically:
|
|
171
|
+
|
|
172
|
+
- ✅ Unregisters all APIs you registered
|
|
173
|
+
- ✅ Unsubscribes from all events
|
|
174
|
+
- ✅ Removes editor widgets you registered
|
|
175
|
+
- ✅ Removes settings tabs/groups/items you added
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
# API Reference
|
|
180
|
+
|
|
181
|
+
## Logger API
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
await ctx.invokeApi('logger:info', 'MyPlugin', 'Operation completed');
|
|
185
|
+
await ctx.invokeApi('logger:warn', 'MyPlugin', 'Config not found');
|
|
186
|
+
await ctx.invokeApi('logger:error', 'MyPlugin', 'Failed to load');
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Config API (Persistent)
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Get value
|
|
193
|
+
const fontSize = await ctx.invokeApi<number>('config:get', 'editor.font-size', 14);
|
|
194
|
+
|
|
195
|
+
// Set value (auto-saved)
|
|
196
|
+
await ctx.invokeApi('config:set', 'my-plugin.option', true);
|
|
197
|
+
|
|
198
|
+
// Delete
|
|
199
|
+
await ctx.invokeApi('config:delete', 'my-plugin.option');
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## State API (Runtime)
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
await ctx.invokeApi('state:set', 'my-plugin.cache', { data: [...] });
|
|
206
|
+
const cache = await ctx.invokeApi<MyData>('state:get', 'my-plugin.cache');
|
|
207
|
+
await ctx.invokeApi('state:delete', 'my-plugin.cache');
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Filesystem API
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Read file
|
|
214
|
+
const content = await ctx.invokeApi<string>('fs:read-text-file', '/path/to/file.md');
|
|
215
|
+
|
|
216
|
+
// Write file
|
|
217
|
+
await ctx.invokeApi('fs:write-text-file', '/path/to/file.md', '# Hello World');
|
|
218
|
+
|
|
219
|
+
// Check existence
|
|
220
|
+
const exists = await ctx.invokeApi<boolean>('fs:exists', '/path/to/file.md');
|
|
221
|
+
|
|
222
|
+
// Read directory
|
|
223
|
+
const entries = await ctx.invokeApi<DirEntry[]>('fs:read-dir', '/path/to/folder');
|
|
224
|
+
|
|
225
|
+
// Create directory
|
|
226
|
+
await ctx.invokeApi('fs:create-dir', '/path/to/new-folder', { recursive: true });
|
|
227
|
+
|
|
228
|
+
// Delete file
|
|
229
|
+
await ctx.invokeApi('fs:remove-file', '/path/to/file.md');
|
|
230
|
+
|
|
231
|
+
// Rename/move
|
|
232
|
+
await ctx.invokeApi('fs:rename', '/old/path.md', '/new/path.md');
|
|
233
|
+
|
|
234
|
+
// Watch for changes
|
|
235
|
+
const unwatch = await ctx.invokeApi<() => void>(
|
|
236
|
+
'fs:watch',
|
|
237
|
+
'/path/to/folder',
|
|
238
|
+
(event) => console.log('Change:', event)
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Dialog API
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// Alert
|
|
246
|
+
await ctx.invokeApi('dialog:alert', 'Warning', 'File has been deleted');
|
|
247
|
+
|
|
248
|
+
// Confirm
|
|
249
|
+
const confirmed = await ctx.invokeApi<boolean>(
|
|
250
|
+
'dialog:confirm', 'Delete File', 'Are you sure?'
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Prompt
|
|
254
|
+
const name = await ctx.invokeApi<string | null>(
|
|
255
|
+
'dialog:prompt', 'Rename File', 'Enter new name:', 'default.md'
|
|
256
|
+
);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Theme API
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// Register theme
|
|
263
|
+
await ctx.invokeApi('theme:register', 'my-theme', {
|
|
264
|
+
'bg-main': '#1a1a2e',
|
|
265
|
+
'accent-primary': '#e94560',
|
|
266
|
+
// ...
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Apply theme
|
|
270
|
+
await ctx.invokeApi('theme:set', 'my-theme');
|
|
271
|
+
|
|
272
|
+
// Get current
|
|
273
|
+
const theme = await ctx.invokeApi<string>('theme:get-current');
|
|
274
|
+
|
|
275
|
+
// List all
|
|
276
|
+
const themes = await ctx.invokeApi<string[]>('theme:list');
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Editor Widget API
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Register widget
|
|
283
|
+
await ctx.invokeApi(
|
|
284
|
+
'editor:register-widget',
|
|
285
|
+
'my-plugin:progress-bar',
|
|
286
|
+
/\[progress:(\d+)\]/g,
|
|
287
|
+
ProgressBarComponent
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Unregister (optional - auto-cleaned)
|
|
291
|
+
await ctx.invokeApi('editor:unregister-widget', 'my-plugin:progress-bar');
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Settings API
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Register tab
|
|
298
|
+
await ctx.invokeApi('settings:register-tab', {
|
|
299
|
+
id: 'my-plugin', label: 'My Plugin', icon: 'puzzle', order: 100
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Register group
|
|
303
|
+
await ctx.invokeApi('settings:register-group', {
|
|
304
|
+
id: 'my-group', tabId: 'my-plugin', label: 'General', order: 0
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Register item
|
|
308
|
+
await ctx.invokeApi('settings:register-item', {
|
|
309
|
+
key: 'my-plugin.enabled',
|
|
310
|
+
type: 'toggle', // 'toggle' | 'text' | 'number' | 'select' | 'color'
|
|
311
|
+
label: 'Enable plugin',
|
|
312
|
+
groupId: 'my-group',
|
|
313
|
+
order: 0,
|
|
314
|
+
defaultValue: true
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Open/close settings
|
|
318
|
+
await ctx.invokeApi('settings:open');
|
|
319
|
+
await ctx.invokeApi('settings:close');
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Context Menu API
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
await ctx.invokeApi(
|
|
326
|
+
'context-menu:register',
|
|
327
|
+
'explorer-item',
|
|
328
|
+
(payload: { path: string }) => [
|
|
329
|
+
{
|
|
330
|
+
type: 'action',
|
|
331
|
+
id: 'my-action',
|
|
332
|
+
label: 'My Action',
|
|
333
|
+
icon: 'star',
|
|
334
|
+
onClick: () => console.log('Clicked:', payload.path)
|
|
335
|
+
},
|
|
336
|
+
{ type: 'separator' },
|
|
337
|
+
{
|
|
338
|
+
type: 'submenu',
|
|
339
|
+
label: 'More Options',
|
|
340
|
+
items: [/* ... */]
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Shell API
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
await ctx.invokeApi('shell:open', 'https://notehub.md');
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Vault API
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
await ctx.invokeApi('vault:close');
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
# Widgets (Portals)
|
|
361
|
+
|
|
362
|
+
Portals are custom React components that render inline within the editor.
|
|
363
|
+
|
|
364
|
+
## How Portals Work
|
|
365
|
+
|
|
366
|
+
1. You define a **regex pattern** that matches text in the document
|
|
367
|
+
2. You provide a **React component** to render for each match
|
|
368
|
+
3. Notehub replaces matched text with your component in **view mode**
|
|
369
|
+
4. When the cursor enters the match, it switches to **edit mode**
|
|
370
|
+
|
|
371
|
+
## Complete Example: Progress Bar
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { NotehubPlugin, PluginContext } from '@notehub/api';
|
|
375
|
+
import React from 'react';
|
|
376
|
+
|
|
377
|
+
const ProgressBar: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
|
|
378
|
+
const percentage = parseInt(match[1], 10);
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<span style={{
|
|
382
|
+
display: 'inline-flex',
|
|
383
|
+
alignItems: 'center',
|
|
384
|
+
gap: '8px',
|
|
385
|
+
padding: '2px 8px',
|
|
386
|
+
background: 'var(--nh-bg-surface)',
|
|
387
|
+
borderRadius: '4px',
|
|
388
|
+
}}>
|
|
389
|
+
<span style={{
|
|
390
|
+
width: '100px',
|
|
391
|
+
height: '8px',
|
|
392
|
+
background: 'var(--nh-bg-secondary)',
|
|
393
|
+
borderRadius: '4px',
|
|
394
|
+
overflow: 'hidden',
|
|
395
|
+
}}>
|
|
396
|
+
<span style={{
|
|
397
|
+
width: `${percentage}%`,
|
|
398
|
+
height: '100%',
|
|
399
|
+
background: 'var(--nh-accent-primary)',
|
|
400
|
+
display: 'block',
|
|
401
|
+
}} />
|
|
402
|
+
</span>
|
|
403
|
+
<span style={{ fontSize: '12px', color: 'var(--nh-text-muted)' }}>
|
|
404
|
+
{percentage}%
|
|
405
|
+
</span>
|
|
406
|
+
</span>
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export default class ProgressBarPlugin extends NotehubPlugin {
|
|
411
|
+
async onload(ctx: PluginContext): Promise<void> {
|
|
412
|
+
await ctx.invokeApi(
|
|
413
|
+
'editor:register-widget',
|
|
414
|
+
'progress-bar',
|
|
415
|
+
/\[progress:(\d+)\]/g,
|
|
416
|
+
ProgressBar
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async onunload(): Promise<void> {}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Usage in documents:**
|
|
425
|
+
```markdown
|
|
426
|
+
Project completion: [progress:75]
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
# Settings Integration
|
|
432
|
+
|
|
433
|
+
## Structure
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
Settings Modal
|
|
437
|
+
└── Tab (e.g., "My Plugin")
|
|
438
|
+
└── Group (e.g., "Appearance")
|
|
439
|
+
└── Item (e.g., "Enable dark mode")
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Item Types
|
|
443
|
+
|
|
444
|
+
| Type | Description |
|
|
445
|
+
|------|-------------|
|
|
446
|
+
| `toggle` | Boolean switch |
|
|
447
|
+
| `text` | Text input |
|
|
448
|
+
| `number` | Number input with min/max/step |
|
|
449
|
+
| `select` | Dropdown with options |
|
|
450
|
+
| `color` | Color picker |
|
|
451
|
+
|
|
452
|
+
## Reading Settings
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const isEnabled = await ctx.invokeApi<boolean>('config:get', 'my-plugin.enabled', true);
|
|
456
|
+
const maxItems = await ctx.invokeApi<number>('config:get', 'my-plugin.max-items', 10);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
# Context Menu
|
|
462
|
+
|
|
463
|
+
## Menu Item Types
|
|
464
|
+
|
|
465
|
+
### Action
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
{
|
|
469
|
+
type: 'action',
|
|
470
|
+
id: 'my-action',
|
|
471
|
+
label: 'My Action',
|
|
472
|
+
icon: 'star',
|
|
473
|
+
onClick: (payload) => { /* handle */ }
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Separator
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
{ type: 'separator' }
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Submenu
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
{
|
|
487
|
+
type: 'submenu',
|
|
488
|
+
label: 'More Options',
|
|
489
|
+
items: [/* nested items */]
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
# Plugin Examples
|
|
496
|
+
|
|
497
|
+
## Hello World
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { NotehubPlugin, PluginContext } from '@notehub/api';
|
|
501
|
+
|
|
502
|
+
export default class HelloWorld extends NotehubPlugin {
|
|
503
|
+
async onload(ctx: PluginContext): Promise<void> {
|
|
504
|
+
await ctx.invokeApi('logger:info', 'HelloWorld', '👋 Hello!');
|
|
505
|
+
|
|
506
|
+
ctx.registerApi('hello:greet', (name: string) => {
|
|
507
|
+
return `Hello, ${name}!`;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async onunload(): Promise<void> {}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Word Counter Widget
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
const WordCounter: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
|
|
519
|
+
const text = match[1];
|
|
520
|
+
const words = text.trim().split(/\s+/).filter(w => w.length > 0).length;
|
|
521
|
+
|
|
522
|
+
return <span>📝 {words} words</span>;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Register with pattern: {{count: your text here}}
|
|
526
|
+
await ctx.invokeApi(
|
|
527
|
+
'editor:register-widget',
|
|
528
|
+
'word-counter',
|
|
529
|
+
/\{\{count:\s*(.+?)\}\}/g,
|
|
530
|
+
WordCounter
|
|
531
|
+
);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Full-Featured Plugin
|
|
535
|
+
|
|
536
|
+
See the [examples documentation](https://github.com/khton-tech/notehub.md/tree/main/docs/forPluginMakers/en/07-examples.md) for complete plugin code combining widgets, settings, and context menus.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Build Configuration
|
|
541
|
+
|
|
542
|
+
### package.json
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"scripts": {
|
|
547
|
+
"build": "nhp build"
|
|
548
|
+
},
|
|
549
|
+
"devDependencies": {
|
|
550
|
+
"@notehub/api": "workspace:*",
|
|
551
|
+
"@types/react": "^18.3.0",
|
|
552
|
+
"typescript": "^5.6.0"
|
|
553
|
+
},
|
|
554
|
+
"peerDependencies": {
|
|
555
|
+
"react": "^18.3.0"
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### External Dependencies
|
|
561
|
+
|
|
562
|
+
These packages are provided by Notehub - mark them as **external**:
|
|
563
|
+
|
|
564
|
+
- `@notehub/api`
|
|
565
|
+
- `react`
|
|
566
|
+
- `react-dom`
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Debugging Tips
|
|
571
|
+
|
|
572
|
+
1. **Open DevTools** (Ctrl+Shift+I) to see console logs
|
|
573
|
+
2. **Use `logger:info`** API for structured logging
|
|
574
|
+
3. **Check the Synapse plugin** logs for load/unload events
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## CSS Variables
|
|
579
|
+
|
|
580
|
+
Use these for consistent styling:
|
|
581
|
+
|
|
582
|
+
- `--nh-bg-main`, `--nh-bg-sidebar`, `--nh-bg-surface`
|
|
583
|
+
- `--nh-text-primary`, `--nh-text-secondary`, `--nh-text-muted`
|
|
584
|
+
- `--nh-accent-primary`, `--nh-accent-secondary`
|
|
585
|
+
- `--nh-border-accent`, `--nh-border-subtle`
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Resources
|
|
590
|
+
|
|
591
|
+
- [GitHub Repository](https://github.com/khton-tech/notehub.md)
|
|
592
|
+
- [API Package](https://github.com/khton-tech/notehub.md/tree/main/packages/api)
|
|
593
|
+
- [Example Plugins](https://github.com/khton-tech/notehub.md/tree/main/packages/plugins)
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
<p align="center">
|
|
598
|
+
<strong>Happy coding! 🎉</strong>
|
|
599
|
+
</p>
|