@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,322 @@
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 Widget
18
+
19
+ Use the `editor:register-widget` API:
20
+
21
+ ```typescript
22
+ await ctx.invokeApi(
23
+ 'editor:register-widget',
24
+ 'unique-id', // Unique identifier
25
+ /regex-pattern/g, // Pattern to match (MUST have global flag 'g')
26
+ ReactComponent // Component to render
27
+ );
28
+ ```
29
+
30
+ ## Component Props
31
+
32
+ Your component receives the regex match array:
33
+
34
+ ```typescript
35
+ interface WidgetProps {
36
+ match: RegExpExecArray;
37
+ }
38
+ ```
39
+
40
+ The `match` array contains:
41
+ - `match[0]` - Full matched string
42
+ - `match[1]`, `match[2]`, ... - Capture groups
43
+
44
+ ## Complete Example: Progress Bar
45
+
46
+ ```typescript
47
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
48
+ import React from 'react';
49
+
50
+ // Widget component
51
+ const ProgressBar: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
52
+ const percentage = parseInt(match[1], 10);
53
+
54
+ return (
55
+ <span style={{
56
+ display: 'inline-flex',
57
+ alignItems: 'center',
58
+ gap: '8px',
59
+ padding: '2px 8px',
60
+ background: 'var(--nh-bg-surface)',
61
+ borderRadius: '4px',
62
+ }}>
63
+ <span style={{
64
+ width: '100px',
65
+ height: '8px',
66
+ background: 'var(--nh-bg-secondary)',
67
+ borderRadius: '4px',
68
+ overflow: 'hidden',
69
+ }}>
70
+ <span style={{
71
+ width: `${percentage}%`,
72
+ height: '100%',
73
+ background: 'var(--nh-accent-primary)',
74
+ display: 'block',
75
+ borderRadius: '4px',
76
+ transition: 'width 0.3s ease',
77
+ }} />
78
+ </span>
79
+ <span style={{ fontSize: '12px', color: 'var(--nh-text-muted)' }}>
80
+ {percentage}%
81
+ </span>
82
+ </span>
83
+ );
84
+ };
85
+
86
+ // Plugin
87
+ export default class ProgressBarPlugin extends NotehubPlugin {
88
+ async onload(ctx: PluginContext): Promise<void> {
89
+ // Match: [progress:XX] where XX is a number
90
+ await ctx.invokeApi(
91
+ 'editor:register-widget',
92
+ 'progress-bar',
93
+ /\[progress:(\d+)\]/g,
94
+ ProgressBar
95
+ );
96
+
97
+ await ctx.invokeApi('logger:info', 'ProgressBar', 'Widget registered');
98
+ }
99
+
100
+ async onunload(): Promise<void> {
101
+ // Widget is automatically unregistered!
102
+ }
103
+ }
104
+ ```
105
+
106
+ **Usage in documents:**
107
+ ```markdown
108
+ Project completion: [progress:75]
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Example: Clickable Button
114
+
115
+ ```typescript
116
+ const ButtonWidget: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
117
+ const label = match[1];
118
+ const action = match[2];
119
+
120
+ const handleClick = async () => {
121
+ // You can't access ctx here directly, but you can use events
122
+ // or call a registered API
123
+ console.log(`Button clicked: ${action}`);
124
+ };
125
+
126
+ return (
127
+ <button
128
+ onClick={handleClick}
129
+ style={{
130
+ background: 'var(--nh-accent-primary)',
131
+ color: 'var(--nh-button-text, #fff)',
132
+ border: 'none',
133
+ borderRadius: '4px',
134
+ padding: '4px 12px',
135
+ cursor: 'pointer',
136
+ fontSize: 'inherit',
137
+ }}
138
+ >
139
+ {label}
140
+ </button>
141
+ );
142
+ };
143
+
144
+ // Register
145
+ await ctx.invokeApi(
146
+ 'editor:register-widget',
147
+ 'btn-widget',
148
+ /\[btn:([^\]:]+):([^\]]+)\]/g,
149
+ ButtonWidget
150
+ );
151
+ ```
152
+
153
+ **Usage:**
154
+ ```markdown
155
+ Click here: [btn:Submit:action-submit]
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Example: Status Badge
161
+
162
+ ```typescript
163
+ const StatusBadge: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
164
+ const status = match[1].toLowerCase();
165
+
166
+ const colors: Record<string, { bg: string; text: string }> = {
167
+ done: { bg: '#22c55e20', text: '#22c55e' },
168
+ 'in-progress': { bg: '#f59e0b20', text: '#f59e0b' },
169
+ todo: { bg: '#6b728020', text: '#6b7280' },
170
+ };
171
+
172
+ const style = colors[status] || colors.todo;
173
+
174
+ return (
175
+ <span style={{
176
+ padding: '2px 8px',
177
+ borderRadius: '12px',
178
+ fontSize: '12px',
179
+ fontWeight: 500,
180
+ background: style.bg,
181
+ color: style.text,
182
+ }}>
183
+ {match[1]}
184
+ </span>
185
+ );
186
+ };
187
+
188
+ await ctx.invokeApi(
189
+ 'editor:register-widget',
190
+ 'status-badge',
191
+ /\[status:([^\]]+)\]/g,
192
+ StatusBadge
193
+ );
194
+ ```
195
+
196
+ **Usage:**
197
+ ```markdown
198
+ Task 1 [status:Done]
199
+ Task 2 [status:In-Progress]
200
+ Task 3 [status:TODO]
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Regex Best Practices
206
+
207
+ ### 1. Always use global flag (`g`)
208
+
209
+ ```typescript
210
+ // ✅ Good
211
+ /\[progress:(\d+)\]/g
212
+
213
+ // ❌ Bad - won't match multiple occurrences
214
+ /\[progress:(\d+)\]/
215
+ ```
216
+
217
+ ### 2. Use capture groups for dynamic content
218
+
219
+ ```typescript
220
+ // Captures two groups: label and value
221
+ /\[meter:([^:]+):(\d+)\]/g
222
+ // match[1] = label
223
+ // match[2] = value
224
+ ```
225
+
226
+ ### 3. Escape special characters
227
+
228
+ ```typescript
229
+ // Match [!note] - brackets need escape
230
+ /\[!note\]/g
231
+ ```
232
+
233
+ ### 4. Be specific to avoid false matches
234
+
235
+ ```typescript
236
+ // ✅ Good - specific pattern
237
+ /\[progress:(\d{1,3})\]/g
238
+
239
+ // ❌ Bad - too greedy
240
+ /\[.*\]/g // Matches ALL bracketed content!
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Styling Tips
246
+
247
+ ### Use CSS Variables
248
+
249
+ Access theme colors for consistent styling:
250
+
251
+ ```typescript
252
+ style={{
253
+ background: 'var(--nh-bg-surface)',
254
+ color: 'var(--nh-text-primary)',
255
+ border: '1px solid var(--nh-border-subtle)',
256
+ }}
257
+ ```
258
+
259
+ Available CSS variables:
260
+ - `--nh-bg-main`, `--nh-bg-sidebar`, `--nh-bg-surface`
261
+ - `--nh-text-primary`, `--nh-text-secondary`, `--nh-text-muted`
262
+ - `--nh-accent-primary`, `--nh-accent-secondary`
263
+ - `--nh-border-accent`, `--nh-border-subtle`
264
+
265
+ ### Keep it inline
266
+
267
+ Widgets render inline with text. Use `display: inline-flex` or `inline-block`:
268
+
269
+ ```typescript
270
+ style={{
271
+ display: 'inline-flex',
272
+ alignItems: 'center',
273
+ verticalAlign: 'middle',
274
+ }}
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Interaction Handling
280
+
281
+ ### Don't prevent default click behavior
282
+
283
+ Widgets exist inside CodeMirror. Avoid stopping event propagation:
284
+
285
+ ```typescript
286
+ // ✅ Good - simple click handler
287
+ onClick={() => doSomething()}
288
+
289
+ // ⚠️ Caution with event manipulation
290
+ onClick={(e) => {
291
+ e.stopPropagation(); // May cause issues!
292
+ doSomething();
293
+ }}
294
+ ```
295
+
296
+ ### Use data attributes for identification
297
+
298
+ ```typescript
299
+ <span data-widget-id="my-widget" data-value={match[1]}>
300
+ ...
301
+ </span>
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Unregistering Widgets
307
+
308
+ Widgets are **automatically unregistered** when your plugin unloads.
309
+
310
+ For manual unregistration:
311
+
312
+ ```typescript
313
+ await ctx.invokeApi('editor:unregister-widget', 'my-widget-id');
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Next Steps
319
+
320
+ - Add **[Settings](05-settings.md)** to configure your widgets
321
+ - Learn about **[Context Menus](06-context-menu.md)**
322
+ - 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)**