@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,289 @@
1
+ # Виджеты (Порталы)
2
+
3
+ Порталы — это кастомные React-компоненты, которые рендерятся инлайн в редакторе, заменяя совпадающие текстовые паттерны.
4
+
5
+ ## Как работают порталы
6
+
7
+ 1. Вы определяете **regex-паттерн**, который ищет текст в документе
8
+ 2. Вы предоставляете **React-компонент** для рендеринга каждого совпадения
9
+ 3. Notehub заменяет совпадающий текст вашим компонентом в **режиме просмотра**
10
+ 4. Когда курсор входит в совпадение, переключается в **режим редактирования** (показывает исходный текст)
11
+
12
+ ```
13
+ Режим просмотра: [████████░░] 80% ← Ваш отрендеренный компонент
14
+ Режим редактирования: [progress:80] ← Исходный текст виден когда курсор внутри
15
+ ```
16
+
17
+ ## Регистрация портала
18
+
19
+ Используйте API `editor:register-portal`:
20
+
21
+ ```typescript
22
+ await ctx.invokeApi('editor:register-portal', {
23
+ id: 'unique-id', // Уникальный идентификатор
24
+ regex: /regex-pattern/g, // Паттерн для поиска (ДОЛЖЕН иметь флаг 'g')
25
+ component: ReactComponent // Компонент для рендеринга
26
+ });
27
+ ```
28
+
29
+ ## Пропсы компонента
30
+
31
+ Ваш компонент получает массив результатов regex:
32
+
33
+ ```typescript
34
+ interface WidgetProps {
35
+ match: RegExpExecArray;
36
+ }
37
+ ```
38
+
39
+ Массив `match` содержит:
40
+ - `match[0]` — Полная совпавшая строка
41
+ - `match[1]`, `match[2]`, ... — Захваченные группы
42
+
43
+ ## Полный пример: Прогресс-бар
44
+
45
+ ```typescript
46
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
47
+ import React from 'react';
48
+
49
+ // Компонент виджета
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
+ // Плагин
86
+ export default class ProgressBarPlugin extends NotehubPlugin {
87
+ async onload(ctx: PluginContext): Promise<void> {
88
+ // Совпадение: [progress:XX] где XX — число
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', 'Портал зарегистрирован');
96
+ }
97
+
98
+ async onunload(): Promise<void> {
99
+ // Портал автоматически отменяет регистрацию!
100
+ }
101
+ }
102
+ ```
103
+
104
+ **Использование в документах:**
105
+ ```markdown
106
+ Завершенность проекта: [progress:75]
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Пример: Кликабельная кнопка
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
+ console.log(`Кнопка нажата: ${action}`);
120
+ };
121
+
122
+ return (
123
+ <button
124
+ onClick={handleClick}
125
+ style={{
126
+ background: 'var(--nh-accent-primary)',
127
+ color: 'var(--nh-button-text, #fff)',
128
+ border: 'none',
129
+ borderRadius: '4px',
130
+ padding: '4px 12px',
131
+ cursor: 'pointer',
132
+ fontSize: 'inherit',
133
+ }}
134
+ >
135
+ {label}
136
+ </button>
137
+ );
138
+ };
139
+
140
+ // Регистрация
141
+ await ctx.invokeApi('editor:register-portal', {
142
+ id: 'btn-widget',
143
+ regex: /\[btn:([^\]:]+):([^\]]+)\]/g,
144
+ component: ButtonWidget
145
+ });
146
+ ```
147
+
148
+ **Использование:**
149
+ ```markdown
150
+ Нажмите сюда: [btn:Отправить:action-submit]
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Пример: Статус-бейдж
156
+
157
+ ```typescript
158
+ const StatusBadge: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
159
+ const status = match[1].toLowerCase();
160
+
161
+ const colors: Record<string, { bg: string; text: string }> = {
162
+ done: { bg: '#22c55e20', text: '#22c55e' },
163
+ 'in-progress': { bg: '#f59e0b20', text: '#f59e0b' },
164
+ todo: { bg: '#6b728020', text: '#6b7280' },
165
+ };
166
+
167
+ const style = colors[status] || colors.todo;
168
+
169
+ return (
170
+ <span style={{
171
+ padding: '2px 8px',
172
+ borderRadius: '12px',
173
+ fontSize: '12px',
174
+ fontWeight: 500,
175
+ background: style.bg,
176
+ color: style.text,
177
+ }}>
178
+ {match[1]}
179
+ </span>
180
+ );
181
+ };
182
+
183
+ await ctx.invokeApi('editor:register-portal', {
184
+ id: 'status-badge',
185
+ regex: /\[status:([^\]]+)\]/g,
186
+ component: StatusBadge
187
+ });
188
+ ```
189
+
190
+ **Использование:**
191
+ ```markdown
192
+ Задача 1 [status:Done]
193
+ Задача 2 [status:In-Progress]
194
+ Задача 3 [status:TODO]
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Лучшие практики Regex
200
+
201
+ ### 1. Всегда используйте глобальный флаг (`g`)
202
+
203
+ ```typescript
204
+ // ✅ Хорошо
205
+ /\[progress:(\d+)\]/g
206
+
207
+ // ❌ Плохо — не найдет множественные вхождения
208
+ /\[progress:(\d+)\]/
209
+ ```
210
+
211
+ ### 2. Используйте группы захвата для динамического контента
212
+
213
+ ```typescript
214
+ // Захватывает две группы: label и value
215
+ /\[meter:([^:]+):(\d+)\]/g
216
+ // match[1] = label
217
+ // match[2] = value
218
+ ```
219
+
220
+ ### 3. Экранируйте специальные символы
221
+
222
+ ```typescript
223
+ // Совпадение [!note] — скобки нужно экранировать
224
+ /\[!note\]/g
225
+ ```
226
+
227
+ ### 4. Будьте конкретны, чтобы избежать ложных совпадений
228
+
229
+ ```typescript
230
+ // ✅ Хорошо — конкретный паттерн
231
+ /\[progress:(\d{1,3})\]/g
232
+
233
+ // ❌ Плохо — слишком жадный
234
+ /\[.*\]/g // Совпадает со ВСЕМ контентом в скобках!
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Советы по стилизации
240
+
241
+ ### Используйте CSS-переменные
242
+
243
+ Используйте цвета темы для согласованной стилизации:
244
+
245
+ ```typescript
246
+ style={{
247
+ background: 'var(--nh-bg-surface)',
248
+ color: 'var(--nh-text-primary)',
249
+ border: '1px solid var(--nh-border-subtle)',
250
+ }}
251
+ ```
252
+
253
+ Доступные CSS-переменные:
254
+ - `--nh-bg-main`, `--nh-bg-sidebar`, `--nh-bg-surface`
255
+ - `--nh-text-primary`, `--nh-text-secondary`, `--nh-text-muted`
256
+ - `--nh-accent-primary`, `--nh-accent-secondary`
257
+ - `--nh-border-accent`, `--nh-border-subtle`
258
+
259
+ ### Держите это инлайн
260
+
261
+ Виджеты рендерятся встроенными в текст. Используйте `display: inline-flex` или `inline-block`:
262
+
263
+ ```typescript
264
+ style={{
265
+ display: 'inline-flex',
266
+ alignItems: 'center',
267
+ verticalAlign: 'middle',
268
+ }}
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Отмена регистрации виджетов
274
+
275
+ Порталы **автоматически отменяют регистрацию** при выгрузке вашего плагина.
276
+
277
+ Для ручной отмены:
278
+
279
+ ```typescript
280
+ await ctx.invokeApi('editor:unregister-portal', 'my-portal-id');
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Следующие шаги
286
+
287
+ - Добавьте **[Настройки](05-settings.md)** для конфигурации ваших виджетов
288
+ - Изучите **[Контекстные меню](06-context-menu.md)**
289
+ - Смотрите **[Полные примеры](07-examples.md)**
@@ -0,0 +1,303 @@
1
+ # Интеграция настроек
2
+
3
+ Добавление параметров конфигурации в ваш плагин через Settings API.
4
+
5
+ ## Структура настроек
6
+
7
+ Настройки организованы в иерархию:
8
+
9
+ ```
10
+ Модальное окно настроек
11
+ └── Вкладка (например, "Мой плагин")
12
+ └── Группа (например, "Внешний вид")
13
+ └── Элемент (например, "Включить темный режим")
14
+ ```
15
+
16
+ ## Шаг 1: Регистрация вкладки
17
+
18
+ ```typescript
19
+ await ctx.invokeApi('settings:register-tab', {
20
+ id: 'my-plugin', // Уникальный идентификатор
21
+ label: 'Мой плагин', // Отображаемое имя
22
+ icon: 'puzzle', // Имя иконки Lucide (kebab-case)
23
+ order: 100 // Позиция (меньше = первее)
24
+ });
25
+ ```
26
+
27
+ ## Шаг 2: Регистрация группы
28
+
29
+ ```typescript
30
+ await ctx.invokeApi('settings:register-group', {
31
+ id: 'my-plugin-general', // Уникальный идентификатор
32
+ tabId: 'my-plugin', // ID родительской вкладки
33
+ label: 'Основные', // Отображаемое имя
34
+ order: 0 // Позиция внутри вкладки
35
+ });
36
+ ```
37
+
38
+ ## Шаг 3: Регистрация элементов
39
+
40
+ ### Toggle (Булевый переключатель)
41
+
42
+ ```typescript
43
+ await ctx.invokeApi('settings:register-item', {
44
+ key: 'my-plugin.enabled',
45
+ type: 'toggle',
46
+ label: 'Включить плагин',
47
+ description: 'Включение или выключение плагина',
48
+ groupId: 'my-plugin-general',
49
+ order: 0,
50
+ defaultValue: true
51
+ });
52
+ ```
53
+
54
+ ### Text (Текстовое поле)
55
+
56
+ ```typescript
57
+ await ctx.invokeApi('settings:register-item', {
58
+ key: 'my-plugin.prefix',
59
+ type: 'text',
60
+ label: 'Кастомный префикс',
61
+ description: 'Текст для добавления перед элементами',
62
+ placeholder: 'Введите префикс...',
63
+ groupId: 'my-plugin-general',
64
+ order: 1,
65
+ defaultValue: ''
66
+ });
67
+ ```
68
+
69
+ ### Number (Числовое поле)
70
+
71
+ ```typescript
72
+ await ctx.invokeApi('settings:register-item', {
73
+ key: 'my-plugin.max-items',
74
+ type: 'number',
75
+ label: 'Максимум элементов',
76
+ description: 'Ограничение количества отображаемых элементов',
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 (Выпадающий список)
87
+
88
+ ```typescript
89
+ await ctx.invokeApi('settings:register-item', {
90
+ key: 'my-plugin.theme',
91
+ type: 'select',
92
+ label: 'Тема виджета',
93
+ description: 'Выбор визуального стиля',
94
+ groupId: 'my-plugin-general',
95
+ order: 3,
96
+ options: [
97
+ { label: 'По умолчанию', value: 'default' },
98
+ { label: 'Компактный', value: 'compact' },
99
+ { label: 'Минимальный', value: 'minimal' }
100
+ ],
101
+ defaultValue: 'default'
102
+ });
103
+ ```
104
+
105
+ ### Color (Выбор цвета)
106
+
107
+ ```typescript
108
+ await ctx.invokeApi('settings:register-item', {
109
+ key: 'my-plugin.accent-color',
110
+ type: 'color',
111
+ label: 'Акцентный цвет',
112
+ description: 'Основной цвет для выделений',
113
+ groupId: 'my-plugin-general',
114
+ order: 4,
115
+ defaultValue: '#3b82f6'
116
+ });
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Чтение значений настроек
122
+
123
+ Используйте API `config:get` для чтения настроек:
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
+ ## Полный пример
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
+ // Регистрация вкладки настроек
143
+ await ctx.invokeApi('settings:register-tab', {
144
+ id: 'my-plugin',
145
+ label: 'Мой плагин',
146
+ icon: 'settings-2',
147
+ order: 100
148
+ });
149
+
150
+ // Регистрация группы
151
+ await ctx.invokeApi('settings:register-group', {
152
+ id: 'my-plugin-appearance',
153
+ tabId: 'my-plugin',
154
+ label: 'Внешний вид',
155
+ order: 0
156
+ });
157
+
158
+ // Регистрация элементов
159
+ await ctx.invokeApi('settings:register-items', [
160
+ {
161
+ key: 'my-plugin.show-icons',
162
+ type: 'toggle',
163
+ label: 'Показывать иконки',
164
+ groupId: 'my-plugin-appearance',
165
+ order: 0,
166
+ defaultValue: true
167
+ },
168
+ {
169
+ key: 'my-plugin.icon-size',
170
+ type: 'select',
171
+ label: 'Размер иконок',
172
+ groupId: 'my-plugin-appearance',
173
+ order: 1,
174
+ options: [
175
+ { label: 'Маленький', value: 16 },
176
+ { label: 'Средний', value: 24 },
177
+ { label: 'Большой', value: 32 }
178
+ ],
179
+ defaultValue: 24
180
+ }
181
+ ]);
182
+
183
+ // Использование значений
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
+ `Настройки: showIcons=${showIcons}, iconSize=${iconSize}`);
189
+ }
190
+
191
+ async onunload(): Promise<void> {
192
+ // Элементы настроек автоматически удаляются!
193
+ }
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Пакетная регистрация
200
+
201
+ Для множества элементов используйте пакетные API:
202
+
203
+ ```typescript
204
+ // Регистрация нескольких вкладок
205
+ await ctx.invokeApi('settings:register-tabs', [
206
+ { id: 'tab1', label: 'Вкладка 1', icon: 'star', order: 0 },
207
+ { id: 'tab2', label: 'Вкладка 2', icon: 'heart', order: 1 }
208
+ ]);
209
+
210
+ // Регистрация нескольких групп
211
+ await ctx.invokeApi('settings:register-groups', [
212
+ { id: 'group1', tabId: 'tab1', label: 'Группа 1', order: 0 },
213
+ { id: 'group2', tabId: 'tab1', label: 'Группа 2', order: 1 }
214
+ ]);
215
+
216
+ // Регистрация нескольких элементов
217
+ await ctx.invokeApi('settings:register-items', [/* массив элементов */]);
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Кастомное представление настроек
223
+
224
+ Для сложного UI настроек зарегистрируйте кастомный React-компонент:
225
+
226
+ ```typescript
227
+ const MyCustomSettings: React.FC = () => {
228
+ return (
229
+ <div>
230
+ <h2>Кастомный UI настроек</h2>
231
+ {/* Ваш кастомный интерфейс */}
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
+ ## Программное управление
245
+
246
+ ```typescript
247
+ // Открыть модальное окно настроек
248
+ await ctx.invokeApi('settings:open');
249
+
250
+ // Закрыть модальное окно настроек
251
+ await ctx.invokeApi('settings:close');
252
+
253
+ // Переключить модальное окно настроек
254
+ await ctx.invokeApi('settings:toggle');
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Определения типов
260
+
261
+ ```typescript
262
+ interface SettingsTabDef {
263
+ id: string; // Уникальный идентификатор
264
+ label: string; // Отображаемый текст
265
+ icon: string; // Имя иконки Lucide
266
+ order: number; // Порядок сортировки
267
+ }
268
+
269
+ interface SettingsGroupDef {
270
+ id: string; // Уникальный идентификатор
271
+ tabId: string; // ID родительской вкладки
272
+ label: string; // Отображаемый текст
273
+ order: number; // Порядок сортировки
274
+ }
275
+
276
+ interface SettingsItemDef {
277
+ key: string; // Ключ конфига (например, 'my-plugin.option')
278
+ type: 'toggle' | 'text' | 'number' | 'select' | 'color';
279
+ label: string; // Отображаемый текст
280
+ description?: string;
281
+ groupId: string; // ID родительской группы
282
+ order: number; // Порядок сортировки
283
+ defaultValue?: unknown;
284
+
285
+ // Для 'text'
286
+ placeholder?: string;
287
+
288
+ // Для 'number'
289
+ min?: number;
290
+ max?: number;
291
+ step?: number;
292
+
293
+ // Для 'select'
294
+ options?: Array<{ label: string; value: unknown }>;
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Следующие шаги
301
+
302
+ - Изучите интеграцию **[Контекстных меню](06-context-menu.md)**
303
+ - Смотрите **[Полные примеры](07-examples.md)**