vg-coder-cli 2.0.30 → 2.0.32
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/ARCHITECTURE.md +255 -0
- package/README.md +0 -11
- package/change.sh +0 -0
- package/dist/vg-coder-bundle.js +42 -0
- package/gulpfile.js +111 -0
- package/package.json +19 -11
- package/scripts/postinstall.js +13 -3
- package/src/index.js +28 -220
- package/src/server/api-server.js +120 -428
- package/src/server/views/css/bubble.css +81 -0
- package/src/server/views/css/code-viewer.css +58 -0
- package/src/server/views/css/terminal.css +59 -155
- package/src/server/views/dashboard.css +78 -678
- package/src/server/views/dashboard.html +39 -278
- package/src/server/views/js/api.js +2 -22
- package/src/server/views/js/config.js +27 -15
- package/src/server/views/js/event-protocol.js +263 -0
- package/src/server/views/js/features/bubble-features/index.js +125 -0
- package/src/server/views/js/features/bubble-features/paste-run-feature.js +16 -0
- package/src/server/views/js/features/bubble-features/terminal-feature.js +16 -0
- package/src/server/views/js/features/bubble.js +175 -0
- package/src/server/views/js/features/code-viewer.js +90 -0
- package/src/server/views/js/features/commands.js +34 -81
- package/src/server/views/js/features/editor-tabs.js +19 -46
- package/src/server/views/js/features/git-view.js +63 -81
- package/src/server/views/js/features/iframe-manager.js +3 -97
- package/src/server/views/js/features/monaco-manager.js +19 -39
- package/src/server/views/js/features/project-switcher.js +7 -63
- package/src/server/views/js/features/resize.js +5 -16
- package/src/server/views/js/features/structure.js +38 -106
- package/src/server/views/js/features/terminal.js +102 -418
- package/src/server/views/js/handlers.js +60 -43
- package/src/server/views/js/main.js +75 -179
- package/src/server/views/js/shadow-entry.js +21 -0
- package/src/server/views/js/utils.js +48 -28
- package/src/server/views/vg-coder/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/src/server/views/vg-coder/controller.js +33 -258
- package/vetgo-auto/chrome/src/utils/injector-script.ts +33 -258
- package/vetgo-auto/vg-coder.zip +0 -0
- package/src/server/views/dashboard.js +0 -457
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VG Coder Event Protocol
|
|
3
|
+
*
|
|
4
|
+
* Standardized event communication system for cross-context messaging.
|
|
5
|
+
* Supports: window, shadow-root, iframe contexts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Event Types Registry
|
|
9
|
+
export const EVENT_TYPES = {
|
|
10
|
+
PASTE_RUN: 'vg:paste-run',
|
|
11
|
+
TERMINAL_NEW: 'vg:terminal-new',
|
|
12
|
+
TERMINAL_EXECUTE: 'vg:terminal-execute',
|
|
13
|
+
FEATURE_TOGGLE: 'vg:feature-toggle',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* EventProtocol - Standardized event format
|
|
18
|
+
*/
|
|
19
|
+
export class EventProtocol {
|
|
20
|
+
constructor({ type, source, target, payload = {}, context = 'window' }) {
|
|
21
|
+
this.type = type;
|
|
22
|
+
this.source = source;
|
|
23
|
+
this.target = target;
|
|
24
|
+
this.payload = payload;
|
|
25
|
+
this.timestamp = Date.now();
|
|
26
|
+
this.context = context;
|
|
27
|
+
this.id = `${type}-${this.timestamp}-${Math.random().toString(36).substr(2, 9)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate event structure
|
|
32
|
+
*/
|
|
33
|
+
isValid() {
|
|
34
|
+
return !!(this.type && this.source && this.target);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert to plain object
|
|
39
|
+
*/
|
|
40
|
+
toObject() {
|
|
41
|
+
return {
|
|
42
|
+
id: this.id,
|
|
43
|
+
type: this.type,
|
|
44
|
+
source: this.source,
|
|
45
|
+
target: this.target,
|
|
46
|
+
payload: this.payload,
|
|
47
|
+
timestamp: this.timestamp,
|
|
48
|
+
context: this.context,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* EventDispatcher - Multi-context event dispatcher
|
|
55
|
+
*
|
|
56
|
+
* Supports communication across:
|
|
57
|
+
* - Window context
|
|
58
|
+
* - Shadow DOM context
|
|
59
|
+
* - Iframe context
|
|
60
|
+
*/
|
|
61
|
+
export class EventDispatcher {
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
this.listeners = new Map();
|
|
64
|
+
this.contexts = options.contexts || ['window'];
|
|
65
|
+
this.debug = options.debug || false;
|
|
66
|
+
this.eventHistory = [];
|
|
67
|
+
this.maxHistorySize = options.maxHistorySize || 100;
|
|
68
|
+
|
|
69
|
+
// Make available globally for debugging
|
|
70
|
+
if (typeof window !== 'undefined') {
|
|
71
|
+
window.__VG_EVENT_DISPATCHER__ = this;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register event listener
|
|
77
|
+
* @param {string} eventType - Event type to listen for
|
|
78
|
+
* @param {Function} handler - Handler function
|
|
79
|
+
* @param {Object} options - Listener options
|
|
80
|
+
*/
|
|
81
|
+
on(eventType, handler, options = {}) {
|
|
82
|
+
if (!this.listeners.has(eventType)) {
|
|
83
|
+
this.listeners.set(eventType, []);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const listener = {
|
|
87
|
+
handler,
|
|
88
|
+
context: options.context || 'all',
|
|
89
|
+
priority: options.priority || 0,
|
|
90
|
+
once: options.once || false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.listeners.get(eventType).push(listener);
|
|
94
|
+
|
|
95
|
+
// Sort by priority (higher first)
|
|
96
|
+
this.listeners.get(eventType).sort((a, b) => b.priority - a.priority);
|
|
97
|
+
|
|
98
|
+
if (this.debug) {
|
|
99
|
+
console.log(`[EventDispatcher] Registered listener for: ${eventType}`, listener);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Return unsubscribe function
|
|
103
|
+
return () => this.off(eventType, handler);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Register one-time event listener
|
|
108
|
+
*/
|
|
109
|
+
once(eventType, handler, options = {}) {
|
|
110
|
+
return this.on(eventType, handler, { ...options, once: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Unregister event listener
|
|
115
|
+
*/
|
|
116
|
+
off(eventType, handler) {
|
|
117
|
+
if (!this.listeners.has(eventType)) return;
|
|
118
|
+
|
|
119
|
+
const listeners = this.listeners.get(eventType);
|
|
120
|
+
const index = listeners.findIndex(l => l.handler === handler);
|
|
121
|
+
|
|
122
|
+
if (index !== -1) {
|
|
123
|
+
listeners.splice(index, 1);
|
|
124
|
+
if (this.debug) {
|
|
125
|
+
console.log(`[EventDispatcher] Unregistered listener for: ${eventType}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clean up empty listener arrays
|
|
130
|
+
if (listeners.length === 0) {
|
|
131
|
+
this.listeners.delete(eventType);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Dispatch event
|
|
137
|
+
* @param {Object|EventProtocol} event - Event to dispatch
|
|
138
|
+
*/
|
|
139
|
+
async dispatch(event) {
|
|
140
|
+
// Convert to EventProtocol if needed
|
|
141
|
+
const eventObj = event instanceof EventProtocol
|
|
142
|
+
? event
|
|
143
|
+
: new EventProtocol(event);
|
|
144
|
+
|
|
145
|
+
if (!eventObj.isValid()) {
|
|
146
|
+
console.error('[EventDispatcher] Invalid event:', eventObj);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add to history
|
|
151
|
+
this.eventHistory.push(eventObj.toObject());
|
|
152
|
+
if (this.eventHistory.length > this.maxHistorySize) {
|
|
153
|
+
this.eventHistory.shift();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.debug) {
|
|
157
|
+
console.log('[EventDispatcher] Dispatching:', eventObj.toObject());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get listeners for this event type
|
|
161
|
+
const listeners = this.listeners.get(eventObj.type) || [];
|
|
162
|
+
|
|
163
|
+
// Filter by context
|
|
164
|
+
const contextListeners = listeners.filter(l =>
|
|
165
|
+
l.context === 'all' || l.context === eventObj.context
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (contextListeners.length === 0) {
|
|
169
|
+
if (this.debug) {
|
|
170
|
+
console.warn(`[EventDispatcher] No listeners for: ${eventObj.type}`);
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Execute handlers
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const listener of contextListeners) {
|
|
178
|
+
try {
|
|
179
|
+
const result = await listener.handler(eventObj.toObject());
|
|
180
|
+
results.push({ success: true, result });
|
|
181
|
+
|
|
182
|
+
// Remove if once
|
|
183
|
+
if (listener.once) {
|
|
184
|
+
this.off(eventObj.type, listener.handler);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`[EventDispatcher] Handler error for ${eventObj.type}:`, error);
|
|
188
|
+
results.push({ success: false, error });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Dispatch event across contexts (window + shadow root + iframes)
|
|
197
|
+
*/
|
|
198
|
+
async dispatchCrossContext(event) {
|
|
199
|
+
const eventObj = event instanceof EventProtocol
|
|
200
|
+
? event
|
|
201
|
+
: new EventProtocol(event);
|
|
202
|
+
|
|
203
|
+
const results = [];
|
|
204
|
+
|
|
205
|
+
// Dispatch in current context
|
|
206
|
+
results.push(await this.dispatch(eventObj));
|
|
207
|
+
|
|
208
|
+
// Dispatch to shadow roots if available
|
|
209
|
+
if (typeof window !== 'undefined' && window.__VG_CODER_ROOT__) {
|
|
210
|
+
const shadowDispatcher = window.__VG_CODER_ROOT__.__VG_EVENT_DISPATCHER__;
|
|
211
|
+
if (shadowDispatcher && shadowDispatcher !== this) {
|
|
212
|
+
results.push(await shadowDispatcher.dispatch(eventObj));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Dispatch to iframes (if needed in future)
|
|
217
|
+
// TODO: Add iframe support
|
|
218
|
+
|
|
219
|
+
return results.flat();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get event history
|
|
224
|
+
*/
|
|
225
|
+
getHistory(limit = 10) {
|
|
226
|
+
return this.eventHistory.slice(-limit);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clear event history
|
|
231
|
+
*/
|
|
232
|
+
clearHistory() {
|
|
233
|
+
this.eventHistory = [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get all registered event types
|
|
238
|
+
*/
|
|
239
|
+
getRegisteredEvents() {
|
|
240
|
+
return Array.from(this.listeners.keys());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get listener count for event type
|
|
245
|
+
*/
|
|
246
|
+
getListenerCount(eventType) {
|
|
247
|
+
return this.listeners.get(eventType)?.length || 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Enable/disable debug mode
|
|
252
|
+
*/
|
|
253
|
+
setDebug(enabled) {
|
|
254
|
+
this.debug = enabled;
|
|
255
|
+
console.log(`[EventDispatcher] Debug mode: ${enabled ? 'ON' : 'OFF'}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Create and export singleton instance
|
|
260
|
+
export const globalDispatcher = new EventDispatcher({
|
|
261
|
+
contexts: ['window', 'shadow-root', 'iframe'],
|
|
262
|
+
debug: false,
|
|
263
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble Features Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for all bubble menu features.
|
|
5
|
+
* Makes it easy to add/remove features without modifying core code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PasteRunFeature } from './paste-run-feature.js';
|
|
9
|
+
import { TerminalFeature } from './terminal-feature.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FeatureRegistry - Manages bubble menu features
|
|
13
|
+
*/
|
|
14
|
+
export class FeatureRegistry {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.features = new Map();
|
|
17
|
+
|
|
18
|
+
// Make available globally for debugging
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
window.__VG_FEATURE_REGISTRY__ = this;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a feature
|
|
26
|
+
* @param {Object} feature - Feature definition
|
|
27
|
+
*/
|
|
28
|
+
register(feature) {
|
|
29
|
+
if (!feature.id) {
|
|
30
|
+
console.error('[FeatureRegistry] Feature must have an id', feature);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.features.has(feature.id)) {
|
|
35
|
+
console.warn(`[FeatureRegistry] Feature ${feature.id} already registered. Overwriting.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.features.set(feature.id, feature);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unregister a feature
|
|
44
|
+
* @param {string} id - Feature id
|
|
45
|
+
*/
|
|
46
|
+
unregister(id) {
|
|
47
|
+
return this.features.delete(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all features
|
|
52
|
+
* @returns {Array} Array of feature objects
|
|
53
|
+
*/
|
|
54
|
+
getFeatures() {
|
|
55
|
+
return Array.from(this.features.values());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get enabled features only
|
|
60
|
+
* @returns {Array} Array of enabled feature objects
|
|
61
|
+
*/
|
|
62
|
+
getEnabledFeatures() {
|
|
63
|
+
return this.getFeatures()
|
|
64
|
+
.filter(f => f.enabled)
|
|
65
|
+
.sort((a, b) => a.order - b.order);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get feature by id
|
|
70
|
+
* @param {string} id - Feature id
|
|
71
|
+
* @returns {Object|null} Feature object or null
|
|
72
|
+
*/
|
|
73
|
+
getFeature(id) {
|
|
74
|
+
return this.features.get(id) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if feature is enabled
|
|
79
|
+
* @param {string} id - Feature id
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
isFeatureEnabled(id) {
|
|
83
|
+
const feature = this.features.get(id);
|
|
84
|
+
return feature ? feature.enabled : false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Enable/disable a feature
|
|
89
|
+
* @param {string} id - Feature id
|
|
90
|
+
* @param {boolean} enabled - Enable state
|
|
91
|
+
*/
|
|
92
|
+
setFeatureEnabled(id, enabled) {
|
|
93
|
+
const feature = this.features.get(id);
|
|
94
|
+
if (feature) {
|
|
95
|
+
feature.enabled = enabled;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get feature count
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
getFeatureCount() {
|
|
106
|
+
return this.features.size;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all features
|
|
111
|
+
*/
|
|
112
|
+
clear() {
|
|
113
|
+
this.features.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create singleton registry
|
|
118
|
+
export const featureRegistry = new FeatureRegistry();
|
|
119
|
+
|
|
120
|
+
// Register built-in features
|
|
121
|
+
featureRegistry.register(PasteRunFeature);
|
|
122
|
+
featureRegistry.register(TerminalFeature);
|
|
123
|
+
|
|
124
|
+
// Export for external registration
|
|
125
|
+
export { PasteRunFeature, TerminalFeature };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble Features - Paste & Run Feature
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EVENT_TYPES } from '../../event-protocol.js';
|
|
6
|
+
|
|
7
|
+
export const PasteRunFeature = {
|
|
8
|
+
id: 'paste-run',
|
|
9
|
+
icon: '📋',
|
|
10
|
+
label: 'Paste & Run from Clipboard',
|
|
11
|
+
tooltip: 'Paste & Run from Clipboard',
|
|
12
|
+
eventType: EVENT_TYPES.PASTE_RUN,
|
|
13
|
+
permissions: ['clipboard-read'],
|
|
14
|
+
enabled: true,
|
|
15
|
+
order: 1,
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble Features - Terminal Feature
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EVENT_TYPES } from '../../event-protocol.js';
|
|
6
|
+
|
|
7
|
+
export const TerminalFeature = {
|
|
8
|
+
id: 'terminal-new',
|
|
9
|
+
icon: '🖥️',
|
|
10
|
+
label: 'New Terminal',
|
|
11
|
+
tooltip: 'Open New Terminal',
|
|
12
|
+
eventType: EVENT_TYPES.TERMINAL_NEW,
|
|
13
|
+
permissions: [],
|
|
14
|
+
enabled: true,
|
|
15
|
+
order: 2,
|
|
16
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { getById, showToast } from '../utils.js';
|
|
2
|
+
import { globalDispatcher } from '../event-protocol.js';
|
|
3
|
+
import { featureRegistry } from './bubble-features/index.js';
|
|
4
|
+
|
|
5
|
+
export function initBubble() {
|
|
6
|
+
const bubble = getById('vg-bubble');
|
|
7
|
+
const appRoot = getById('vg-app-root');
|
|
8
|
+
const bubbleMenu = bubble?.querySelector('.vg-bubble-menu');
|
|
9
|
+
|
|
10
|
+
if (!bubble || !appRoot) return;
|
|
11
|
+
|
|
12
|
+
// --- RENDER FEATURES DYNAMICALLY ---
|
|
13
|
+
if (bubbleMenu) {
|
|
14
|
+
renderFeatures(bubbleMenu);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- DRAG LOGIC ---
|
|
18
|
+
let isDragging = false;
|
|
19
|
+
let startX, startY, initialLeft, initialTop;
|
|
20
|
+
let hasMoved = false; // To distinguish click vs drag
|
|
21
|
+
let mouseDownTarget = null; // Track where mousedown started
|
|
22
|
+
|
|
23
|
+
bubble.addEventListener('mousedown', (e) => {
|
|
24
|
+
isDragging = true;
|
|
25
|
+
hasMoved = false;
|
|
26
|
+
mouseDownTarget = e.target; // Remember click target
|
|
27
|
+
startX = e.clientX;
|
|
28
|
+
startY = e.clientY;
|
|
29
|
+
|
|
30
|
+
// Get computed style for accurate position
|
|
31
|
+
const rect = bubble.getBoundingClientRect();
|
|
32
|
+
initialLeft = rect.left;
|
|
33
|
+
initialTop = rect.top;
|
|
34
|
+
|
|
35
|
+
// Prevent text selection
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
|
|
38
|
+
// Set cursor
|
|
39
|
+
bubble.style.cursor = 'grabbing';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Use window events to handle drag outside shadow root if needed,
|
|
43
|
+
// but here we are inside Shadow DOM, so document/root events work
|
|
44
|
+
// We attach to root to capture movement
|
|
45
|
+
const root = window.__VG_CODER_ROOT__ || document;
|
|
46
|
+
|
|
47
|
+
root.addEventListener('mousemove', (e) => {
|
|
48
|
+
if (!isDragging) return;
|
|
49
|
+
|
|
50
|
+
const dx = e.clientX - startX;
|
|
51
|
+
const dy = e.clientY - startY;
|
|
52
|
+
|
|
53
|
+
// Threshold to consider it a move
|
|
54
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) hasMoved = true;
|
|
55
|
+
|
|
56
|
+
bubble.style.left = `${initialLeft + dx}px`;
|
|
57
|
+
bubble.style.top = `${initialTop + dy}px`;
|
|
58
|
+
bubble.style.bottom = 'auto'; // Clear bottom/right if set by CSS
|
|
59
|
+
bubble.style.right = 'auto';
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
root.addEventListener('mouseup', (e) => {
|
|
63
|
+
if (!isDragging) return;
|
|
64
|
+
isDragging = false;
|
|
65
|
+
bubble.style.cursor = 'grab';
|
|
66
|
+
|
|
67
|
+
if (hasMoved) {
|
|
68
|
+
snapToEdge(bubble);
|
|
69
|
+
} else {
|
|
70
|
+
// Click logic - only toggle if clicked on bubble icon, NOT on menu buttons
|
|
71
|
+
const clickedOnMenu = mouseDownTarget?.closest('.vg-bubble-menu');
|
|
72
|
+
const clickedOnIcon = mouseDownTarget?.closest('.vg-bubble-icon');
|
|
73
|
+
|
|
74
|
+
// Only toggle dashboard if clicked on icon or bubble itself, but NOT on menu
|
|
75
|
+
if (!clickedOnMenu && (clickedOnIcon || mouseDownTarget === bubble)) {
|
|
76
|
+
toggleDashboard(appRoot);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Reset
|
|
81
|
+
mouseDownTarget = null;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render features dynamically from feature registry
|
|
87
|
+
*/
|
|
88
|
+
function renderFeatures(container) {
|
|
89
|
+
// Clear existing content
|
|
90
|
+
container.innerHTML = '';
|
|
91
|
+
|
|
92
|
+
// Get enabled features
|
|
93
|
+
const features = featureRegistry.getEnabledFeatures();
|
|
94
|
+
|
|
95
|
+
if (features.length === 0) {
|
|
96
|
+
console.warn('[Bubble] No enabled features found');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Render each feature
|
|
101
|
+
features.forEach(feature => {
|
|
102
|
+
const btn = document.createElement('button');
|
|
103
|
+
btn.id = `bubble-feature-${feature.id}`;
|
|
104
|
+
btn.className = 'bubble-action-btn';
|
|
105
|
+
btn.title = feature.tooltip;
|
|
106
|
+
btn.textContent = feature.label;
|
|
107
|
+
|
|
108
|
+
// Add click handler to dispatch event
|
|
109
|
+
btn.addEventListener('click', (e) => {
|
|
110
|
+
e.stopPropagation(); // Prevent toggling dashboard
|
|
111
|
+
|
|
112
|
+
// Dispatch event via event protocol
|
|
113
|
+
globalDispatcher.dispatch({
|
|
114
|
+
type: feature.eventType,
|
|
115
|
+
source: 'bubble-menu',
|
|
116
|
+
target: 'handlers',
|
|
117
|
+
payload: {
|
|
118
|
+
featureId: feature.id,
|
|
119
|
+
label: feature.label,
|
|
120
|
+
},
|
|
121
|
+
context: 'shadow-root',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
container.appendChild(btn);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(`[Bubble] Rendered ${features.length} features`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function snapToEdge(bubble) {
|
|
132
|
+
const rect = bubble.getBoundingClientRect();
|
|
133
|
+
const winWidth = window.innerWidth;
|
|
134
|
+
const winHeight = window.innerHeight;
|
|
135
|
+
|
|
136
|
+
// Determine nearest horizontal edge
|
|
137
|
+
const distLeft = rect.left;
|
|
138
|
+
const distRight = winWidth - rect.right;
|
|
139
|
+
|
|
140
|
+
let targetLeft;
|
|
141
|
+
if (distLeft < distRight) {
|
|
142
|
+
targetLeft = 20; // 20px padding
|
|
143
|
+
} else {
|
|
144
|
+
targetLeft = winWidth - rect.width - 20;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Keep vertical within bounds
|
|
148
|
+
let targetTop = rect.top;
|
|
149
|
+
if (targetTop < 20) targetTop = 20;
|
|
150
|
+
if (targetTop > winHeight - rect.height - 20) targetTop = winHeight - rect.height - 20;
|
|
151
|
+
|
|
152
|
+
// Animate snap
|
|
153
|
+
bubble.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
|
|
154
|
+
bubble.style.left = `${targetLeft}px`;
|
|
155
|
+
bubble.style.top = `${targetTop}px`;
|
|
156
|
+
|
|
157
|
+
// Remove transition after snap to allow immediate drag next time
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
bubble.style.transition = '';
|
|
160
|
+
}, 300);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toggleDashboard(appRoot) {
|
|
164
|
+
const isVisible = appRoot.classList.contains('visible');
|
|
165
|
+
|
|
166
|
+
if (isVisible) {
|
|
167
|
+
// Hide
|
|
168
|
+
appRoot.classList.remove('visible');
|
|
169
|
+
// Pointer events auto is handled by CSS (when hidden opacity=0, pointer-events usually stay but size is 0x0)
|
|
170
|
+
// Our gulpfile sets width:0, height:0 for base state, so clicks pass through
|
|
171
|
+
} else {
|
|
172
|
+
// Show
|
|
173
|
+
appRoot.classList.add('visible');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { API_BASE } from '../config.js';
|
|
2
|
+
import { showToast, getById } from '../utils.js';
|
|
3
|
+
// Import Highlight.js core
|
|
4
|
+
import hljs from 'highlight.js/lib/core';
|
|
5
|
+
|
|
6
|
+
// Import common languages to reduce bundle size
|
|
7
|
+
import javascript from 'highlight.js/lib/languages/javascript';
|
|
8
|
+
import typescript from 'highlight.js/lib/languages/typescript';
|
|
9
|
+
import json from 'highlight.js/lib/languages/json';
|
|
10
|
+
import xml from 'highlight.js/lib/languages/xml';
|
|
11
|
+
import css from 'highlight.js/lib/languages/css';
|
|
12
|
+
import bash from 'highlight.js/lib/languages/bash';
|
|
13
|
+
import java from 'highlight.js/lib/languages/java';
|
|
14
|
+
import python from 'highlight.js/lib/languages/python';
|
|
15
|
+
import sql from 'highlight.js/lib/languages/sql';
|
|
16
|
+
import markdown from 'highlight.js/lib/languages/markdown';
|
|
17
|
+
|
|
18
|
+
// Register languages
|
|
19
|
+
hljs.registerLanguage('javascript', javascript);
|
|
20
|
+
hljs.registerLanguage('typescript', typescript);
|
|
21
|
+
hljs.registerLanguage('json', json);
|
|
22
|
+
hljs.registerLanguage('xml', xml);
|
|
23
|
+
hljs.registerLanguage('html', xml);
|
|
24
|
+
hljs.registerLanguage('css', css);
|
|
25
|
+
hljs.registerLanguage('bash', bash);
|
|
26
|
+
hljs.registerLanguage('java', java);
|
|
27
|
+
hljs.registerLanguage('python', python);
|
|
28
|
+
hljs.registerLanguage('sql', sql);
|
|
29
|
+
hljs.registerLanguage('markdown', markdown);
|
|
30
|
+
|
|
31
|
+
export async function openFileInViewer(path) {
|
|
32
|
+
const container = getById('code-viewer-container');
|
|
33
|
+
if (!container) return;
|
|
34
|
+
|
|
35
|
+
// Show loading state
|
|
36
|
+
container.innerHTML = '<div class="cv-loading">Loading file content...</div>';
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${API_BASE}/api/read-file?path=${encodeURIComponent(path)}`);
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
const ext = path.split('.').pop().toLowerCase();
|
|
44
|
+
const language = getLanguageFromExt(ext);
|
|
45
|
+
|
|
46
|
+
// Escape HTML tags to prevent XSS and ensure correct rendering
|
|
47
|
+
const escapedCode = escapeHtml(data.content);
|
|
48
|
+
|
|
49
|
+
// Render Code Block
|
|
50
|
+
container.innerHTML = `
|
|
51
|
+
<div class="cv-wrapper">
|
|
52
|
+
<pre><code class="language-${language}">${escapedCode}</code></pre>
|
|
53
|
+
</div>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
// Apply Highlight.js
|
|
57
|
+
const codeBlock = container.querySelector('code');
|
|
58
|
+
if(codeBlock) {
|
|
59
|
+
hljs.highlightElement(codeBlock);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} else {
|
|
63
|
+
container.innerHTML = `<div class="cv-error">Error: ${data.error}</div>`;
|
|
64
|
+
showToast(`Error opening file: ${data.error}`, 'error');
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
container.innerHTML = `<div class="cv-error">Failed: ${err.message}</div>`;
|
|
68
|
+
showToast(`Failed to load file: ${err.message}`, 'error');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getLanguageFromExt(ext) {
|
|
73
|
+
const map = {
|
|
74
|
+
js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
|
|
75
|
+
html: 'xml', xml: 'xml', css: 'css', scss: 'css',
|
|
76
|
+
json: 'json', java: 'java', py: 'python', sh: 'bash', sql: 'sql',
|
|
77
|
+
md: 'markdown'
|
|
78
|
+
};
|
|
79
|
+
return map[ext] || 'plaintext';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeHtml(text) {
|
|
83
|
+
if (!text) return '';
|
|
84
|
+
return text
|
|
85
|
+
.replace(/&/g, "&")
|
|
86
|
+
.replace(/</g, "<")
|
|
87
|
+
.replace(/>/g, ">")
|
|
88
|
+
.replace(/"/g, """)
|
|
89
|
+
.replace(/'/g, "'");
|
|
90
|
+
}
|