@u-devtools/core 0.1.6 → 0.2.0

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/src/control.ts CHANGED
@@ -2,38 +2,38 @@ import type { AppBridge } from './bridge-app';
2
2
 
3
3
  export interface OverlayContext {
4
4
  /**
5
- * Открыть основное окно DevTools
5
+ * Open the main DevTools window
6
6
  */
7
7
  open: () => void;
8
8
 
9
9
  /**
10
- * Закрыть основное окно DevTools
10
+ * Close the main DevTools window
11
11
  */
12
12
  close: () => void;
13
13
 
14
14
  /**
15
- * Переключить состояние окна
15
+ * Toggle window state
16
16
  */
17
17
  toggle: () => void;
18
18
 
19
19
  /**
20
- * Текущее состояние
20
+ * Current state
21
21
  */
22
22
  isOpen: boolean;
23
23
 
24
24
  /**
25
- * Переключить на плагин по имени
25
+ * Switch to plugin by name
26
26
  */
27
27
  switchPlugin: (pluginName: string) => void;
28
28
 
29
29
  /**
30
- * Переключить таб внутри плагина по имени таба
30
+ * Switch tab within plugin by tab name
31
31
  */
32
32
  switchTab: (pluginName: string, tabName: string) => void;
33
33
 
34
34
  /**
35
- * Создать временный мост для отправки сообщения.
36
- * Полезно, если у вас нет доступа к глобальному мосту плагина в данной области видимости.
35
+ * Create a temporary bridge for sending messages.
36
+ * Useful if you don't have access to the plugin's global bridge in this scope.
37
37
  */
38
38
  createBridge: (namespace: string) => AppBridge;
39
39
  }
