@gxp-dev/tools 2.0.63 → 2.0.64

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.
Files changed (182) hide show
  1. package/README.md +32 -31
  2. package/bin/gx-devtools.js +74 -54
  3. package/bin/lib/cli.js +23 -21
  4. package/bin/lib/commands/add-dependency.js +366 -325
  5. package/bin/lib/commands/assets.js +137 -139
  6. package/bin/lib/commands/build.js +169 -174
  7. package/bin/lib/commands/datastore.js +181 -183
  8. package/bin/lib/commands/dev.js +127 -131
  9. package/bin/lib/commands/extensions.js +147 -149
  10. package/bin/lib/commands/extract-config.js +73 -67
  11. package/bin/lib/commands/index.js +12 -12
  12. package/bin/lib/commands/init.js +342 -240
  13. package/bin/lib/commands/publish.js +69 -75
  14. package/bin/lib/commands/socket.js +69 -69
  15. package/bin/lib/commands/ssl.js +14 -14
  16. package/bin/lib/constants.js +10 -24
  17. package/bin/lib/tui/App.tsx +761 -705
  18. package/bin/lib/tui/components/AIPanel.tsx +191 -171
  19. package/bin/lib/tui/components/CommandInput.tsx +394 -343
  20. package/bin/lib/tui/components/GeminiPanel.tsx +175 -151
  21. package/bin/lib/tui/components/Header.tsx +23 -21
  22. package/bin/lib/tui/components/LogPanel.tsx +244 -220
  23. package/bin/lib/tui/components/TabBar.tsx +50 -48
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +126 -71
  25. package/bin/lib/tui/index.tsx +37 -39
  26. package/bin/lib/tui/services/AIService.ts +518 -462
  27. package/bin/lib/tui/services/ExtensionService.ts +140 -129
  28. package/bin/lib/tui/services/GeminiService.ts +367 -337
  29. package/bin/lib/tui/services/ServiceManager.ts +344 -322
  30. package/bin/lib/tui/services/SocketService.ts +168 -168
  31. package/bin/lib/tui/services/ViteService.ts +88 -88
  32. package/bin/lib/tui/services/index.ts +47 -22
  33. package/bin/lib/utils/ai-scaffold.js +291 -280
  34. package/bin/lib/utils/extract-config.js +157 -140
  35. package/bin/lib/utils/files.js +82 -86
  36. package/bin/lib/utils/index.js +7 -7
  37. package/bin/lib/utils/paths.js +34 -34
  38. package/bin/lib/utils/prompts.js +194 -169
  39. package/bin/lib/utils/ssl.js +79 -81
  40. package/browser-extensions/README.md +0 -1
  41. package/browser-extensions/chrome/background.js +244 -237
  42. package/browser-extensions/chrome/content.js +32 -29
  43. package/browser-extensions/chrome/devtools.html +7 -7
  44. package/browser-extensions/chrome/devtools.js +19 -19
  45. package/browser-extensions/chrome/inspector.js +802 -767
  46. package/browser-extensions/chrome/manifest.json +71 -63
  47. package/browser-extensions/chrome/panel.html +674 -636
  48. package/browser-extensions/chrome/panel.js +722 -712
  49. package/browser-extensions/chrome/popup.html +586 -543
  50. package/browser-extensions/chrome/popup.js +282 -244
  51. package/browser-extensions/chrome/rules.json +1 -1
  52. package/browser-extensions/chrome/test-chrome.html +216 -136
  53. package/browser-extensions/chrome/test-mixed-content.html +284 -189
  54. package/browser-extensions/chrome/test-uri-pattern.html +221 -198
  55. package/browser-extensions/firefox/README.md +9 -6
  56. package/browser-extensions/firefox/background.js +221 -218
  57. package/browser-extensions/firefox/content.js +55 -52
  58. package/browser-extensions/firefox/debug-errors.html +386 -228
  59. package/browser-extensions/firefox/debug-https.html +153 -105
  60. package/browser-extensions/firefox/devtools.html +7 -7
  61. package/browser-extensions/firefox/devtools.js +23 -20
  62. package/browser-extensions/firefox/inspector.js +802 -767
  63. package/browser-extensions/firefox/manifest.json +68 -68
  64. package/browser-extensions/firefox/panel.html +674 -636
  65. package/browser-extensions/firefox/panel.js +722 -712
  66. package/browser-extensions/firefox/popup.html +572 -535
  67. package/browser-extensions/firefox/popup.js +281 -236
  68. package/browser-extensions/firefox/test-gramercy.html +170 -125
  69. package/browser-extensions/firefox/test-imports.html +59 -55
  70. package/browser-extensions/firefox/test-masking.html +231 -140
  71. package/browser-extensions/firefox/test-uri-pattern.html +221 -198
  72. package/dist/tui/App.d.ts +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +154 -150
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/components/AIPanel.d.ts.map +1 -1
  77. package/dist/tui/components/AIPanel.js +42 -35
  78. package/dist/tui/components/AIPanel.js.map +1 -1
  79. package/dist/tui/components/CommandInput.d.ts +1 -1
  80. package/dist/tui/components/CommandInput.d.ts.map +1 -1
  81. package/dist/tui/components/CommandInput.js +92 -62
  82. package/dist/tui/components/CommandInput.js.map +1 -1
  83. package/dist/tui/components/GeminiPanel.d.ts.map +1 -1
  84. package/dist/tui/components/GeminiPanel.js +37 -30
  85. package/dist/tui/components/GeminiPanel.js.map +1 -1
  86. package/dist/tui/components/Header.d.ts.map +1 -1
  87. package/dist/tui/components/Header.js +1 -1
  88. package/dist/tui/components/Header.js.map +1 -1
  89. package/dist/tui/components/LogPanel.d.ts +1 -1
  90. package/dist/tui/components/LogPanel.d.ts.map +1 -1
  91. package/dist/tui/components/LogPanel.js +26 -24
  92. package/dist/tui/components/LogPanel.js.map +1 -1
  93. package/dist/tui/components/TabBar.d.ts +2 -2
  94. package/dist/tui/components/TabBar.d.ts.map +1 -1
  95. package/dist/tui/components/TabBar.js +11 -11
  96. package/dist/tui/components/TabBar.js.map +1 -1
  97. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -1
  98. package/dist/tui/components/WelcomeScreen.js +6 -6
  99. package/dist/tui/components/WelcomeScreen.js.map +1 -1
  100. package/dist/tui/index.d.ts.map +1 -1
  101. package/dist/tui/index.js +8 -8
  102. package/dist/tui/index.js.map +1 -1
  103. package/dist/tui/services/AIService.d.ts +2 -2
  104. package/dist/tui/services/AIService.d.ts.map +1 -1
  105. package/dist/tui/services/AIService.js +165 -125
  106. package/dist/tui/services/AIService.js.map +1 -1
  107. package/dist/tui/services/ExtensionService.d.ts +1 -1
  108. package/dist/tui/services/ExtensionService.d.ts.map +1 -1
  109. package/dist/tui/services/ExtensionService.js +33 -26
  110. package/dist/tui/services/ExtensionService.js.map +1 -1
  111. package/dist/tui/services/GeminiService.d.ts +1 -1
  112. package/dist/tui/services/GeminiService.d.ts.map +1 -1
  113. package/dist/tui/services/GeminiService.js +87 -76
  114. package/dist/tui/services/GeminiService.js.map +1 -1
  115. package/dist/tui/services/ServiceManager.d.ts +3 -3
  116. package/dist/tui/services/ServiceManager.d.ts.map +1 -1
  117. package/dist/tui/services/ServiceManager.js +72 -58
  118. package/dist/tui/services/ServiceManager.js.map +1 -1
  119. package/dist/tui/services/SocketService.d.ts.map +1 -1
  120. package/dist/tui/services/SocketService.js +32 -32
  121. package/dist/tui/services/SocketService.js.map +1 -1
  122. package/dist/tui/services/ViteService.d.ts.map +1 -1
  123. package/dist/tui/services/ViteService.js +26 -28
  124. package/dist/tui/services/ViteService.js.map +1 -1
  125. package/dist/tui/services/index.d.ts +6 -6
  126. package/dist/tui/services/index.d.ts.map +1 -1
  127. package/dist/tui/services/index.js +6 -6
  128. package/dist/tui/services/index.js.map +1 -1
  129. package/mcp/gxp-api-server.js +83 -81
  130. package/package.json +109 -93
  131. package/runtime/PortalContainer.vue +258 -234
  132. package/runtime/dev-tools/DevToolsModal.vue +153 -155
  133. package/runtime/dev-tools/LayoutSwitcher.vue +144 -140
  134. package/runtime/dev-tools/MockDataEditor.vue +456 -433
  135. package/runtime/dev-tools/SocketSimulator.vue +379 -371
  136. package/runtime/dev-tools/StoreInspector.vue +517 -455
  137. package/runtime/dev-tools/index.js +5 -5
  138. package/runtime/fallback-layouts/PrivateLayout.vue +2 -2
  139. package/runtime/fallback-layouts/PublicLayout.vue +2 -2
  140. package/runtime/fallback-layouts/SystemLayout.vue +2 -2
  141. package/runtime/gxpStringsPlugin.js +159 -134
  142. package/runtime/index.html +17 -19
  143. package/runtime/main.js +24 -22
  144. package/runtime/mock-api/auth-middleware.js +15 -15
  145. package/runtime/mock-api/image-generator.js +46 -46
  146. package/runtime/mock-api/index.js +55 -55
  147. package/runtime/mock-api/response-generator.js +116 -105
  148. package/runtime/mock-api/route-generator.js +107 -84
  149. package/runtime/mock-api/socket-triggers.js +94 -93
  150. package/runtime/mock-api/spec-loader.js +79 -80
  151. package/runtime/package.json +3 -0
  152. package/runtime/server.js +68 -68
  153. package/runtime/stores/gxpPortalConfigStore.js +204 -186
  154. package/runtime/stores/index.js +2 -2
  155. package/runtime/vite-inspector-plugin.js +858 -707
  156. package/runtime/vite-source-tracker-plugin.js +132 -113
  157. package/runtime/vite.config.js +191 -139
  158. package/scripts/launch-chrome.js +41 -41
  159. package/scripts/pack-chrome.js +38 -39
  160. package/socket-events/AiSessionMessageCreated.json +17 -17
  161. package/socket-events/SocialStreamPostCreated.json +23 -23
  162. package/socket-events/SocialStreamPostVariantCompleted.json +22 -22
  163. package/template/.claude/agents/gxp-developer.md +100 -99
  164. package/template/.claude/settings.json +7 -7
  165. package/template/AGENTS.md +30 -23
  166. package/template/GEMINI.md +20 -20
  167. package/template/README.md +70 -53
  168. package/template/app-manifest.json +2 -4
  169. package/template/configuration.json +10 -10
  170. package/template/default-styling.css +1 -1
  171. package/template/index.html +18 -20
  172. package/template/main.js +24 -22
  173. package/template/src/DemoPage.vue +415 -362
  174. package/template/src/Plugin.vue +76 -85
  175. package/template/src/stores/index.js +3 -3
  176. package/template/src/stores/test-data.json +164 -172
  177. package/template/theme-layouts/AdditionalStyling.css +50 -50
  178. package/template/theme-layouts/PrivateLayout.vue +8 -12
  179. package/template/theme-layouts/PublicLayout.vue +8 -12
  180. package/template/theme-layouts/SystemLayout.vue +8 -12
  181. package/template/vite.extend.js +45 -0
  182. package/template/vite.config.js +0 -409
