@gxp-dev/tools 2.0.6 → 2.0.8

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