@gxp-dev/tools 2.0.5
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/.github/workflows/npm-publish.yml +48 -0
- package/CLAUDE.md +400 -0
- package/README.md +247 -0
- package/REFACTOR_PLAN.md +194 -0
- package/bin/gx-devtools.js +87 -0
- package/bin/lib/cli.js +251 -0
- package/bin/lib/commands/assets.js +337 -0
- package/bin/lib/commands/build.js +259 -0
- package/bin/lib/commands/datastore.js +433 -0
- package/bin/lib/commands/dev.js +328 -0
- package/bin/lib/commands/extensions.js +298 -0
- package/bin/lib/commands/index.js +35 -0
- package/bin/lib/commands/init.js +307 -0
- package/bin/lib/commands/publish.js +189 -0
- package/bin/lib/commands/socket.js +158 -0
- package/bin/lib/commands/ssl.js +47 -0
- package/bin/lib/constants.js +120 -0
- package/bin/lib/tui/App.tsx +600 -0
- package/bin/lib/tui/components/CommandInput.tsx +278 -0
- package/bin/lib/tui/components/GeminiPanel.tsx +161 -0
- package/bin/lib/tui/components/Header.tsx +27 -0
- package/bin/lib/tui/components/LogPanel.tsx +122 -0
- package/bin/lib/tui/components/TabBar.tsx +56 -0
- package/bin/lib/tui/components/WelcomeScreen.tsx +80 -0
- package/bin/lib/tui/index.tsx +63 -0
- package/bin/lib/tui/services/ExtensionService.ts +122 -0
- package/bin/lib/tui/services/GeminiService.ts +395 -0
- package/bin/lib/tui/services/ServiceManager.ts +336 -0
- package/bin/lib/tui/services/SocketService.ts +204 -0
- package/bin/lib/tui/services/ViteService.ts +107 -0
- package/bin/lib/tui/services/index.ts +13 -0
- package/bin/lib/utils/files.js +180 -0
- package/bin/lib/utils/index.js +17 -0
- package/bin/lib/utils/paths.js +138 -0
- package/bin/lib/utils/prompts.js +71 -0
- package/bin/lib/utils/ssl.js +233 -0
- package/browser-extensions/README.md +1 -0
- package/browser-extensions/chrome/background.js +857 -0
- package/browser-extensions/chrome/content.js +51 -0
- package/browser-extensions/chrome/devtools.html +9 -0
- package/browser-extensions/chrome/devtools.js +23 -0
- package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
- package/browser-extensions/chrome/inspector.js +1087 -0
- package/browser-extensions/chrome/manifest.json +70 -0
- package/browser-extensions/chrome/panel.html +638 -0
- package/browser-extensions/chrome/panel.js +862 -0
- package/browser-extensions/chrome/popup.html +399 -0
- package/browser-extensions/chrome/popup.js +515 -0
- package/browser-extensions/chrome/rules.json +1 -0
- package/browser-extensions/chrome/test-chrome.html +145 -0
- package/browser-extensions/chrome/test-mixed-content.html +190 -0
- package/browser-extensions/chrome/test-uri-pattern.html +199 -0
- package/browser-extensions/firefox/README.md +134 -0
- package/browser-extensions/firefox/background.js +804 -0
- package/browser-extensions/firefox/content.js +120 -0
- package/browser-extensions/firefox/debug-errors.html +229 -0
- package/browser-extensions/firefox/debug-https.html +113 -0
- package/browser-extensions/firefox/devtools.html +9 -0
- package/browser-extensions/firefox/devtools.js +24 -0
- package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
- package/browser-extensions/firefox/inspector.js +1087 -0
- package/browser-extensions/firefox/manifest.json +67 -0
- package/browser-extensions/firefox/panel.html +638 -0
- package/browser-extensions/firefox/panel.js +862 -0
- package/browser-extensions/firefox/popup.html +525 -0
- package/browser-extensions/firefox/popup.js +536 -0
- package/browser-extensions/firefox/test-gramercy.html +126 -0
- package/browser-extensions/firefox/test-imports.html +58 -0
- package/browser-extensions/firefox/test-masking.html +147 -0
- package/browser-extensions/firefox/test-uri-pattern.html +199 -0
- package/docs/DOCUSAURUS_IMPORT.md +378 -0
- package/docs/_category_.json +8 -0
- package/docs/app-manifest.md +272 -0
- package/docs/building-for-platform.md +315 -0
- package/docs/dev-tools.md +291 -0
- package/docs/getting-started.md +180 -0
- package/docs/gxp-store.md +305 -0
- package/docs/index.md +44 -0
- package/package.json +77 -0
- package/runtime/PortalContainer.vue +326 -0
- package/runtime/dev-tools/DevToolsModal.vue +217 -0
- package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
- package/runtime/dev-tools/MockDataEditor.vue +621 -0
- package/runtime/dev-tools/SocketSimulator.vue +562 -0
- package/runtime/dev-tools/StoreInspector.vue +644 -0
- package/runtime/dev-tools/index.js +6 -0
- package/runtime/gxpStringsPlugin.js +428 -0
- package/runtime/index.html +22 -0
- package/runtime/main.js +32 -0
- package/runtime/mock-api/auth-middleware.js +97 -0
- package/runtime/mock-api/image-generator.js +221 -0
- package/runtime/mock-api/index.js +197 -0
- package/runtime/mock-api/response-generator.js +394 -0
- package/runtime/mock-api/route-generator.js +323 -0
- package/runtime/mock-api/socket-triggers.js +371 -0
- package/runtime/mock-api/spec-loader.js +300 -0
- package/runtime/server.js +180 -0
- package/runtime/stores/gxpPortalConfigStore.js +554 -0
- package/runtime/stores/index.js +6 -0
- package/runtime/vite-inspector-plugin.js +749 -0
- package/runtime/vite-source-tracker-plugin.js +232 -0
- package/runtime/vite.config.js +402 -0
- package/scripts/launch-chrome.js +90 -0
- package/scripts/pack-chrome.js +91 -0
- package/socket-events/AiSessionMessageCreated.json +18 -0
- package/socket-events/SocialStreamPostCreated.json +24 -0
- package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
- package/template/README.md +332 -0
- package/template/app-manifest.json +32 -0
- package/template/dev-assets/images/avatar-placeholder.png +0 -0
- package/template/dev-assets/images/background-placeholder.jpg +0 -0
- package/template/dev-assets/images/banner-placeholder.jpg +0 -0
- package/template/dev-assets/images/icon-placeholder.png +0 -0
- package/template/dev-assets/images/logo-placeholder.png +0 -0
- package/template/dev-assets/images/product-placeholder.jpg +0 -0
- package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
- package/template/env.example +51 -0
- package/template/gitignore +53 -0
- package/template/index.html +22 -0
- package/template/main.js +28 -0
- package/template/src/DemoPage.vue +459 -0
- package/template/src/Plugin.vue +38 -0
- package/template/src/stores/index.js +9 -0
- package/template/src/stores/test-data.json +173 -0
- package/template/theme-layouts/AdditionalStyling.css +0 -0
- package/template/theme-layouts/PrivateLayout.vue +39 -0
- package/template/theme-layouts/PublicLayout.vue +39 -0
- package/template/theme-layouts/SystemLayout.vue +39 -0
- package/template/vite.config.js +333 -0
- package/tsconfig.tui.json +21 -0
- package/vite.config.js +164 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GxP Inspector DevTools Panel
|
|
3
|
+
*
|
|
4
|
+
* This script runs in the DevTools panel context and communicates
|
|
5
|
+
* with the content script via chrome.devtools.inspectedWindow.eval()
|
|
6
|
+
* and the background script for messaging.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function() {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Configuration
|
|
13
|
+
const DEV_SERVER_URL = 'https://localhost:3060';
|
|
14
|
+
const API_PREFIX = '/__gxp-inspector';
|
|
15
|
+
|
|
16
|
+
// State
|
|
17
|
+
let isSelectMode = false;
|
|
18
|
+
let isConnected = false;
|
|
19
|
+
let currentComponent = null;
|
|
20
|
+
let selectedString = null;
|
|
21
|
+
let hasSelection = false; // Track if an element is currently selected
|
|
22
|
+
|
|
23
|
+
// DOM Elements
|
|
24
|
+
const statusIndicator = document.getElementById('statusIndicator');
|
|
25
|
+
const selectBtn = document.getElementById('selectBtn');
|
|
26
|
+
const refreshBtn = document.getElementById('refreshBtn');
|
|
27
|
+
const emptyState = document.getElementById('emptyState');
|
|
28
|
+
const inspectorContent = document.getElementById('inspectorContent');
|
|
29
|
+
const componentName = document.getElementById('componentName');
|
|
30
|
+
const componentFile = document.getElementById('componentFile');
|
|
31
|
+
const stringsSection = document.getElementById('stringsSection');
|
|
32
|
+
const stringsCount = document.getElementById('stringsCount');
|
|
33
|
+
const stringsList = document.getElementById('stringsList');
|
|
34
|
+
const extractForm = document.getElementById('extractForm');
|
|
35
|
+
const extractText = document.getElementById('extractText');
|
|
36
|
+
const extractKey = document.getElementById('extractKey');
|
|
37
|
+
const extractFile = document.getElementById('extractFile');
|
|
38
|
+
const cancelExtract = document.getElementById('cancelExtract');
|
|
39
|
+
const doExtract = document.getElementById('doExtract');
|
|
40
|
+
const extractStatus = document.getElementById('extractStatus');
|
|
41
|
+
const propsSection = document.getElementById('propsSection');
|
|
42
|
+
const propsTree = document.getElementById('propsTree');
|
|
43
|
+
const dataSection = document.getElementById('dataSection');
|
|
44
|
+
const dataTree = document.getElementById('dataTree');
|
|
45
|
+
|
|
46
|
+
// Edit form elements
|
|
47
|
+
const editForm = document.getElementById('editForm');
|
|
48
|
+
const editKey = document.getElementById('editKey');
|
|
49
|
+
const editValue = document.getElementById('editValue');
|
|
50
|
+
const editFile = document.getElementById('editFile');
|
|
51
|
+
const cancelEdit = document.getElementById('cancelEdit');
|
|
52
|
+
const doEdit = document.getElementById('doEdit');
|
|
53
|
+
const editStatus = document.getElementById('editStatus');
|
|
54
|
+
|
|
55
|
+
// Track string info for editing
|
|
56
|
+
let currentStringInfo = null;
|
|
57
|
+
|
|
58
|
+
// ============================================================
|
|
59
|
+
// API Communication
|
|
60
|
+
// ============================================================
|
|
61
|
+
|
|
62
|
+
async function apiCall(endpoint, options = {}) {
|
|
63
|
+
const url = `${DEV_SERVER_URL}${API_PREFIX}${endpoint}`;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
...options,
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
...options.headers
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return await response.json();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('[GxP Panel] API Error:', error);
|
|
75
|
+
return { success: false, error: error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function checkConnection() {
|
|
80
|
+
try {
|
|
81
|
+
const result = await apiCall('/ping');
|
|
82
|
+
isConnected = result.success;
|
|
83
|
+
updateConnectionStatus();
|
|
84
|
+
return isConnected;
|
|
85
|
+
} catch {
|
|
86
|
+
isConnected = false;
|
|
87
|
+
updateConnectionStatus();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function extractString(data) {
|
|
93
|
+
return apiCall('/extract-string', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
body: JSON.stringify(data)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function lookupString(text, filePath) {
|
|
100
|
+
return apiCall('/lookup-string', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify({ text, filePath })
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function updateString(data) {
|
|
107
|
+
return apiCall('/update-string', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify(data)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function getStrings() {
|
|
114
|
+
return apiCall('/strings');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function analyzeText(text, filePath) {
|
|
118
|
+
return apiCall('/analyze-text', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: JSON.stringify({ text, filePath })
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================
|
|
125
|
+
// Content Script Communication
|
|
126
|
+
// ============================================================
|
|
127
|
+
|
|
128
|
+
function evalInPage(code) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
|
|
131
|
+
if (exceptionInfo) {
|
|
132
|
+
reject(exceptionInfo);
|
|
133
|
+
} else {
|
|
134
|
+
resolve(result);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function enableInspectorInPage() {
|
|
141
|
+
try {
|
|
142
|
+
await evalInPage(`
|
|
143
|
+
if (window.gxpInspector) {
|
|
144
|
+
window.gxpInspector.enable();
|
|
145
|
+
true;
|
|
146
|
+
} else {
|
|
147
|
+
false;
|
|
148
|
+
}
|
|
149
|
+
`);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Failed to enable inspector:', error);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function disableInspectorInPage() {
|
|
158
|
+
try {
|
|
159
|
+
await evalInPage(`
|
|
160
|
+
if (window.gxpInspector) {
|
|
161
|
+
window.gxpInspector.disable();
|
|
162
|
+
true;
|
|
163
|
+
} else {
|
|
164
|
+
false;
|
|
165
|
+
}
|
|
166
|
+
`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Failed to disable inspector:', error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function clearSelectionInPage() {
|
|
173
|
+
try {
|
|
174
|
+
await evalInPage(`
|
|
175
|
+
if (window.gxpInspector) {
|
|
176
|
+
window.gxpInspector.clearSelection();
|
|
177
|
+
true;
|
|
178
|
+
} else {
|
|
179
|
+
false;
|
|
180
|
+
}
|
|
181
|
+
`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Failed to clear selection:', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function getSelectedElement() {
|
|
188
|
+
// Uses $0 which is the last selected element in Elements panel
|
|
189
|
+
// or we can use our custom selection from inspector.js
|
|
190
|
+
try {
|
|
191
|
+
const result = await evalInPage(`
|
|
192
|
+
(function() {
|
|
193
|
+
// Try to get from our inspector's selected element
|
|
194
|
+
if (window.__gxpSelectedElement) {
|
|
195
|
+
const el = window.__gxpSelectedElement;
|
|
196
|
+
|
|
197
|
+
// Get Vue instance
|
|
198
|
+
function getVueInstance(el) {
|
|
199
|
+
if (el.__vueParentComponent) return el.__vueParentComponent;
|
|
200
|
+
let current = el;
|
|
201
|
+
while (current) {
|
|
202
|
+
if (current.__vueParentComponent) return current.__vueParentComponent;
|
|
203
|
+
current = current.parentElement;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const vueInstance = getVueInstance(el);
|
|
209
|
+
|
|
210
|
+
// Get component info
|
|
211
|
+
let componentInfo = null;
|
|
212
|
+
if (vueInstance) {
|
|
213
|
+
const type = vueInstance.type;
|
|
214
|
+
componentInfo = {
|
|
215
|
+
name: type?.name || type?.__name || type?.__file?.split('/').pop()?.replace('.vue', '') || 'Anonymous',
|
|
216
|
+
file: type?.__file || null,
|
|
217
|
+
props: {},
|
|
218
|
+
data: {}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Get props
|
|
222
|
+
if (vueInstance.props) {
|
|
223
|
+
Object.keys(vueInstance.props).forEach(key => {
|
|
224
|
+
try {
|
|
225
|
+
componentInfo.props[key] = JSON.parse(JSON.stringify(vueInstance.props[key]));
|
|
226
|
+
} catch {
|
|
227
|
+
componentInfo.props[key] = String(vueInstance.props[key]);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get data/state
|
|
233
|
+
if (vueInstance.setupState) {
|
|
234
|
+
Object.keys(vueInstance.setupState).forEach(key => {
|
|
235
|
+
const value = vueInstance.setupState[key];
|
|
236
|
+
if (typeof value !== 'function') {
|
|
237
|
+
try {
|
|
238
|
+
componentInfo.data[key] = JSON.parse(JSON.stringify(value));
|
|
239
|
+
} catch {
|
|
240
|
+
componentInfo.data[key] = String(value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get text content (plain)
|
|
248
|
+
const texts = [];
|
|
249
|
+
el.childNodes.forEach(node => {
|
|
250
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
251
|
+
const text = node.textContent.trim();
|
|
252
|
+
if (text) texts.push(text);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Check for gxp-string attribute on this element
|
|
257
|
+
const gxpStringKey = el.getAttribute ? el.getAttribute('gxp-string') : null;
|
|
258
|
+
|
|
259
|
+
// Check for data-gxp-expr attribute (injected by vite plugin)
|
|
260
|
+
const sourceExpression = el.getAttribute ? el.getAttribute('data-gxp-expr') : null;
|
|
261
|
+
|
|
262
|
+
// Get text content with gxp-string attribute info
|
|
263
|
+
const textsWithAttributes = [];
|
|
264
|
+
el.childNodes.forEach(node => {
|
|
265
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
266
|
+
const text = node.textContent.trim();
|
|
267
|
+
if (text) {
|
|
268
|
+
textsWithAttributes.push({
|
|
269
|
+
text: text,
|
|
270
|
+
gxpStringKey: gxpStringKey,
|
|
271
|
+
isExtracted: gxpStringKey !== null,
|
|
272
|
+
sourceExpression: sourceExpression,
|
|
273
|
+
isDynamic: sourceExpression !== null
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Find child elements with gxp-string attributes
|
|
280
|
+
const childGxpStrings = [];
|
|
281
|
+
const gxpElements = el.querySelectorAll ? el.querySelectorAll('[gxp-string]') : [];
|
|
282
|
+
gxpElements.forEach(child => {
|
|
283
|
+
const key = child.getAttribute('gxp-string');
|
|
284
|
+
const text = child.textContent.trim();
|
|
285
|
+
if (key && text) {
|
|
286
|
+
childGxpStrings.push({
|
|
287
|
+
key: key,
|
|
288
|
+
text: text,
|
|
289
|
+
element: child.tagName.toLowerCase()
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
tagName: el.tagName.toLowerCase(),
|
|
296
|
+
component: componentInfo,
|
|
297
|
+
texts: texts,
|
|
298
|
+
textsWithAttributes: textsWithAttributes,
|
|
299
|
+
childGxpStrings: childGxpStrings,
|
|
300
|
+
gxpStringKey: gxpStringKey,
|
|
301
|
+
isExtracted: gxpStringKey !== null,
|
|
302
|
+
sourceExpression: sourceExpression,
|
|
303
|
+
isDynamic: sourceExpression !== null
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
})()
|
|
308
|
+
`);
|
|
309
|
+
return result;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Failed to get selected element:', error);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================
|
|
317
|
+
// UI Updates
|
|
318
|
+
// ============================================================
|
|
319
|
+
|
|
320
|
+
function updateConnectionStatus() {
|
|
321
|
+
if (isConnected) {
|
|
322
|
+
statusIndicator.classList.add('connected');
|
|
323
|
+
statusIndicator.title = 'Connected to Vite dev server';
|
|
324
|
+
} else {
|
|
325
|
+
statusIndicator.classList.remove('connected');
|
|
326
|
+
statusIndicator.title = 'Not connected - start Vite dev server';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function showEmptyState() {
|
|
331
|
+
emptyState.classList.remove('hidden');
|
|
332
|
+
inspectorContent.classList.add('hidden');
|
|
333
|
+
currentComponent = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function showInspectorContent() {
|
|
337
|
+
emptyState.classList.add('hidden');
|
|
338
|
+
inspectorContent.classList.remove('hidden');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function updateComponentInfo(data) {
|
|
342
|
+
if (!data) {
|
|
343
|
+
showEmptyState();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
showInspectorContent();
|
|
348
|
+
currentComponent = data;
|
|
349
|
+
|
|
350
|
+
// Update component name and file
|
|
351
|
+
if (data.component) {
|
|
352
|
+
componentName.textContent = `<${data.component.name}>`;
|
|
353
|
+
componentFile.textContent = data.component.file || 'Unknown file';
|
|
354
|
+
} else {
|
|
355
|
+
componentName.textContent = `<${data.tagName}>`;
|
|
356
|
+
componentFile.textContent = 'Not a Vue component';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Build string info list from attribute detection
|
|
360
|
+
const stringInfos = [];
|
|
361
|
+
const filePath = data.component?.file || null;
|
|
362
|
+
|
|
363
|
+
// Add strings from textsWithAttributes (direct element text with gxp-string detection)
|
|
364
|
+
if (data.textsWithAttributes && data.textsWithAttributes.length > 0) {
|
|
365
|
+
data.textsWithAttributes.forEach(info => {
|
|
366
|
+
stringInfos.push({
|
|
367
|
+
text: info.text,
|
|
368
|
+
isExtracted: info.isExtracted,
|
|
369
|
+
key: info.gxpStringKey || null,
|
|
370
|
+
// Use injected data-gxp-source attribute if available (from vite plugin)
|
|
371
|
+
isDynamic: info.isDynamic || false,
|
|
372
|
+
expression: info.sourceExpression || null,
|
|
373
|
+
expressionType: info.sourceExpression ? detectExpressionType(info.sourceExpression) : null
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
} else if (data.texts && data.texts.length > 0) {
|
|
377
|
+
// Fallback to plain texts if textsWithAttributes not available
|
|
378
|
+
data.texts.forEach(text => {
|
|
379
|
+
stringInfos.push({
|
|
380
|
+
text: text,
|
|
381
|
+
isExtracted: data.isExtracted || false,
|
|
382
|
+
key: data.gxpStringKey || null,
|
|
383
|
+
// Check element-level source expression
|
|
384
|
+
isDynamic: data.isDynamic || false,
|
|
385
|
+
expression: data.sourceExpression || null,
|
|
386
|
+
expressionType: data.sourceExpression ? detectExpressionType(data.sourceExpression) : null
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Also add child elements with gxp-string attributes
|
|
392
|
+
if (data.childGxpStrings && data.childGxpStrings.length > 0) {
|
|
393
|
+
data.childGxpStrings.forEach(child => {
|
|
394
|
+
// Check if this text is already in the list
|
|
395
|
+
const exists = stringInfos.some(info => info.text === child.text && info.key === child.key);
|
|
396
|
+
if (!exists) {
|
|
397
|
+
stringInfos.push({
|
|
398
|
+
text: child.text,
|
|
399
|
+
isExtracted: true,
|
|
400
|
+
key: child.key,
|
|
401
|
+
element: child.element,
|
|
402
|
+
isDynamic: false,
|
|
403
|
+
expression: null,
|
|
404
|
+
expressionType: null
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Analyze each string to check if it's dynamic (from a template expression)
|
|
411
|
+
// Only call API for strings that don't already have source info from data-gxp-source attribute
|
|
412
|
+
if (isConnected && filePath) {
|
|
413
|
+
for (const info of stringInfos) {
|
|
414
|
+
// Skip if already extracted with gxp-string or already has source expression
|
|
415
|
+
if (info.isExtracted || info.isDynamic) continue;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const analysis = await analyzeText(info.text, filePath);
|
|
419
|
+
if (analysis.success && analysis.isDynamic) {
|
|
420
|
+
info.isDynamic = true;
|
|
421
|
+
info.expression = analysis.expression;
|
|
422
|
+
info.expressionType = analysis.expressionType;
|
|
423
|
+
info.sourceKey = analysis.sourceKey;
|
|
424
|
+
}
|
|
425
|
+
} catch (e) {
|
|
426
|
+
// Ignore analysis errors, treat as static
|
|
427
|
+
console.warn('[GxP Panel] Failed to analyze text:', e);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Update strings list display
|
|
433
|
+
if (stringInfos.length > 0) {
|
|
434
|
+
stringsSection.classList.remove('hidden');
|
|
435
|
+
stringsCount.textContent = stringInfos.length;
|
|
436
|
+
|
|
437
|
+
// Render the strings with their status
|
|
438
|
+
stringsList.innerHTML = stringInfos.map((info, index) => {
|
|
439
|
+
let badgeClass, badgeText, actionText, itemClass, showAction;
|
|
440
|
+
|
|
441
|
+
if (info.isExtracted) {
|
|
442
|
+
badgeClass = 'extracted';
|
|
443
|
+
badgeText = 'gxp-string';
|
|
444
|
+
actionText = 'Edit';
|
|
445
|
+
itemClass = 'string-item extracted';
|
|
446
|
+
showAction = true;
|
|
447
|
+
} else if (info.isDynamic) {
|
|
448
|
+
badgeClass = 'dynamic';
|
|
449
|
+
badgeText = info.expressionType || 'dynamic';
|
|
450
|
+
actionText = '';
|
|
451
|
+
itemClass = 'string-item dynamic';
|
|
452
|
+
showAction = false;
|
|
453
|
+
} else {
|
|
454
|
+
badgeClass = 'raw';
|
|
455
|
+
badgeText = 'raw text';
|
|
456
|
+
actionText = 'Extract';
|
|
457
|
+
itemClass = 'string-item';
|
|
458
|
+
showAction = true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const expressionHtml = info.expression
|
|
462
|
+
? `<span class="string-expression" title="Source: ${escapeHtml(info.expression)}">${escapeHtml(info.expression)}</span>`
|
|
463
|
+
: '';
|
|
464
|
+
|
|
465
|
+
const actionHtml = showAction
|
|
466
|
+
? `<div class="string-actions"><button class="action-btn" data-action="${info.isExtracted ? 'edit' : 'extract'}">${actionText}</button></div>`
|
|
467
|
+
: '';
|
|
468
|
+
|
|
469
|
+
return `
|
|
470
|
+
<div class="${itemClass}" data-index="${index}" data-text="${escapeHtml(info.text)}"
|
|
471
|
+
data-extracted="${info.isExtracted}" data-key="${info.key || ''}"
|
|
472
|
+
data-dynamic="${info.isDynamic}" data-expression="${escapeHtml(info.expression || '')}">
|
|
473
|
+
<div class="string-content">
|
|
474
|
+
<span class="string-text">"${escapeHtml(info.text)}"</span>
|
|
475
|
+
${expressionHtml}
|
|
476
|
+
</div>
|
|
477
|
+
<span class="string-badge ${badgeClass}">${badgeText}</span>
|
|
478
|
+
${actionHtml}
|
|
479
|
+
</div>
|
|
480
|
+
`;
|
|
481
|
+
}).join('');
|
|
482
|
+
|
|
483
|
+
// Add click handlers
|
|
484
|
+
stringsList.querySelectorAll('.string-item').forEach(item => {
|
|
485
|
+
item.addEventListener('click', () => {
|
|
486
|
+
selectStringItem(item);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const actionBtn = item.querySelector('.action-btn');
|
|
490
|
+
if (actionBtn) {
|
|
491
|
+
const action = actionBtn.dataset.action;
|
|
492
|
+
|
|
493
|
+
actionBtn.addEventListener('click', (e) => {
|
|
494
|
+
e.stopPropagation();
|
|
495
|
+
if (action === 'edit') {
|
|
496
|
+
showEditForm(item.dataset.text, item.dataset.key);
|
|
497
|
+
} else if (action === 'extract') {
|
|
498
|
+
showExtractForm(item.dataset.text);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
stringsSection.classList.add('hidden');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Update props
|
|
508
|
+
if (data.component && Object.keys(data.component.props).length > 0) {
|
|
509
|
+
propsSection.classList.remove('hidden');
|
|
510
|
+
propsTree.innerHTML = formatProps(data.component.props);
|
|
511
|
+
} else {
|
|
512
|
+
propsSection.classList.add('hidden');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Update data
|
|
516
|
+
if (data.component && Object.keys(data.component.data).length > 0) {
|
|
517
|
+
dataSection.classList.remove('hidden');
|
|
518
|
+
dataTree.innerHTML = formatProps(data.component.data);
|
|
519
|
+
} else {
|
|
520
|
+
dataSection.classList.add('hidden');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Hide extract form
|
|
524
|
+
hideExtractForm();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function formatProps(obj, indent = 0) {
|
|
528
|
+
let html = '';
|
|
529
|
+
const indentStr = ' '.repeat(indent);
|
|
530
|
+
|
|
531
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
532
|
+
const type = typeof value;
|
|
533
|
+
let valueHtml = '';
|
|
534
|
+
|
|
535
|
+
if (value === null) {
|
|
536
|
+
valueHtml = '<span class="prop-value boolean">null</span>';
|
|
537
|
+
} else if (type === 'boolean') {
|
|
538
|
+
valueHtml = `<span class="prop-value boolean">${value}</span>`;
|
|
539
|
+
} else if (type === 'number') {
|
|
540
|
+
valueHtml = `<span class="prop-value number">${value}</span>`;
|
|
541
|
+
} else if (type === 'string') {
|
|
542
|
+
valueHtml = `<span class="prop-value">"${escapeHtml(value)}"</span>`;
|
|
543
|
+
} else if (Array.isArray(value)) {
|
|
544
|
+
if (value.length === 0) {
|
|
545
|
+
valueHtml = '<span class="prop-value">[]</span>';
|
|
546
|
+
} else {
|
|
547
|
+
valueHtml = `<span class="prop-value">[${value.length} items]</span>`;
|
|
548
|
+
}
|
|
549
|
+
} else if (type === 'object') {
|
|
550
|
+
valueHtml = `<span class="prop-value">{...}</span>`;
|
|
551
|
+
} else {
|
|
552
|
+
valueHtml = `<span class="prop-value">${escapeHtml(String(value))}</span>`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
html += `<div class="prop-item">${indentStr}<span class="prop-key">${key}</span>: ${valueHtml}</div>`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return html;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function selectStringItem(item) {
|
|
562
|
+
// Remove selection from all items
|
|
563
|
+
stringsList.querySelectorAll('.string-item').forEach(i => {
|
|
564
|
+
i.classList.remove('selected');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Select this item
|
|
568
|
+
item.classList.add('selected');
|
|
569
|
+
selectedString = item.dataset.text;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function showExtractForm(text) {
|
|
573
|
+
extractForm.classList.remove('hidden');
|
|
574
|
+
extractText.value = text;
|
|
575
|
+
extractKey.value = textToKey(text);
|
|
576
|
+
extractFile.value = currentComponent?.component?.file || '';
|
|
577
|
+
extractStatus.classList.add('hidden');
|
|
578
|
+
selectedString = text;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function hideExtractForm() {
|
|
582
|
+
extractForm.classList.add('hidden');
|
|
583
|
+
extractStatus.classList.add('hidden');
|
|
584
|
+
selectedString = null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function showEditForm(text, key) {
|
|
588
|
+
// Hide extract form if visible
|
|
589
|
+
hideExtractForm();
|
|
590
|
+
|
|
591
|
+
editForm.classList.remove('hidden');
|
|
592
|
+
editKey.value = key || '';
|
|
593
|
+
editValue.value = text;
|
|
594
|
+
editFile.value = currentComponent?.component?.file || '';
|
|
595
|
+
editStatus.classList.add('hidden');
|
|
596
|
+
|
|
597
|
+
// Store current string info for the update
|
|
598
|
+
currentStringInfo = {
|
|
599
|
+
oldKey: key,
|
|
600
|
+
text: text,
|
|
601
|
+
filePath: currentComponent?.component?.file || ''
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function hideEditForm() {
|
|
606
|
+
editForm.classList.add('hidden');
|
|
607
|
+
editStatus.classList.add('hidden');
|
|
608
|
+
currentStringInfo = null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function showEditStatus(message, type = 'info') {
|
|
612
|
+
editStatus.textContent = message;
|
|
613
|
+
editStatus.className = `status-message ${type}`;
|
|
614
|
+
editStatus.classList.remove('hidden');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function showStatus(message, type = 'info') {
|
|
618
|
+
extractStatus.textContent = message;
|
|
619
|
+
extractStatus.className = `status-message ${type}`;
|
|
620
|
+
extractStatus.classList.remove('hidden');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ============================================================
|
|
624
|
+
// Event Handlers
|
|
625
|
+
// ============================================================
|
|
626
|
+
|
|
627
|
+
selectBtn.addEventListener('click', async () => {
|
|
628
|
+
if (isSelectMode) {
|
|
629
|
+
// Cancel selection mode (clicking during selection)
|
|
630
|
+
isSelectMode = false;
|
|
631
|
+
selectBtn.classList.remove('active');
|
|
632
|
+
selectBtn.querySelector('span').textContent = hasSelection ? 'Cancel Selection' : 'Select Element';
|
|
633
|
+
await disableInspectorInPage();
|
|
634
|
+
} else if (hasSelection) {
|
|
635
|
+
// Clear selection (clicking when element is already selected)
|
|
636
|
+
hasSelection = false;
|
|
637
|
+
currentComponent = null;
|
|
638
|
+
await clearSelectionInPage();
|
|
639
|
+
showEmptyState();
|
|
640
|
+
selectBtn.querySelector('span').textContent = 'Select Element';
|
|
641
|
+
} else {
|
|
642
|
+
// Start selection mode
|
|
643
|
+
isSelectMode = true;
|
|
644
|
+
selectBtn.classList.add('active');
|
|
645
|
+
selectBtn.querySelector('span').textContent = 'Cancel Selection';
|
|
646
|
+
await enableInspectorInPage();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
refreshBtn.addEventListener('click', async () => {
|
|
651
|
+
await checkConnection();
|
|
652
|
+
const data = await getSelectedElement();
|
|
653
|
+
updateComponentInfo(data);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
cancelExtract.addEventListener('click', () => {
|
|
657
|
+
hideExtractForm();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
cancelEdit.addEventListener('click', () => {
|
|
661
|
+
hideEditForm();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
doEdit.addEventListener('click', async () => {
|
|
665
|
+
if (!currentStringInfo) {
|
|
666
|
+
showEditStatus('No string selected for editing', 'error');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const newKey = editKey.value;
|
|
671
|
+
const newValue = editValue.value;
|
|
672
|
+
const filePath = editFile.value;
|
|
673
|
+
|
|
674
|
+
if (!newKey) {
|
|
675
|
+
showEditStatus('String key is required', 'error');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (!filePath) {
|
|
680
|
+
showEditStatus('Cannot determine source file', 'error');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!isConnected) {
|
|
685
|
+
showEditStatus('Not connected to Vite dev server', 'error');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
doEdit.disabled = true;
|
|
690
|
+
doEdit.textContent = 'Updating...';
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const result = await updateString({
|
|
694
|
+
oldKey: currentStringInfo.oldKey,
|
|
695
|
+
newKey: newKey,
|
|
696
|
+
newValue: newValue,
|
|
697
|
+
filePath: filePath
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
if (result.success) {
|
|
701
|
+
showEditStatus(`Success! Updated gxp-string="${newKey}"`, 'success');
|
|
702
|
+
|
|
703
|
+
// Refresh the component info to reflect changes
|
|
704
|
+
setTimeout(async () => {
|
|
705
|
+
hideEditForm();
|
|
706
|
+
const data = await getSelectedElement();
|
|
707
|
+
updateComponentInfo(data);
|
|
708
|
+
}, 1500);
|
|
709
|
+
} else {
|
|
710
|
+
showEditStatus(result.error || 'Update failed', 'error');
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
showEditStatus(error.message, 'error');
|
|
714
|
+
} finally {
|
|
715
|
+
doEdit.disabled = false;
|
|
716
|
+
doEdit.textContent = 'Update gxp-string';
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
doExtract.addEventListener('click', async () => {
|
|
721
|
+
const text = extractText.value;
|
|
722
|
+
const key = extractKey.value;
|
|
723
|
+
const filePath = extractFile.value;
|
|
724
|
+
|
|
725
|
+
if (!text || !key) {
|
|
726
|
+
showStatus('Text and key are required', 'error');
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!filePath) {
|
|
731
|
+
showStatus('Cannot determine source file', 'error');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!isConnected) {
|
|
736
|
+
showStatus('Not connected to Vite dev server', 'error');
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
doExtract.disabled = true;
|
|
741
|
+
doExtract.textContent = 'Extracting...';
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
const result = await extractString({ text, key, filePath });
|
|
745
|
+
|
|
746
|
+
if (result.success) {
|
|
747
|
+
showStatus(`Success! Added gxp-string="${key}" attribute`, 'success');
|
|
748
|
+
// Refresh the component info to reflect changes
|
|
749
|
+
setTimeout(async () => {
|
|
750
|
+
hideExtractForm();
|
|
751
|
+
const data = await getSelectedElement();
|
|
752
|
+
updateComponentInfo(data);
|
|
753
|
+
}, 1500);
|
|
754
|
+
} else {
|
|
755
|
+
showStatus(result.error || 'Extraction failed', 'error');
|
|
756
|
+
}
|
|
757
|
+
} catch (error) {
|
|
758
|
+
showStatus(error.message, 'error');
|
|
759
|
+
} finally {
|
|
760
|
+
doExtract.disabled = false;
|
|
761
|
+
doExtract.textContent = 'Extract to gxp-string';
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// ============================================================
|
|
766
|
+
// Message Listener for Content Script Updates
|
|
767
|
+
// ============================================================
|
|
768
|
+
|
|
769
|
+
// Create a connection to the background script
|
|
770
|
+
const backgroundConnection = chrome.runtime.connect({
|
|
771
|
+
name: 'gxp-devtools-panel'
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
backgroundConnection.postMessage({
|
|
775
|
+
name: 'init',
|
|
776
|
+
tabId: chrome.devtools.inspectedWindow.tabId
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
backgroundConnection.onMessage.addListener((message) => {
|
|
780
|
+
if (message.type === 'elementSelected') {
|
|
781
|
+
updateComponentInfo(message.data);
|
|
782
|
+
isSelectMode = false;
|
|
783
|
+
hasSelection = true;
|
|
784
|
+
selectBtn.classList.remove('active');
|
|
785
|
+
selectBtn.querySelector('span').textContent = 'Cancel Selection';
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Also listen for selection changes via polling
|
|
790
|
+
// (backup method since content script communication can be tricky)
|
|
791
|
+
setInterval(async () => {
|
|
792
|
+
if (isSelectMode) {
|
|
793
|
+
const data = await getSelectedElement();
|
|
794
|
+
if (data && JSON.stringify(data) !== JSON.stringify(currentComponent)) {
|
|
795
|
+
updateComponentInfo(data);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}, 500);
|
|
799
|
+
|
|
800
|
+
// ============================================================
|
|
801
|
+
// Utility Functions
|
|
802
|
+
// ============================================================
|
|
803
|
+
|
|
804
|
+
function textToKey(text) {
|
|
805
|
+
return text
|
|
806
|
+
.toLowerCase()
|
|
807
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
808
|
+
.replace(/\s+/g, '_')
|
|
809
|
+
.substring(0, 40)
|
|
810
|
+
.replace(/_+$/, '');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Detect the type of expression from a source expression string
|
|
815
|
+
* @param {string} expression - The source expression (e.g., "gxpStore.getString('key')")
|
|
816
|
+
* @returns {string} - The expression type: 'getString', 'store', 'variable', 'computed'
|
|
817
|
+
*/
|
|
818
|
+
function detectExpressionType(expression) {
|
|
819
|
+
if (!expression) return null;
|
|
820
|
+
|
|
821
|
+
// Check for getString calls
|
|
822
|
+
if (expression.includes('getString')) {
|
|
823
|
+
return 'getString';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check for store access
|
|
827
|
+
if (expression.includes('Store') || expression.includes('store.')) {
|
|
828
|
+
return 'store';
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Check for computed/method calls
|
|
832
|
+
if (expression.includes('(')) {
|
|
833
|
+
return 'computed';
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Default to variable
|
|
837
|
+
return 'variable';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function escapeHtml(text) {
|
|
841
|
+
const div = document.createElement('div');
|
|
842
|
+
div.textContent = text;
|
|
843
|
+
return div.innerHTML;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ============================================================
|
|
847
|
+
// Panel Lifecycle
|
|
848
|
+
// ============================================================
|
|
849
|
+
|
|
850
|
+
// Called when panel becomes visible
|
|
851
|
+
window.panelShown = function() {
|
|
852
|
+
checkConnection();
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Initial setup
|
|
856
|
+
checkConnection();
|
|
857
|
+
|
|
858
|
+
// Check connection periodically
|
|
859
|
+
setInterval(checkConnection, 10000);
|
|
860
|
+
|
|
861
|
+
console.log('[GxP Panel] DevTools panel initialized');
|
|
862
|
+
})();
|