nextjs-ide-helper 1.4.1 → 1.5.2

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
@@ -8,7 +8,8 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
8
8
  - 🎯 **Smart Detection**: Only processes components in specified directories
9
9
  - 🔧 **Zero Configuration**: Works out of the box with sensible defaults (Cursor as default IDE)
10
10
  - 🏎️ **Development Only**: Only active in development mode, no production overhead
11
- - 🎨 **Non-intrusive**: Uses absolute positioning to avoid layout disruption
11
+ - 🎨 **Non-intrusive**: Minimal blue dot indicators that don't disrupt your layout
12
+ - 👁️ **Toggle Visibility**: Floating button to show/hide all IDE dots instantly
12
13
  - ⚡ **Hydration Safe**: No SSR/client hydration mismatches
13
14
  - 📱 **TypeScript Support**: Full TypeScript definitions included
14
15
  - 🔌 **Multi-IDE Support**: Supports Cursor, VS Code, WebStorm, and Atom
@@ -18,8 +19,7 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
18
19
  ## Installation
19
20
 
20
21
  ```bash
21
- # or
22
- # or
22
+ npm install nextjs-ide-helper
23
23
  ```
24
24
 
25
25
  ## Quick Start
@@ -29,7 +29,7 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
29
29
  Add the plugin to your `next.config.js`:
30
30
 
31
31
  ```javascript
32
- const withIdeHelper = require('nextjs-ide-plugin');
32
+ const withIdeHelper = require('nextjs-ide-helper');
33
33
 
34
34
  /** @type {import('next').NextConfig} */
35
35
  const nextConfig = {
@@ -43,12 +43,30 @@ module.exports = withIdeHelper()(nextConfig);
43
43
 
44
44
  All React components in your `src/components` directory will automatically get IDE buttons in development mode (defaults to Cursor IDE).
45
45
 
46
+ ## User Interface
47
+
48
+ ### IDE Indicator Dots
49
+
50
+ Each wrapped component displays a small **blue dot** (10x10px) in the top-right corner:
51
+ - **Click** the dot to open the component's source file in your IDE
52
+ - **Hover** to see the file path tooltip
53
+ - The dots are subtle (60% opacity) and brighten on hover
54
+
55
+ ### Toggle Button
56
+
57
+ A **floating toggle button** appears in the bottom-right corner of your page:
58
+ - **Blue** = IDE dots are visible
59
+ - **Gray** = IDE dots are hidden
60
+ - **Click** to toggle all dots on/off instantly
61
+
62
+ This lets you quickly hide the dots when you want an unobstructed view of your app, then bring them back when needed.
63
+
46
64
  ## Configuration
47
65
 
48
66
  You can customize the plugin behavior:
49
67
 
50
68
  ```javascript
51
- const withIdeHelper = require('nextjs-ide-plugin');
69
+ const withIdeHelper = require('nextjs-ide-helper');
52
70
 
