@gxp-dev/tools 2.0.63 → 2.0.65

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