@@ -13,299 +13,306 @@
13
13
 
14
14
  // Check if we're in the page context (already injected) vs content script context
15
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;
16
+ const isContentScriptContext =
17
+ (typeof browser !== "undefined" && browser.runtime) ||
18
+ (typeof chrome !== "undefined" && chrome.runtime)
19
+
20
+ if (
21
+ isContentScriptContext &&
22
+ typeof window.__gxpInspectorInjected === "undefined"
23
+ ) {
24
+ // We're in the content script context - inject into page
25
+ const runtime = typeof browser !== "undefined" ? browser : chrome
26
+ const script = document.createElement("script")
27
+ script.src = runtime.runtime.getURL("inspector.js")
28
+ script.onload = function () {
29
+ this.remove()
30
+ }
31
+ ;(document.head || document.documentElement).appendChild(script)
32
+
33
+ // Also set up message relay from page to extension
34
+ window.addEventListener("message", (event) => {
35
+ if (event.source !== window) return
36
+ if (event.data?.type === "GXP_INSPECTOR_MESSAGE") {
37
+ runtime.runtime.sendMessage(event.data.payload)
38
+ }
39
+ })
40
+
41
+ // Mark that content script has run (for content script context)
42
+ window.__gxpInspectorContentScriptLoaded = true
39
43
  }