53
71
  const nextConfig = {
54
72
  // your existing config
@@ -57,7 +75,7 @@ const nextConfig = {
57
75
  module.exports = withIdeHelper({
58
76
  componentPaths: ['src/components', 'components', 'src/ui'], // directories to scan (supports glob patterns)
59
77
  projectRoot: process.cwd(), // project root directory
60
- importPath: 'nextjs-ide-plugin/withIdeButton', // import path for the HOC
78
+ importPath: 'nextjs-ide-helper/withIdeButton', // import path for the HOC
61
79
  enabled: process.env.NODE_ENV === 'development', // enable/disable
62
80
  ideType: 'cursor' // IDE to use: 'cursor', 'vscode', 'webstorm', 'atom'
63
81
  })(nextConfig);
@@ -69,7 +87,7 @@ module.exports = withIdeHelper({
69
87
  |--------|------|---------|-------------|
70
88
  | `componentPaths` | `string[]` | `['src/components']` | Directories to scan for React components (supports glob patterns) |
71
89
  | `projectRoot` | `string` | `process.cwd()` | Root directory of your project |
72
- | `importPath` | `string` | `'nextjs-ide-plugin/withIdeButton'` | Import path for the withIdeButton HOC |
90
+ | `importPath` | `string` | `'nextjs-ide-helper/withIdeButton'` | Import path for the withIdeButton HOC |
73
91
  | `enabled` | `boolean` | `process.env.NODE_ENV === 'development'` | Enable/disable the plugin |
74
92
  | `ideType` | `'cursor' \| 'vscode' \| 'webstorm' \| 'atom'` | `'cursor'` | IDE to open files in |
75
93
 
@@ -100,7 +118,7 @@ module.exports = withIdeHelper({
100
118
  You can also manually wrap components:
101
119
 
102
120
  ```tsx
103
- import { withIdeButton } from 'nextjs-ide-plugin';
121
+ import { withIdeButton } from 'nextjs-ide-helper';
104
122
 
105
123
  const MyComponent = () => {
106
124
  return <div>Hello World</div>;
@@ -257,7 +275,7 @@ After (automatically transformed):
257
275
  ```tsx
258
276
  // src/components/Button.tsx (transformed by the plugin)
259
277
  import React from 'react';
260
- import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
278
+ import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
261
279
 
262
280
  const Button = ({ children, onClick }) => {
263
281
  return (
@@ -297,7 +315,7 @@ export const SecondaryButton = ({ children, onClick }) => {
297
315
  After (automatically transformed):
298
316
  ```tsx
299
317
  // src/components/Buttons.tsx (transformed by the plugin)
300
- import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
318
+ import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
301
319
 
302
320
  export const PrimaryButton = withIdeButton(({ children, onClick }) => {
303
321
  return (
@@ -343,7 +361,7 @@ export default Layout;
343
361
  After (automatically transformed):
344
362
  ```tsx
345
363
  // src/components/Layout.tsx (transformed by the plugin)
346
- import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
364
+ import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
347
365
 
348
366
  export const Header = withIdeButton(() => <header>My App Header</header>, 'src/components/Layout.tsx', {
349
367
  projectRoot: '/path/to/project',
@@ -381,7 +399,7 @@ export default function Header() {
381
399
  After (automatically transformed):
382
400
  ```tsx
383
401
  // src/components/Header.tsx (transformed by the plugin)
384
- import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
402
+ import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
385
403
 
386
404
  function Header() {
387
405
  return <header>My App Header</header>;
@@ -405,7 +423,7 @@ export default () => {
405
423
  After (automatically transformed):
406
424
  ```tsx
407
425
  // src/components/Footer.tsx (transformed by the plugin)
408
- import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
426
+ import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
409
427
 
410
428
  export default withIdeButton(() => {
411
429
  return <footer>© 2025 My App</footer>;
@@ -456,6 +474,17 @@ See [CHANGELOG.md](./CHANGELOG.md) for detailed release notes.
456
474
 
457
475
  ### Recent Releases
458
476
 
477
+ ### 1.5.0 - Toggle Visibility
478
+ - Added floating toggle button to show/hide all IDE dots
479
+ - Toggle button appears in bottom-right corner
480
+ - Blue when dots visible, gray when hidden
481
+ - State synchronized across all components via custom events
482
+
483
+ ### 1.4.1 - Minimal UI
484
+ - Changed IDE buttons from text+emoji to minimal 10x10px blue dots
485
+ - Reduced visual footprint for less intrusive development experience
486
+ - Removed console.log statements from plugin
487
+
459
488
  ### 1.4.0 - Named Export Support
460
489
  - Added comprehensive support for ES6 named exports in React components
461
490
  - Support for `export const Component = () => {}` and `export function Component() {}` patterns
package/lib/loader.js CHANGED
@@ -221,22 +221,28 @@ module.exports = function cursorButtonLoader(source) {
221
221
  modified = true;
222
222
  } else if (namedExport.declaration) {
223
223
  // Handle export function Component() {}
224
- const funcDeclaration = namedExport.declaration;
224
+ // Convert FunctionDeclaration to FunctionExpression (preserves name for stack traces)
225
+ const funcDecl = namedExport.declaration;
226
+ const funcExpr = t.functionExpression(
227
+ funcDecl.id, // Keep the name for debugging
228
+ funcDecl.params,
229
+ funcDecl.body,
230
+ funcDecl.generator,
231
+ funcDecl.async
232
+ );
233
+
225
234
  const wrappedCall = t.callExpression(
226
235
  t.identifier('withIdeButton'),
227
236
  [
228
- t.identifier(namedExport.name),
237
+ funcExpr,
229
238
  t.stringLiteral(relativePath),
230
239
  t.objectExpression([
231
240
  t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
232
241
  ])
233
242
  ]
234
243
  );
235
-
236
- // Insert the function declaration before the export
237
- namedExport.path.insertBefore(funcDeclaration);
238
-
239
- // Replace the export with wrapped call
244
+
245
+ // Replace: export function X() {} -> export const X = withIdeButton(function X() {}, ...)
240
246
  namedExport.path.node.declaration = t.variableDeclaration('const', [
241
247
  t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
242
248
  ]);
@@ -290,15 +296,33 @@ module.exports = function cursorButtonLoader(source) {
290
296
  );
291
297
  }
292
298
 
293
- // If the export is a named function or class declaration, we need to handle it differently
294
- if (!isAnonymousComponent &&
295
- (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
296
- t.isClassDeclaration(defaultExportPath.node.declaration))) {
297
- // Insert the function/class declaration before the export
299
+ // If the export is a named function declaration, convert to function expression inline
300
+ if (!isAnonymousComponent && t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
301
+ const funcDecl = defaultExportPath.node.declaration;
302
+ const funcExpr = t.functionExpression(
303
+ funcDecl.id, // Keep the name for debugging
304
+ funcDecl.params,
305
+ funcDecl.body,
306
+ funcDecl.generator,
307
+ funcDecl.async
308
+ );
309
+
310
+ // Wrap the function expression directly
311
+ wrappedCall = t.callExpression(
312
+ t.identifier('withIdeButton'),
313
+ [
314
+ funcExpr,
315
+ t.stringLiteral(relativePath),
316
+ t.objectExpression([
317
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
318
+ ])
319
+ ]
320
+ );
321
+ defaultExportPath.node.declaration = wrappedCall;
322
+ } else if (!isAnonymousComponent && t.isClassDeclaration(defaultExportPath.node.declaration)) {
323
+ // For classes, we still need to separate declaration (can't inline class expressions easily)
298
324
  const declaration = defaultExportPath.node.declaration;
299
325
  defaultExportPath.insertBefore(declaration);
300
-
301
- // Replace the export declaration with the wrapped call
302
326
  defaultExportPath.node.declaration = wrappedCall;
303
327
  } else {
304
328
  // For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
@@ -4,27 +4,76 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.withIdeButton = withIdeButton;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
+ // Inject global styles once
8
+ let stylesInjected = false;
9
+ function injectStyles() {
10
+ if (stylesInjected || typeof document === 'undefined')
11
+ return;
12
+ stylesInjected = true;
13
+ const style = document.createElement('style');
14
+ style.id = 'ide-helper-styles';
15
+ style.textContent = `
16
+ .ide-helper-dot { display: block; }
17
+ .ide-helper-hidden .ide-helper-dot { display: none; }
18
+ .ide-helper-toggle {
19
+ background: transparent;
20
+ border: 2px solid #007acc !important;
21
+ box-sizing: border-box;
22
+ }
23
+ .ide-helper-hidden .ide-helper-toggle {
24
+ background: #666;
25
+ border-color: #666 !important;
26
+ }
27
+ `;
28
+ document.head.appendChild(style);
29
+ }
30
+ // Toggle button - renders once via singleton pattern
31
+ let toggleInjected = false;
32
+ function injectToggle() {
33
+ if (toggleInjected || typeof document === 'undefined')
34
+ return;
35
+ toggleInjected = true;
36
+ const btn = document.createElement('button');
37
+ btn.className = 'ide-helper-toggle';
38
+ btn.title = 'Toggle IDE dots (click to show/hide)';
39
+ Object.assign(btn.style, {
40
+ position: 'fixed',
41
+ bottom: '16px',
42
+ right: '16px',
43
+ width: '18px',
44
+ height: '18px',
45
+ border: 'none',
46
+ borderRadius: '50%',
47
+ padding: '0',
48
+ cursor: 'pointer',
49
+ zIndex: '9999',
50
+ opacity: '0.7',
51
+ transition: 'opacity 0.2s'
52
+ });
53
+ btn.onmouseenter = () => btn.style.opacity = '1';
54
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
55
+ btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
56
+ document.body.appendChild(btn);
57
+ }
7
58
  const IdeButton = ({ filePath, projectRoot, ideType = 'cursor' }) => {
59
+ (0, react_1.useEffect)(() => {
60
+ injectStyles();
61
+ injectToggle();
62
+ }, []);
8
63
  const getIdeUrl = (ide, path) => {
9
64
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
10
65
  switch (ide) {
11
- case 'cursor':
12
- return `cursor://file${fullPath}`;
13
- case 'vscode':
14
- return `vscode://file${fullPath}`;
15
- case 'webstorm':
16
- return `webstorm://open?file=${fullPath}`;
17
- case 'atom':
18
- return `atom://open?path=${fullPath}`;
19
- default:
20
- return `cursor://file${fullPath}`;
66
+ case 'cursor': return `cursor://file${fullPath}`;
67
+ case 'vscode': return `vscode://file${fullPath}`;
68
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
69
+ case 'atom': return `atom://open?path=${fullPath}`;
70
+ default: return `cursor://file${fullPath}`;
21
71
  }
22
72
  };
23
73
  const handleClick = () => {
24
- const url = getIdeUrl(ideType, filePath);
25
- window.open(url, '_blank');
74
+ window.open(getIdeUrl(ideType, filePath), '_blank');
26
75
  };
27
- return ((0, jsx_runtime_1.jsx)("button", { onClick: handleClick, style: {
76
+ return ((0, jsx_runtime_1.jsx)("button", { className: "ide-helper-dot", onClick: handleClick, style: {
28
77
  position: 'absolute',
29
78
  top: '4px',
30
79
  right: '4px',
@@ -47,7 +96,6 @@ function withIdeButton(WrappedComponent, filePath, options = {}) {
47
96
  (0, react_1.useEffect)(() => {
48
97
  setIsClient(true);
49
98
  }, []);
50
- // In production or when disabled, just return the component without wrapper
51
99
  if (!enabled || !isClient) {
52
100
  return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props });
53
101
  }
@@ -1,6 +1,61 @@
1
1
  'use client';
2
2
  import React, { useState, useEffect } from 'react';
3
3
 
4
+ // Inject global styles once
5
+ let stylesInjected = false;
6
+
7
+ function injectStyles() {
8
+ if (stylesInjected || typeof document === 'undefined') return;
9
+ stylesInjected = true;
10
+
11
+ const style = document.createElement('style');
12
+ style.id = 'ide-helper-styles';
13
+ style.textContent = `
14
+ .ide-helper-dot { display: block; }
15
+ .ide-helper-hidden .ide-helper-dot { display: none; }
16
+ .ide-helper-toggle {
17
+ background: transparent;
18
+ border: 2px solid #007acc !important;
19
+ box-sizing: border-box;
20
+ }
21
+ .ide-helper-hidden .ide-helper-toggle {
22
+ background: #666;
23
+ border-color: #666 !important;
24
+ }
25
+ `;
26
+ document.head.appendChild(style);
27
+ }
28
+
29
+ // Toggle button - renders once via singleton pattern
30
+ let toggleInjected = false;
31
+
32
+ function injectToggle() {
33
+ if (toggleInjected || typeof document === 'undefined') return;
34
+ toggleInjected = true;
35
+
36
+ const btn = document.createElement('button');
37
+ btn.className = 'ide-helper-toggle';
38
+ btn.title = 'Toggle IDE dots (click to show/hide)';
39
+ Object.assign(btn.style, {
40
+ position: 'fixed',
41
+ bottom: '16px',
42
+ right: '16px',
43
+ width: '18px',
44
+ height: '18px',
45
+ border: 'none',
46
+ borderRadius: '50%',
47
+ padding: '0',
48
+ cursor: 'pointer',
49
+ zIndex: '9999',
50
+ opacity: '0.7',
51
+ transition: 'opacity 0.2s'
52
+ });
53
+ btn.onmouseenter = () => btn.style.opacity = '1';
54
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
55
+ btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
56
+ document.body.appendChild(btn);
57
+ }
58
+
4
59
  interface IdeButtonProps {
5
60
  filePath: string;
6
61
  projectRoot?: string;
@@ -8,30 +63,29 @@ interface IdeButtonProps {
8
63
  }
9
64
 
10
65
  const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
66
+ useEffect(() => {
67
+ injectStyles();
68
+ injectToggle();
69
+ }, []);
70
+
11
71
  const getIdeUrl = (ide: string, path: string) => {
12
72
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
13
-
14
73
  switch (ide) {
15
- case 'cursor':
16
- return `cursor://file${fullPath}`;
17
- case 'vscode':
18
- return `vscode://file${fullPath}`;
19
- case 'webstorm':
20
- return `webstorm://open?file=${fullPath}`;
21
- case 'atom':
22
- return `atom://open?path=${fullPath}`;
23
- default:
24
- return `cursor://file${fullPath}`;
74
+ case 'cursor': return `cursor://file${fullPath}`;
75
+ case 'vscode': return `vscode://file${fullPath}`;
76
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
77
+ case 'atom': return `atom://open?path=${fullPath}`;
78
+ default: return `cursor://file${fullPath}`;
25
79
  }
26
80
  };
27
81
 
28
82
  const handleClick = () => {
29
- const url = getIdeUrl(ideType, filePath);
30
- window.open(url, '_blank');
83
+ window.open(getIdeUrl(ideType, filePath), '_blank');
31
84
  };
32
85
 
33
86
  return (
34
87
  <button
88
+ className="ide-helper-dot"
35
89
  onClick={handleClick}
36
90
  style={{
37
91
  position: 'absolute',
@@ -75,7 +129,6 @@ export function withIdeButton<T extends object>(
75
129
  setIsClient(true);
76
130
  }, []);
77
131
 
78
- // In production or when disabled, just return the component without wrapper
79
132
  if (!enabled || !isClient) {
80
133
  return <WrappedComponent {...props} />;
81
134
  }
@@ -91,4 +144,4 @@ export function withIdeButton<T extends object>(
91
144
  WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
92
145
 
93
146
  return WithIdeButtonComponent;
94
- }
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-ide-helper",
3
- "version": "1.4.1",
3
+ "version": "1.5.2",
4
4
  "description": "A Next.js plugin that automatically adds IDE buttons to React components for seamless IDE integration. Supports Cursor, VS Code, WebStorm, and Atom.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/src/loader.js CHANGED
@@ -221,22 +221,28 @@ module.exports = function cursorButtonLoader(source) {
221
221
  modified = true;
222
222
  } else if (namedExport.declaration) {
223
223
  // Handle export function Component() {}
224
- const funcDeclaration = namedExport.declaration;
224
+ // Convert FunctionDeclaration to FunctionExpression (preserves name for stack traces)
225
+ const funcDecl = namedExport.declaration;
226
+ const funcExpr = t.functionExpression(
227
+ funcDecl.id, // Keep the name for debugging
228
+ funcDecl.params,
229
+ funcDecl.body,
230
+ funcDecl.generator,
231
+ funcDecl.async
232
+ );
233
+
225
234
  const wrappedCall = t.callExpression(
226
235
  t.identifier('withIdeButton'),
227
236
  [
228
- t.identifier(namedExport.name),
237
+ funcExpr,
229
238
  t.stringLiteral(relativePath),
230
239
  t.objectExpression([
231
240
  t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
232
241
  ])
233
242
  ]
234
243
  );
235
-
236
- // Insert the function declaration before the export
237
- namedExport.path.insertBefore(funcDeclaration);
238
-
239
- // Replace the export with wrapped call
244
+
245
+ // Replace: export function X() {} -> export const X = withIdeButton(function X() {}, ...)
240
246
  namedExport.path.node.declaration = t.variableDeclaration('const', [
241
247
  t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
242
248
  ]);
@@ -290,15 +296,33 @@ module.exports = function cursorButtonLoader(source) {
290
296
  );
291
297
  }
292
298
 
293
- // If the export is a named function or class declaration, we need to handle it differently
294
- if (!isAnonymousComponent &&
295
- (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
296
- t.isClassDeclaration(defaultExportPath.node.declaration))) {
297
- // Insert the function/class declaration before the export
299
+ // If the export is a named function declaration, convert to function expression inline
300
+ if (!isAnonymousComponent && t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
301
+ const funcDecl = defaultExportPath.node.declaration;
302
+ const funcExpr = t.functionExpression(
303
+ funcDecl.id, // Keep the name for debugging
304
+ funcDecl.params,
305
+ funcDecl.body,
306
+ funcDecl.generator,
307
+ funcDecl.async
308
+ );
309
+
310
+ // Wrap the function expression directly
311
+ wrappedCall = t.callExpression(
312
+ t.identifier('withIdeButton'),
313
+ [
314
+ funcExpr,
315
+ t.stringLiteral(relativePath),
316
+ t.objectExpression([
317
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
318
+ ])
319
+ ]
320
+ );
321
+ defaultExportPath.node.declaration = wrappedCall;
322
+ } else if (!isAnonymousComponent && t.isClassDeclaration(defaultExportPath.node.declaration)) {
323
+ // For classes, we still need to separate declaration (can't inline class expressions easily)
298
324
  const declaration = defaultExportPath.node.declaration;
299
325
  defaultExportPath.insertBefore(declaration);
300
-
301
- // Replace the export declaration with the wrapped call
302
326
  defaultExportPath.node.declaration = wrappedCall;
303
327
  } else {
304
328
  // For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
@@ -1,6 +1,61 @@
1
1
  'use client';
2
2
  import React, { useState, useEffect } from 'react';
3
3
 
4
+ // Inject global styles once
5
+ let stylesInjected = false;
6
+
7
+ function injectStyles() {
8
+ if (stylesInjected || typeof document === 'undefined') return;
9
+ stylesInjected = true;
10
+
11
+ const style = document.createElement('style');
12
+ style.id = 'ide-helper-styles';
13
+ style.textContent = `
14
+ .ide-helper-dot { display: block; }
15
+ .ide-helper-hidden .ide-helper-dot { display: none; }
16
+ .ide-helper-toggle {
17
+ background: transparent;
18
+ border: 2px solid #007acc !important;
19
+ box-sizing: border-box;
20
+ }
21
+ .ide-helper-hidden .ide-helper-toggle {
22
+ background: #666;
23
+ border-color: #666 !important;
24
+ }
25
+ `;
26
+ document.head.appendChild(style);
27
+ }
28
+
29
+ // Toggle button - renders once via singleton pattern
30
+ let toggleInjected = false;
31
+
32
+ function injectToggle() {
33
+ if (toggleInjected || typeof document === 'undefined') return;
34
+ toggleInjected = true;
35
+
36
+ const btn = document.createElement('button');
37
+ btn.className = 'ide-helper-toggle';
38
+ btn.title = 'Toggle IDE dots (click to show/hide)';
39
+ Object.assign(btn.style, {
40
+ position: 'fixed',
41
+ bottom: '16px',
42
+ right: '16px',
43
+ width: '18px',
44
+ height: '18px',
45
+ border: 'none',
46
+ borderRadius: '50%',
47
+ padding: '0',
48
+ cursor: 'pointer',
49
+ zIndex: '9999',
50
+ opacity: '0.7',
51
+ transition: 'opacity 0.2s'
52
+ });
53
+ btn.onmouseenter = () => btn.style.opacity = '1';
54
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
55
+ btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
56
+ document.body.appendChild(btn);
57
+ }
58
+
4
59
  interface IdeButtonProps {
5
60
  filePath: string;
6
61
  projectRoot?: string;
@@ -8,30 +63,29 @@ interface IdeButtonProps {
8
63
  }
9
64
 
10
65
  const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
66
+ useEffect(() => {
67
+ injectStyles();
68
+ injectToggle();
69
+ }, []);
70
+
11
71
  const getIdeUrl = (ide: string, path: string) => {
12
72
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
13
-
14
73
  switch (ide) {
15
- case 'cursor':
16
- return `cursor://file${fullPath}`;
17
- case 'vscode':
18
- return `vscode://file${fullPath}`;
19
- case 'webstorm':
20
- return `webstorm://open?file=${fullPath}`;
21
- case 'atom':
22
- return `atom://open?path=${fullPath}`;
23
- default:
24
- return `cursor://file${fullPath}`;
74
+ case 'cursor': return `cursor://file${fullPath}`;
75
+ case 'vscode': return `vscode://file${fullPath}`;
76
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
77
+ case 'atom': return `atom://open?path=${fullPath}`;
78
+ default: return `cursor://file${fullPath}`;
25
79
  }
26
80
  };
27
81
 
28
82
  const handleClick = () => {
29
- const url = getIdeUrl(ideType, filePath);
30
- window.open(url, '_blank');
83
+ window.open(getIdeUrl(ideType, filePath), '_blank');
31
84
  };
32
85
 
33
86
  return (
34
87
  <button
88
+ className="ide-helper-dot"
35
89
  onClick={handleClick}
36
90
  style={{
37
91
  position: 'absolute',
@@ -75,7 +129,6 @@ export function withIdeButton<T extends object>(
75
129
  setIsClient(true);
76
130
  }, []);
77
131
 
78
- // In production or when disabled, just return the component without wrapper
79
132
  if (!enabled || !isClient) {
80
133
  return <WrappedComponent {...props} />;
81
134
  }
@@ -91,4 +144,4 @@ export function withIdeButton<T extends object>(
91
144
  WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
92
145
 
93
146
  return WithIdeButtonComponent;
94
- }
147
+ }