@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.
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +1 -3
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +13 -1
- package/dist/commands/create.js.map +1 -1
- package/package.json +2 -2
- package/templates/docs/en/01-getting-started.md +207 -0
- package/templates/docs/en/02-architecture.md +228 -0
- package/templates/docs/en/03-api-reference.md +747 -0
- package/templates/docs/en/04-widgets.md +322 -0
- package/templates/docs/en/05-settings.md +303 -0
- package/templates/docs/en/06-context-menu.md +283 -0
- package/templates/docs/en/07-examples.md +547 -0
- package/templates/docs/en/README.md +125 -0
- package/templates/docs/ru/01-getting-started.md +207 -0
- package/templates/docs/ru/02-architecture.md +228 -0
- package/templates/docs/ru/03-api-reference.md +747 -0
- package/templates/docs/ru/04-widgets.md +293 -0
- package/templates/docs/ru/05-settings.md +303 -0
- package/templates/docs/ru/06-context-menu.md +283 -0
- package/templates/docs/ru/07-examples.md +547 -0
- package/templates/docs/ru/README.md +125 -0
- package/templates/docs.html +6 -4
|
@@ -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)**
|