@notehub.md/cli 0.1.9 → 0.1.11

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,318 @@
1
+ # Widgets (Portals)
2
+
3
+ Portals are custom React components that render inline within the editor, replacing matched text patterns.
4
+
5
+ ## How Portals Work
6
+
7
+ 1. You define a **regex pattern** that matches text in the document
8
+ 2. You provide a **React component** to render for each match
9
+ 3. Notehub replaces matched text with your component in **view mode**
10
+ 4. When the cursor enters the match, it switches to **edit mode** (shows source)
11
+
12
+ ```
13
+ View Mode: [████████░░] 80% ← Your rendered component
14
+ Edit Mode: [progress:80] ← Source text visible when cursor inside
15
+ ```
16
+
17
+ ## Registering a Portal
18
+
19
+ Use the `editor:register-portal` API:
20
+
21
+ ```typescript
22
+ await ctx.invokeApi('editor:register-portal', {
23
+ id: 'unique-id', // Unique identifier
24
+ regex: /regex-pattern/g, // Pattern to match (MUST have global flag 'g')
25
+ component: ReactComponent // Component to render
26
+ });
27
+ ```
28
+
29
+ ## Component Props
30
+
31
+ Your component receives the regex match array:
32
+
33
+ ```typescript
34
+ interface WidgetProps {
35
+ match: RegExpExecArray;
36
+ }
37
+ ```
38
+
39
+ The `match` array contains:
40
+ - `match[0]` - Full matched string
41
+ - `match[1]`, `match[2]`, ... - Capture groups
42
+
43
+ ## Complete Example: Progress Bar
44
+
45
+ ```typescript
46
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
47
+ import React from 'react';
48
+
49
+ // Widget component
50
+ const ProgressBar: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
51
+ const percentage = parseInt(match[1], 10);
52
+
53
+ return (
54
+ <span style={{
55
+ display: 'inline-flex',
56
+ alignItems: 'center',
57
+ gap: '8px',
58
+ padding: '2px 8px',
59
+ background: 'var(--nh-bg-surface)',
60
+ borderRadius: '4px',
61
+ }}>
62
+ <span style={{
63
+ width: '100px',
64
+ height: '8px',
65
+ background: 'var(--nh-bg-secondary)',
66
+ borderRadius: '4px',
67
+ overflow: 'hidden',
68
+ }}>
69
+ <span style={{
70
+ width: `${percentage}%`,
71
+ height: '100%',
72
+ background: 'var(--nh-accent-primary)',
73
+ display: 'block',
74
+ borderRadius: '4px',
75
+ transition: 'width 0.3s ease',
76
+ }} />
77
+ </span>
78
+ <span style={{ fontSize: '12px', color: 'var(--nh-text-muted)' }}>
79
+ {percentage}%
80
+ </span>
81
+ </span>
82
+ );
83
+ };
84
+
85
+ // Plugin
86
+ export default class ProgressBarPlugin extends NotehubPlugin {
87
+ async onload(ctx: PluginContext): Promise<void> {
88
+ // Match: [progress:XX] where XX is a number
89
+ await ctx.invokeApi('editor:register-portal', {
90
+ id: 'progress-bar',
91
+ regex: /\[progress:(\d+)\]/g,
92
+ component: ProgressBar
93
+ });
94
+
95
+ await ctx.invokeApi('logger:info', 'ProgressBar', 'Portal registered');
96
+ }
97
+
98
+ async onunload(): Promise<void> {
99
+ // Portal is automatically unregistered!
100
+ }
101
+ }
102
+ ```
103
+
104
+ **Usage in documents:**
105
+ ```markdown
106
+ Project completion: [progress:75]
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Example: Clickable Button
112
+
113
+ ```typescript
114
+ const ButtonWidget: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
115
+ const label = match[1];
116
+ const action = match[2];
117
+
118
+ const handleClick = async () => {
119
+ // You can't access ctx here directly, but you can use events
120
+ // or call a registered API
121
+ console.log(`Button clicked: ${action}`);
122
+ };
123
+
124
+ return (
125
+ <button
126
+ onClick={handleClick}
127
+ style={{
128
+ background: 'var(--nh-accent-primary)',
129
+ color: 'var(--nh-button-text, #fff)',
130
+ border: 'none',
131
+ borderRadius: '4px',
132
+ padding: '4px 12px',
133
+ cursor: 'pointer',
134
+ fontSize: 'inherit',
135
+ }}
136
+ >
137
+ {label}
138
+ </button>
139
+ );
140
+ };
141
+
142
+ // Register
143
+ await ctx.invokeApi('editor:register-portal', {
144
+ id: 'btn-widget',
145
+ regex: /\[btn:([^\]:]+):([^\]]+)\]/g,
146
+ component: ButtonWidget
147
+ });
148
+ ```
149
+
150
+ **Usage:**
151
+ ```markdown
152
+ Click here: [btn:Submit:action-submit]
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Example: Status Badge
158
+
159
+ ```typescript
160
+ const StatusBadge: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
161
+ const status = match[1].toLowerCase();
162
+
163
+ const colors: Record<string, { bg: string; text: string }> = {
164
+ done: { bg: '#22c55e20', text: '#22c55e' },
165
+ 'in-progress': { bg: '#f59e0b20', text: '#f59e0b' },
166
+ todo: { bg: '#6b728020', text: '#6b7280' },
167
+ };
168
+
169
+ const style = colors[status] || colors.todo;
170
+
171
+ return (
172
+ <span style={{
173
+ padding: '2px 8px',
174
+ borderRadius: '12px',
175
+ fontSize: '12px',
176
+ fontWeight: 500,
177
+ background: style.bg,
178
+ color: style.text,
179
+ }}>
180
+ {match[1]}
181
+ </span>
182
+ );
183
+ };
184
+
185
+ await ctx.invokeApi('editor:register-portal', {
186
+ id: 'status-badge',
187
+ regex: /\[status:([^\]]+)\]/g,
188
+ component: StatusBadge
189
+ });
190
+ ```
191
+
192
+ **Usage:**
193
+ ```markdown
194
+ Task 1 [status:Done]
195
+ Task 2 [status:In-Progress]
196
+ Task 3 [status:TODO]
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Regex Best Practices
202
+
203
+ ### 1. Always use global flag (`g`)
204
+
205
+ ```typescript
206
+ // ✅ Good
207
+ /\[progress:(\d+)\]/g
208
+
209
+ // ❌ Bad - won't match multiple occurrences
210
+ /\[progress:(\d+)\]/
211
+ ```
212
+
213
+ ### 2. Use capture groups for dynamic content
214
+
215
+ ```typescript
216
+ // Captures two groups: label and value
217
+ /\[meter:([^:]+):(\d+)\]/g
218
+ // match[1] = label
219
+ // match[2] = value
220
+ ```
221
+
222
+ ### 3. Escape special characters
223
+
224
+ ```typescript
225
+ // Match [!note] - brackets need escape
226
+ /\[!note\]/g
227
+ ```
228
+
229
+ ### 4. Be specific to avoid false matches
230
+
231
+ ```typescript
232
+ // ✅ Good - specific pattern
233
+ /\[progress:(\d{1,3})\]/g
234
+
235
+ // ❌ Bad - too greedy
236
+ /\[.*\]/g // Matches ALL bracketed content!
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Styling Tips
242
+
243
+ ### Use CSS Variables
244
+
245
+ Access theme colors for consistent styling:
246
+
247
+ ```typescript
248
+ style={{
249
+ background: 'var(--nh-bg-surface)',
250
+ color: 'var(--nh-text-primary)',
251
+ border: '1px solid var(--nh-border-subtle)',
252
+ }}
253
+ ```
254
+
255
+ Available CSS variables:
256
+ - `--nh-bg-main`, `--nh-bg-sidebar`, `--nh-bg-surface`
257
+ - `--nh-text-primary`, `--nh-text-secondary`, `--nh-text-muted`
258
+ - `--nh-accent-primary`, `--nh-accent-secondary`
259
+ - `--nh-border-accent`, `--nh-border-subtle`
260
+
261
+ ### Keep it inline
262
+
263
+ Widgets render inline with text. Use `display: inline-flex` or `inline-block`:
264
+
265
+ ```typescript
266
+ style={{
267
+ display: 'inline-flex',
268
+ alignItems: 'center',
269
+ verticalAlign: 'middle',
270
+ }}
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Interaction Handling
276
+
277
+ ### Don't prevent default click behavior
278
+
279
+ Widgets exist inside CodeMirror. Avoid stopping event propagation:
280
+
281
+ ```typescript
282
+ // ✅ Good - simple click handler
283
+ onClick={() => doSomething()}
284
+
285
+ // ⚠️ Caution with event manipulation
286
+ onClick={(e) => {
287
+ e.stopPropagation(); // May cause issues!
288
+ doSomething();
289
+ }}
290
+ ```
291
+
292
+ ### Use data attributes for identification
293
+
294
+ ```typescript
295
+ <span data-widget-id="my-widget" data-value={match[1]}>
296
+ ...
297
+ </span>
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Unregistering Widgets
303
+
304
+ Portals are **automatically unregistered** when your plugin unloads.
305
+
306
+ For manual unregistration:
307
+
308
+ ```typescript
309
+ await ctx.invokeApi('editor:unregister-portal', 'my-portal-id');
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Next Steps
315
+
316
+ - Add **[Settings](05-settings.md)** to configure your widgets
317
+ - Learn about **[Context Menus](06-context-menu.md)**
318
+ - See **[Complete Examples](07-examples.md)**
@@ -0,0 +1,303 @@
1
+ # Settings Integration
2
+
3
+ Add configuration options to your plugin with the Settings API.
4
+
5
+ ## Settings Structure
6
+
7
+ Settings are organized in a hierarchy:
8
+
9
+ ```
10
+ Settings Modal
11
+ └── Tab (e.g., "My Plugin")
12
+ └── Group (e.g., "Appearance")
13
+ └── Item (e.g., "Enable dark mode")
14
+ ```
15
+
16
+ ## Step 1: Register a Tab
17
+
18
+ ```typescript
19
+ await ctx.invokeApi('settings:register-tab', {
20
+ id: 'my-plugin', // Unique identifier
21
+ label: 'My Plugin', // Display name
22
+ icon: 'puzzle', // Lucide icon name (kebab-case)
23
+ order: 100 // Position (lower = first)
24
+ });
25
+ ```
26
+
27
+ ## Step 2: Register a Group
28
+
29
+ ```typescript
30
+ await ctx.invokeApi('settings:register-group', {
31
+ id: 'my-plugin-general', // Unique identifier
32
+ tabId: 'my-plugin', // Parent tab ID
33
+ label: 'General', // Display name
34
+ order: 0 // Position within tab
35
+ });
36
+ ```
37
+
38
+ ## Step 3: Register Items
39
+
40
+ ### Toggle (Boolean)
41
+
42
+ ```typescript
43
+ await ctx.invokeApi('settings:register-item', {
44
+ key: 'my-plugin.enabled',
45
+ type: 'toggle',
46
+ label: 'Enable plugin',
47
+ description: 'Turn the plugin on or off',
48
+ groupId: 'my-plugin-general',
49
+ order: 0,
50
+ defaultValue: true
51
+ });
52
+ ```
53
+
54
+ ### Text Input
55
+
56
+ ```typescript
57
+ await ctx.invokeApi('settings:register-item', {
58
+ key: 'my-plugin.prefix',
59
+ type: 'text',
60
+ label: 'Custom prefix',
61
+ description: 'Text to prepend to all items',
62
+ placeholder: 'Enter prefix...',
63
+ groupId: 'my-plugin-general',
64
+ order: 1,
65
+ defaultValue: ''
66
+ });
67
+ ```
68
+
69
+ ### Number Input
70
+
71
+ ```typescript
72
+ await ctx.invokeApi('settings:register-item', {
73
+ key: 'my-plugin.max-items',
74
+ type: 'number',
75
+ label: 'Maximum items',
76
+ description: 'Limit the number of items shown',
77
+ groupId: 'my-plugin-general',
78
+ order: 2,
79
+ min: 1,
80
+ max: 100,
81
+ step: 1,
82
+ defaultValue: 10
83
+ });
84
+ ```
85
+
86
+ ### Select (Dropdown)
87
+
88
+ ```typescript
89
+ await ctx.invokeApi('settings:register-item', {
90
+ key: 'my-plugin.theme',
91
+ type: 'select',
92
+ label: 'Widget theme',
93
+ description: 'Choose the visual style',
94
+ groupId: 'my-plugin-general',
95
+ order: 3,
96
+ options: [
97
+ { label: 'Default', value: 'default' },
98
+ { label: 'Compact', value: 'compact' },
99
+ { label: 'Minimal', value: 'minimal' }
100
+ ],
101
+ defaultValue: 'default'
102
+ });
103
+ ```
104
+
105
+ ### Color Picker
106
+
107
+ ```typescript
108
+ await ctx.invokeApi('settings:register-item', {
109
+ key: 'my-plugin.accent-color',
110
+ type: 'color',
111
+ label: 'Accent color',
112
+ description: 'Primary color for highlights',
113
+ groupId: 'my-plugin-general',
114
+ order: 4,
115
+ defaultValue: '#3b82f6'
116
+ });
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Reading Settings Values
122
+
123
+ Use the `config:get` API to read settings:
124
+
125
+ ```typescript
126
+ const isEnabled = await ctx.invokeApi<boolean>('config:get', 'my-plugin.enabled', true);
127
+ const prefix = await ctx.invokeApi<string>('config:get', 'my-plugin.prefix', '');
128
+ const maxItems = await ctx.invokeApi<number>('config:get', 'my-plugin.max-items', 10);
129
+ const theme = await ctx.invokeApi<string>('config:get', 'my-plugin.theme', 'default');
130
+ const color = await ctx.invokeApi<string>('config:get', 'my-plugin.accent-color', '#3b82f6');
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Complete Example
136
+
137
+ ```typescript
138
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
139
+
140
+ export default class ConfigurablePlugin extends NotehubPlugin {
141
+ async onload(ctx: PluginContext): Promise<void> {
142
+ // Register settings tab
143
+ await ctx.invokeApi('settings:register-tab', {
144
+ id: 'my-plugin',
145
+ label: 'My Plugin',
146
+ icon: 'settings-2',
147
+ order: 100
148
+ });
149
+
150
+ // Register settings group
151
+ await ctx.invokeApi('settings:register-group', {
152
+ id: 'my-plugin-appearance',
153
+ tabId: 'my-plugin',
154
+ label: 'Appearance',
155
+ order: 0
156
+ });
157
+
158
+ // Register settings items
159
+ await ctx.invokeApi('settings:register-items', [
160
+ {
161
+ key: 'my-plugin.show-icons',
162
+ type: 'toggle',
163
+ label: 'Show icons',
164
+ groupId: 'my-plugin-appearance',
165
+ order: 0,
166
+ defaultValue: true
167
+ },
168
+ {
169
+ key: 'my-plugin.icon-size',
170
+ type: 'select',
171
+ label: 'Icon size',
172
+ groupId: 'my-plugin-appearance',
173
+ order: 1,
174
+ options: [
175
+ { label: 'Small', value: 16 },
176
+ { label: 'Medium', value: 24 },
177
+ { label: 'Large', value: 32 }
178
+ ],
179
+ defaultValue: 24
180
+ }
181
+ ]);
182
+
183
+ // Use settings values
184
+ const showIcons = await ctx.invokeApi<boolean>('config:get', 'my-plugin.show-icons', true);
185
+ const iconSize = await ctx.invokeApi<number>('config:get', 'my-plugin.icon-size', 24);
186
+
187
+ await ctx.invokeApi('logger:info', 'MyPlugin',
188
+ `Settings: showIcons=${showIcons}, iconSize=${iconSize}`);
189
+ }
190
+
191
+ async onunload(): Promise<void> {
192
+ // Settings items are automatically unregistered!
193
+ }
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Batch Registration
200
+
201
+ For multiple items, use the batch APIs:
202
+
203
+ ```typescript
204
+ // Register multiple tabs
205
+ await ctx.invokeApi('settings:register-tabs', [
206
+ { id: 'tab1', label: 'Tab 1', icon: 'star', order: 0 },
207
+ { id: 'tab2', label: 'Tab 2', icon: 'heart', order: 1 }
208
+ ]);
209
+
210
+ // Register multiple groups
211
+ await ctx.invokeApi('settings:register-groups', [
212
+ { id: 'group1', tabId: 'tab1', label: 'Group 1', order: 0 },
213
+ { id: 'group2', tabId: 'tab1', label: 'Group 2', order: 1 }
214
+ ]);
215
+
216
+ // Register multiple items
217
+ await ctx.invokeApi('settings:register-items', [/* items array */]);
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Custom Settings View
223
+
224
+ For complex settings UI, register a custom React component:
225
+
226
+ ```typescript
227
+ const MyCustomSettings: React.FC = () => {
228
+ return (
229
+ <div>
230
+ <h2>Custom Settings UI</h2>
231
+ {/* Your custom settings interface */}
232
+ </div>
233
+ );
234
+ };
235
+
236
+ await ctx.invokeApi('settings:register-custom-view', {
237
+ tabId: 'my-plugin',
238
+ view: MyCustomSettings
239
+ });
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Programmatic Control
245
+
246
+ ```typescript
247
+ // Open settings modal
248
+ await ctx.invokeApi('settings:open');
249
+
250
+ // Close settings modal
251
+ await ctx.invokeApi('settings:close');
252
+
253
+ // Toggle settings modal
254
+ await ctx.invokeApi('settings:toggle');
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Type Definitions
260
+
261
+ ```typescript
262
+ interface SettingsTabDef {
263
+ id: string; // Unique identifier
264
+ label: string; // Display text
265
+ icon: string; // Lucide icon name
266
+ order: number; // Sort order
267
+ }
268
+
269
+ interface SettingsGroupDef {
270
+ id: string; // Unique identifier
271
+ tabId: string; // Parent tab ID
272
+ label: string; // Display text
273
+ order: number; // Sort order
274
+ }
275
+
276
+ interface SettingsItemDef {
277
+ key: string; // Config key (e.g., 'my-plugin.option')
278
+ type: 'toggle' | 'text' | 'number' | 'select' | 'color';
279
+ label: string; // Display text
280
+ description?: string;
281
+ groupId: string; // Parent group ID
282
+ order: number; // Sort order
283
+ defaultValue?: unknown;
284
+
285
+ // For 'text'
286
+ placeholder?: string;
287
+
288
+ // For 'number'
289
+ min?: number;
290
+ max?: number;
291
+ step?: number;
292
+
293
+ // For 'select'
294
+ options?: Array<{ label: string; value: unknown }>;
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Next Steps
301
+
302
+ - Learn about **[Context Menu](06-context-menu.md)** integration
303
+ - See **[Complete Examples](07-examples.md)**