Skip to content

Commit 413688c

Browse files
committed
Redesign the Plugins gallery
1 parent 5b6d9ae commit 413688c

12 files changed

Lines changed: 791 additions & 520 deletions

‎bun.lock‎

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/plugins-ui/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test": "bun test"
1515
},
1616
"dependencies": {
17+
"@corbits/error-sink": "workspace:*",
1718
"@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635",
1819
"@corbits/settings-ui": "workspace:*",
1920
"@corbits/connections": "workspace:*",

‎packages/plugins-ui/src/index.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export type { SkillCardData } from "./skill-card";
77
export { InstalledStrip } from "./installed-strip";
88
export { PluginConnectPanel } from "./plugin-connect-panel";
99
export { McpServersSection } from "./mcp-servers-section";
10-
export { McpPresetCardsSection } from "./mcp-preset-cards";
1110

1211
export { PLUGINS_STRINGS } from "./strings";
1312

@@ -27,10 +26,8 @@ export type {
2726
} from "./mcp-servers-api";
2827

2928
export {
30-
FEATURED_CONNECTOR_IDS,
31-
PLUGIN_CATEGORY_ORDER,
32-
pluginCategory,
29+
pluginCatalogCategory,
3330
pluginIcon,
3431
pluginOutcome,
3532
} from "./plugin-meta";
36-
export type { PluginCategory } from "./plugin-meta";
33+
export type { PluginCatalogCategory } from "./plugin-meta";

‎packages/plugins-ui/src/mcp-preset-cards.tsx‎

Lines changed: 68 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// get the catalog's one-click installation path. Presets and previously
33
// connected custom servers share the same server-side store.
44

5+
import { reportError } from "@corbits/error-sink";
56
import { Button, ConfirmButton, Input, toast } from "@corbits/react-ui";
67
import {
78
CONNECTOR_REGISTRY,
@@ -65,7 +66,7 @@ function mcpOauthConnectedReturn():
6566
return { slug, toolCount };
6667
}
6768

68-
function McpPresetCard({
69+
export function McpPresetCard({
6970
tenantId,
7071
preset,
7172
toolCount,
@@ -140,7 +141,9 @@ function McpPresetCard({
140141

141142
return (
142143
<div
143-
className="border-b border-border px-2 py-2.5"
144+
className="plugins-catalog-card min-w-0 px-2 py-2.5"
145+
data-plugin-card
146+
data-plugin-name={preset.displayName}
144147
data-plugin-slug={preset.slug}
145148
>
146149
<div className="flex min-h-11 min-w-0 items-center gap-3">
@@ -161,32 +164,37 @@ function McpPresetCard({
161164
</span>
162165
) : null}
163166
</div>
164-
<span className="hidden shrink-0 text-xs text-muted-foreground xl:block">
165-
{status}
166-
</span>
167-
{preset.connected ? (
168-
<ConfirmButton
169-
variant="destructive"
170-
size="sm"
171-
confirmLabel="Disconnect"
172-
disabled={busy}
173-
aria-label={`Disconnect ${preset.displayName}`}
174-
onConfirm={handleDisconnect}
175-
>
176-
{busy ? "Disconnecting…" : "Disconnect"}
177-
</ConfirmButton>
178-
) : tokenFieldOpen ? null : (
179-
<Button
180-
type="button"
181-
size="sm"
182-
variant="ghost"
183-
disabled={busy}
184-
aria-label={`Connect ${preset.displayName}`}
185-
onClick={handleConnect}
186-
>
187-
{busy ? "Connecting…" : "Connect"}
188-
</Button>
189-
)}
167+
<div className="plugins-catalog-card-actions">
168+
<span className="text-xs text-muted-foreground">{status}</span>
169+
{preset.connected ? (
170+
<ConfirmButton
171+
variant="ghost"
172+
size="sm"
173+
confirmLabel={
174+
<>
175+
Disconnect
176+
<span className="sr-only"> {preset.displayName}</span>
177+
</>
178+
}
179+
disabled={busy}
180+
onConfirm={handleDisconnect}
181+
>
182+
{busy ? "Disconnecting…" : "Manage"}
183+
<span className="sr-only"> {preset.displayName}</span>
184+
</ConfirmButton>
185+
) : tokenFieldOpen ? null : (
186+
<Button
187+
type="button"
188+
size="sm"
189+
variant="ghost"
190+
disabled={busy}
191+
aria-label={`Connect ${preset.displayName}`}
192+
onClick={handleConnect}
193+
>
194+
{busy ? "Connecting…" : "Connect"}
195+
</Button>
196+
)}
197+
</div>
190198
</div>
191199
{tokenFieldOpen && !preset.connected ? (
192200
<div className="mt-2 flex flex-col gap-2 pl-11">
@@ -248,22 +256,7 @@ function McpPresetCard({
248256
);
249257
}
250258

251-
export function McpPresetCardsSection({
252-
tenantId,
253-
query = "",
254-
autoConnectSlug = null,
255-
onAutoConnectHandled,
256-
}: {
257-
readonly tenantId: string;
258-
readonly query?: string;
259-
/** A preset slug named by a `/plugins?connect=mcp:<slug>` deep link
260-
* (CL-7141) — once the catalog has loaded, that preset's row gets
261-
* focused so a person lands on the right card without hunting for it.
262-
* Never auto-fires the connect action itself: connecting still takes
263-
* a person's own click, the same as every other card here. */
264-
readonly autoConnectSlug?: string | null;
265-
readonly onAutoConnectHandled?: () => void;
266-
}) {
259+
export function useMcpPresetCatalog(tenantId: string) {
267260
const [presets, setPresets] = useState<readonly McpPreset[]>([]);
268261
const [toolCounts, setToolCounts] = useState<ReadonlyMap<string, number>>(
269262
() => {
@@ -274,86 +267,43 @@ export function McpPresetCardsSection({
274267
},
275268
);
276269
const [loadError, setLoadError] = useState<string | null>(null);
277-
// Whether the presets fetch has resolved at least once — distinct from
278-
// "zero presets": this catalog is the same ~10 curated apps for every
279-
// tenant, so an empty `presets` array before this flips true is a
280-
// loading gap, never a real "nothing to connect" state (CL-6472). The
281-
// section used to `return null` whenever presets were empty regardless
282-
// of why, which let a load-in-progress render as if the whole catalog
283-
// had vanished — this component must never go quiet like that again.
284270
const [loaded, setLoaded] = useState(false);
271+
const [reloadKey, setReloadKey] = useState(0);
285272

286-
function reload() {
273+
useEffect(() => {
274+
let cancelled = false;
275+
setLoaded(false);
287276
listMcpPresets(tenantId)
288277
.then((data) => {
289-
setPresets(data);
290-
setLoadError(null);
278+
if (!cancelled) {
279+
setPresets(data);
280+
setLoadError(null);
281+
}
291282
})
292-
.catch((cause: unknown) => setLoadError(messageOf(cause)))
293-
.finally(() => setLoaded(true));
294-
}
295-
296-
useEffect(() => {
297-
reload();
298-
}, [tenantId]);
283+
.catch((cause: unknown) => {
284+
reportError(cause, {
285+
operation: "plugins.catalog.load-presets",
286+
tenantId,
287+
});
288+
if (!cancelled) {
289+
setPresets([]);
290+
setLoadError(messageOf(cause));
291+
}
292+
})
293+
.finally(() => {
294+
if (!cancelled) setLoaded(true);
295+
});
296+
return () => {
297+
cancelled = true;
298+
};
299+
}, [tenantId, reloadKey]);
299300

300-
useEffect(() => {
301-
if (!loaded || autoConnectSlug === null) return;
302-
if (presets.some((preset) => preset.slug === autoConnectSlug)) {
303-
const row = document.querySelector(
304-
`[data-plugin-slug="${autoConnectSlug}"] button`,
305-
);
306-
(row as HTMLButtonElement | null)?.focus();
301+
function handleChanged(slug: string, toolCount: number | undefined) {
302+
if (toolCount !== undefined) {
303+
setToolCounts((current) => new Map(current).set(slug, toolCount));
307304
}
308-
onAutoConnectHandled?.();
309-
}, [loaded, autoConnectSlug, presets, onAutoConnectHandled]);
310-
311-
const needle = query.trim().toLowerCase();
312-
const visiblePresets = presets.filter(
313-
(preset) =>
314-
needle === "" ||
315-
`${preset.displayName} ${preset.description}`
316-
.toLowerCase()
317-
.includes(needle),
318-
);
305+
setReloadKey((key) => key + 1);
306+
}
319307

320-
return (
321-
<section className="flex flex-col gap-2">
322-
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
323-
Connect apps
324-
</h3>
325-
{loadError !== null ? (
326-
<p className="text-sm text-destructive" role="alert">
327-
{loadError}
328-
</p>
329-
) : !loaded ? (
330-
<p className="text-sm text-muted-foreground">Loading…</p>
331-
) : visiblePresets.length === 0 ? (
332-
<p className="text-sm text-muted-foreground">
333-
{presets.length === 0
334-
? "No apps to connect right now."
335-
: `No app matches "${query.trim()}".`}
336-
</p>
337-
) : (
338-
<div className="border border-border [&>*:last-child]:border-b-0">
339-
{visiblePresets.map((preset) => (
340-
<McpPresetCard
341-
key={preset.slug}
342-
tenantId={tenantId}
343-
preset={preset}
344-
toolCount={toolCounts.get(preset.slug)}
345-
onChanged={(toolCount) => {
346-
if (toolCount !== undefined) {
347-
setToolCounts((prev) =>
348-
new Map(prev).set(preset.slug, toolCount),
349-
);
350-
}
351-
reload();
352-
}}
353-
/>
354-
))}
355-
</div>
356-
)}
357-
</section>
358-
);
308+
return { presets, toolCounts, loaded, loadError, handleChanged };
359309
}

‎packages/plugins-ui/src/plugin-card.tsx‎

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function PluginCard({
3838
: STATUS_CAPTION[plugin.status];
3939

4040
return (
41-
<div className="flex min-h-16 items-center gap-3 border-b border-border px-2 py-3">
41+
<div
42+
className="plugins-catalog-card flex min-h-16 min-w-0 items-center gap-3 px-2 py-3"
43+
data-plugin-card
44+
data-plugin-name={plugin.descriptor.displayName}
45+
>
4246
<PluginLogo
4347
name={plugin.descriptor.displayName}
4448
icon={plugin.descriptor.icon}
@@ -52,33 +56,30 @@ export function PluginCard({
5256
{pluginOutcome(plugin.descriptor.id, plugin.descriptor.displayName)}
5357
</span>
5458
</div>
55-
{/* A dense directory row's status column is core information, not
56-
* an overflow item to drop on narrow viewports (CL-6467). */}
57-
<span className="shrink-0 text-xs text-muted-foreground">{caption}</span>
58-
{plugin.status === "not_connected" ? (
59-
// The same single Connect verb the MCP preset rows use — one
60-
// idiom for "not connected yet" across the whole gallery, never
61-
// a bare "+" glyph that hides the action.
62-
<Button
63-
type="button"
64-
size="sm"
65-
variant="ghost"
66-
aria-label={`Connect ${plugin.descriptor.displayName}`}
67-
onClick={onOpen}
68-
>
69-
Connect
70-
</Button>
71-
) : (
72-
<Button
73-
type="button"
74-
size="sm"
75-
variant="ghost"
76-
aria-label={`Manage ${plugin.descriptor.displayName}`}
77-
onClick={onOpen}
78-
>
79-
Manage
80-
</Button>
81-
)}
59+
<div className="plugins-catalog-card-actions">
60+
<span className="text-xs text-muted-foreground">{caption}</span>
61+
{plugin.status === "not_connected" ? (
62+
<Button
63+
type="button"
64+
size="sm"
65+
variant="ghost"
66+
aria-label={`Connect ${plugin.descriptor.displayName}`}
67+
onClick={onOpen}
68+
>
69+
Connect
70+
</Button>
71+
) : (
72+
<Button
73+
type="button"
74+
size="sm"
75+
variant="ghost"
76+
aria-label={`Manage ${plugin.descriptor.displayName}`}
77+
onClick={onOpen}
78+
>
79+
Manage
80+
</Button>
81+
)}
82+
</div>
8283
</div>
8384
);
8485
}

0 commit comments

Comments
 (0)