@@ -41,12 +41,12 @@ export interface OverlayContext {
41
41
  export interface OverlayMenuItem {
42
42
  id: string;
43
43
  label: string;
44
- icon?: string; // Имя иконки (Heroicons) - для обратной совместимости
45
- iconSvg?: string; // SVG как текст
46
- iconUrl?: string; // URL к иконке
44
+ icon?: string; // Icon name (Heroicons) - for backward compatibility
45
+ iconSvg?: string; // SVG as text
46
+ iconUrl?: string; // URL to icon
47
47
  order?: number;
48
48
  /**
49
- * Обработчики событий (принимают контекст)
49
+ * Event handlers (receive context)
50
50
  */
51
51
  onClick?: (ctx: OverlayContext, event: MouseEvent) => void;
52
52
  onDoubleClick?: (ctx: OverlayContext, event: MouseEvent) => void;
@@ -61,72 +61,43 @@ export interface OverlayMenuItem {
61
61
  onBlur?: (ctx: OverlayContext, event: FocusEvent) => void;
62
62
  }
63
63
 
64
- export const OVERLAY_EVENT = 'u-devtools:register-menu-item';
65
-
66
64
  declare global {
67
65
  interface Window {
68
66
  __UDEVTOOLS_MENU_ITEMS__?: OverlayMenuItem[];
69
67
  }
70
68
  }
71
69
 
72
- /**
73
- * Функция для регистрации кнопки в оверлее (вызывается из app.ts плагина)
74
- */
75
- export function registerMenuItem(item: OverlayMenuItem) {
76
- if (typeof window === 'undefined') return;
77
-
78
- // Инициализируем глобальный массив, если его нет
79
- if (!window.__UDEVTOOLS_MENU_ITEMS__) {
80
- window.__UDEVTOOLS_MENU_ITEMS__ = [];
81
- }
82
-
83
- // Сохраняем элемент в глобальный массив (для случаев, когда overlay еще не загружен)
84
- const existingIdx = window.__UDEVTOOLS_MENU_ITEMS__.findIndex((i) => i.id === item.id);
85
- if (existingIdx !== -1) {
86
- window.__UDEVTOOLS_MENU_ITEMS__[existingIdx] = item;
87
- } else {
88
- window.__UDEVTOOLS_MENU_ITEMS__.push(item);
89
- }
90
-
91
- // Отправляем событие, которое поймает Vue-приложение оверлея (для обратной совместимости)
92
- window.dispatchEvent(
93
- new CustomEvent(OVERLAY_EVENT, {
94
- detail: item,
95
- })
96
- );
97
- }
98
-
99
70
  export class DevToolsControl {
100
71
  private channel: BroadcastChannel;
101
72
 
102
73
  constructor() {
103
- // Единый канал для управления состоянием
74
+ // Single channel for state management
104
75
  this.channel = new BroadcastChannel('u-devtools:control');
105
76
  }
106
77
 
107
78
  /**
108
- * Открыть DevTools
79
+ * Open DevTools
109
80
  */
110
81
  open() {
111
82
  this.channel.postMessage({ action: 'open' });
112
83
  }
113
84
 
114
85
  /**
115
- * Закрыть DevTools
86
+ * Close DevTools
116
87
  */
117
88
  close() {
118
89
  this.channel.postMessage({ action: 'close' });
119
90
  }
120
91
 
121
92
  /**
122
- * Переключить состояние
93
+ * Toggle state
123
94
  */
124
95
  toggle() {
125
96
  this.channel.postMessage({ action: 'toggle' });
126
97
  }
127
98
 
128
99
  /**
129
- * Получить текущее состояние (асинхронно)
100
+ * Get current state (asynchronously)
130
101
  */
131
102
  isOpen(): Promise<boolean> {
132
103
  return new Promise((resolve) => {
@@ -138,10 +109,10 @@ export class DevToolsControl {
138
109
  };
139
110
 
140
111
  this.channel.addEventListener('message', handler);
141
- // Запрашиваем состояние
112
+ // Request state
142
113
  this.channel.postMessage({ action: 'get-state' });
143
114
 
144
- // Таймаут на случай, если девтулс не загружен
115
+ // Timeout in case DevTools is not loaded
145
116
  setTimeout(() => {
146
117
  this.channel.removeEventListener('message', handler);
147
118
  resolve(false);
@@ -150,7 +121,7 @@ export class DevToolsControl {
150
121
  }
151
122
 
152
123
  /**
153
- * Подписаться на изменение состояния
124
+ * Subscribe to state changes
154
125
  */
155
126
  onStateChange(cb: (isOpen: boolean) => void) {
156
127
  const handler = (e: MessageEvent) => {
@@ -163,14 +134,14 @@ export class DevToolsControl {
163
134
  }
164
135
 
165
136
  /**
166
- * Переключить на плагин по имени
137
+ * Switch to plugin by name
167
138
  */
168
139
  switchPlugin(pluginName: string) {
169
140
  this.channel.postMessage({ action: 'switch-plugin', pluginName });
170
141
  }
171
142
 
172
143
  /**
173
- * Переключить таб внутри плагина по имени таба
144
+ * Switch tab within plugin by tab name
174
145
  */
175
146
  switchTab(pluginName: string, tabName: string) {
176
147
  this.channel.postMessage({ action: 'switch-tab', pluginName, tabName });
@@ -181,5 +152,5 @@ export class DevToolsControl {
181
152
  }
182
153
  }
183
154
 
184
- // Экспортируем синглтон для удобства
155
+ // Export singleton for convenience
185
156
  export const devtools = new DevToolsControl();
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Typed Event Bus for inter-plugin communication
3
+ */
4
+
5
+ /**
6
+ * EventBus events interface.
7
+ * Plugins can extend this interface through module augmentation.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // In your plugin
12
+ * declare module '@u-devtools/core' {
13
+ * interface BusEvents {
14
+ * 'my-plugin:custom-event': { data: string };
15
+ * }
16
+ * }
17
+ * ```
18
+ */
19
+ export interface BusEvents {
20
+ 'plugin:mounted': { name: string };
21
+ 'plugin:unmounted': { name: string };
22
+ navigate: { path: string };
23
+ 'settings:changed': { key: string; value: unknown };
24
+ 'storage:changed': { key: string; value: unknown };
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ /**
29
+ * Typed Event Emitter for inter-plugin communication.
30
+ * Provides type-safe event emission and subscription.
31
+ *
32
+ * @template T - Type definition for events and their data
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { TypedEventBus, type BusEvents } from '@u-devtools/core';
37
+ *
38
+ * // Create event bus instance
39
+ * const bus = new TypedEventBus<BusEvents>();
40
+ *
41
+ * // Emit events
42
+ * bus.emit('plugin:mounted', { name: 'my-plugin' });
43
+ * bus.emit('navigate', { path: '/settings' });
44
+ * bus.emit('settings:changed', { key: 'theme', value: 'dark' });
45
+ *
46
+ * // Subscribe to events
47
+ * const unsubscribe1 = bus.on('plugin:mounted', ({ name }) => {
48
+ * console.log(`Plugin ${name} was mounted`);
49
+ * });
50
+ *
51
+ * const unsubscribe2 = bus.on('navigate', ({ path }) => {
52
+ * console.log(`Navigating to: ${path}`);
53
+ * });
54
+ *
55
+ * // Unsubscribe
56
+ * unsubscribe1();
57
+ * unsubscribe2();
58
+ *
59
+ * // Or use off method (handler must be the same function reference)
60
+ * const handler = ({ name }: { name: string }) => {
61
+ * console.log(`Plugin ${name} was mounted`);
62
+ * };
63
+ * bus.on('plugin:mounted', handler);
64
+ * bus.off('plugin:mounted', handler);
65
+ * ```
66
+ */
67
+ export class TypedEventBus<T extends Record<string, unknown> = BusEvents> {
68
+ private listeners = new Map<keyof T, Set<(data: T[keyof T]) => void>>();
69
+
70
+ /**
71
+ * Emit an event
72
+ */
73
+ emit<K extends keyof T>(event: K, data: T[K]): void {
74
+ const handlers = this.listeners.get(event);
75
+ if (handlers) {
76
+ handlers.forEach((fn) => {
77
+ fn(data);
78
+ });
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Subscribe to an event
84
+ */
85
+ on<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void {
86
+ if (!this.listeners.has(event)) {
87
+ this.listeners.set(event, new Set());
88
+ }
89
+ const handlers = this.listeners.get(event);
90
+ if (handlers) {
91
+ handlers.add(handler as (data: T[keyof T]) => void);
92
+ }
93
+
94
+ // Return unsubscribe function
95
+ return () => {
96
+ this.off(event, handler);
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Unsubscribe from an event
102
+ */
103
+ off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
104
+ const handlers = this.listeners.get(event);
105
+ if (handlers) {
106
+ handlers.delete(handler as (data: T[keyof T]) => void);
107
+ if (handlers.size === 0) {
108
+ this.listeners.delete(event);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Clear all subscriptions
115
+ */
116
+ clear(): void {
117
+ this.listeners.clear();
118
+ }
119
+
120
+ /**
121
+ * Get the number of subscribers for an event
122
+ */
123
+ listenerCount<K extends keyof T>(event: K): number {
124
+ return this.listeners.get(event)?.size || 0;
125
+ }
126
+ }