Skip to content

Commit 3ebc54d

Browse files
committed
feat: use dialog instead of popover
1 parent 4807a3b commit 3ebc54d

16 files changed

+496
-342
lines changed

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default defineConfig([
3232
rules: {
3333
...reactHooks.configs.recommended.rules,
3434
'@typescript-eslint/no-explicit-any': ['warn'],
35-
'react-refresh/only-export-components': ['warn'],
35+
// 'react-refresh/only-export-components': ['warn'],
3636
},
3737
},
3838
]);

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@hookform/resolvers": "^5.2.2",
2121
"@orama/orama": "^3.1.16",
2222
"@orama/plugin-data-persistence": "^3.1.16",
23+
"@radix-ui/react-dialog": "^1.1.15",
2324
"@radix-ui/react-dropdown-menu": "^2.1.16",
2425
"@radix-ui/react-label": "^2.1.7",
2526
"@radix-ui/react-popover": "^1.1.15",

src/App.tsx

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ import type {
3333
IdeConfigRemote,
3434
IdeConfig,
3535
McpServerItem,
36-
McpServerPackage,
36+
McpServerPkg,
3737
McpServerRemote,
3838
StackItem,
39+
StackCtrl,
3940
} from '~/lib/types';
4041
// import { initOrama, queryOrama, upsertServers } from '~/lib/orama';
4142

@@ -160,42 +161,38 @@ export default function App() {
160161
localStorage.setItem('mcp-registry-stack', JSON.stringify(stack));
161162
}, [stack]);
162163

