@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,293 @@
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-widget`:
20
+
21
+ ```typescript
22
+ await ctx.invokeApi(
23
+ 'editor:register-widget',
24
+ 'unique-id', // Уникальный идентификатор
25
+ /regex-pattern/g, // Паттерн для поиска (ДОЛЖЕН иметь флаг 'g')
26
+ ReactComponent // Компонент для рендеринга
27
+ );
28
+ ```
29
+
30
+ ## Пропсы компонента
31
+
32
+ Ваш компонент получает массив результатов regex:
33
+
34
+ ```typescript
35
+ interface WidgetProps {
36
+ match: RegExpExecArray;
37
+ }
38
+ ```
39
+
40
+ Массив `match` содержит:
41
+ - `match[0]` — Полная совпавшая строка
42
+ - `match[1]`, `match[2]`, ... — Захваченные группы
43
+
44
+ ## Полный пример: Прогресс-бар
45
+
46
+ ```typescript
47
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
48
+ import React from 'react';
49
+
50
+ // Компонент виджета
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
+ // Плагин
87
+ export default class ProgressBarPlugin extends NotehubPlugin {
88
+ async onload(ctx: PluginContext): Promise<void> {
89
+ // Совпадение: [progress:XX] где XX — число
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', 'Виджет зарегистрирован');
98
+ }
99
+
100
+ async onunload(): Promise<void> {
101
+ // Виджет автоматически отменяет регистрацию!
102
+ }
103
+ }
104
+ ```
105
+
106
+ **Использование в документах:**
107
+ ```markdown
108
+ Завершенность проекта: [progress:75]
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Пример: Кликабельная кнопка
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
+ console.log(`Кнопка нажата: ${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
+ // Регистрация
143
+ await ctx.invokeApi(
144
+ 'editor:register-widget',
145
+ 'btn-widget',
146
+ /\[btn:([^\]:]+):([^\]]+)\]/g,
147
+ ButtonWidget
148
+ );
149
+ ```
150
+
151
+ **Использование:**
152
+ ```markdown
153
+ Нажмите сюда: [btn:Отправить:action-submit]
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Пример: Статус-бейдж
159
+
160
+ ```typescript
161
+ const StatusBadge: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
162
+ const status = match[1].toLowerCase();
163
+
164
+ const colors: Record<string, { bg: string; text: string }> = {
165
+ done: { bg: '#22c55e20', text: '#22c55e' },
166
+ 'in-progress': { bg: '#f59e0b20', text: '#f59e0b' },
167
+ todo: { bg: '#6b728020', text: '#6b7280' },
168
+ };
169
+
170
+ const style = colors[status] || colors.todo;
171
+
172
+ return (
173
+ <span style={{
174
+ padding: '2px 8px',
175
+ borderRadius: '12px',
176
+ fontSize: '12px',
177
+ fontWeight: 500,
178
+ background: style.bg,
179
+ color: style.text,
180
+ }}>
181
+ {match[1]}
182
+ </span>
183
+ );
184
+ };
185
+
186
+ await ctx.invokeApi(
187
+ 'editor:register-widget',
188
+ 'status-badge',
189
+ /\[status:([^\]]+)\]/g,
190
+ StatusBadge
191
+ );
192
+ ```
193
+
194
+ **Использование:**
195
+ ```markdown
196
+ Задача 1 [status:Done]
197
+ Задача 2 [status:In-Progress]
198
+ Задача 3 [status:TODO]
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Лучшие практики Regex
204
+
205
+ ### 1. Всегда используйте глобальный флаг (`g`)
206
+
207
+ ```typescript
208
+ // ✅ Хорошо
209
+ /\[progress:(\d+)\]/g
210
+
211
+ // ❌ Плохо — не найдет множественные вхождения
212
+ /\[progress:(\d+)\]/
213
+ ```
214
+
215
+ ### 2. Используйте группы захвата для динамического контента
216
+
217
+ ```typescript
218
+ // Захватывает две группы: label и value
219
+ /\[meter:([^:]+):(\d+)\]/g
220
+ // match[1] = label
221
+ // match[2] = value
222
+ ```
223
+
224
+ ### 3. Экранируйте специальные символы
225
+
226
+ ```typescript
227
+ // Совпадение [!note] — скобки нужно экранировать
228
+ /\[!note\]/g
229
+ ```
230
+
231
+ ### 4. Будьте конкретны, чтобы избежать ложных совпадений
232
+
233
+ ```typescript
234
+ // ✅ Хорошо — конкретный паттерн
235
+ /\[progress:(\d{1,3})\]/g
236
+
237
+ // ❌ Плохо — слишком жадный
238
+ /\[.*\]/g // Совпадает со ВСЕМ контентом в скобках!
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Советы по стилизации
244
+
245
+ ### Используйте CSS-переменные
246
+
247
+ Используйте цвета темы для согласованной стилизации:
248
+
249
+ ```typescript
250
+ style={{
251
+ background: 'var(--nh-bg-surface)',
252
+ color: 'var(--nh-text-primary)',
253
+ border: '1px solid var(--nh-border-subtle)',
254
+ }}
255
+ ```
256
+
257
+ Доступные CSS-переменные:
258
+ - `--nh-bg-main`, `--nh-bg-sidebar`, `--nh-bg-surface`
259
+ - `--nh-text-primary`, `--nh-text-secondary`, `--nh-text-muted`
260
+ - `--nh-accent-primary`, `--nh-accent-secondary`
261
+ - `--nh-border-accent`, `--nh-border-subtle`
262
+
263
+ ### Держите это инлайн
264
+
265
+ Виджеты рендерятся встроенными в текст. Используйте `display: inline-flex` или `inline-block`:
266
+
267
+ ```typescript
268
+ style={{
269
+ display: 'inline-flex',
270
+ alignItems: 'center',
271
+ verticalAlign: 'middle',
272
+ }}
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Отмена регистрации виджетов
278
+
279
+ Виджеты **автоматически отменяют регистрацию** при выгрузке вашего плагина.
280
+
281
+ Для ручной отмены:
282
+
283
+ ```typescript
284
+ await ctx.invokeApi('editor:unregister-widget', 'my-widget-id');
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Следующие шаги
290
+
291
+ - Добавьте **[Настройки](05-settings.md)** для конфигурации ваших виджетов
292
+ - Изучите **[Контекстные меню](06-context-menu.md)**
293
+ - Смотрите **[Полные примеры](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)**