@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,1087 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GxP Component Inspector
|
|
3
|
+
*
|
|
4
|
+
* Browser extension content script that provides:
|
|
5
|
+
* - Component highlighting on hover
|
|
6
|
+
* - Vue component detection
|
|
7
|
+
* - String extraction panel
|
|
8
|
+
* - File editing via Vite dev server API
|
|
9
|
+
*
|
|
10
|
+
* This script injects itself into the page's main world so that
|
|
11
|
+
* DevTools panel can access window.gxpInspector via eval().
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Check if we're in the page context (already injected) vs content script context
|
|
15
|
+
// In page context, neither 'browser' nor 'chrome' runtime APIs are available
|
|
16
|
+
const isContentScriptContext = (typeof browser !== 'undefined' && browser.runtime) ||
|
|
17
|
+
(typeof chrome !== 'undefined' && chrome.runtime);
|
|
18
|
+
|
|
19
|
+
if (isContentScriptContext && typeof window.__gxpInspectorInjected === 'undefined') {
|
|
20
|
+
// We're in the content script context - inject into page
|
|
21
|
+
const runtime = typeof browser !== 'undefined' ? browser : chrome;
|
|
22
|
+
const script = document.createElement('script');
|
|
23
|
+
script.src = runtime.runtime.getURL('inspector.js');
|
|
24
|
+
script.onload = function() {
|
|
25
|
+
this.remove();
|
|
26
|
+
};
|
|
27
|
+
(document.head || document.documentElement).appendChild(script);
|
|
28
|
+
|
|
29
|
+
// Also set up message relay from page to extension
|
|
30
|
+
window.addEventListener('message', (event) => {
|
|
31
|
+
if (event.source !== window) return;
|
|
32
|
+
if (event.data?.type === 'GXP_INSPECTOR_MESSAGE') {
|
|
33
|
+
runtime.runtime.sendMessage(event.data.payload);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Mark that content script has run (for content script context)
|
|
38
|
+
window.__gxpInspectorContentScriptLoaded = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Mark as injected (for both contexts)
|
|
42
|
+
window.__gxpInspectorInjected = true;
|
|
43
|
+
|
|
44
|
+
(function () {
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
// If gxpInspector already exists, we're done (prevent double init)
|
|
48
|
+
if (window.gxpInspector) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Configuration
|
|
53
|
+
const DEV_SERVER_URL = 'https://localhost:3060';
|
|
54
|
+
const API_PREFIX = '/__gxp-inspector';
|
|
55
|
+
|
|
56
|
+
// State
|
|
57
|
+
let inspectorEnabled = false;
|
|
58
|
+
let highlightOverlay = null;
|
|
59
|
+
let inspectorPanel = null;
|
|
60
|
+
let selectedElement = null;
|
|
61
|
+
let hoveredElement = null;
|
|
62
|
+
let selectionHighlight = null; // Persistent highlight for selected element
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// API Communication
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
async function apiCall(endpoint, options = {}) {
|
|
69
|
+
const url = `${DEV_SERVER_URL}${API_PREFIX}${endpoint}`;
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
...options,
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
...options.headers
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return await response.json();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[GxP Inspector] API Error:', error);
|
|
81
|
+
return { success: false, error: error.message };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function ping() {
|
|
86
|
+
return apiCall('/ping');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function extractString(data) {
|
|
90
|
+
return apiCall('/extract-string', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: JSON.stringify(data)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getStrings() {
|
|
97
|
+
return apiCall('/strings');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// Vue Component Detection
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
function getVueInstance(el) {
|
|
105
|
+
// Vue 3 detection
|
|
106
|
+
if (el.__vueParentComponent) {
|
|
107
|
+
return el.__vueParentComponent;
|
|
108
|
+
}
|
|
109
|
+
// Walk up to find Vue component
|
|
110
|
+
let current = el;
|
|
111
|
+
while (current) {
|
|
112
|
+
if (current.__vueParentComponent) {
|
|
113
|
+
return current.__vueParentComponent;
|
|
114
|
+
}
|
|
115
|
+
current = current.parentElement;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getComponentInfo(vueInstance) {
|
|
121
|
+
if (!vueInstance) return null;
|
|
122
|
+
|
|
123
|
+
const type = vueInstance.type;
|
|
124
|
+
const name = type?.name || type?.__name || type?.__file?.split('/').pop()?.replace('.vue', '') || 'Anonymous';
|
|
125
|
+
const file = type?.__file || null;
|
|
126
|
+
|
|
127
|
+
// Helper to safely serialize a value (handles circular refs, functions, etc.)
|
|
128
|
+
function safeSerialize(value) {
|
|
129
|
+
if (value === null || value === undefined) return value;
|
|
130
|
+
if (typeof value === 'function') return '[Function]';
|
|
131
|
+
if (typeof value !== 'object') return value;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Try JSON stringify/parse to get a clean copy
|
|
135
|
+
return JSON.parse(JSON.stringify(value));
|
|
136
|
+
} catch {
|
|
137
|
+
// If that fails, return a string representation
|
|
138
|
+
if (Array.isArray(value)) return `[Array(${value.length})]`;
|
|
139
|
+
return '{...}';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get props
|
|
144
|
+
const props = {};
|
|
145
|
+
if (vueInstance.props) {
|
|
146
|
+
Object.keys(vueInstance.props).forEach(key => {
|
|
147
|
+
props[key] = safeSerialize(vueInstance.props[key]);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get component data/state
|
|
152
|
+
const data = {};
|
|
153
|
+
if (vueInstance.setupState) {
|
|
154
|
+
Object.keys(vueInstance.setupState).forEach(key => {
|
|
155
|
+
const value = vueInstance.setupState[key];
|
|
156
|
+
if (typeof value !== 'function') {
|
|
157
|
+
data[key] = safeSerialize(value);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { name, file, props, data };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getTextContent(el) {
|
|
166
|
+
// Get direct text content, excluding child elements
|
|
167
|
+
const texts = [];
|
|
168
|
+
el.childNodes.forEach(node => {
|
|
169
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
170
|
+
const text = node.textContent.trim();
|
|
171
|
+
if (text) texts.push(text);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return texts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if an element has a gxp-string attribute (indicating it's already extracted)
|
|
179
|
+
*/
|
|
180
|
+
function getGxpStringKey(el) {
|
|
181
|
+
return el?.getAttribute?.('gxp-string') || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check for a data-gxp-expr attribute on the element
|
|
186
|
+
* The vite-source-tracker-plugin adds this attribute in dev mode
|
|
187
|
+
* @param {Element} el - The element to check
|
|
188
|
+
* @returns {string|null} - The source expression or null
|
|
189
|
+
*/
|
|
190
|
+
function getGxpSourceExpression(el) {
|
|
191
|
+
if (!el || !el.getAttribute) return null;
|
|
192
|
+
return el.getAttribute('data-gxp-expr') || null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get text content with gxp-string attribute info
|
|
197
|
+
* Returns array of objects: { text, gxpStringKey, isExtracted, sourceExpression, isDynamic }
|
|
198
|
+
*/
|
|
199
|
+
function getTextContentWithAttributes(el) {
|
|
200
|
+
const results = [];
|
|
201
|
+
|
|
202
|
+
// Check if this element itself has gxp-string
|
|
203
|
+
const elementKey = getGxpStringKey(el);
|
|
204
|
+
// Check for data-gxp-source attribute (injected by vite plugin for {{ expressions }})
|
|
205
|
+
const sourceExpression = getGxpSourceExpression(el);
|
|
206
|
+
|
|
207
|
+
el.childNodes.forEach(node => {
|
|
208
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
209
|
+
const text = node.textContent.trim();
|
|
210
|
+
if (text) {
|
|
211
|
+
results.push({
|
|
212
|
+
text: text,
|
|
213
|
+
gxpStringKey: elementKey,
|
|
214
|
+
isExtracted: elementKey !== null,
|
|
215
|
+
sourceExpression: sourceExpression,
|
|
216
|
+
isDynamic: sourceExpression !== null
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Find all child elements with gxp-string attributes
|
|
227
|
+
*/
|
|
228
|
+
function findChildGxpStrings(el) {
|
|
229
|
+
const strings = [];
|
|
230
|
+
const elements = el.querySelectorAll('[gxp-string]');
|
|
231
|
+
|
|
232
|
+
elements.forEach(child => {
|
|
233
|
+
const key = child.getAttribute('gxp-string');
|
|
234
|
+
const text = child.textContent.trim();
|
|
235
|
+
if (key && text) {
|
|
236
|
+
strings.push({
|
|
237
|
+
key: key,
|
|
238
|
+
text: text,
|
|
239
|
+
element: child.tagName.toLowerCase()
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return strings;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Analyze text content to detect if it comes from getString() calls
|
|
249
|
+
* Returns an array of text info objects with type: 'raw' or 'getString'
|
|
250
|
+
*/
|
|
251
|
+
function analyzeTextContent(el, vueInstance) {
|
|
252
|
+
const textInfos = [];
|
|
253
|
+
const texts = getTextContent(el);
|
|
254
|
+
|
|
255
|
+
// Get the component's source info to help detect getString usage
|
|
256
|
+
const componentFile = vueInstance?.type?.__file || null;
|
|
257
|
+
|
|
258
|
+
// Try to detect getString calls by checking if this element's text
|
|
259
|
+
// is likely from a getString call. We look for patterns in the rendered output
|
|
260
|
+
// and can cross-reference with the app-manifest.json via API later.
|
|
261
|
+
|
|
262
|
+
texts.forEach((text, index) => {
|
|
263
|
+
// Default to raw text
|
|
264
|
+
const textInfo = {
|
|
265
|
+
text: text,
|
|
266
|
+
type: 'raw',
|
|
267
|
+
index: index
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// We'll mark it as potentially from getString if we can detect it
|
|
271
|
+
// The panel will verify against the manifest
|
|
272
|
+
textInfos.push(textInfo);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return textInfos;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Try to find getString calls in the Vue component by inspecting
|
|
280
|
+
* the component's template bindings (if accessible)
|
|
281
|
+
*/
|
|
282
|
+
function findGetStringCalls(vueInstance, filePath) {
|
|
283
|
+
const getStringCalls = [];
|
|
284
|
+
|
|
285
|
+
if (!vueInstance) return getStringCalls;
|
|
286
|
+
|
|
287
|
+
// Check setupState for gxpStore reference
|
|
288
|
+
const hasGxpStore = vueInstance.setupState?.gxpStore !== undefined;
|
|
289
|
+
|
|
290
|
+
// We can't directly see the template, but we can check for gxpStore usage
|
|
291
|
+
// The actual verification happens on the server side by checking the source file
|
|
292
|
+
|
|
293
|
+
return { hasGxpStore, getStringCalls };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================
|
|
297
|
+
// Overlay UI
|
|
298
|
+
// ============================================================
|
|
299
|
+
|
|
300
|
+
function createHighlightOverlay() {
|
|
301
|
+
if (highlightOverlay) return highlightOverlay;
|
|
302
|
+
|
|
303
|
+
highlightOverlay = document.createElement('div');
|
|
304
|
+
highlightOverlay.id = 'gxp-inspector-highlight';
|
|
305
|
+
|
|
306
|
+
const style = document.createElement('style');
|
|
307
|
+
style.id = 'gxp-highlight-style';
|
|
308
|
+
style.textContent = `
|
|
309
|
+
/* Pointer cursor when in selection mode */
|
|
310
|
+
body.gxp-inspector-selecting,
|
|
311
|
+
body.gxp-inspector-selecting * {
|
|
312
|
+
cursor: crosshair !important;
|
|
313
|
+
}
|
|
314
|
+
#gxp-inspector-highlight {
|
|
315
|
+
position: fixed;
|
|
316
|
+
pointer-events: none;
|
|
317
|
+
z-index: 999999;
|
|
318
|
+
display: none;
|
|
319
|
+
border: 2px dashed #f59e0b;
|
|
320
|
+
background: rgba(245, 158, 11, 0.1);
|
|
321
|
+
border-radius: 4px;
|
|
322
|
+
box-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
|
|
323
|
+
}
|
|
324
|
+
#gxp-inspector-highlight .gxp-highlight-label {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: -24px;
|
|
327
|
+
left: -2px;
|
|
328
|
+
background: #f59e0b;
|
|
329
|
+
color: #1e1e1e;
|
|
330
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
331
|
+
font-size: 11px;
|
|
332
|
+
font-weight: 600;
|
|
333
|
+
padding: 2px 8px;
|
|
334
|
+
border-radius: 3px 3px 0 0;
|
|
335
|
+
white-space: nowrap;
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
highlightOverlay.innerHTML = `<div class="gxp-highlight-label"></div>`;
|
|
340
|
+
|
|
341
|
+
if (!document.getElementById('gxp-highlight-style')) {
|
|
342
|
+
document.head.appendChild(style);
|
|
343
|
+
}
|
|
344
|
+
document.body.appendChild(highlightOverlay);
|
|
345
|
+
return highlightOverlay;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function updateHighlight(el) {
|
|
349
|
+
if (!el || !highlightOverlay) return;
|
|
350
|
+
|
|
351
|
+
const rect = el.getBoundingClientRect();
|
|
352
|
+
const label = highlightOverlay.querySelector('.gxp-highlight-label');
|
|
353
|
+
|
|
354
|
+
// Position the highlight overlay directly
|
|
355
|
+
highlightOverlay.style.display = 'block';
|
|
356
|
+
highlightOverlay.style.left = `${rect.left}px`;
|
|
357
|
+
highlightOverlay.style.top = `${rect.top}px`;
|
|
358
|
+
highlightOverlay.style.width = `${rect.width}px`;
|
|
359
|
+
highlightOverlay.style.height = `${rect.height}px`;
|
|
360
|
+
|
|
361
|
+
// Build label: Component::element::gxp-string-key
|
|
362
|
+
label.textContent = buildElementLabel(el);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hideHighlight() {
|
|
366
|
+
if (highlightOverlay) {
|
|
367
|
+
highlightOverlay.style.display = 'none';
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ============================================================
|
|
372
|
+
// Selection Highlight (persistent border on selected element)
|
|
373
|
+
// ============================================================
|
|
374
|
+
|
|
375
|
+
function createSelectionHighlight() {
|
|
376
|
+
if (selectionHighlight) return selectionHighlight;
|
|
377
|
+
|
|
378
|
+
selectionHighlight = document.createElement('div');
|
|
379
|
+
selectionHighlight.id = 'gxp-inspector-selection';
|
|
380
|
+
|
|
381
|
+
const style = document.createElement('style');
|
|
382
|
+
style.id = 'gxp-selection-style';
|
|
383
|
+
style.textContent = `
|
|
384
|
+
#gxp-inspector-selection {
|
|
385
|
+
position: fixed;
|
|
386
|
+
pointer-events: none;
|
|
387
|
+
z-index: 999998;
|
|
388
|
+
display: none;
|
|
389
|
+
border: 3px solid #61dafb;
|
|
390
|
+
background: rgba(97, 218, 251, 0.1);
|
|
391
|
+
border-radius: 4px;
|
|
392
|
+
box-shadow: 0 0 0 1px rgba(97, 218, 251, 0.3),
|
|
393
|
+
0 0 12px 3px rgba(97, 218, 251, 0.5),
|
|
394
|
+
0 0 24px 6px rgba(97, 218, 251, 0.25),
|
|
395
|
+
inset 0 0 20px rgba(97, 218, 251, 0.1);
|
|
396
|
+
animation: gxp-selection-pulse 2s ease-in-out infinite;
|
|
397
|
+
}
|
|
398
|
+
#gxp-inspector-selection .gxp-selection-label {
|
|
399
|
+
position: absolute;
|
|
400
|
+
top: -26px;
|
|
401
|
+
left: -3px;
|
|
402
|
+
background: #61dafb;
|
|
403
|
+
color: #1e1e1e;
|
|
404
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
405
|
+
font-size: 11px;
|
|
406
|
+
font-weight: 600;
|
|
407
|
+
padding: 3px 10px;
|
|
408
|
+
border-radius: 4px 4px 0 0;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
box-shadow: 0 0 10px rgba(97, 218, 251, 0.6);
|
|
411
|
+
}
|
|
412
|
+
@keyframes gxp-selection-pulse {
|
|
413
|
+
0%, 100% {
|
|
414
|
+
box-shadow: 0 0 0 1px rgba(97, 218, 251, 0.3),
|
|
415
|
+
0 0 12px 3px rgba(97, 218, 251, 0.5),
|
|
416
|
+
0 0 24px 6px rgba(97, 218, 251, 0.25),
|
|
417
|
+
inset 0 0 20px rgba(97, 218, 251, 0.1);
|
|
418
|
+
}
|
|
419
|
+
50% {
|
|
420
|
+
box-shadow: 0 0 0 2px rgba(97, 218, 251, 0.5),
|
|
421
|
+
0 0 20px 5px rgba(97, 218, 251, 0.7),
|
|
422
|
+
0 0 40px 10px rgba(97, 218, 251, 0.35),
|
|
423
|
+
inset 0 0 30px rgba(97, 218, 251, 0.15);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
`;
|
|
427
|
+
|
|
428
|
+
selectionHighlight.innerHTML = `<div class="gxp-selection-label"></div>`;
|
|
429
|
+
|
|
430
|
+
if (!document.getElementById('gxp-selection-style')) {
|
|
431
|
+
document.head.appendChild(style);
|
|
432
|
+
}
|
|
433
|
+
document.body.appendChild(selectionHighlight);
|
|
434
|
+
return selectionHighlight;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function updateSelectionHighlight(el) {
|
|
438
|
+
if (!el) {
|
|
439
|
+
hideSelectionHighlight();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
createSelectionHighlight();
|
|
444
|
+
const rect = el.getBoundingClientRect();
|
|
445
|
+
const label = selectionHighlight.querySelector('.gxp-selection-label');
|
|
446
|
+
|
|
447
|
+
// Position the main overlay element directly
|
|
448
|
+
selectionHighlight.style.display = 'block';
|
|
449
|
+
selectionHighlight.style.left = `${rect.left}px`;
|
|
450
|
+
selectionHighlight.style.top = `${rect.top}px`;
|
|
451
|
+
selectionHighlight.style.width = `${rect.width}px`;
|
|
452
|
+
selectionHighlight.style.height = `${rect.height}px`;
|
|
453
|
+
|
|
454
|
+
// Build label: Component::element::gxp-string-key
|
|
455
|
+
label.textContent = buildElementLabel(el);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function hideSelectionHighlight() {
|
|
459
|
+
if (selectionHighlight) {
|
|
460
|
+
selectionHighlight.style.display = 'none';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Update selection highlight position on scroll/resize
|
|
465
|
+
function updateSelectionPosition() {
|
|
466
|
+
if (selectedElement && selectionHighlight && selectionHighlight.style.display !== 'none') {
|
|
467
|
+
updateSelectionHighlight(selectedElement);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================
|
|
472
|
+
// Inspector Panel
|
|
473
|
+
// ============================================================
|
|
474
|
+
|
|
475
|
+
function createInspectorPanel() {
|
|
476
|
+
if (inspectorPanel) return inspectorPanel;
|
|
477
|
+
|
|
478
|
+
inspectorPanel = document.createElement('div');
|
|
479
|
+
inspectorPanel.id = 'gxp-inspector-panel';
|
|
480
|
+
inspectorPanel.innerHTML = `
|
|
481
|
+
<div class="gxp-panel-header">
|
|
482
|
+
<span class="gxp-panel-title">GxP Component Inspector</span>
|
|
483
|
+
<button class="gxp-panel-close">×</button>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="gxp-panel-content">
|
|
486
|
+
<div class="gxp-panel-section gxp-component-info">
|
|
487
|
+
<div class="gxp-section-title">Component</div>
|
|
488
|
+
<div class="gxp-component-name">Click on an element to inspect</div>
|
|
489
|
+
<div class="gxp-component-file"></div>
|
|
490
|
+
</div>
|
|
491
|
+
<div class="gxp-panel-section gxp-strings-section" style="display: none;">
|
|
492
|
+
<div class="gxp-section-title">Text Content</div>
|
|
493
|
+
<div class="gxp-strings-list"></div>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="gxp-panel-section gxp-extract-section" style="display: none;">
|
|
496
|
+
<div class="gxp-section-title">Extract String</div>
|
|
497
|
+
<div class="gxp-extract-form">
|
|
498
|
+
<div class="gxp-form-group">
|
|
499
|
+
<label>Text:</label>
|
|
500
|
+
<input type="text" class="gxp-extract-text" readonly>
|
|
501
|
+
</div>
|
|
502
|
+
<div class="gxp-form-group">
|
|
503
|
+
<label>Key:</label>
|
|
504
|
+
<input type="text" class="gxp-extract-key" placeholder="e.g., welcome_title">
|
|
505
|
+
</div>
|
|
506
|
+
<div class="gxp-form-group">
|
|
507
|
+
<label>File:</label>
|
|
508
|
+
<input type="text" class="gxp-extract-file" readonly>
|
|
509
|
+
</div>
|
|
510
|
+
<button class="gxp-extract-button">Extract to getString()</button>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="gxp-extract-status"></div>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="gxp-panel-section gxp-props-section" style="display: none;">
|
|
515
|
+
<div class="gxp-section-title">Props</div>
|
|
516
|
+
<pre class="gxp-props-content"></pre>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
`;
|
|
520
|
+
|
|
521
|
+
const style = document.createElement('style');
|
|
522
|
+
style.textContent = `
|
|
523
|
+
#gxp-inspector-panel {
|
|
524
|
+
position: fixed;
|
|
525
|
+
bottom: 20px;
|
|
526
|
+
right: 20px;
|
|
527
|
+
width: 380px;
|
|
528
|
+
max-height: 500px;
|
|
529
|
+
background: #1e1e1e;
|
|
530
|
+
border: 1px solid #3d3d3d;
|
|
531
|
+
border-radius: 8px;
|
|
532
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
533
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
534
|
+
font-size: 13px;
|
|
535
|
+
color: #e0e0e0;
|
|
536
|
+
z-index: 999998;
|
|
537
|
+
overflow: hidden;
|
|
538
|
+
display: flex;
|
|
539
|
+
flex-direction: column;
|
|
540
|
+
}
|
|
541
|
+
.gxp-panel-header {
|
|
542
|
+
display: flex;
|
|
543
|
+
justify-content: space-between;
|
|
544
|
+
align-items: center;
|
|
545
|
+
padding: 12px 16px;
|
|
546
|
+
background: #2d2d2d;
|
|
547
|
+
border-bottom: 1px solid #3d3d3d;
|
|
548
|
+
}
|
|
549
|
+
.gxp-panel-title {
|
|
550
|
+
font-weight: 600;
|
|
551
|
+
color: #61dafb;
|
|
552
|
+
}
|
|
553
|
+
.gxp-panel-close {
|
|
554
|
+
background: none;
|
|
555
|
+
border: none;
|
|
556
|
+
color: #888;
|
|
557
|
+
font-size: 20px;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
padding: 0;
|
|
560
|
+
line-height: 1;
|
|
561
|
+
}
|
|
562
|
+
.gxp-panel-close:hover {
|
|
563
|
+
color: #ff6b6b;
|
|
564
|
+
}
|
|
565
|
+
.gxp-panel-content {
|
|
566
|
+
padding: 16px;
|
|
567
|
+
overflow-y: auto;
|
|
568
|
+
flex: 1;
|
|
569
|
+
}
|
|
570
|
+
.gxp-panel-section {
|
|
571
|
+
margin-bottom: 16px;
|
|
572
|
+
}
|
|
573
|
+
.gxp-section-title {
|
|
574
|
+
font-size: 11px;
|
|
575
|
+
font-weight: 600;
|
|
576
|
+
text-transform: uppercase;
|
|
577
|
+
color: #888;
|
|
578
|
+
margin-bottom: 8px;
|
|
579
|
+
}
|
|
580
|
+
.gxp-component-name {
|
|
581
|
+
font-size: 15px;
|
|
582
|
+
font-weight: 600;
|
|
583
|
+
color: #61dafb;
|
|
584
|
+
margin-bottom: 4px;
|
|
585
|
+
}
|
|
586
|
+
.gxp-component-file {
|
|
587
|
+
font-size: 11px;
|
|
588
|
+
color: #888;
|
|
589
|
+
word-break: break-all;
|
|
590
|
+
}
|
|
591
|
+
.gxp-strings-list {
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
}
|
|
596
|
+
.gxp-string-item {
|
|
597
|
+
display: flex;
|
|
598
|
+
justify-content: space-between;
|
|
599
|
+
align-items: center;
|
|
600
|
+
padding: 8px 10px;
|
|
601
|
+
background: #2d2d2d;
|
|
602
|
+
border-radius: 4px;
|
|
603
|
+
cursor: pointer;
|
|
604
|
+
}
|
|
605
|
+
.gxp-string-item:hover {
|
|
606
|
+
background: #3d3d3d;
|
|
607
|
+
}
|
|
608
|
+
.gxp-string-text {
|
|
609
|
+
flex: 1;
|
|
610
|
+
overflow: hidden;
|
|
611
|
+
text-overflow: ellipsis;
|
|
612
|
+
white-space: nowrap;
|
|
613
|
+
}
|
|
614
|
+
.gxp-string-extract {
|
|
615
|
+
background: #61dafb;
|
|
616
|
+
color: #1e1e1e;
|
|
617
|
+
border: none;
|
|
618
|
+
padding: 4px 10px;
|
|
619
|
+
border-radius: 3px;
|
|
620
|
+
font-size: 11px;
|
|
621
|
+
font-weight: 600;
|
|
622
|
+
cursor: pointer;
|
|
623
|
+
margin-left: 8px;
|
|
624
|
+
}
|
|
625
|
+
.gxp-string-extract:hover {
|
|
626
|
+
background: #4fc3f7;
|
|
627
|
+
}
|
|
628
|
+
.gxp-extract-form {
|
|
629
|
+
display: flex;
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 10px;
|
|
632
|
+
}
|
|
633
|
+
.gxp-form-group {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex-direction: column;
|
|
636
|
+
gap: 4px;
|
|
637
|
+
}
|
|
638
|
+
.gxp-form-group label {
|
|
639
|
+
font-size: 11px;
|
|
640
|
+
color: #888;
|
|
641
|
+
}
|
|
642
|
+
.gxp-form-group input {
|
|
643
|
+
background: #2d2d2d;
|
|
644
|
+
border: 1px solid #3d3d3d;
|
|
645
|
+
border-radius: 4px;
|
|
646
|
+
padding: 8px 10px;
|
|
647
|
+
color: #e0e0e0;
|
|
648
|
+
font-size: 12px;
|
|
649
|
+
}
|
|
650
|
+
.gxp-form-group input:focus {
|
|
651
|
+
outline: none;
|
|
652
|
+
border-color: #61dafb;
|
|
653
|
+
}
|
|
654
|
+
.gxp-extract-button {
|
|
655
|
+
background: #28a745;
|
|
656
|
+
color: white;
|
|
657
|
+
border: none;
|
|
658
|
+
padding: 10px 16px;
|
|
659
|
+
border-radius: 4px;
|
|
660
|
+
font-size: 13px;
|
|
661
|
+
font-weight: 600;
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
margin-top: 8px;
|
|
664
|
+
}
|
|
665
|
+
.gxp-extract-button:hover {
|
|
666
|
+
background: #218838;
|
|
667
|
+
}
|
|
668
|
+
.gxp-extract-button:disabled {
|
|
669
|
+
background: #6c757d;
|
|
670
|
+
cursor: not-allowed;
|
|
671
|
+
}
|
|
672
|
+
.gxp-extract-status {
|
|
673
|
+
margin-top: 10px;
|
|
674
|
+
padding: 10px;
|
|
675
|
+
border-radius: 4px;
|
|
676
|
+
font-size: 12px;
|
|
677
|
+
display: none;
|
|
678
|
+
}
|
|
679
|
+
.gxp-extract-status.success {
|
|
680
|
+
display: block;
|
|
681
|
+
background: #28a74520;
|
|
682
|
+
border: 1px solid #28a745;
|
|
683
|
+
color: #28a745;
|
|
684
|
+
}
|
|
685
|
+
.gxp-extract-status.error {
|
|
686
|
+
display: block;
|
|
687
|
+
background: #dc354520;
|
|
688
|
+
border: 1px solid #dc3545;
|
|
689
|
+
color: #dc3545;
|
|
690
|
+
}
|
|
691
|
+
.gxp-props-content {
|
|
692
|
+
background: #2d2d2d;
|
|
693
|
+
padding: 10px;
|
|
694
|
+
border-radius: 4px;
|
|
695
|
+
font-size: 11px;
|
|
696
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
697
|
+
overflow-x: auto;
|
|
698
|
+
max-height: 150px;
|
|
699
|
+
margin: 0;
|
|
700
|
+
}
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
document.head.appendChild(style);
|
|
704
|
+
document.body.appendChild(inspectorPanel);
|
|
705
|
+
|
|
706
|
+
// Event handlers
|
|
707
|
+
inspectorPanel.querySelector('.gxp-panel-close').addEventListener('click', () => {
|
|
708
|
+
disableInspector();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
inspectorPanel.querySelector('.gxp-extract-button').addEventListener('click', handleExtract);
|
|
712
|
+
|
|
713
|
+
return inspectorPanel;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function updatePanel(el) {
|
|
717
|
+
if (!inspectorPanel || !el) return;
|
|
718
|
+
|
|
719
|
+
const vueInstance = getVueInstance(el);
|
|
720
|
+
const info = getComponentInfo(vueInstance);
|
|
721
|
+
const texts = getTextContent(el);
|
|
722
|
+
|
|
723
|
+
// Update component info
|
|
724
|
+
const nameEl = inspectorPanel.querySelector('.gxp-component-name');
|
|
725
|
+
const fileEl = inspectorPanel.querySelector('.gxp-component-file');
|
|
726
|
+
|
|
727
|
+
if (info) {
|
|
728
|
+
nameEl.textContent = `<${info.name}>`;
|
|
729
|
+
fileEl.textContent = info.file || 'Unknown file';
|
|
730
|
+
} else {
|
|
731
|
+
nameEl.textContent = `<${el.tagName.toLowerCase()}>`;
|
|
732
|
+
fileEl.textContent = 'Not a Vue component';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Update strings list
|
|
736
|
+
const stringsSection = inspectorPanel.querySelector('.gxp-strings-section');
|
|
737
|
+
const stringsList = inspectorPanel.querySelector('.gxp-strings-list');
|
|
738
|
+
|
|
739
|
+
if (texts.length > 0) {
|
|
740
|
+
stringsSection.style.display = 'block';
|
|
741
|
+
stringsList.innerHTML = texts.map(text => `
|
|
742
|
+
<div class="gxp-string-item" data-text="${escapeHtml(text)}">
|
|
743
|
+
<span class="gxp-string-text">${escapeHtml(text)}</span>
|
|
744
|
+
<button class="gxp-string-extract">Extract</button>
|
|
745
|
+
</div>
|
|
746
|
+
`).join('');
|
|
747
|
+
|
|
748
|
+
// Add click handlers
|
|
749
|
+
stringsList.querySelectorAll('.gxp-string-item').forEach(item => {
|
|
750
|
+
item.querySelector('.gxp-string-extract').addEventListener('click', (e) => {
|
|
751
|
+
e.stopPropagation();
|
|
752
|
+
showExtractForm(item.dataset.text, info?.file);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
} else {
|
|
756
|
+
stringsSection.style.display = 'none';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Update props section
|
|
760
|
+
const propsSection = inspectorPanel.querySelector('.gxp-props-section');
|
|
761
|
+
const propsContent = inspectorPanel.querySelector('.gxp-props-content');
|
|
762
|
+
|
|
763
|
+
if (info && Object.keys(info.props).length > 0) {
|
|
764
|
+
propsSection.style.display = 'block';
|
|
765
|
+
propsContent.textContent = JSON.stringify(info.props, null, 2);
|
|
766
|
+
} else {
|
|
767
|
+
propsSection.style.display = 'none';
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function showExtractForm(text, filePath) {
|
|
772
|
+
const extractSection = inspectorPanel.querySelector('.gxp-extract-section');
|
|
773
|
+
const textInput = inspectorPanel.querySelector('.gxp-extract-text');
|
|
774
|
+
const keyInput = inspectorPanel.querySelector('.gxp-extract-key');
|
|
775
|
+
const fileInput = inspectorPanel.querySelector('.gxp-extract-file');
|
|
776
|
+
const statusEl = inspectorPanel.querySelector('.gxp-extract-status');
|
|
777
|
+
|
|
778
|
+
extractSection.style.display = 'block';
|
|
779
|
+
textInput.value = text;
|
|
780
|
+
keyInput.value = textToKey(text);
|
|
781
|
+
fileInput.value = filePath || '';
|
|
782
|
+
statusEl.style.display = 'none';
|
|
783
|
+
statusEl.className = 'gxp-extract-status';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function handleExtract() {
|
|
787
|
+
const textInput = inspectorPanel.querySelector('.gxp-extract-text');
|
|
788
|
+
const keyInput = inspectorPanel.querySelector('.gxp-extract-key');
|
|
789
|
+
const fileInput = inspectorPanel.querySelector('.gxp-extract-file');
|
|
790
|
+
const button = inspectorPanel.querySelector('.gxp-extract-button');
|
|
791
|
+
const statusEl = inspectorPanel.querySelector('.gxp-extract-status');
|
|
792
|
+
|
|
793
|
+
const text = textInput.value;
|
|
794
|
+
const key = keyInput.value;
|
|
795
|
+
const filePath = fileInput.value;
|
|
796
|
+
|
|
797
|
+
if (!text || !key || !filePath) {
|
|
798
|
+
statusEl.textContent = 'All fields are required';
|
|
799
|
+
statusEl.className = 'gxp-extract-status error';
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
button.disabled = true;
|
|
804
|
+
button.textContent = 'Extracting...';
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const result = await extractString({
|
|
808
|
+
text,
|
|
809
|
+
key,
|
|
810
|
+
filePath
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
if (result.success) {
|
|
814
|
+
statusEl.textContent = `Success! Added getString('${key}') to ${filePath}`;
|
|
815
|
+
statusEl.className = 'gxp-extract-status success';
|
|
816
|
+
} else {
|
|
817
|
+
statusEl.textContent = result.error || 'Extraction failed';
|
|
818
|
+
statusEl.className = 'gxp-extract-status error';
|
|
819
|
+
}
|
|
820
|
+
} catch (error) {
|
|
821
|
+
statusEl.textContent = error.message;
|
|
822
|
+
statusEl.className = 'gxp-extract-status error';
|
|
823
|
+
} finally {
|
|
824
|
+
button.disabled = false;
|
|
825
|
+
button.textContent = 'Extract to getString()';
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================================
|
|
830
|
+
// Event Handlers
|
|
831
|
+
// ============================================================
|
|
832
|
+
|
|
833
|
+
function handleMouseMove(e) {
|
|
834
|
+
if (!inspectorEnabled) return;
|
|
835
|
+
|
|
836
|
+
const el = e.target;
|
|
837
|
+
if (el === highlightOverlay || highlightOverlay?.contains(el) ||
|
|
838
|
+
el === inspectorPanel || inspectorPanel?.contains(el)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (el !== hoveredElement) {
|
|
843
|
+
hoveredElement = el;
|
|
844
|
+
updateHighlight(el);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function handleClick(e) {
|
|
849
|
+
if (!inspectorEnabled) return;
|
|
850
|
+
|
|
851
|
+
const el = e.target;
|
|
852
|
+
if (el === highlightOverlay || highlightOverlay?.contains(el) ||
|
|
853
|
+
el === inspectorPanel || inspectorPanel?.contains(el) ||
|
|
854
|
+
el === selectionHighlight || selectionHighlight?.contains(el)) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
e.stopPropagation();
|
|
860
|
+
|
|
861
|
+
selectedElement = el;
|
|
862
|
+
// Store for DevTools panel access via eval
|
|
863
|
+
window.__gxpSelectedElement = el;
|
|
864
|
+
|
|
865
|
+
updatePanel(el);
|
|
866
|
+
|
|
867
|
+
// Show persistent selection highlight
|
|
868
|
+
updateSelectionHighlight(el);
|
|
869
|
+
|
|
870
|
+
// Hide hover highlight
|
|
871
|
+
hideHighlight();
|
|
872
|
+
|
|
873
|
+
// Disable inspector (selection mode) after selecting
|
|
874
|
+
// This prevents accidental re-selection on next click
|
|
875
|
+
disableInspector();
|
|
876
|
+
|
|
877
|
+
// Send selection to background for DevTools panel
|
|
878
|
+
sendElementToDevTools(el);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function sendElementToDevTools(el) {
|
|
882
|
+
const vueInstance = getVueInstance(el);
|
|
883
|
+
const info = getComponentInfo(vueInstance);
|
|
884
|
+
const texts = getTextContent(el);
|
|
885
|
+
const textsWithAttrs = getTextContentWithAttributes(el);
|
|
886
|
+
const childGxpStrings = findChildGxpStrings(el);
|
|
887
|
+
|
|
888
|
+
// Check if the element itself has gxp-string
|
|
889
|
+
const gxpStringKey = getGxpStringKey(el);
|
|
890
|
+
// Check for data-gxp-source attribute (injected by vite plugin)
|
|
891
|
+
const sourceExpression = getGxpSourceExpression(el);
|
|
892
|
+
|
|
893
|
+
const data = {
|
|
894
|
+
tagName: el.tagName.toLowerCase(),
|
|
895
|
+
component: info,
|
|
896
|
+
texts: texts,
|
|
897
|
+
textsWithAttributes: textsWithAttrs,
|
|
898
|
+
childGxpStrings: childGxpStrings,
|
|
899
|
+
gxpStringKey: gxpStringKey,
|
|
900
|
+
isExtracted: gxpStringKey !== null,
|
|
901
|
+
sourceExpression: sourceExpression,
|
|
902
|
+
isDynamic: sourceExpression !== null
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// Send to content script via postMessage (since we're in page context)
|
|
906
|
+
// The content script will relay to the background script
|
|
907
|
+
window.postMessage({
|
|
908
|
+
type: 'GXP_INSPECTOR_MESSAGE',
|
|
909
|
+
payload: {
|
|
910
|
+
type: 'elementSelected',
|
|
911
|
+
data: data
|
|
912
|
+
}
|
|
913
|
+
}, '*');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function handleKeyDown(e) {
|
|
917
|
+
// Escape to disable inspector
|
|
918
|
+
if (e.key === 'Escape' && inspectorEnabled) {
|
|
919
|
+
disableInspector();
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Ctrl+Shift+I to toggle inspector
|
|
923
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'I') {
|
|
924
|
+
e.preventDefault();
|
|
925
|
+
toggleInspector();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ============================================================
|
|
930
|
+
// Public API
|
|
931
|
+
// ============================================================
|
|
932
|
+
|
|
933
|
+
function enableInspector() {
|
|
934
|
+
if (inspectorEnabled) return;
|
|
935
|
+
|
|
936
|
+
inspectorEnabled = true;
|
|
937
|
+
createHighlightOverlay();
|
|
938
|
+
createInspectorPanel();
|
|
939
|
+
|
|
940
|
+
// Add selecting class for pointer cursor
|
|
941
|
+
document.body.classList.add('gxp-inspector-selecting');
|
|
942
|
+
|
|
943
|
+
// Hide previous selection highlight when entering selection mode
|
|
944
|
+
hideSelectionHighlight();
|
|
945
|
+
selectedElement = null;
|
|
946
|
+
window.__gxpSelectedElement = null;
|
|
947
|
+
|
|
948
|
+
document.addEventListener('mousemove', handleMouseMove, true);
|
|
949
|
+
document.addEventListener('click', handleClick, true);
|
|
950
|
+
window.addEventListener('scroll', updateSelectionPosition, true);
|
|
951
|
+
window.addEventListener('resize', updateSelectionPosition);
|
|
952
|
+
|
|
953
|
+
console.log('[GxP Inspector] Enabled - Hover over elements to inspect');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function disableInspector() {
|
|
957
|
+
if (!inspectorEnabled) return;
|
|
958
|
+
|
|
959
|
+
inspectorEnabled = false;
|
|
960
|
+
hideHighlight();
|
|
961
|
+
|
|
962
|
+
// Remove selecting class for pointer cursor
|
|
963
|
+
document.body.classList.remove('gxp-inspector-selecting');
|
|
964
|
+
|
|
965
|
+
if (inspectorPanel) {
|
|
966
|
+
inspectorPanel.remove();
|
|
967
|
+
inspectorPanel = null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
971
|
+
document.removeEventListener('click', handleClick, true);
|
|
972
|
+
// Keep scroll/resize listeners for selection highlight position updates
|
|
973
|
+
// They will be removed when a new selection starts
|
|
974
|
+
|
|
975
|
+
// Don't clear selectedElement here - we want to keep the selection
|
|
976
|
+
hoveredElement = null;
|
|
977
|
+
|
|
978
|
+
console.log('[GxP Inspector] Disabled - Selection preserved');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Clear selection completely (called when user wants to deselect)
|
|
982
|
+
function clearSelection() {
|
|
983
|
+
hideSelectionHighlight();
|
|
984
|
+
selectedElement = null;
|
|
985
|
+
window.__gxpSelectedElement = null;
|
|
986
|
+
window.removeEventListener('scroll', updateSelectionPosition, true);
|
|
987
|
+
window.removeEventListener('resize', updateSelectionPosition);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function toggleInspector() {
|
|
991
|
+
if (inspectorEnabled) {
|
|
992
|
+
disableInspector();
|
|
993
|
+
} else {
|
|
994
|
+
enableInspector();
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ============================================================
|
|
999
|
+
// Utility Functions
|
|
1000
|
+
// ============================================================
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Build a descriptive label for an element
|
|
1004
|
+
* Format: ComponentName::element::gxp-string-key
|
|
1005
|
+
* Examples:
|
|
1006
|
+
* - DemoPage::h1::welcome_title
|
|
1007
|
+
* - DemoPage::h1 (no gxp-string)
|
|
1008
|
+
* - div::gxp-string-key (no Vue component)
|
|
1009
|
+
* - div (plain element)
|
|
1010
|
+
*/
|
|
1011
|
+
function buildElementLabel(el) {
|
|
1012
|
+
const parts = [];
|
|
1013
|
+
|
|
1014
|
+
// Get Vue component name
|
|
1015
|
+
const vueInstance = getVueInstance(el);
|
|
1016
|
+
const info = getComponentInfo(vueInstance);
|
|
1017
|
+
if (info && info.name) {
|
|
1018
|
+
parts.push(info.name);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Add element tag name
|
|
1022
|
+
parts.push(el.tagName.toLowerCase());
|
|
1023
|
+
|
|
1024
|
+
// Check for gxp-string attribute
|
|
1025
|
+
const gxpStringKey = el.getAttribute('gxp-string');
|
|
1026
|
+
if (gxpStringKey) {
|
|
1027
|
+
parts.push(gxpStringKey);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Check for gxp-src attribute (for images/assets)
|
|
1031
|
+
const gxpSrcKey = el.getAttribute('gxp-src');
|
|
1032
|
+
if (gxpSrcKey && !gxpStringKey) {
|
|
1033
|
+
parts.push(gxpSrcKey);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return parts.join('::');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function textToKey(text) {
|
|
1040
|
+
return text
|
|
1041
|
+
.toLowerCase()
|
|
1042
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
1043
|
+
.replace(/\s+/g, '_')
|
|
1044
|
+
.substring(0, 40)
|
|
1045
|
+
.replace(/_+$/, '');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function escapeHtml(text) {
|
|
1049
|
+
const div = document.createElement('div');
|
|
1050
|
+
div.textContent = text;
|
|
1051
|
+
return div.innerHTML;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ============================================================
|
|
1055
|
+
// Initialize
|
|
1056
|
+
// ============================================================
|
|
1057
|
+
|
|
1058
|
+
// Add keyboard listener
|
|
1059
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
1060
|
+
|
|
1061
|
+
// Expose API
|
|
1062
|
+
window.gxpInspector = {
|
|
1063
|
+
enable: enableInspector,
|
|
1064
|
+
disable: disableInspector,
|
|
1065
|
+
toggle: toggleInspector,
|
|
1066
|
+
isEnabled: () => inspectorEnabled,
|
|
1067
|
+
clearSelection: clearSelection,
|
|
1068
|
+
ping: ping
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// Listen for messages from content script (which relays from popup/background)
|
|
1072
|
+
window.addEventListener('message', (event) => {
|
|
1073
|
+
if (event.source !== window) return;
|
|
1074
|
+
if (event.data?.type === 'GXP_INSPECTOR_ACTION') {
|
|
1075
|
+
const action = event.data.action;
|
|
1076
|
+
if (action === 'toggleInspector') {
|
|
1077
|
+
toggleInspector();
|
|
1078
|
+
} else if (action === 'enable') {
|
|
1079
|
+
enableInspector();
|
|
1080
|
+
} else if (action === 'disable') {
|
|
1081
|
+
disableInspector();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
console.log('[GxP Inspector] Loaded in page context. Press Ctrl+Shift+I to toggle inspector.');
|
|
1087
|
+
})();
|