sparkecode-devtools 0.1.40 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,16 +109,37 @@ You can also send copy events to any endpoint:
109
109
 
110
110
  ## Controls
111
111
 
112
- - Click the floating button to toggle selection
112
+ - Click the floating button to toggle selection mode
113
+ - Click the panel icon (right side of button) to open the SparkeCode web UI
113
114
  - Drag the button to reposition
114
- - Press `Cmd/Ctrl+Shift+S` to toggle
115
+ - Press `Cmd/Ctrl+Shift+S` to toggle selection mode
115
116
  - Press `Esc` to exit selection mode
116
117
 
118
+ ## Floating Web UI Panel
119
+
120
+ Click the panel icon on the button to open a draggable, collapsible panel showing the full SparkeCode web interface. This lets you see the agent's responses and chat history while working on your app.
121
+
122
+ - Drag the header to move the panel
123
+ - Click the minus button to collapse
124
+ - Click the X to close
125
+
126
+ Configure the web UI URL:
127
+
128
+ ```bash
129
+ # In your .env.local
130
+ NEXT_PUBLIC_SPARKECODER_WEB_UI_URL=http://localhost:6969
131
+ ```
132
+
133
+ Or via prop:
134
+ ```tsx
135
+ <SparkeCodeSelect webUiUrl="http://localhost:6969" />
136
+ ```
137
+
117
138
  ## Auto-send to Agent
118
139
 
119
140
  When connected to a running Sparkecoder instance, clicking a component will:
120
141
 
121
- 1. Show a prompt input modal
142
+ 1. Show a prompt input
122
143
  2. Let you type what you want to do with the component (e.g., "make this button red")
123
144
  3. Press Enter to send the request + component context directly to the agent
124
145
  4. The agent will start working immediately
package/next.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface SparkeCodeSelectConfig {
10
10
  enabled?: boolean;
11
11
  apiBase?: string | null;
12
12
  sessionId?: string | null;
13
+ webUiUrl?: string | null;
13
14
  };
14
15
  }
15
16
 
@@ -20,10 +21,16 @@ export interface SparkeCodeSelectProps {
20
21
  sparkecoderEnabled?: boolean;
21
22
  strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker';
22
23
  /**
23
- * Override the Sparkecoder server URL.
24
+ * Override the Sparkecoder server URL (API).
24
25
  * Defaults to NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL or SPARKECODER_DEV_TOOLS_SERVER_URL env variable.
25
26
  */
26
27
  serverUrl?: string;
28
+ /**
29
+ * Override the Sparkecoder Web UI URL.
30
+ * Defaults to NEXT_PUBLIC_SPARKECODER_WEB_UI_URL or SPARKECODER_WEB_UI_URL env variable.
31
+ * Falls back to http://localhost:6969 if not set.
32
+ */
33
+ webUiUrl?: string;
27
34
  }
28
35
 
29
36
  export function SparkeCodeSelect(props: SparkeCodeSelectProps): JSX.Element | null;
package/next.js CHANGED
@@ -5,12 +5,16 @@ import { useEffect, useMemo } from 'react';
5
5
 
6
6
  const DEFAULT_SCRIPT_URL = 'https://unpkg.com/sparkecode-devtools/sparkecode-select.js';
7
7
 
8
- // Check for env variable to override the default Sparkecoder server URL
9
- // This needs to be prefixed with NEXT_PUBLIC_ to be available in the browser
8
+ // Check for env variables to override defaults
9
+ // These need to be prefixed with NEXT_PUBLIC_ to be available in the browser
10
10
  const ENV_SERVER_URL = typeof process !== 'undefined'
11
11
  ? (process.env.NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL || process.env.SPARKECODER_DEV_TOOLS_SERVER_URL)
12
12
  : undefined;
13
13
 