40
44
 
41
45
  // 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 = `
46
+ window.__gxpInspectorInjected = true
47
+ ;(function () {
48
+ "use strict"
49
+
50
+ // If gxpInspector already exists, we're done (prevent double init)
51
+ if (window.gxpInspector) {
52
+ return
53
+ }
54
+
55
+ // Configuration
56
+ const DEV_SERVER_URL = "https://localhost:3060"
57
+ const API_PREFIX = "/__gxp-inspector"
58
+
59
+ // State
60
+ let inspectorEnabled = false
61
+ let highlightOverlay = null
62
+ let inspectorPanel = null
63
+ let selectedElement = null
64
+ let hoveredElement = null
65
+ let selectionHighlight = null // Persistent highlight for selected element
66
+
67
+ // ============================================================
68
+ // API Communication
69
+ // ============================================================
70
+
71
+ async function apiCall(endpoint, options = {}) {
72
+ const url = `${DEV_SERVER_URL}${API_PREFIX}${endpoint}`
73
+ try {
74
+ const response = await fetch(url, {
75
+ ...options,
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ ...options.headers,
79
+ },
80
+ })
81
+ return await response.json()
82
+ } catch (error) {
83
+ console.error("[GxP Inspector] API Error:", error)
84
+ return { success: false, error: error.message }
85
+ }
86
+ }
87
+
88
+ async function ping() {
89
+ return apiCall("/ping")
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 getStrings() {
100
+ return apiCall("/strings")
101
+ }
102
+
103
+ // ============================================================
104
+ // Vue Component Detection
105
+ // ============================================================
106
+
107
+ function getVueInstance(el) {
108
+ // Vue 3 detection
109
+ if (el.__vueParentComponent) {
110
+ return el.__vueParentComponent
111
+ }
112
+ // Walk up to find Vue component
113
+ let current = el
114
+ while (current) {
115
+ if (current.__vueParentComponent) {
116
+ return current.__vueParentComponent
117
+ }
118
+ current = current.parentElement
119
+ }
120
+ return null
121
+ }
122
+
123
+ function getComponentInfo(vueInstance) {
124
+ if (!vueInstance) return null
125
+
126
+ const type = vueInstance.type
127
+ const name =
128
+ type?.name ||
129
+ type?.__name ||
130
+ type?.__file?.split("/").pop()?.replace(".vue", "") ||
131
+ "Anonymous"
132
+ const file = type?.__file || null
133
+
134
+ // Helper to safely serialize a value (handles circular refs, functions, etc.)
135
+ function safeSerialize(value) {
136
+ if (value === null || value === undefined) return value
137
+ if (typeof value === "function") return "[Function]"
138
+ if (typeof value !== "object") return value
139
+
140
+ try {
141
+ // Try JSON stringify/parse to get a clean copy
142
+ return JSON.parse(JSON.stringify(value))
143
+ } catch {
144
+ // If that fails, return a string representation
145
+ if (Array.isArray(value)) return `[Array(${value.length})]`
146
+ return "{...}"
147
+ }
148
+ }
149
+
150
+ // Get props
151
+ const props = {}
152
+ if (vueInstance.props) {
153
+ Object.keys(vueInstance.props).forEach((key) => {
154
+ props[key] = safeSerialize(vueInstance.props[key])
155
+ })
156
+ }
157
+
158
+ // Get component data/state
159
+ const data = {}
160
+ if (vueInstance.setupState) {
161
+ Object.keys(vueInstance.setupState).forEach((key) => {
162
+ const value = vueInstance.setupState[key]
163
+ if (typeof value !== "function") {
164
+ data[key] = safeSerialize(value)
165
+ }
166
+ })
167
+ }
168
+
169
+ return { name, file, props, data }
170
+ }
171
+
172
+ function getTextContent(el) {
173
+ // Get direct text content, excluding child elements
174
+ const texts = []
175
+ el.childNodes.forEach((node) => {
176
+ if (node.nodeType === Node.TEXT_NODE) {
177
+ const text = node.textContent.trim()
178
+ if (text) texts.push(text)
179
+ }
180
+ })
181
+ return texts
182
+ }
183
+
184
+ /**
185
+ * Check if an element has a gxp-string attribute (indicating it's already extracted)
186
+ */
187
+ function getGxpStringKey(el) {
188
+ return el?.getAttribute?.("gxp-string") || null
189
+ }
190
+
191
+ /**
192
+ * Check for a data-gxp-expr attribute on the element
193
+ * The vite-source-tracker-plugin adds this attribute in dev mode
194
+ * @param {Element} el - The element to check
195
+ * @returns {string|null} - The source expression or null
196
+ */
197
+ function getGxpSourceExpression(el) {
198
+ if (!el || !el.getAttribute) return null
199
+ return el.getAttribute("data-gxp-expr") || null
200
+ }
201
+
202
+ /**
203
+ * Get text content with gxp-string attribute info
204
+ * Returns array of objects: { text, gxpStringKey, isExtracted, sourceExpression, isDynamic }
205
+ */
206
+ function getTextContentWithAttributes(el) {
207
+ const results = []
208
+
209
+ // Check if this element itself has gxp-string
210
+ const elementKey = getGxpStringKey(el)
211
+ // Check for data-gxp-source attribute (injected by vite plugin for {{ expressions }})
212
+ const sourceExpression = getGxpSourceExpression(el)
213
+
214
+ el.childNodes.forEach((node) => {
215
+ if (node.nodeType === Node.TEXT_NODE) {
216
+ const text = node.textContent.trim()
217
+ if (text) {
218
+ results.push({
219
+ text: text,
220
+ gxpStringKey: elementKey,
221
+ isExtracted: elementKey !== null,
222
+ sourceExpression: sourceExpression,
223
+ isDynamic: sourceExpression !== null,
224
+ })
225
+ }
226
+ }
227
+ })
228
+
229
+ return results
230
+ }
231
+
232
+ /**
233
+ * Find all child elements with gxp-string attributes
234
+ */
235
+ function findChildGxpStrings(el) {
236
+ const strings = []
237
+ const elements = el.querySelectorAll("[gxp-string]")
238
+
239
+ elements.forEach((child) => {
240
+ const key = child.getAttribute("gxp-string")
241
+ const text = child.textContent.trim()
242
+ if (key && text) {
243
+ strings.push({
244
+ key: key,
245
+ text: text,
246
+ element: child.tagName.toLowerCase(),
247
+ })
248
+ }
249
+ })
250
+
251
+ return strings
252
+ }
253
+
254
+ /**
255
+ * Analyze text content to detect if it comes from getString() calls
256
+ * Returns an array of text info objects with type: 'raw' or 'getString'
257
+ */
258
+ function analyzeTextContent(el, vueInstance) {
259
+ const textInfos = []
260
+ const texts = getTextContent(el)
261
+
262
+ // Get the component's source info to help detect getString usage
263
+ const componentFile = vueInstance?.type?.__file || null
264
+
265
+ // Try to detect getString calls by checking if this element's text
266
+ // is likely from a getString call. We look for patterns in the rendered output
267
+ // and can cross-reference with the app-manifest.json via API later.
268
+
269
+ texts.forEach((text, index) => {
270
+ // Default to raw text
271
+ const textInfo = {
272
+ text: text,
273
+ type: "raw",
274
+ index: index,
275
+ }
276
+
277
+ // We'll mark it as potentially from getString if we can detect it
278
+ // The panel will verify against the manifest
279
+ textInfos.push(textInfo)
280
+ })
281
+
282
+ return textInfos
283
+ }
284
+
285
+ /**
286
+ * Try to find getString calls in the Vue component by inspecting
287
+ * the component's template bindings (if accessible)
288
+ */
289
+ function findGetStringCalls(vueInstance, filePath) {
290
+ const getStringCalls = []
291
+
292
+ if (!vueInstance) return getStringCalls
293
+
294
+ // Check setupState for gxpStore reference
295
+ const hasGxpStore = vueInstance.setupState?.gxpStore !== undefined
296
+
297
+ // We can't directly see the template, but we can check for gxpStore usage
298
+ // The actual verification happens on the server side by checking the source file
299
+
300
+ return { hasGxpStore, getStringCalls }
301
+ }
302
+
303
+ // ============================================================
304
+ // Overlay UI
305
+ // ============================================================
306
+
307
+ function createHighlightOverlay() {
308
+ if (highlightOverlay) return highlightOverlay
309
+
310
+ highlightOverlay = document.createElement("div")
311
+ highlightOverlay.id = "gxp-inspector-highlight"
312
+
313
+ const style = document.createElement("style")
314
+ style.id = "gxp-highlight-style"
315
+ style.textContent = `
309
316
  /* Pointer cursor when in selection mode */
310
317
  body.gxp-inspector-selecting,
311
318
  body.gxp-inspector-selecting * {
@@ -334,53 +341,53 @@ window.__gxpInspectorInjected = true;
334
341
  border-radius: 3px 3px 0 0;
335
342
  white-space: nowrap;
336
343
  }
337
- `;
344
+ `
338
345
 
339
- highlightOverlay.innerHTML = `<div class="gxp-highlight-label"></div>`;
346
+ highlightOverlay.innerHTML = `<div class="gxp-highlight-label"></div>`
340
347
 
341
- if (!document.getElementById('gxp-highlight-style')) {
342
- document.head.appendChild(style);
343
- }
344
- document.body.appendChild(highlightOverlay);
345
- return highlightOverlay;
346
- }
348
+ if (!document.getElementById("gxp-highlight-style")) {
349
+ document.head.appendChild(style)
350
+ }
351
+ document.body.appendChild(highlightOverlay)
352
+ return highlightOverlay
353
+ }
347
354
 
348
- function updateHighlight(el) {
349
- if (!el || !highlightOverlay) return;
355
+ function updateHighlight(el) {
356
+ if (!el || !highlightOverlay) return
350
357
 
351
- const rect = el.getBoundingClientRect();
352
- const label = highlightOverlay.querySelector('.gxp-highlight-label');
358
+ const rect = el.getBoundingClientRect()
359
+ const label = highlightOverlay.querySelector(".gxp-highlight-label")
353
360
 
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`;
361
+ // Position the highlight overlay directly
362
+ highlightOverlay.style.display = "block"
363
+ highlightOverlay.style.left = `${rect.left}px`
364
+ highlightOverlay.style.top = `${rect.top}px`
365
+ highlightOverlay.style.width = `${rect.width}px`
366
+ highlightOverlay.style.height = `${rect.height}px`
360
367
 
361
- // Build label: Component::element::gxp-string-key
362
- label.textContent = buildElementLabel(el);
363
- }
368
+ // Build label: Component::element::gxp-string-key
369
+ label.textContent = buildElementLabel(el)
370
+ }
364
371
 
365
- function hideHighlight() {
366
- if (highlightOverlay) {
367
- highlightOverlay.style.display = 'none';
368
- }
369
- }
372
+ function hideHighlight() {
373
+ if (highlightOverlay) {
374
+ highlightOverlay.style.display = "none"
375
+ }
376
+ }
370
377
 
371
- // ============================================================
372
- // Selection Highlight (persistent border on selected element)
373
- // ============================================================
378
+ // ============================================================
379
+ // Selection Highlight (persistent border on selected element)
380
+ // ============================================================
374
381
 
375
- function createSelectionHighlight() {
376
- if (selectionHighlight) return selectionHighlight;
382
+ function createSelectionHighlight() {
383
+ if (selectionHighlight) return selectionHighlight
377
384
 
378
- selectionHighlight = document.createElement('div');
379
- selectionHighlight.id = 'gxp-inspector-selection';
385
+ selectionHighlight = document.createElement("div")
386
+ selectionHighlight.id = "gxp-inspector-selection"
380
387
 
381
- const style = document.createElement('style');
382
- style.id = 'gxp-selection-style';
383
- style.textContent = `
388
+ const style = document.createElement("style")
389
+ style.id = "gxp-selection-style"
390
+ style.textContent = `
384
391
  #gxp-inspector-selection {
385
392
  position: fixed;
386
393
  pointer-events: none;
@@ -423,61 +430,65 @@ window.__gxpInspectorInjected = true;
423
430
  inset 0 0 30px rgba(97, 218, 251, 0.15);
424
431
  }
425
432
  }
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 = `
433
+ `
434
+
435
+ selectionHighlight.innerHTML = `<div class="gxp-selection-label"></div>`
436
+
437
+ if (!document.getElementById("gxp-selection-style")) {
438
+ document.head.appendChild(style)
439
+ }
440
+ document.body.appendChild(selectionHighlight)
441
+ return selectionHighlight
442
+ }
443
+
444
+ function updateSelectionHighlight(el) {
445
+ if (!el) {
446
+ hideSelectionHighlight()
447
+ return
448
+ }
449
+
450
+ createSelectionHighlight()
451
+ const rect = el.getBoundingClientRect()
452
+ const label = selectionHighlight.querySelector(".gxp-selection-label")
453
+
454
+ // Position the main overlay element directly
455
+ selectionHighlight.style.display = "block"
456
+ selectionHighlight.style.left = `${rect.left}px`
457
+ selectionHighlight.style.top = `${rect.top}px`
458
+ selectionHighlight.style.width = `${rect.width}px`
459
+ selectionHighlight.style.height = `${rect.height}px`
460
+
461
+ // Build label: Component::element::gxp-string-key
462
+ label.textContent = buildElementLabel(el)
463
+ }
464
+
465
+ function hideSelectionHighlight() {
466
+ if (selectionHighlight) {
467
+ selectionHighlight.style.display = "none"
468
+ }
469
+ }
470
+
471
+ // Update selection highlight position on scroll/resize
472
+ function updateSelectionPosition() {
473
+ if (
474
+ selectedElement &&
475
+ selectionHighlight &&
476
+ selectionHighlight.style.display !== "none"
477
+ ) {
478
+ updateSelectionHighlight(selectedElement)
479
+ }
480
+ }
481
+
482
+ // ============================================================
483
+ // Inspector Panel
484
+ // ============================================================
485
+
486
+ function createInspectorPanel() {
487
+ if (inspectorPanel) return inspectorPanel
488
+
489
+ inspectorPanel = document.createElement("div")
490
+ inspectorPanel.id = "gxp-inspector-panel"
491
+ inspectorPanel.innerHTML = `
481
492
  <div class="gxp-panel-header">
482
493
  <span class="gxp-panel-title">GxP Component Inspector</span>
483
494
  <button class="gxp-panel-close">&times;</button>
@@ -516,10 +527,10 @@ window.__gxpInspectorInjected = true;
516
527
  <pre class="gxp-props-content"></pre>
517
528
  </div>
518
529
  </div>
519
- `;
530
+ `
520
531
 
521
- const style = document.createElement('style');
522
- style.textContent = `
532
+ const style = document.createElement("style")
533
+ style.textContent = `
523
534
  #gxp-inspector-panel {
524
535
  position: fixed;
525
536
  bottom: 20px;
@@ -698,390 +709,414 @@ window.__gxpInspectorInjected = true;
698
709
  max-height: 150px;
699
710
  margin: 0;
700
711
  }
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 => `
712
+ `
713
+
714
+ document.head.appendChild(style)
715
+ document.body.appendChild(inspectorPanel)
716
+
717
+ // Event handlers
718
+ inspectorPanel
719
+ .querySelector(".gxp-panel-close")
720
+ .addEventListener("click", () => {
721
+ disableInspector()
722
+ })
723
+
724
+ inspectorPanel
725
+ .querySelector(".gxp-extract-button")
726
+ .addEventListener("click", handleExtract)
727
+
728
+ return inspectorPanel
729
+ }
730
+
731
+ function updatePanel(el) {
732
+ if (!inspectorPanel || !el) return
733
+
734
+ const vueInstance = getVueInstance(el)
735
+ const info = getComponentInfo(vueInstance)
736
+ const texts = getTextContent(el)
737
+
738
+ // Update component info
739
+ const nameEl = inspectorPanel.querySelector(".gxp-component-name")
740
+ const fileEl = inspectorPanel.querySelector(".gxp-component-file")
741
+
742
+ if (info) {
743
+ nameEl.textContent = `<${info.name}>`
744
+ fileEl.textContent = info.file || "Unknown file"
745
+ } else {
746
+ nameEl.textContent = `<${el.tagName.toLowerCase()}>`
747
+ fileEl.textContent = "Not a Vue component"
748
+ }
749
+
750
+ // Update strings list
751
+ const stringsSection = inspectorPanel.querySelector(".gxp-strings-section")
752
+ const stringsList = inspectorPanel.querySelector(".gxp-strings-list")
753
+
754
+ if (texts.length > 0) {
755
+ stringsSection.style.display = "block"
756
+ stringsList.innerHTML = texts
757
+ .map(
758
+ (text) => `
742
759
  <div class="gxp-string-item" data-text="${escapeHtml(text)}">
743
760
  <span class="gxp-string-text">${escapeHtml(text)}</span>
744
761
  <button class="gxp-string-extract">Extract</button>
745
762
  </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
- })();
763
+ `,
764
+ )
765
+ .join("")
766
+
767
+ // Add click handlers
768
+ stringsList.querySelectorAll(".gxp-string-item").forEach((item) => {
769
+ item
770
+ .querySelector(".gxp-string-extract")
771
+ .addEventListener("click", (e) => {
772
+ e.stopPropagation()
773
+ showExtractForm(item.dataset.text, info?.file)
774
+ })
775
+ })
776
+ } else {
777
+ stringsSection.style.display = "none"
778
+ }
779
+
780
+ // Update props section
781
+ const propsSection = inspectorPanel.querySelector(".gxp-props-section")
782
+ const propsContent = inspectorPanel.querySelector(".gxp-props-content")
783
+
784
+ if (info && Object.keys(info.props).length > 0) {
785
+ propsSection.style.display = "block"
786
+ propsContent.textContent = JSON.stringify(info.props, null, 2)
787
+ } else {
788
+ propsSection.style.display = "none"
789
+ }
790
+ }
791
+
792
+ function showExtractForm(text, filePath) {
793
+ const extractSection = inspectorPanel.querySelector(".gxp-extract-section")
794
+ const textInput = inspectorPanel.querySelector(".gxp-extract-text")
795
+ const keyInput = inspectorPanel.querySelector(".gxp-extract-key")
796
+ const fileInput = inspectorPanel.querySelector(".gxp-extract-file")
797
+ const statusEl = inspectorPanel.querySelector(".gxp-extract-status")
798
+
799
+ extractSection.style.display = "block"
800
+ textInput.value = text
801
+ keyInput.value = textToKey(text)
802
+ fileInput.value = filePath || ""
803
+ statusEl.style.display = "none"
804
+ statusEl.className = "gxp-extract-status"
805
+ }
806
+
807
+ async function handleExtract() {
808
+ const textInput = inspectorPanel.querySelector(".gxp-extract-text")
809
+ const keyInput = inspectorPanel.querySelector(".gxp-extract-key")
810
+ const fileInput = inspectorPanel.querySelector(".gxp-extract-file")
811
+ const button = inspectorPanel.querySelector(".gxp-extract-button")
812
+ const statusEl = inspectorPanel.querySelector(".gxp-extract-status")
813
+
814
+ const text = textInput.value
815
+ const key = keyInput.value
816
+ const filePath = fileInput.value
817
+
818
+ if (!text || !key || !filePath) {
819
+ statusEl.textContent = "All fields are required"
820
+ statusEl.className = "gxp-extract-status error"
821
+ return
822
+ }
823
+
824
+ button.disabled = true
825
+ button.textContent = "Extracting..."
826
+
827
+ try {
828
+ const result = await extractString({
829
+ text,
830
+ key,
831
+ filePath,
832
+ })
833
+
834
+ if (result.success) {
835
+ statusEl.textContent = `Success! Added getString('${key}') to ${filePath}`
836
+ statusEl.className = "gxp-extract-status success"
837
+ } else {
838
+ statusEl.textContent = result.error || "Extraction failed"
839
+ statusEl.className = "gxp-extract-status error"
840
+ }
841
+ } catch (error) {
842
+ statusEl.textContent = error.message
843
+ statusEl.className = "gxp-extract-status error"
844
+ } finally {
845
+ button.disabled = false
846
+ button.textContent = "Extract to getString()"
847
+ }
848
+ }
849
+
850
+ // ============================================================
851
+ // Event Handlers
852
+ // ============================================================
853
+
854
+ function handleMouseMove(e) {
855
+ if (!inspectorEnabled) return
856
+
857
+ const el = e.target
858
+ if (
859
+ el === highlightOverlay ||
860
+ highlightOverlay?.contains(el) ||
861
+ el === inspectorPanel ||
862
+ inspectorPanel?.contains(el)
863
+ ) {
864
+ return
865
+ }
866
+
867
+ if (el !== hoveredElement) {
868
+ hoveredElement = el
869
+ updateHighlight(el)
870
+ }
871
+ }
872
+
873
+ function handleClick(e) {
874
+ if (!inspectorEnabled) return
875
+
876
+ const el = e.target
877
+ if (
878
+ el === highlightOverlay ||
879
+ highlightOverlay?.contains(el) ||
880
+ el === inspectorPanel ||
881
+ inspectorPanel?.contains(el) ||
882
+ el === selectionHighlight ||
883
+ selectionHighlight?.contains(el)
884
+ ) {
885
+ return
886
+ }
887
+
888
+ e.preventDefault()
889
+ e.stopPropagation()
890
+
891
+ selectedElement = el
892
+ // Store for DevTools panel access via eval
893
+ window.__gxpSelectedElement = el
894
+
895
+ updatePanel(el)
896
+
897
+ // Show persistent selection highlight
898
+ updateSelectionHighlight(el)
899
+
900
+ // Hide hover highlight
901
+ hideHighlight()
902
+
903
+ // Disable inspector (selection mode) after selecting
904
+ // This prevents accidental re-selection on next click
905
+ disableInspector()
906
+
907
+ // Send selection to background for DevTools panel
908
+ sendElementToDevTools(el)
909
+ }
910
+
911
+ function sendElementToDevTools(el) {
912
+ const vueInstance = getVueInstance(el)
913
+ const info = getComponentInfo(vueInstance)
914
+ const texts = getTextContent(el)
915
+ const textsWithAttrs = getTextContentWithAttributes(el)
916
+ const childGxpStrings = findChildGxpStrings(el)
917
+
918
+ // Check if the element itself has gxp-string
919
+ const gxpStringKey = getGxpStringKey(el)
920
+ // Check for data-gxp-source attribute (injected by vite plugin)
921
+ const sourceExpression = getGxpSourceExpression(el)
922
+
923
+ const data = {
924
+ tagName: el.tagName.toLowerCase(),
925
+ component: info,
926
+ texts: texts,
927
+ textsWithAttributes: textsWithAttrs,
928
+ childGxpStrings: childGxpStrings,
929
+ gxpStringKey: gxpStringKey,
930
+ isExtracted: gxpStringKey !== null,
931
+ sourceExpression: sourceExpression,
932
+ isDynamic: sourceExpression !== null,
933
+ }
934
+
935
+ // Send to content script via postMessage (since we're in page context)
936
+ // The content script will relay to the background script
937
+ window.postMessage(
938
+ {
939
+ type: "GXP_INSPECTOR_MESSAGE",
940
+ payload: {
941
+ type: "elementSelected",
942
+ data: data,
943
+ },
944
+ },
945
+ "*",
946
+ )
947
+ }
948
+
949
+ function handleKeyDown(e) {
950
+ // Escape to disable inspector
951
+ if (e.key === "Escape" && inspectorEnabled) {
952
+ disableInspector()
953
+ }
954
+
955
+ // Ctrl+Shift+I to toggle inspector
956
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "I") {
957
+ e.preventDefault()
958
+ toggleInspector()
959
+ }
960
+ }
961
+
962
+ // ============================================================
963
+ // Public API
964
+ // ============================================================
965
+
966
+ function enableInspector() {
967
+ if (inspectorEnabled) return
968
+
969
+ inspectorEnabled = true
970
+ createHighlightOverlay()
971
+ createInspectorPanel()
972
+
973
+ // Add selecting class for pointer cursor
974
+ document.body.classList.add("gxp-inspector-selecting")
975
+
976
+ // Hide previous selection highlight when entering selection mode
977
+ hideSelectionHighlight()
978
+ selectedElement = null
979
+ window.__gxpSelectedElement = null
980
+
981
+ document.addEventListener("mousemove", handleMouseMove, true)
982
+ document.addEventListener("click", handleClick, true)
983
+ window.addEventListener("scroll", updateSelectionPosition, true)
984
+ window.addEventListener("resize", updateSelectionPosition)
985
+
986
+ console.log("[GxP Inspector] Enabled - Hover over elements to inspect")
987
+ }
988
+
989
+ function disableInspector() {
990
+ if (!inspectorEnabled) return
991
+
992
+ inspectorEnabled = false
993
+ hideHighlight()
994
+
995
+ // Remove selecting class for pointer cursor
996
+ document.body.classList.remove("gxp-inspector-selecting")
997
+
998
+ if (inspectorPanel) {
999
+ inspectorPanel.remove()
1000
+ inspectorPanel = null
1001
+ }
1002
+
1003
+ document.removeEventListener("mousemove", handleMouseMove, true)
1004
+ document.removeEventListener("click", handleClick, true)
1005
+ // Keep scroll/resize listeners for selection highlight position updates
1006
+ // They will be removed when a new selection starts
1007
+
1008
+ // Don't clear selectedElement here - we want to keep the selection
1009
+ hoveredElement = null
1010
+
1011
+ console.log("[GxP Inspector] Disabled - Selection preserved")
1012
+ }
1013
+
1014
+ // Clear selection completely (called when user wants to deselect)
1015
+ function clearSelection() {
1016
+ hideSelectionHighlight()
1017
+ selectedElement = null
1018
+ window.__gxpSelectedElement = null
1019
+ window.removeEventListener("scroll", updateSelectionPosition, true)
1020
+ window.removeEventListener("resize", updateSelectionPosition)
1021
+ }
1022
+
1023
+ function toggleInspector() {
1024
+ if (inspectorEnabled) {
1025
+ disableInspector()
1026
+ } else {
1027
+ enableInspector()
1028
+ }
1029
+ }
1030
+
1031
+ // ============================================================
1032
+ // Utility Functions
1033
+ // ============================================================
1034
+
1035
+ /**
1036
+ * Build a descriptive label for an element
1037
+ * Format: ComponentName::element::gxp-string-key
1038
+ * Examples:
1039
+ * - DemoPage::h1::welcome_title
1040
+ * - DemoPage::h1 (no gxp-string)
1041
+ * - div::gxp-string-key (no Vue component)
1042
+ * - div (plain element)
1043
+ */
1044
+ function buildElementLabel(el) {
1045
+ const parts = []
1046
+
1047
+ // Get Vue component name
1048
+ const vueInstance = getVueInstance(el)
1049
+ const info = getComponentInfo(vueInstance)
1050
+ if (info && info.name) {
1051
+ parts.push(info.name)
1052
+ }
1053
+
1054
+ // Add element tag name
1055
+ parts.push(el.tagName.toLowerCase())
1056
+
1057
+ // Check for gxp-string attribute
1058
+ const gxpStringKey = el.getAttribute("gxp-string")
1059
+ if (gxpStringKey) {
1060
+ parts.push(gxpStringKey)
1061
+ }
1062
+
1063
+ // Check for gxp-src attribute (for images/assets)
1064
+ const gxpSrcKey = el.getAttribute("gxp-src")
1065
+ if (gxpSrcKey && !gxpStringKey) {
1066
+ parts.push(gxpSrcKey)
1067
+ }
1068
+
1069
+ return parts.join("::")
1070
+ }
1071
+
1072
+ function textToKey(text) {
1073
+ return text
1074
+ .toLowerCase()
1075
+ .replace(/[^a-z0-9\s]/g, "")
1076
+ .replace(/\s+/g, "_")
1077
+ .substring(0, 40)
1078
+ .replace(/_+$/, "")
1079
+ }
1080
+
1081
+ function escapeHtml(text) {
1082
+ const div = document.createElement("div")
1083
+ div.textContent = text
1084
+ return div.innerHTML
1085
+ }
1086
+
1087
+ // ============================================================
1088
+ // Initialize
1089
+ // ============================================================
1090
+
1091
+ // Add keyboard listener
1092
+ document.addEventListener("keydown", handleKeyDown)
1093
+
1094
+ // Expose API
1095
+ window.gxpInspector = {
1096
+ enable: enableInspector,
1097
+ disable: disableInspector,
1098
+ toggle: toggleInspector,
1099
+ isEnabled: () => inspectorEnabled,
1100
+ clearSelection: clearSelection,
1101
+ ping: ping,
1102
+ }
1103
+
1104
+ // Listen for messages from content script (which relays from popup/background)
1105
+ window.addEventListener("message", (event) => {
1106
+ if (event.source !== window) return
1107
+ if (event.data?.type === "GXP_INSPECTOR_ACTION") {
1108
+ const action = event.data.action
1109
+ if (action === "toggleInspector") {
1110
+ toggleInspector()
1111
+ } else if (action === "enable") {
1112
+ enableInspector()
1113
+ } else if (action === "disable") {
1114
+ disableInspector()
1115
+ }
1116
+ }
1117
+ })
1118
+
1119
+ console.log(
1120
+ "[GxP Inspector] Loaded in page context. Press Ctrl+Shift+I to toggle inspector.",
1121
+ )
1122
+ })()