163-
const addToStack = (
164-
serverName: string,
165-
type: 'remote' | 'package',
166-
data: McpServerPackage | McpServerRemote,
167-
index: number,
168-
ideConfig?: IdeConfig
169-
) => {
170-
// const stackKey = `${serverName}-${type}-${index}`;
171-
const existingItem = stack.find(
172-
(item) => item.serverName === serverName && item.type === type && item.index === index
173-
);
174-
if (existingItem) {
175-
// If an ideConfig is provided, update the existing item's config
176-
if (ideConfig) {
177-
setStack((prev) =>
178-
prev.map((it) =>
179-
it.serverName === serverName && it.type === type && it.index === index ? { ...it, ideConfig } : it
180-
)
181-
);
164+
/** Bundle stack manipulation functions to avoid re-defining them inline in props */
165+
const stackCtrl: StackCtrl = {
166+
getFromStack: (serverName: string, type: 'remote' | 'package', index: number): StackItem | null => {
167+
const found = stack.find((item) => item.serverName === serverName && item.type === type && item.index === index);
168+
return found || null;
169+
},
170+
addToStack: (
171+
serverName: string,
172+
type: 'remote' | 'package',
173+
data: McpServerPkg | McpServerRemote,
174+
index: number,
175+
ideConfig?: IdeConfig
176+
) => {
177+
const existingItem = stack.find(
178+
(item) => item.serverName === serverName && item.type === type && item.index === index
179+
);
180+
if (existingItem) {
181+
// If an ideConfig is provided, update the existing item's config
182+
if (ideConfig) {
183+
setStack((prev) =>
184+
prev.map((it) =>
185+
it.serverName === serverName && it.type === type && it.index === index ? { ...it, ideConfig } : it
186+
)
187+
);
188+
}
189+
return;
182190
}
183-
return;
184-
}
185-
setStack((prev) => [...prev, { serverName, type, data, index, ideConfig }]);
186-
};
187-
188-
/** Remove item from stack */
189-
const removeFromStack = (serverName: string, type: 'remote' | 'package', index: number) => {
190-
setStack(stack.filter((item) => !(item.serverName === serverName && item.type === type && item.index === index)));
191-
};
192-
193-
/** Get item from stack (returns the StackItem.data when present, otherwise null)
194-
* Returns McpServerPackage when type='package', McpServerRemote when type='remote', or null.
195-
*/
196-
const getFromStack = (serverName: string, type: 'remote' | 'package', index: number): StackItem | null => {
197-
const found = stack.find((item) => item.serverName === serverName && item.type === type && item.index === index);
198-
return found || null;
191+
setStack((prev) => [...prev, { serverName, type, data, index, ideConfig }]);
192+
},
193+
removeFromStack: (serverName: string, type: 'remote' | 'package', index: number) => {
194+
setStack(stack.filter((item) => !(item.serverName === serverName && item.type === type && item.index === index)));
195+
},
199196
};
200197

201198
/** Check if server has any items in stack */
@@ -214,7 +211,7 @@ export default function App() {
214211
servers[item.serverName] =
215212
item.type === 'remote'
216213
? buildIdeConfigForRemote(item.data as McpServerRemote)
217-
: buildIdeConfigForPkg(item.data as McpServerPackage);
214+
: buildIdeConfigForPkg(item.data as McpServerPkg);
218215
}
219216
});
220217
if (configType === 'vscode') {
@@ -465,7 +462,7 @@ export default function App() {
465462
onClick={(e) => {
466463
e.stopPropagation();
467464
e.preventDefault();
468-
removeFromStack(item.serverName, item.type, item.index);
465+
stackCtrl.removeFromStack(item.serverName, item.type, item.index);
469466
}}
470467
className="p-1 hover:bg-destructive/10 rounded transition-colors"
471468
>
@@ -627,9 +624,10 @@ export default function App() {
627624
<ServerCard
628625
item={item}
629626
registryUrl={registryUrl}
630-
addToStack={addToStack}
631-
removeFromStack={removeFromStack}
632-
getFromStack={getFromStack}
627+
stackCtrl={stackCtrl}
628+
// addToStack={addToStack}
629+
// removeFromStack={removeFromStack}
630+
// getFromStack={getFromStack}
633631
/>
634632
</Card>
635633
))}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Plus, Delete } from 'lucide-react';
2+
3+
import type {
4+
IdeConfigPkg,
5+
IdeConfigRemote,
6+
McpServerItem,
7+
McpServerPkg,
8+
McpServerRemote,
9+
StackCtrl,
10+
} from '~/lib/types';
11+
import { Button } from '~/components/ui/button';
12+
import VscodeLogo from '~/components/logos/vscode-logo.svg';
13+
import CursorLogo from '~/components/logos/cursor-logo.svg';
14+
import { CopyButton } from './ui/copy-button';
15+
import { useMemo } from 'react';
16+
17+
/** Display action buttons for a MCP server access point (copy config, install VSCode/Cursor) */
18+
export const ServerActionButtons = ({
19+
item,
20+
endpoint,
21+
endpointIndex,
22+
formValues,
23+
stackCtrl,
24+
onClickCopy,
25+
}: {
26+
formValues: IdeConfigPkg | IdeConfigRemote;
27+
item: McpServerItem;
28+
endpoint: McpServerPkg | McpServerRemote;
29+
endpointIndex: number;
30+
stackCtrl: StackCtrl;
31+
onClickCopy?: (e: React.MouseEvent<HTMLButtonElement>) => void;
32+
}) => {
33+
const formValuesPretty = useMemo(() => JSON.stringify(formValues, null, 2), [formValues]);
34+
const formValuesEncoded = useMemo(() => encodeURIComponent(JSON.stringify(formValues)), [formValues]);
35+
const vscodeInstallPayload = useMemo(
36+
() => JSON.stringify({ name: item.server.name, ...formValues }),
37+
[item.server.name, formValues]
38+
);
39+
40+
const endpointType = (endpoint as McpServerPkg).identifier ? 'package' : 'remote';
41+
42+
return (
43+
<div className="flex flex-col gap-2 items-center">
44+
{/* Copy config and add to stack buttons */}
45+
<div className="flex flex-wrap md:flex-nowrap gap-2 justify-center">
46+
<CopyButton
47+
variant="outline"
48+
size="sm"
49+
className="w-fit"
50+
content={formValuesPretty}
51+
{...(onClickCopy && { onClick: onClickCopy })}
52+
>
53+
Copy server config
54+
</CopyButton>
55+
<Button
56+
className="w-fit"
57+
variant="outline"
58+
size="sm"
59+
onClick={() => {
60+
return stackCtrl.getFromStack(item.server.name, endpointType, endpointIndex)
61+
? stackCtrl.removeFromStack(item.server.name, endpointType, endpointIndex)
62+
: stackCtrl.addToStack(item.server.name, endpointType, endpoint, endpointIndex);
63+
}}
64+
>
65+
{stackCtrl.getFromStack(item.server.name, endpointType, endpointIndex) ? (
66+
<>
67+
<Delete /> Remove from your stack
68+
</>
69+
) : (
70+
<>
71+
<Plus /> Add to your stack
72+
</>
73+
)}
74+
</Button>
75+
</div>
76+
77+
{/* Client installation buttons */}
78+
<div className="flex flex-wrap md:flex-nowrap gap-2 justify-center">
79+
<a href={`vscode:mcp/install?${vscodeInstallPayload}`} onClick={(e) => e.stopPropagation()} className="flex">
80+
<Button variant="outline" size="sm">
81+
<img src={VscodeLogo} alt="VSCode" className="h-4 w-4" /> Install in VSCode
82+
</Button>
83+
</a>
84+
<a
85+
href={`cursor://anysphere.cursor-deeplink/mcp/install?name=${item.server.name}&config=${formValuesEncoded}`}
86+
onClick={(e) => e.stopPropagation()}
87+
>
88+
<Button variant="outline" size="sm">
89+
<img src={CursorLogo} alt="Cursor" className="[filter:invert(0)] dark:[filter:invert(1)]" /> Install in
90+
Cursor
91+
</Button>
92+
</a>
93+
</div>
94+
</div>
95+
);
96+
};

0 commit comments

Comments
 (0)