14
+ const ENV_WEB_UI_URL = typeof process !== 'undefined'
15
+ ? (process.env.NEXT_PUBLIC_SPARKECODER_WEB_UI_URL || process.env.SPARKECODER_WEB_UI_URL)
16
+ : undefined;
17
+
14
18
  export function SparkeCodeSelect({
15
19
  enabled = process.env.NODE_ENV !== 'production',
16
20
  scriptUrl = DEFAULT_SCRIPT_URL,
@@ -18,16 +22,18 @@ export function SparkeCodeSelect({
18
22
  sparkecoderEnabled = true,
19
23
  strategy = 'afterInteractive',
20
24
  serverUrl = ENV_SERVER_URL,
25
+ webUiUrl = ENV_WEB_UI_URL,
21
26
  }) {
22
27
  const mergedConfig = useMemo(() => ({
23
28
  ...(config || {}),
24
29
  sparkecoder: {
25
30
  enabled: sparkecoderEnabled,
26
- // Use serverUrl prop, which defaults to env variable if set
31
+ // Use props which default to env variables if set
27
32
  ...(serverUrl ? { apiBase: serverUrl } : {}),
33
+ ...(webUiUrl ? { webUiUrl } : {}),
28
34
  ...(config && config.sparkecoder ? config.sparkecoder : {}),
29
35
  },
30
- }), [config, sparkecoderEnabled, serverUrl]);
36
+ }), [config, sparkecoderEnabled, serverUrl, webUiUrl]);
31
37
 
32
38
  useEffect(() => {
33
39
  if (!enabled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkecode-devtools",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "Sparkecode Dev Tools for selecting and sharing components.",
5
5
  "type": "module",
6
6
  "main": "sparkecode-select.js",
@@ -14,14 +14,18 @@
14
14
 
15
15
  const DEFAULT_CONFIG = {
16
16
  webhookUrl: null,
17
- primaryColor: '#8b5cf6',
18
- successColor: '#10b981',
19
- bgColor: '#1e1e2e',
20
- textColor: '#ffffff',
17
+ // Colors matched to SparkeCoder web dark theme
18
+ primaryColor: '#91AEFF', // Focus Blue 200
19
+ successColor: '#10B981', // Tests green
20
+ bgColor: '#171717', // Card background
21
+ borderColor: 'rgba(255, 255, 255, 0.1)',
22
+ textColor: '#FAFAFA', // Foreground
23
+ mutedColor: '#A3A3A3', // Muted foreground
21
24
  sparkecoder: {
22
25
  enabled: false,
23
26
  apiBase: null,
24
27
  sessionId: null,
28
+ webUiUrl: null, // Web UI URL (default: http://localhost:6969)
25
29
  },
26
30
  };
27
31
 
@@ -335,7 +339,7 @@
335
339
 
336
340
  async function runAgentWithPrompt(promptText) {
337
341
  const connection = await ensureSparkecoderConnection();
338
- if (!connection) return false;
342
+ if (!connection) return { success: false };
339
343
 
340
344
  try {
341
345
  // Post to agents run endpoint to actually run the agent
@@ -347,10 +351,13 @@
347
351
  body: JSON.stringify({ prompt: promptText }),
348
352
  }
349
353
  );
350
- return res.ok;
354
+ if (res.ok) {
355
+ return { success: true, sessionId: connection.sessionId };
356
+ }
357
+ return { success: false };
351
358
  } catch (e) {
352
359
  console.warn('[SparkeCode] Failed to run agent:', e.message);
353
- return false;
360
+ return { success: false };
354
361
  }
355
362
  }
356
363
 
@@ -458,79 +465,139 @@
458
465
  function createFloatingButton() {
459
466
  floatingButton = document.createElement('div');
460
467
  floatingButton.id = 'sparkecode-button';
461
- floatingButton.innerHTML = `<span style="font-weight: 600; font-size: 13px;">SparkeCode Select</span>`;
462
468
  floatingButton.style.cssText = `
463
469
  position: fixed;
464
470
  top: ${buttonPos.y}px;
465
471
  left: ${buttonPos.x}px;
466
- height: 40px;
467
- padding: 0 16px;
468
- background: ${config.primaryColor};
469
- border-radius: 10px;
472
+ height: 32px;
473
+ background: ${config.bgColor};
474
+ border: 1px solid ${config.borderColor};
475
+ border-radius: 6px;
470
476
  display: flex;
471
477
  align-items: center;
472
- justify-content: center;
473
- cursor: grab;
474
478
  z-index: 999998;
475
- box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
476
- transition: box-shadow 0.2s, background 0.2s;
477
- color: white;
479
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
480
+ transition: all 0.15s ease;
478
481
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
479
482
  user-select: none;
483
+ overflow: hidden;
484
+ `;
485
+
486
+ // Main button area (select toggle)
487
+ const mainBtn = document.createElement('div');
488
+ mainBtn.id = 'sparkecode-button-main';
489
+ mainBtn.innerHTML = `
490
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
491
+ <path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
492
+ </svg>
493
+ <span style="font-weight: 500; font-size: 12px;">SparkeCode Select</span>
494
+ `;
495
+ mainBtn.style.cssText = `
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 6px;
499
+ padding: 0 10px;
500
+ height: 100%;
501
+ cursor: grab;
502
+ color: ${config.primaryColor};
503
+ `;
504
+ mainBtn.title = 'Click to toggle select mode • Drag to move';
505
+
506
+ // Expand button (show web UI)
507
+ const expandBtn = document.createElement('div');
508
+ expandBtn.id = 'sparkecode-button-expand';
509
+ expandBtn.innerHTML = `
510
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
511
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
512
+ <path d="M9 3v18"/>
513
+ </svg>
514
+ `;
515
+ expandBtn.style.cssText = `
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ width: 32px;
520
+ height: 100%;
521
+ cursor: pointer;
522
+ color: ${config.mutedColor};
523
+ border-left: 1px solid ${config.borderColor};
524
+ transition: background 0.15s, color 0.15s;
480
525
  `;
481
- floatingButton.title = 'Drag to move • Click to toggle';
526
+ expandBtn.title = 'Open SparkeCode panel';
527
+ expandBtn.addEventListener('mouseenter', () => {
528
+ expandBtn.style.background = 'rgba(255,255,255,0.05)';
529
+ expandBtn.style.color = config.primaryColor;
530
+ });
531
+ expandBtn.addEventListener('mouseleave', () => {
532
+ expandBtn.style.background = 'transparent';
533
+ expandBtn.style.color = config.mutedColor;
534
+ });
535
+ expandBtn.addEventListener('click', (e) => {
536
+ e.stopPropagation();
537
+ if (isWebFrameVisible()) {
538
+ hideWebFrame();
539
+ } else {
540
+ showWebFrame();
541
+ }
542
+ });
543
+
544
+ floatingButton.appendChild(mainBtn);
545
+ floatingButton.appendChild(expandBtn);
482
546
 
483
- // Drag handling
484
- floatingButton.addEventListener('mousedown', startDrag);
485
- floatingButton.addEventListener('touchstart', startDrag, { passive: false });
547
+ // Drag handling - only on main button area
548
+ mainBtn.addEventListener('mousedown', startDrag);
549
+ mainBtn.addEventListener('touchstart', startDrag, { passive: false });
486
550
 
487
551
  document.body.appendChild(floatingButton);
488
552
  }
489
553
 
490
554
  function startDrag(e) {
491
- if (e.target.closest('#sparkecode-button')) {
492
- const clientX = e.touches ? e.touches[0].clientX : e.clientX;
493
- const clientY = e.touches ? e.touches[0].clientY : e.clientY;
555
+ // Only handle drag on main button, not expand
556
+ if (e.target.closest('#sparkecode-button-expand')) return;
557
+
558
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
559
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
560
+
561
+ dragOffset.x = clientX - buttonPos.x;
562
+ dragOffset.y = clientY - buttonPos.y;
563
+ isDragging = false;
564
+
565
+ const startX = clientX;
566
+ const startY = clientY;
567
+ const mainBtn = document.getElementById('sparkecode-button-main');
568
+
569
+ const onMove = (e) => {
570
+ const moveX = e.touches ? e.touches[0].clientX : e.clientX;
571
+ const moveY = e.touches ? e.touches[0].clientY : e.clientY;
494
572
 
495
- dragOffset.x = clientX - buttonPos.x;
496
- dragOffset.y = clientY - buttonPos.y;
497
- isDragging = false;
573
+ // Only start dragging if moved more than 5px
574
+ if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
575
+ isDragging = true;
576
+ if (mainBtn) mainBtn.style.cursor = 'grabbing';
577
+ }
498
578
 
499
- const startX = clientX;
500
- const startY = clientY;
579
+ if (isDragging) {
580
+ e.preventDefault();
581
+ buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
582
+ buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
583
+ floatingButton.style.left = buttonPos.x + 'px';
584
+ floatingButton.style.top = buttonPos.y + 'px';
585
+ }
586
+ };
587
+
588
+ const onUp = () => {
589
+ document.removeEventListener('mousemove', onMove);
590
+ document.removeEventListener('mouseup', onUp);
591
+ document.removeEventListener('touchmove', onMove);
592
+ document.removeEventListener('touchend', onUp);
501
593
 
502
- const onMove = (e) => {
503
- const moveX = e.touches ? e.touches[0].clientX : e.clientX;
504
- const moveY = e.touches ? e.touches[0].clientY : e.clientY;
505
-
506
- // Only start dragging if moved more than 5px
507
- if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
508
- isDragging = true;
509
- floatingButton.style.cursor = 'grabbing';
510
- }
511
-
512
- if (isDragging) {
513
- e.preventDefault();
514
- buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
515
- buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
516
- floatingButton.style.left = buttonPos.x + 'px';
517
- floatingButton.style.top = buttonPos.y + 'px';
518
- }
519
- };
594
+ if (mainBtn) mainBtn.style.cursor = 'grab';
520
595
 
521
- const onUp = () => {
522
- document.removeEventListener('mousemove', onMove);
523
- document.removeEventListener('mouseup', onUp);
524
- document.removeEventListener('touchmove', onMove);
525
- document.removeEventListener('touchend', onUp);
526
-
527
- floatingButton.style.cursor = 'grab';
528
-
529
- // If not dragging, it was a click
530
- if (!isDragging) {
531
- toggleActive();
532
- }
533
- isDragging = false;
596
+ // If not dragging, it was a click - toggle select mode
597
+ if (!isDragging) {
598
+ toggleActive();
599
+ }
600
+ isDragging = false;
534
601
  };
535
602
 
536
603
  document.addEventListener('mousemove', onMove);
@@ -685,17 +752,18 @@
685
752
  bottom: 20px;
686
753
  left: 50%;
687
754
  transform: translateX(-50%) translateY(10px);
688
- background: ${config.successColor};
689
- color: white;
690
- padding: 12px 24px;
691
- border-radius: 10px;
755
+ background: ${config.bgColor};
756
+ color: ${config.textColor};
757
+ padding: 10px 20px;
758
+ border-radius: 8px;
759
+ border: 1px solid ${config.borderColor};
692
760
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
693
- font-size: 14px;
694
- font-weight: 600;
761
+ font-size: 13px;
762
+ font-weight: 500;
695
763
  z-index: 1000002;
696
764
  opacity: 0;
697
- transition: all 0.2s ease;
698
- box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4);
765
+ transition: all 0.15s ease;
766
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
699
767
  `;
700
768
  toast.textContent = message;
701
769
  document.body.appendChild(toast);
@@ -746,16 +814,16 @@
746
814
  position: fixed;
747
815
  top: 20%;
748
816
  left: 50%;
749
- transform: translateX(-50%) scale(0.95);
817
+ transform: translateX(-50%) scale(0.98);
750
818
  background: ${config.bgColor};
751
819
  border-radius: 12px;
752
820
  z-index: 1000003;
753
- box-shadow: 0 16px 70px rgba(0,0,0,0.5);
754
- border: 1px solid rgba(255,255,255,0.1);
821
+ box-shadow: 0 24px 80px rgba(0,0,0,0.6);
822
+ border: 1px solid ${config.borderColor};
755
823
  display: none;
756
824
  opacity: 0;
757
825
  transition: all 0.15s ease;
758
- width: 500px;
826
+ width: 480px;
759
827
  max-width: calc(100vw - 32px);
760
828
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
761
829
  overflow: hidden;
@@ -765,15 +833,14 @@
765
833
  const tag = document.createElement('div');
766
834
  tag.id = 'sparkecode-prompt-tag';
767
835
  tag.style.cssText = `
768
- padding: 10px 16px;
769
- background: rgba(139, 92, 246, 0.1);
770
- border-bottom: 1px solid rgba(255,255,255,0.05);
836
+ padding: 12px 16px;
837
+ border-bottom: 1px solid ${config.borderColor};
771
838
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
772
- font-size: 13px;
773
- color: ${config.primaryColor};
839
+ font-size: 12px;
840
+ color: ${config.mutedColor};
774
841
  display: flex;
775
842
  align-items: center;
776
- gap: 8px;
843
+ gap: 6px;
777
844
  `;
778
845
  promptBar.appendChild(tag);
779
846
 
@@ -781,16 +848,18 @@
781
848
  const input = document.createElement('input');
782
849
  input.id = 'sparkecode-prompt-input';
783
850
  input.type = 'text';
784
- input.placeholder = 'What do you want to do? (Enter to send, Esc to cancel)';
851
+ input.placeholder = 'What do you want to do?';
785
852
  input.style.cssText = `
786
853
  width: 100%;
787
854
  background: transparent;
788
855
  border: none;
789
- padding: 16px;
856
+ padding: 14px 16px;
790
857
  color: ${config.textColor};
791
- font-size: 16px;
858
+ font-size: 15px;
792
859
  outline: none;
793
860
  `;
861
+ input.setAttribute('autocomplete', 'off');
862
+ input.setAttribute('spellcheck', 'false');
794
863
  input.addEventListener('keydown', (e) => {
795
864
  if (e.key === 'Enter') {
796
865
  e.preventDefault();
@@ -814,7 +883,7 @@
814
883
  // Update tag
815
884
  const tag = document.getElementById('sparkecode-prompt-tag');
816
885
  if (tag && componentInfo) {
817
- tag.innerHTML = `<span style="opacity: 0.6;">Selected:</span> &lt;${componentInfo.name}&gt;`;
886
+ tag.innerHTML = `<span style="color: ${config.primaryColor};">&lt;${componentInfo.name}&gt;</span>`;
818
887
  }
819
888
 
820
889
  const input = document.getElementById('sparkecode-prompt-input');
@@ -864,10 +933,15 @@
864
933
  hidePromptBar();
865
934
  showToast('Sending...');
866
935
 
867
- const success = await runAgentWithPrompt(fullPrompt);
936
+ const result = await runAgentWithPrompt(fullPrompt);
868
937
 
869
- if (success) {
938
+ if (result.success) {
870
939
  showToast('Sent ✓');
940
+ // Auto-show the web frame to see the agent working
941
+ if (result.sessionId) {
942
+ sparkecoderState.sessionId = result.sessionId;
943
+ showWebFrame();
944
+ }
871
945
  } else {
872
946
  await copyToClipboard(fullPrompt);
873
947
  showToast('Copied (no connection)');
@@ -877,10 +951,284 @@
877
951
  component: pendingComponentInfo?.name,
878
952
  fileName: pendingComponentInfo?.fileName,
879
953
  hasUserMessage: !!userMessage,
880
- success,
954
+ success: result.success,
881
955
  });
882
956
  }
883
957
 
958
+ // ============================================
959
+ // Floating Web UI Frame
960
+ // ============================================
961
+
962
+ const DEFAULT_WEB_UI_URL = 'http://localhost:6969';
963
+ let webFrame = null;
964
+ let webFrameCollapsed = false;
965
+ let webFramePos = { x: window.innerWidth - 420, y: 60 };
966
+ let webFrameSize = { width: 400, height: 600 };
967
+ let isWebFrameDragging = false;
968
+ let webFrameDragOffset = { x: 0, y: 0 };
969
+
970
+ function getWebUiUrl() {
971
+ return config.sparkecoder?.webUiUrl || DEFAULT_WEB_UI_URL;
972
+ }
973
+
974
+ function createWebFrame() {
975
+ if (webFrame) return;
976
+
977
+ webFrame = document.createElement('div');
978
+ webFrame.id = 'sparkecode-webframe';
979
+ webFrame.style.cssText = `
980
+ position: fixed;
981
+ top: ${webFramePos.y}px;
982
+ right: 16px;
983
+ width: ${webFrameSize.width}px;
984
+ height: ${webFrameSize.height}px;
985
+ background: ${config.bgColor};
986
+ border: 1px solid ${config.borderColor};
987
+ border-radius: 12px;
988
+ z-index: 999997;
989
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
990
+ display: flex;
991
+ flex-direction: column;
992
+ overflow: hidden;
993
+ opacity: 0;
994
+ transform: scale(0.95) translateY(10px);
995
+ transition: opacity 0.2s ease, transform 0.2s ease;
996
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
997
+ `;
998
+
999
+ // Header
1000
+ const header = document.createElement('div');
1001
+ header.id = 'sparkecode-webframe-header';
1002
+ header.style.cssText = `
1003
+ display: flex;
1004
+ align-items: center;
1005
+ justify-content: space-between;
1006
+ padding: 10px 12px;
1007
+ background: rgba(255,255,255,0.03);
1008
+ border-bottom: 1px solid ${config.borderColor};
1009
+ cursor: grab;
1010
+ user-select: none;
1011
+ `;
1012
+
1013
+ const title = document.createElement('div');
1014
+ title.style.cssText = `
1015
+ font-size: 12px;
1016
+ font-weight: 500;
1017
+ color: ${config.mutedColor};
1018
+ display: flex;
1019
+ align-items: center;
1020
+ gap: 6px;
1021
+ `;
1022
+ title.innerHTML = `
1023
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="${config.primaryColor}">
1024
+ <path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
1025
+ </svg>
1026
+ <span>SparkeCode</span>
1027
+ `;
1028
+
1029
+ const buttons = document.createElement('div');
1030
+ buttons.style.cssText = `display: flex; gap: 6px;`;
1031
+
1032
+ // Collapse button
1033
+ const collapseBtn = document.createElement('button');
1034
+ collapseBtn.id = 'sparkecode-webframe-collapse';
1035
+ collapseBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
1036
+ collapseBtn.style.cssText = `
1037
+ width: 24px;
1038
+ height: 24px;
1039
+ border: none;
1040
+ background: transparent;
1041
+ color: ${config.mutedColor};
1042
+ cursor: pointer;
1043
+ border-radius: 4px;
1044
+ display: flex;
1045
+ align-items: center;
1046
+ justify-content: center;
1047
+ transition: background 0.15s;
1048
+ `;
1049
+ collapseBtn.addEventListener('mouseenter', () => collapseBtn.style.background = 'rgba(255,255,255,0.1)');
1050
+ collapseBtn.addEventListener('mouseleave', () => collapseBtn.style.background = 'transparent');
1051
+ collapseBtn.addEventListener('click', toggleWebFrameCollapse);
1052
+
1053
+ // Close button
1054
+ const closeBtn = document.createElement('button');
1055
+ closeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`;
1056
+ closeBtn.style.cssText = `
1057
+ width: 24px;
1058
+ height: 24px;
1059
+ border: none;
1060
+ background: transparent;
1061
+ color: ${config.mutedColor};
1062
+ cursor: pointer;
1063
+ border-radius: 4px;
1064
+ display: flex;
1065
+ align-items: center;
1066
+ justify-content: center;
1067
+ transition: background 0.15s;
1068
+ `;
1069
+ closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = 'rgba(255,255,255,0.1)');
1070
+ closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = 'transparent');
1071
+ closeBtn.addEventListener('click', hideWebFrame);
1072
+
1073
+ buttons.appendChild(collapseBtn);
1074
+ buttons.appendChild(closeBtn);
1075
+ header.appendChild(title);
1076
+ header.appendChild(buttons);
1077
+
1078
+ // Drag handling for header
1079
+ header.addEventListener('mousedown', startWebFrameDrag);
1080
+
1081
+ // Iframe container
1082
+ const iframeContainer = document.createElement('div');
1083
+ iframeContainer.id = 'sparkecode-webframe-content';
1084
+ iframeContainer.style.cssText = `
1085
+ flex: 1;
1086
+ overflow: hidden;
1087
+ background: #0A0A0A;
1088
+ `;
1089
+
1090
+ // Loading state
1091
+ const loading = document.createElement('div');
1092
+ loading.id = 'sparkecode-webframe-loading';
1093
+ loading.style.cssText = `
1094
+ position: absolute;
1095
+ inset: 0;
1096
+ display: flex;
1097
+ align-items: center;
1098
+ justify-content: center;
1099
+ color: ${config.mutedColor};
1100
+ font-size: 13px;
1101
+ `;
1102
+ loading.textContent = 'Loading...';
1103
+ iframeContainer.appendChild(loading);
1104
+
1105
+ // Iframe
1106
+ const iframe = document.createElement('iframe');
1107
+ iframe.id = 'sparkecode-webframe-iframe';
1108
+ iframe.style.cssText = `
1109
+ width: 100%;
1110
+ height: 100%;
1111
+ border: none;
1112
+ opacity: 0;
1113
+ transition: opacity 0.2s;
1114
+ `;
1115
+ iframe.addEventListener('load', () => {
1116
+ loading.style.display = 'none';
1117
+ iframe.style.opacity = '1';
1118
+ });
1119
+ iframeContainer.appendChild(iframe);
1120
+
1121
+ webFrame.appendChild(header);
1122
+ webFrame.appendChild(iframeContainer);
1123
+ document.body.appendChild(webFrame);
1124
+ }
1125
+
1126
+ function startWebFrameDrag(e) {
1127
+ if (e.target.closest('button')) return;
1128
+
1129
+ isWebFrameDragging = true;
1130
+ const rect = webFrame.getBoundingClientRect();
1131
+ webFrameDragOffset.x = e.clientX - rect.left;
1132
+ webFrameDragOffset.y = e.clientY - rect.top;
1133
+
1134
+ const header = document.getElementById('sparkecode-webframe-header');
1135
+ if (header) header.style.cursor = 'grabbing';
1136
+
1137
+ const onMove = (e) => {
1138
+ if (!isWebFrameDragging) return;
1139
+ const x = Math.max(0, Math.min(window.innerWidth - webFrameSize.width, e.clientX - webFrameDragOffset.x));
1140
+ const y = Math.max(0, Math.min(window.innerHeight - 100, e.clientY - webFrameDragOffset.y));
1141
+ webFrame.style.left = x + 'px';
1142
+ webFrame.style.right = 'auto';
1143
+ webFrame.style.top = y + 'px';
1144
+ webFramePos = { x, y };
1145
+ };
1146
+
1147
+ const onUp = () => {
1148
+ isWebFrameDragging = false;
1149
+ const header = document.getElementById('sparkecode-webframe-header');
1150
+ if (header) header.style.cursor = 'grab';
1151
+ document.removeEventListener('mousemove', onMove);
1152
+ document.removeEventListener('mouseup', onUp);
1153
+ };
1154
+
1155
+ document.addEventListener('mousemove', onMove);
1156
+ document.addEventListener('mouseup', onUp);
1157
+ }
1158
+
1159
+ function showWebFrame() {
1160
+ createWebFrame();
1161
+
1162
+ const iframe = document.getElementById('sparkecode-webframe-iframe');
1163
+ const loading = document.getElementById('sparkecode-webframe-loading');
1164
+
1165
+ // Build embed URL with session ID
1166
+ let url = getWebUiUrl();
1167
+ if (sparkecoderState.sessionId) {
1168
+ url += `/embed/${sparkecoderState.sessionId}`;
1169
+ } else {
1170
+ url += '/embed';
1171
+ }
1172
+
1173
+ if (iframe && iframe.src !== url) {
1174
+ if (loading) loading.style.display = 'flex';
1175
+ iframe.style.opacity = '0';
1176
+ iframe.src = url;
1177
+ }
1178
+
1179
+ webFrame.style.display = 'flex';
1180
+ webFrameCollapsed = false;
1181
+ updateWebFrameSize();
1182
+
1183
+ requestAnimationFrame(() => {
1184
+ webFrame.style.opacity = '1';
1185
+ webFrame.style.transform = 'scale(1) translateY(0)';
1186
+ });
1187
+ }
1188
+
1189
+ function hideWebFrame() {
1190
+ if (!webFrame) return;
1191
+
1192
+ webFrame.style.opacity = '0';
1193
+ webFrame.style.transform = 'scale(0.95) translateY(10px)';
1194
+
1195
+ setTimeout(() => {
1196
+ if (webFrame) webFrame.style.display = 'none';
1197
+ // Clear iframe to stop any running content
1198
+ const iframe = document.getElementById('sparkecode-webframe-iframe');
1199
+ if (iframe) iframe.src = 'about:blank';
1200
+ }, 200);
1201
+ }
1202
+
1203
+ function toggleWebFrameCollapse() {
1204
+ webFrameCollapsed = !webFrameCollapsed;
1205
+ updateWebFrameSize();
1206
+
1207
+ const collapseBtn = document.getElementById('sparkecode-webframe-collapse');
1208
+ if (collapseBtn) {
1209
+ collapseBtn.innerHTML = webFrameCollapsed
1210
+ ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`
1211
+ : `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
1212
+ }
1213
+ }
1214
+
1215
+ function updateWebFrameSize() {
1216
+ if (!webFrame) return;
1217
+
1218
+ const content = document.getElementById('sparkecode-webframe-content');
1219
+ if (webFrameCollapsed) {
1220
+ webFrame.style.height = '44px';
1221
+ if (content) content.style.display = 'none';
1222
+ } else {
1223
+ webFrame.style.height = webFrameSize.height + 'px';
1224
+ if (content) content.style.display = 'block';
1225
+ }
1226
+ }
1227
+
1228
+ function isWebFrameVisible() {
1229
+ return webFrame && webFrame.style.display !== 'none' && webFrame.style.opacity !== '0';
1230
+ }
1231
+
884
1232
  // ============================================
885
1233
  // Event Handlers
886
1234
  // ============================================
@@ -946,12 +1294,18 @@
946
1294
  page: getPagePath(),
947
1295
  });
948
1296
 
949
- // If Sparkecoder is enabled and connected, show the prompt bar
1297
+ // If Sparkecoder is enabled and connected, open embed with context
950
1298
  if (config.sparkecoder?.enabled) {
951
1299
  const connection = await ensureSparkecoderConnection();
952
1300
  if (connection) {
953
1301
  deactivate();
954
- showPromptBar(content, info);
1302
+
1303
+ // Send component context to pending-input so it appears in the embed's input
1304
+ await sendToSparkecoder(content);
1305
+ showToast(`Selected <${info?.name || 'Component'}>`);
1306
+
1307
+ // Open the embed panel
1308
+ showWebFrame();
955
1309
  return;
956
1310
  }
957
1311
  }
@@ -1004,8 +1358,9 @@
1004
1358
  createOverlay();
1005
1359
 
1006
1360
  if (floatingButton) {
1007
- floatingButton.style.background = config.successColor;
1008
- floatingButton.style.boxShadow = '0 4px 20px rgba(16, 185, 129, 0.4)';
1361
+ floatingButton.style.background = config.primaryColor;
1362
+ floatingButton.style.color = '#171717';
1363
+ floatingButton.style.borderColor = config.primaryColor;
1009
1364
  }
1010
1365
 
1011
1366
  document.addEventListener('mousemove', handleMouseMove, true);
@@ -1025,8 +1380,9 @@
1025
1380
  removeOverlay();
1026
1381
 
1027
1382
  if (floatingButton) {
1028
- floatingButton.style.background = config.primaryColor;
1029
- floatingButton.style.boxShadow = '0 4px 20px rgba(139, 92, 246, 0.4)';
1383
+ floatingButton.style.background = config.bgColor;
1384
+ floatingButton.style.color = config.primaryColor;
1385
+ floatingButton.style.borderColor = config.borderColor;
1030
1386
  }
1031
1387
 
1032
1388
  document.removeEventListener('mousemove', handleMouseMove, true);