History

feat: add react/forbid-elements lint rule (#156)

* feat: add react/forbid-elements lint rule and migrate all raw HTML elements Adds oxlint react/forbid-elements rule banning raw <button>, <input>, <textarea>, <select>, and <table> in favor of the @executor/react UI components. Migrates all 19 affected files to use Button, Input, and Textarea components with appropriate variants and sizes. * style: format * feat: ban raw <label> elements, migrate to Label component

Rhys Sullivan committed 7 months ago

Commit 12a58631 parent cdbaf1b20 files · +185 −134Browse files

Unified · Split

20 files
  1. .oxlintrc.jsonc +15 −1
  2. apps/cloud/src/routes/billing.tsx +4 −2
  3. apps/cloud/src/routes/billing_.plans.tsx +5 −4
  4. apps/cloud/src/web/shell.tsx +17 −10
  5. apps/local/src/web/shell.tsx +11 −8
  6. packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx +10 −8
  7. packages/plugins/mcp/src/react/AddMcpSource.tsx +27 −14
  8. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +8 −8
  9. packages/react/src/components/code-block.tsx +15 −11
  10. packages/react/src/components/expandable-code-block.tsx +5 −3
  11. packages/react/src/components/input.tsx +1 −0
  12. packages/react/src/components/native-select.tsx +1 −0
  13. packages/react/src/components/sidebar.tsx +1 −0
  14. packages/react/src/components/table.tsx +1 −0
  15. packages/react/src/components/textarea.tsx +1 −0
  16. packages/react/src/components/tool-detail.tsx +15 −14
  17. packages/react/src/components/tool-tree.tsx +15 −12
  18. packages/react/src/pages/source-detail.tsx +22 −28
  19. packages/react/src/pages/sources.tsx +7 −9
  20. packages/react/src/plugins/secret-header-auth.tsx +4 −2
.oxlintrc.jsonc+15−1
@@ -1,6 +1,20 @@
1{1{
2 "ignorePatterns": [".astro/"],2 "plugins": ["react"],
3 "rules": {3 "rules": {
4 "typescript/no-explicit-any": "error",4 "typescript/no-explicit-any": "error",
5 "react/forbid-elements": [
6 "error",
7 {
8 "forbid": [
9 { "element": "button", "message": "Use <Button> from @executor/react" },
10 { "element": "input", "message": "Use <Input> from @executor/react" },
11 { "element": "textarea", "message": "Use <Textarea> from @executor/react" },
12 { "element": "select", "message": "Use <Select> or <NativeSelect> from @executor/react" },
13 { "element": "table", "message": "Use <Table> from @executor/react" },
14 { "element": "label", "message": "Use <Label> from @executor/react" },
15 ],
16 },
17 ],
5 },18 },
19 "ignorePatterns": [".astro/"],
6}20}
apps/cloud/src/routes/billing.tsx+4−2
@@ -1,5 +1,6 @@
1import { createFileRoute, Link } from "@tanstack/react-router";1import { createFileRoute, Link } from "@tanstack/react-router";
2import { useCustomer, useListPlans } from "autumn-js/react";2import { useCustomer, useListPlans } from "autumn-js/react";
3import { Button } from "@executor/react/components/button";
3 4
4export const Route = createFileRoute("/billing")({5export const Route = createFileRoute("/billing")({
5 component: BillingPage,6 component: BillingPage,
@@ -87,13 +88,14 @@ function BillingPage() { · 81 unmodified lines
87 </div>88 </div>
88 <div className="flex items-center gap-2">89 <div className="flex items-center gap-2">
89 {activePlan && !isCanceling && (90 {activePlan && !isCanceling && (
90 <button91 <Button
92 variant="ghost"
91 type="button"93 type="button"
92 onClick={() => openCustomerPortal()}94 onClick={() => openCustomerPortal()}
93 className="rounded-md px-3 py-1.5 text-[0.75rem] font-medium text-destructive transition-colors hover:bg-destructive/10"95 className="rounded-md px-3 py-1.5 text-[0.75rem] font-medium text-destructive transition-colors hover:bg-destructive/10"
94 >96 >
95 Cancel plan97 Cancel plan
96 </button>98 </Button>
97 )}99 )}
98 <Link100 <Link
99 to="/billing/plans"101 to="/billing/plans"
apps/cloud/src/routes/billing_.plans.tsx+5−4
@@ -1,6 +1,7 @@
1import { useState } from "react";1import { useState } from "react";
2import { createFileRoute, Link } from "@tanstack/react-router";2import { createFileRoute, Link } from "@tanstack/react-router";
3import { useCustomer, useListPlans } from "autumn-js/react";3import { useCustomer, useListPlans } from "autumn-js/react";
4import { Button } from "@executor/react/components/button";
4 5
5export const Route = createFileRoute("/billing_/plans")({6export const Route = createFileRoute("/billing_/plans")({
6 component: PlansPage,7 component: PlansPage,
@@ -149,16 +150,16 @@ function PlansPage() { · 142 unmodified lines
149 {isCurrent ? "Current plan" : "Scheduled"}150 {isCurrent ? "Current plan" : "Scheduled"}
150 </div>151 </div>
151 ) : isCanceling ? (152 ) : isCanceling ? (
152 <button153 <Button
153 type="button"154 type="button"
154 disabled={loadingPlan !== null}155 disabled={loadingPlan !== null}
155 onClick={() => openCustomerPortal()}156 onClick={() => openCustomerPortal()}
156 className="flex h-9 w-full items-center justify-center rounded-md bg-primary text-[0.8125rem] font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"157 className="flex h-9 w-full items-center justify-center rounded-md bg-primary text-[0.8125rem] font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"
157 >158 >
158 Resume159 Resume
159 </button>160 </Button>
160 ) : (161 ) : (
161 <button162 <Button
162 type="button"163 type="button"
163 disabled={loadingPlan !== null}164 disabled={loadingPlan !== null}
164 onClick={async () => {165 onClick={async () => {
@@ -174,7 +175,7 @@ function PlansPage() { · 9 unmodified lines
174 ].join(" ")}175 ].join(" ")}
175 >176 >
176 {loadingPlan === plan.id ? "Loading…" : label}177 {loadingPlan === plan.id ? "Loading…" : label}
177 </button>178 </Button>
178 )}179 )}
179 </div>180 </div>
180 181
apps/cloud/src/web/shell.tsx+17−10
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; · 2 unmodified lines
3import { useAtomValue, Result } from "@effect-atom/atom-react";3import { useAtomValue, Result } from "@effect-atom/atom-react";
4import { sourcesAtom } from "@executor/react/api/atoms";4import { sourcesAtom } from "@executor/react/api/atoms";
5import { useScope } from "@executor/react/api/scope-context";5import { useScope } from "@executor/react/api/scope-context";
6 6import { Button } from "@executor/react/components/button";
7import { AUTH_PATHS } from "../auth/api";7import { AUTH_PATHS } from "../auth/api";
8import { useAuth } from "./auth";8import { useAuth } from "./auth";
9 9
@@ -109,9 +109,11 @@ function UserFooter() { · 99 unmodified lines
109 )}109 )}
110 </div>110 </div>
111 <form action={AUTH_PATHS.logout} method="post">111 <form action={AUTH_PATHS.logout} method="post">
112 <button112 <Button
113 variant="ghost"
114 size="icon-xs"
113 type="submit"115 type="submit"
114 className="shrink-0 rounded-md p-1 text-muted-foreground/50 transition-colors hover:bg-sidebar-active hover:text-foreground"116 className="shrink-0 text-muted-foreground/50 hover:bg-sidebar-active hover:text-foreground"
115 title="Sign out"117 title="Sign out"
116 >118 >
117 <svg viewBox="0 0 16 16" fill="none" className="size-3.5">119 <svg viewBox="0 0 16 16" fill="none" className="size-3.5">
@@ -123,7 +125,7 @@ function UserFooter() { · 5 unmodified lines
123 strokeLinejoin="round"125 strokeLinejoin="round"
124 />126 />
125 </svg>127 </svg>
126 </button>128 </Button>
127 </form>129 </form>
128 </div>130 </div>
129 </div>131 </div>
@@ -196,6 +198,7 @@ export function Shell() { · 66 unmodified lines
196 {/* Mobile sidebar overlay */}198 {/* Mobile sidebar overlay */}
197 {mobileSidebarOpen && (199 {mobileSidebarOpen && (
198 <div className="fixed inset-0 z-50 flex md:hidden">200 <div className="fixed inset-0 z-50 flex md:hidden">
201 {/* oxlint-disable-next-line react/forbid-elements */}
199 <button202 <button
200 type="button"203 type="button"
201 aria-label="Close navigation"204 aria-label="Close navigation"
@@ -209,11 +212,13 @@ export function Shell() { · 7 unmodified lines
209 executor212 executor
210 </span>213 </span>
211 </Link>214 </Link>
212 <button215 <Button
216 variant="ghost"
217 size="icon-sm"
213 type="button"218 type="button"
214 aria-label="Close navigation"219 aria-label="Close navigation"
215 onClick={() => setMobileSidebarOpen(false)}220 onClick={() => setMobileSidebarOpen(false)}
216 className="size-8 flex items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"221 className="text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"
217 >222 >
218 <svg viewBox="0 0 16 16" className="size-3.5">223 <svg viewBox="0 0 16 16" className="size-3.5">
219 <path224 <path
@@ -223,7 +228,7 @@ export function Shell() { · 3 unmodified lines
223 strokeLinecap="round"228 strokeLinecap="round"
224 />229 />
225 </svg>230 </svg>
226 </button>231 </Button>
227 </div>232 </div>
228 <SidebarContent233 <SidebarContent
229 pathname={pathname}234 pathname={pathname}
@@ -238,11 +243,13 @@ export function Shell() { · 8 unmodified lines
238 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">243 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">
239 {/* Mobile top bar */}244 {/* Mobile top bar */}
240 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">245 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">
241 <button246 <Button
247 variant="outline"
248 size="icon-sm"
242 type="button"249 type="button"
243 aria-label="Open navigation"250 aria-label="Open navigation"
244 onClick={() => setMobileSidebarOpen(true)}251 onClick={() => setMobileSidebarOpen(true)}
245 className="size-8 flex items-center justify-center rounded-md border border-border bg-card hover:bg-accent/50"252 className="bg-card hover:bg-accent/50"
246 >253 >
247 <svg viewBox="0 0 16 16" className="size-4">254 <svg viewBox="0 0 16 16" className="size-4">
248 <path255 <path
@@ -252,7 +259,7 @@ export function Shell() { · 3 unmodified lines
252 strokeLinecap="round"259 strokeLinecap="round"
253 />260 />
254 </svg>261 </svg>
255 </button>262 </Button>
256 <Link to="/" className="flex items-center gap-1.5">263 <Link to="/" className="flex items-center gap-1.5">
257 <span className="font-display text-base tracking-tight text-foreground">executor</span>264 <span className="font-display text-base tracking-tight text-foreground">executor</span>
258 </Link>265 </Link>
apps/local/src/web/shell.tsx+11−8
@@ -402,6 +402,7 @@ export function Shell() { · 401 unmodified lines
402 {/* Mobile sidebar overlay */}402 {/* Mobile sidebar overlay */}
403 {mobileSidebarOpen && (403 {mobileSidebarOpen && (
404 <div className="fixed inset-0 z-50 flex md:hidden">404 <div className="fixed inset-0 z-50 flex md:hidden">
405 {/* oxlint-disable-next-line react/forbid-elements */}
405 <button406 <button
406 type="button"407 type="button"
407 aria-label="Close navigation"408 aria-label="Close navigation"
@@ -415,11 +416,12 @@ export function Shell() { · 7 unmodified lines
415 executor416 executor
416 </span>417 </span>
417 </Link>418 </Link>
418 <button419 <Button
419 type="button"420 variant="ghost"
421 size="icon-sm"
420 aria-label="Close navigation"422 aria-label="Close navigation"
421 onClick={() => setMobileSidebarOpen(false)}423 onClick={() => setMobileSidebarOpen(false)}
422 className="size-8 flex items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"424 className="text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"
423 >425 >
424 <svg viewBox="0 0 16 16" className="size-3.5">426 <svg viewBox="0 0 16 16" className="size-3.5">
425 <path427 <path
@@ -429,7 +431,7 @@ export function Shell() { · 3 unmodified lines
429 strokeLinecap="round"431 strokeLinecap="round"
430 />432 />
431 </svg>433 </svg>
432 </button>434 </Button>
433 </div>435 </div>
434 <SidebarContent436 <SidebarContent
435 pathname={pathname}437 pathname={pathname}
@@ -447,11 +449,12 @@ export function Shell() { · 11 unmodified lines
447 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">449 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">
448 {/* Mobile top bar */}450 {/* Mobile top bar */}
449 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">451 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">
450 <button452 <Button
451 type="button"453 variant="outline"
454 size="icon-sm"
452 aria-label="Open navigation"455 aria-label="Open navigation"
453 onClick={() => setMobileSidebarOpen(true)}456 onClick={() => setMobileSidebarOpen(true)}
454 className="size-8 flex items-center justify-center rounded-md border border-border bg-card hover:bg-accent/50"457 className="bg-card hover:bg-accent/50"
455 >458 >
456 <svg viewBox="0 0 16 16" className="size-4">459 <svg viewBox="0 0 16 16" className="size-4">
457 <path460 <path
@@ -461,7 +464,7 @@ export function Shell() { · 3 unmodified lines
461 strokeLinecap="round"464 strokeLinecap="round"
462 />465 />
463 </svg>466 </svg>
464 </button>467 </Button>
465 <Link to="/" className="flex items-center gap-1.5">468 <Link to="/" className="flex items-center gap-1.5">
466 <span className="font-display text-base tracking-tight text-foreground">executor</span>469 <span className="font-display text-base tracking-tight text-foreground">executor</span>
467 </Link>470 </Link>
packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx+10−8
@@ -481,7 +481,7 @@ export default function AddGoogleDiscoverySource(props: { · 480 unmodified lines
481 } finally {481 } finally {
482 setLoadingProbe(false);482 setLoadingProbe(false);
483 }483 }
484 }, [discoveryUrl, doProbe, name]);484 }, [discoveryUrl, doProbe, name, scopeId]);
485 485
486 const autoProbed = useRef(false);486 const autoProbed = useRef(false);
487 useEffect(() => {487 useEffect(() => {
@@ -573,7 +573,7 @@ export default function AddGoogleDiscoverySource(props: { · 85 unmodified lines
573 setError(e instanceof Error ? e.message : "Failed to add source");573 setError(e instanceof Error ? e.message : "Failed to add source");
574 setAdding(false);574 setAdding(false);
575 }575 }
576 }, [probe, doAdd, name, discoveryUrl, authKind, oauthAuth, props]);576 }, [probe, doAdd, name, discoveryUrl, authKind, oauthAuth, props, scopeId]);
577 577
578 const addDisabled =578 const addDisabled =
579 !probe || adding || (authKind === "oauth2" && (!canUseOAuth || oauthAuth === null));579 !probe || adding || (authKind === "oauth2" && (!canUseOAuth || oauthAuth === null));
@@ -598,11 +598,12 @@ export default function AddGoogleDiscoverySource(props: { · 18 unmodified lines
598 {GOOGLE_DISCOVERY_TEMPLATES.map((template) => {598 {GOOGLE_DISCOVERY_TEMPLATES.map((template) => {
599 const selected = template.id === selectedTemplateId;599 const selected = template.id === selectedTemplateId;
600 return (600 return (
601 <button601 <Button
602 key={template.id}602 key={template.id}
603 variant="ghost"
603 type="button"604 type="button"
604 onClick={() => applyTemplate(template)}605 onClick={() => applyTemplate(template)}
605 className={`relative rounded-xl border px-4 py-3 text-left transition-colors ${606 className={`relative h-auto rounded-xl border px-4 py-3 text-left transition-colors ${
606 selected607 selected
607 ? "border-primary bg-primary/5 shadow-[0_0_0_1px_rgba(0,0,0,0.02)]"608 ? "border-primary bg-primary/5 shadow-[0_0_0_1px_rgba(0,0,0,0.02)]"
608 : "border-border bg-card hover:border-primary/30 hover:bg-card/80"609 : "border-border bg-card hover:border-primary/30 hover:bg-card/80"
@@ -628,7 +629,7 @@ export default function AddGoogleDiscoverySource(props: { · 19 unmodified lines
628 </p>629 </p>
629 <div className="h-px flex-1 bg-border/70" />630 <div className="h-px flex-1 bg-border/70" />
630 </div>631 </div>
631 </button>632 </Button>
632 );633 );
633 })}634 })}
634 </div>635 </div>
@@ -737,12 +738,13 @@ export default function AddGoogleDiscoverySource(props: { · 102 unmodified lines
737 </p>738 </p>
738 {canUseOAuth && (probe?.scopes.length ?? 0) > 0 && (739 {canUseOAuth && (probe?.scopes.length ?? 0) > 0 && (
739 <CollapsibleTrigger asChild>740 <CollapsibleTrigger asChild>
740 <button741 <Button
742 variant="link"
741 type="button"743 type="button"
742 className="text-xs font-medium text-primary hover:underline"744 className="h-auto p-0 text-xs font-medium text-primary hover:underline"
743 >745 >
744 {showScopes ? "Hide scopes" : "View scopes"}746 {showScopes ? "Hide scopes" : "View scopes"}
745 </button>747 </Button>
746 </CollapsibleTrigger>748 </CollapsibleTrigger>
747 )}749 )}
748 </div>750 </div>
packages/plugins/mcp/src/react/AddMcpSource.tsx+27−14
@@ -8,6 +8,7 @@ import { Label } from "@executor/react/components/label"; · 7 unmodified lines
8import { Badge } from "@executor/react/components/badge";8import { Badge } from "@executor/react/components/badge";
9import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";9import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";
10import { Spinner } from "@executor/react/components/spinner";10import { Spinner } from "@executor/react/components/spinner";
11import { Textarea } from "@executor/react/components/textarea";
11import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth";12import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth";
12import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";13import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";
13import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms";14import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms";
@@ -420,7 +421,17 @@ export default function AddMcpSource(props: { · 406 unmodified lines
420 error: e instanceof Error ? e.message : "Failed to add source",421 error: e instanceof Error ? e.message : "Failed to add source",
421 });422 });
422 }423 }
423 }, [probe, remoteAuthMode, remoteHeaderAuth, remoteHeaders, tokens, state.url, doAdd, props]);424 }, [
425 probe,
426 remoteAuthMode,
427 remoteHeaderAuth,
428 remoteHeaders,
429 tokens,
430 state.url,
431 doAdd,
432 props,
433 scopeId,
434 ]);
424 435
425 // ---- Stdio actions ----436 // ---- Stdio actions ----
426 437
@@ -483,7 +494,8 @@ export default function AddMcpSource(props: { · 56 unmodified lines
483 494
484 {/* Transport toggle */}495 {/* Transport toggle */}
485 <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1">496 <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1">
486 <button497 <Button
498 variant="ghost"
487 type="button"499 type="button"
488 onClick={() => setTransport("remote")}500 onClick={() => setTransport("remote")}
489 className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${501 className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
@@ -493,8 +505,9 @@ export default function AddMcpSource(props: { · 3 unmodified lines
493 }`}505 }`}
494 >506 >
495 Remote507 Remote
496 </button>508 </Button>
497 <button509 <Button
510 variant="ghost"
498 type="button"511 type="button"
499 onClick={() => setTransport("stdio")}512 onClick={() => setTransport("stdio")}
500 className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${513 className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
@@ -504,7 +517,7 @@ export default function AddMcpSource(props: { · 3 unmodified lines
504 }`}517 }`}
505 >518 >
506 Stdio519 Stdio
507 </button>520 </Button>
508 </div>521 </div>
509 522
510 {transport === "remote" ? (523 {transport === "remote" ? (
@@ -603,7 +616,7 @@ export default function AddMcpSource(props: { · 92 unmodified lines
603 className="gap-1.5"616 className="gap-1.5"
604 >617 >
605 {!probe.requiresOAuth && (618 {!probe.requiresOAuth && (
606 <label619 <Label
607 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${620 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
608 remoteAuthMode === "none"621 remoteAuthMode === "none"
609 ? "border-primary/50 bg-primary/[0.03]"622 ? "border-primary/50 bg-primary/[0.03]"
@@ -615,10 +628,10 @@ export default function AddMcpSource(props: { · 5 unmodified lines
615 <span className="ml-auto text-[10px] text-muted-foreground">628 <span className="ml-auto text-[10px] text-muted-foreground">
616 no auth header629 no auth header
617 </span>630 </span>
618 </label>631 </Label>
619 )}632 )}
620 633
621 <label634 <Label
622 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${635 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
623 remoteAuthMode === "header"636 remoteAuthMode === "header"
624 ? "border-primary/50 bg-primary/[0.03]"637 ? "border-primary/50 bg-primary/[0.03]"
@@ -630,10 +643,10 @@ export default function AddMcpSource(props: { · 5 unmodified lines
630 <span className="ml-auto text-[10px] text-muted-foreground">643 <span className="ml-auto text-[10px] text-muted-foreground">
631 use a secret-backed auth header644 use a secret-backed auth header
632 </span>645 </span>
633 </label>646 </Label>
634 647
635 {probe.requiresOAuth && (648 {probe.requiresOAuth && (
636 <label649 <Label
637 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${650 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
638 remoteAuthMode === "oauth2"651 remoteAuthMode === "oauth2"
639 ? "border-primary/50 bg-primary/[0.03]"652 ? "border-primary/50 bg-primary/[0.03]"
@@ -645,7 +658,7 @@ export default function AddMcpSource(props: { · 5 unmodified lines
645 <span className="ml-auto text-[10px] text-muted-foreground">658 <span className="ml-auto text-[10px] text-muted-foreground">
646 sign in with the server&apos;s OAuth flow659 sign in with the server&apos;s OAuth flow
647 </span>660 </span>
648 </label>661 </Label>
649 )}662 )}
650 </RadioGroup>663 </RadioGroup>
651 664
@@ -936,12 +949,12 @@ export default function AddMcpSource(props: { · 284 unmodified lines
936 Environment variables{" "}949 Environment variables{" "}
937 <span className="text-muted-foreground font-normal">(optional)</span>950 <span className="text-muted-foreground font-normal">(optional)</span>
938 </Label>951 </Label>
939 <textarea952 <Textarea
940 value={stdioEnv}953 value={stdioEnv}
941 onChange={(e) => setStdioEnv(e.target.value)}954 onChange={(e) => setStdioEnv((e.target as HTMLTextAreaElement).value)}
942 placeholder={"KEY=value\nANOTHER=value"}955 placeholder={"KEY=value\nANOTHER=value"}
943 rows={3}956 rows={3}
944 className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"957 className="font-mono text-sm"
945 />958 />
946 <p className="text-[12px] text-muted-foreground">One per line, KEY=value format.</p>959 <p className="text-[12px] text-muted-foreground">One per line, KEY=value format.</p>
947 </div>960 </div>
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+8−8
@@ -296,7 +296,7 @@ export default function AddOpenApiSource(props: { · 295 unmodified lines
296 {servers.map((s, i) => {296 {servers.map((s, i) => {
297 const url = s.url ?? "";297 const url = s.url ?? "";
298 return (298 return (
299 <label299 <Label
300 key={i}300 key={i}
301 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${301 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
302 baseUrl === url302 baseUrl === url
@@ -306,7 +306,7 @@ export default function AddOpenApiSource(props: { · 3 unmodified lines
306 >306 >
307 <RadioGroupItem value={url} />307 <RadioGroupItem value={url} />
308 <span className="font-mono text-xs text-foreground truncate">{url}</span>308 <span className="font-mono text-xs text-foreground truncate">{url}</span>
309 </label>309 </Label>
310 );310 );
311 })}311 })}
312 </RadioGroup>312 </RadioGroup>
@@ -345,7 +345,7 @@ export default function AddOpenApiSource(props: { · 32 unmodified lines
345 className="gap-1.5"345 className="gap-1.5"
346 >346 >
347 {presets.map((preset, i) => (347 {presets.map((preset, i) => (
348 <label348 <Label
349 key={i}349 key={i}
350 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${350 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
351 presetIndex === i351 presetIndex === i
@@ -361,10 +361,10 @@ export default function AddOpenApiSource(props: { · 9 unmodified lines
361 {preset.secretHeaders.length > 1 ? "s" : ""}361 {preset.secretHeaders.length > 1 ? "s" : ""}
362 </span>362 </span>
363 )}363 )}
364 </label>364 </Label>
365 ))}365 ))}
366 366
367 <label367 <Label
368 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${368 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
369 presetIndex === -2369 presetIndex === -2
370 ? "border-primary/50 bg-primary/[0.03]"370 ? "border-primary/50 bg-primary/[0.03]"
@@ -376,9 +376,9 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines
376 <span className="ml-auto text-[10px] text-muted-foreground">376 <span className="ml-auto text-[10px] text-muted-foreground">
377 configure manually377 configure manually
378 </span>378 </span>
379 </label>379 </Label>
380 380
381 <label381 <Label
382 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${382 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
383 presetIndex === -1383 presetIndex === -1
384 ? "border-primary/50 bg-primary/[0.03]"384 ? "border-primary/50 bg-primary/[0.03]"
@@ -388,7 +388,7 @@ export default function AddOpenApiSource(props: { · 3 unmodified lines
388 <RadioGroupItem value="-1" />388 <RadioGroupItem value="-1" />
389 <span className="text-xs font-medium text-foreground">None</span>389 <span className="text-xs font-medium text-foreground">None</span>
390 <span className="ml-auto text-[10px] text-muted-foreground">skip auth</span>390 <span className="ml-auto text-[10px] text-muted-foreground">skip auth</span>
391 </label>391 </Label>
392 </RadioGroup>392 </RadioGroup>
393 )}393 )}
394 394
packages/react/src/components/code-block.tsx+15−11
@@ -3,6 +3,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime"; · 2 unmodified lines
3import { toJsxRuntime } from "hast-util-to-jsx-runtime";3import { toJsxRuntime } from "hast-util-to-jsx-runtime";
4import { getHighlighter, resolveLang, THEME } from "../lib/shiki";4import { getHighlighter, resolveLang, THEME } from "../lib/shiki";
5import { cn } from "../lib/utils";5import { cn } from "../lib/utils";
6import { Button } from "./button";
6 7
7// ---------------------------------------------------------------------------8// ---------------------------------------------------------------------------
8// Language detection9// Language detection
@@ -113,26 +114,28 @@ export function CodeBlock(props: { · 104 unmodified lines
113 <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">114 <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
114 {title}115 {title}
115 </span>116 </span>
116 <button117 <Button
117 type="button"118 variant="ghost"
119 size="icon-xs"
118 onClick={handleCopy}120 onClick={handleCopy}
119 className="size-6 flex items-center justify-center text-muted-foreground/30 hover:text-muted-foreground transition-colors"121 className="text-muted-foreground/30 hover:text-muted-foreground"
120 title="Copy"122 title="Copy"
121 >123 >
122 {copied ? <CheckIcon /> : <CopyIcon />}124 {copied ? <CheckIcon /> : <CopyIcon />}
123 </button>125 </Button>
124 </div>126 </div>
125 )}127 )}
126 <div className="group relative">128 <div className="group relative">
127 {!title && (129 {!title && (
128 <button130 <Button
129 type="button"131 variant="ghost"
132 size="icon-xs"
130 onClick={handleCopy}133 onClick={handleCopy}
131 className="absolute right-2 top-2 z-10 rounded-md border border-border bg-card/90 p-1.5 text-muted-foreground/40 opacity-0 backdrop-blur-sm hover:text-foreground group-hover:opacity-100 transition-opacity"134 className="absolute right-2 top-2 z-10 rounded-md border border-border bg-card/90 p-1.5 text-muted-foreground/40 opacity-0 backdrop-blur-sm hover:text-foreground group-hover:opacity-100 transition-opacity"
132 title="Copy to clipboard"135 title="Copy to clipboard"
133 >136 >
134 {copied ? <CheckIcon /> : <CopyIcon />}137 {copied ? <CheckIcon /> : <CopyIcon />}
135 </button>138 </Button>
136 )}139 )}
137 140
138 <div141 <div
@@ -148,13 +151,14 @@ export function CodeBlock(props: { · 9 unmodified lines
148 151
149 {isLong && !expanded && (152 {isLong && !expanded && (
150 <div className="absolute bottom-0 left-0 right-0 flex justify-center bg-gradient-to-t from-card/90 to-transparent pb-2 pt-8">153 <div className="absolute bottom-0 left-0 right-0 flex justify-center bg-gradient-to-t from-card/90 to-transparent pb-2 pt-8">
151 <button154 <Button
152 type="button"155 variant="outline"
156 size="sm"
153 onClick={() => setExpanded(true)}157 onClick={() => setExpanded(true)}
154 className="rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground hover:text-foreground transition-colors"158 className="text-[11px] font-medium text-muted-foreground hover:text-foreground"
155 >159 >
156 Show all ({lines.length} lines)160 Show all ({lines.length} lines)
157 </button>161 </Button>
158 </div>162 </div>
159 )}163 )}
160 </div>164 </div>
packages/react/src/components/expandable-code-block.tsx+5−3
@@ -1,6 +1,7 @@
1import { useCallback, useEffect, useMemo, useState, startTransition } from "react";1import { useCallback, useEffect, useMemo, useState, startTransition } from "react";
2import { getHighlighter, THEME } from "../lib/shiki";2import { getHighlighter, THEME } from "../lib/shiki";
3import { cn } from "../lib/utils";3import { cn } from "../lib/utils";
4import { Button } from "./button";
4import type { ThemedToken } from "shiki/core";5import type { ThemedToken } from "shiki/core";
5 6
6// ---------------------------------------------------------------------------7// ---------------------------------------------------------------------------
@@ -371,14 +372,15 @@ export function ExpandableCodeBlock(props: { · 364 unmodified lines
371 return (372 return (
372 <div className={cn("rounded-lg border border-border/40 bg-card/60 overflow-hidden", className)}>373 <div className={cn("rounded-lg border border-border/40 bg-card/60 overflow-hidden", className)}>
373 <div className="group relative">374 <div className="group relative">
374 <button375 <Button
375 type="button"376 variant="ghost"
377 size="icon-xs"
376 onClick={handleCopy}378 onClick={handleCopy}
377 className="absolute right-2 top-2 z-10 rounded-md border border-border bg-card/90 p-1.5 text-muted-foreground/40 opacity-0 backdrop-blur-sm hover:text-foreground group-hover:opacity-100 transition-opacity"379 className="absolute right-2 top-2 z-10 rounded-md border border-border bg-card/90 p-1.5 text-muted-foreground/40 opacity-0 backdrop-blur-sm hover:text-foreground group-hover:opacity-100 transition-opacity"
378 title="Copy to clipboard"380 title="Copy to clipboard"
379 >381 >
380 {copied ? <CheckIcon /> : <CopyIcon />}382 {copied ? <CheckIcon /> : <CopyIcon />}
381 </button>383 </Button>
382 384
383 <pre className="overflow-auto p-3 font-mono text-[0.75rem] leading-6 !bg-transparent">385 <pre className="overflow-auto p-3 font-mono text-[0.75rem] leading-6 !bg-transparent">
384 <code>386 <code>
packages/react/src/components/input.tsx+1−0
@@ -4,6 +4,7 @@ import { cn } from "../lib/utils"; · 3 unmodified lines
4 4
5function Input({ className, type, ...props }: React.ComponentProps<"input">) {5function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 return (6 return (
7 // oxlint-disable-next-line react/forbid-elements
7 <input8 <input
8 type={type}9 type={type}
9 data-slot="input"10 data-slot="input"
packages/react/src/components/native-select.tsx+1−0
@@ -13,6 +13,7 @@ function NativeSelect({ · 12 unmodified lines
13 className="group/native-select relative w-fit has-[select:disabled]:opacity-50"13 className="group/native-select relative w-fit has-[select:disabled]:opacity-50"
14 data-slot="native-select-wrapper"14 data-slot="native-select-wrapper"
15 >15 >
16 {/* oxlint-disable-next-line react/forbid-elements */}
16 <select17 <select
17 data-slot="native-select"18 data-slot="native-select"
18 data-size={size}19 data-size={size}
packages/react/src/components/sidebar.tsx+1−0
@@ -263,6 +263,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { · 262 unmodified lines
263 const { toggleSidebar } = useSidebar();263 const { toggleSidebar } = useSidebar();
264 264
265 return (265 return (
266 // oxlint-disable-next-line react/forbid-elements
266 <button267 <button
267 data-sidebar="rail"268 data-sidebar="rail"
268 data-slot="sidebar-rail"269 data-slot="sidebar-rail"
packages/react/src/components/table.tsx+1−0
@@ -7,6 +7,7 @@ import { cn } from "../lib/utils"; · 6 unmodified lines
7function Table({ className, ...props }: React.ComponentProps<"table">) {7function Table({ className, ...props }: React.ComponentProps<"table">) {
8 return (8 return (
9 <div data-slot="table-container" className="relative w-full overflow-x-auto">9 <div data-slot="table-container" className="relative w-full overflow-x-auto">
10 {/* oxlint-disable-next-line react/forbid-elements */}
10 <table11 <table
11 data-slot="table"12 data-slot="table"
12 className={cn("w-full caption-bottom text-sm", className)}13 className={cn("w-full caption-bottom text-sm", className)}
packages/react/src/components/textarea.tsx+1−0
@@ -4,6 +4,7 @@ import { cn } from "../lib/utils"; · 3 unmodified lines
4 4
5function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {5function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 return (6 return (
7 // oxlint-disable-next-line react/forbid-elements
7 <textarea8 <textarea
8 data-slot="textarea"9 data-slot="textarea"
9 className={cn(10 className={cn(
packages/react/src/components/tool-detail.tsx+15−14
@@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; · 1 unmodified line
2import { useAtomValue, Result } from "@effect-atom/atom-react";2import { useAtomValue, Result } from "@effect-atom/atom-react";
3import { toolSchemaAtom } from "../api/atoms";3import { toolSchemaAtom } from "../api/atoms";
4import { ScopeId, ToolId } from "@executor/sdk";4import { ScopeId, ToolId } from "@executor/sdk";
5 5import { Button } from "./button";
6import { Markdown } from "./markdown";6import { Markdown } from "./markdown";
7import { SchemaExplorer } from "./schema-explorer";7import { SchemaExplorer } from "./schema-explorer";
8import { ExpandableCodeBlock } from "./expandable-code-block";8import { ExpandableCodeBlock } from "./expandable-code-block";
@@ -16,19 +16,20 @@ function CopyButton(props: { text: string; label?: string }) { · 7 unmodified lines
16 const [copied, setCopied] = useState(false);16 const [copied, setCopied] = useState(false);
17 17
18 return (18 return (
19 <button19 <Button
20 type="button"20 variant="ghost"
21 size="icon-xs"
21 onClick={() => {22 onClick={() => {
22 void navigator.clipboard.writeText(props.text).then(() => {23 void navigator.clipboard.writeText(props.text).then(() => {
23 setCopied(true);24 setCopied(true);
24 setTimeout(() => setCopied(false), 1500);25 setTimeout(() => setCopied(false), 1500);
25 });26 });
26 }}27 }}
27 className="size-6 shrink-0 flex items-center justify-center text-muted-foreground/30 hover:text-muted-foreground transition-colors"28 className="shrink-0 text-muted-foreground/30 hover:text-muted-foreground transition-colors"
28 title={props.label ?? "Copy"}29 title={props.label ?? "Copy"}
29 >30 >
30 {copied ? <Check className="size-3.5 shrink-0" /> : <Copy className="size-3.5 shrink-0" />}31 {copied ? <Check className="size-3.5 shrink-0" /> : <Copy className="size-3.5 shrink-0" />}
31 </button>32 </Button>
32 );33 );
33}34}
34 35
@@ -80,7 +81,7 @@ export function ToolDetail(props: { · 45 unmodified lines
80 outputTypeScript: v.outputTypeScript ? `type Output = ${v.outputTypeScript}` : null,81 outputTypeScript: v.outputTypeScript ? `type Output = ${v.outputTypeScript}` : null,
81 definitions,82 definitions,
82 };83 };
83 }, [toolContract, props.toolName]);84 }, [toolContract]);
84 85
85 const crumbs = breadcrumbParts(props.toolName);86 const crumbs = breadcrumbParts(props.toolName);
86 const displayName = friendlyName(props.toolName);87 const displayName = friendlyName(props.toolName);
@@ -112,34 +113,34 @@ export function ToolDetail(props: { · 25 unmodified lines
112 113
113 {/* Tabs */}114 {/* Tabs */}
114 <div className="mt-3 flex gap-4" role="tablist">115 <div className="mt-3 flex gap-4" role="tablist">
115 <button116 <Button
116 type="button"117 variant="ghost"
117 role="tab"118 role="tab"
118 aria-selected={tab === "schema"}119 aria-selected={tab === "schema"}
119 onClick={() => setTab("schema")}120 onClick={() => setTab("schema")}
120 className={[121 className={[
121 "border-b-2 pb-2.5 text-sm font-medium transition-colors",122 "border-b-2 pb-2.5 text-sm font-medium transition-colors rounded-none",
122 tab === "schema"123 tab === "schema"
123 ? "border-primary text-foreground"124 ? "border-primary text-foreground"
124 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",125 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",
125 ].join(" ")}126 ].join(" ")}
126 >127 >
127 Schema128 Schema
128 </button>129 </Button>
129 <button130 <Button
130 type="button"131 variant="ghost"
131 role="tab"132 role="tab"
132 aria-selected={tab === "typescript"}133 aria-selected={tab === "typescript"}
133 onClick={() => setTab("typescript")}134 onClick={() => setTab("typescript")}
134 className={[135 className={[
135 "border-b-2 pb-2.5 text-sm font-medium transition-colors",136 "border-b-2 pb-2.5 text-sm font-medium transition-colors rounded-none",
136 tab === "typescript"137 tab === "typescript"
137 ? "border-primary text-foreground"138 ? "border-primary text-foreground"
138 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",139 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",
139 ].join(" ")}140 ].join(" ")}
140 >141 >
141 TypeScript142 TypeScript
142 </button>143 </Button>
143 </div>144 </div>
144 </div>145 </div>
145 </div>146 </div>
packages/react/src/components/tool-tree.tsx+15−12
@@ -1,4 +1,6 @@
1import { useEffect, useMemo, useRef, useState } from "react";1import { useEffect, useMemo, useRef, useState } from "react";
2import { Button } from "./button";
3import { Input } from "./input";
2 4
3// ---------------------------------------------------------------------------5// ---------------------------------------------------------------------------
4// Types6// Types
@@ -161,8 +163,8 @@ function TreeNodeView(props: { · 156 unmodified lines
161 163
162 return (164 return (
163 <div>165 <div>
164 <button166 <Button
165 type="button"167 variant="ghost"
166 onClick={() => setOpen((v) => !v)}168 onClick={() => setOpen((v) => !v)}
167 aria-expanded={open}169 aria-expanded={open}
168 className="group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40 text-left"170 className="group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40 text-left"
@@ -184,7 +186,7 @@ function TreeNodeView(props: { · 15 unmodified lines
184 <span className="shrink-0 tabular-nums text-[10px] text-muted-foreground/25">186 <span className="shrink-0 tabular-nums text-[10px] text-muted-foreground/25">
185 {leafCount}187 {leafCount}
186 </span>188 </span>
187 </button>189 </Button>
188 190
189 {open && hasChildren && (191 {open && hasChildren && (
190 <div className="relative flex flex-col gap-px">192 <div className="relative flex flex-col gap-px">
@@ -232,9 +234,9 @@ function ToolLeafItem(props: { · 41 unmodified lines
232 }, [props.active]);234 }, [props.active]);
233 235
234 return (236 return (
235 <button237 <Button
236 ref={ref}238 ref={ref}
237 type="button"239 variant="ghost"
238 onClick={props.onSelect}240 onClick={props.onSelect}
239 className={[241 className={[
240 "group flex h-auto w-full items-center gap-2 rounded-md py-1.5 pr-2.5 text-left",242 "group flex h-auto w-full items-center gap-2 rounded-md py-1.5 pr-2.5 text-left",
@@ -255,7 +257,7 @@ function ToolLeafItem(props: { · 14 unmodified lines
255 <span className="flex-1 truncate font-mono text-[12px]">257 <span className="flex-1 truncate font-mono text-[12px]">
256 {highlightMatch(label, props.search)}258 {highlightMatch(label, props.search)}
257 </span>259 </span>
258 </button>260 </Button>
259 );261 );
260}262}
261 263
@@ -313,21 +315,22 @@ export function ToolTree(props: { · 51 unmodified lines
313 strokeLinecap="round"315 strokeLinecap="round"
314 />316 />
315 </svg>317 </svg>
316 <input318 <Input
317 ref={searchRef}319 ref={searchRef}
318 value={search}320 value={search}
319 onChange={(e) => setSearch(e.target.value)}321 onChange={(e) => setSearch(e.target.value)}
320 placeholder={`Filter ${props.tools.length} tools…`}322 placeholder={`Filter ${props.tools.length} tools…`}
321 className="min-w-0 flex-1 border-0 bg-transparent p-0 text-[13px] shadow-none outline-none placeholder:text-muted-foreground/40"323 className="min-w-0 flex-1 border-0 bg-transparent p-0 text-[13px] shadow-none outline-none placeholder:text-muted-foreground/40 h-auto rounded-none focus-visible:ring-0 focus-visible:border-transparent"
322 />324 />
323 {search.length > 0 ? (325 {search.length > 0 ? (
324 <button326 <Button
325 type="button"327 variant="ghost"
328 size="icon-xs"
326 onClick={() => setSearch("")}329 onClick={() => setSearch("")}
327 className="size-5 shrink-0 flex items-center justify-center text-muted-foreground/40 hover:text-foreground"330 className="size-5 shrink-0 text-muted-foreground/40 hover:text-foreground"
328 >331 >
329 ×332 ×
330 </button>333 </Button>
331 ) : (334 ) : (
332 <kbd className="shrink-0 rounded border border-border bg-muted px-1 py-px text-[10px] leading-none text-muted-foreground/50">335 <kbd className="shrink-0 rounded border border-border bg-muted px-1 py-px text-[10px] leading-none text-muted-foreground/50">
333 /336 /
packages/react/src/pages/source-detail.tsx+22−28
@@ -13,6 +13,7 @@ import { ToolDetail, ToolDetailEmpty } from "../components/tool-detail"; · 12 unmodified lines
13import type { ToolSummary } from "../components/tool-tree";13import type { ToolSummary } from "../components/tool-tree";
14import { useScope } from "../hooks/use-scope";14import { useScope } from "../hooks/use-scope";
15import type { SourcePlugin } from "../plugins/source-plugin";15import type { SourcePlugin } from "../plugins/source-plugin";
16import { Button } from "../components/button";
16 17
17export function SourceDetailPage(props: {18export function SourceDetailPage(props: {
18 namespace: string;19 namespace: string;
@@ -131,34 +132,26 @@ export function SourceDetailPage(props: { · 112 unmodified lines
131 132
132 <div className="flex shrink-0 items-center gap-2">133 <div className="flex shrink-0 items-center gap-2">
133 {canEdit && editPlugin && !editing && (134 {canEdit && editPlugin && !editing && (
134 <button135 <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
135 type="button"
136 onClick={() => setEditing(true)}
137 className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50"
138 >
139 Edit136 Edit
140 </button>137 </Button>
141 )}138 )}
142 139
143 {editing && (140 {editing && (
144 <button141 <Button variant="outline" size="sm" onClick={() => setEditing(false)}>
145 type="button"
146 onClick={() => setEditing(false)}
147 className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-accent"
148 >
149 Back to tools142 Back to tools
150 </button>143 </Button>
151 )}144 )}
152 145
153 {canRefresh && !editing && (146 {canRefresh && !editing && (
154 <button147 <Button
155 type="button"148 variant="outline"
149 size="sm"
156 onClick={() => void handleRefresh()}150 onClick={() => void handleRefresh()}
157 disabled={refreshing}151 disabled={refreshing}
158 className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50"
159 >152 >
160 {refreshing ? "Refreshing..." : "Refresh"}153 {refreshing ? "Refreshing..." : "Refresh"}
161 </button>154 </Button>
162 )}155 )}
163 156
164 {canRemove &&157 {canRemove &&
@@ -166,31 +159,32 @@ export function SourceDetailPage(props: { · 1 unmodified line
166 (confirmDelete ? (159 (confirmDelete ? (
167 <div className="flex items-center gap-2">160 <div className="flex items-center gap-2">
168 <span className="text-[11px] font-medium text-destructive">Confirm?</span>161 <span className="text-[11px] font-medium text-destructive">Confirm?</span>
169 <button162 <Button
170 type="button"163 variant="outline"
164 size="sm"
171 onClick={() => setConfirmDelete(false)}165 onClick={() => setConfirmDelete(false)}
172 disabled={deleting}166 disabled={deleting}
173 className="inline-flex items-center rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50"
174 >167 >
175 Cancel168 Cancel
176 </button>169 </Button>
177 <button170 <Button
178 type="button"171 variant="destructive"
172 size="sm"
179 onClick={() => void handleDelete()}173 onClick={() => void handleDelete()}
180 disabled={deleting}174 disabled={deleting}
181 className="inline-flex items-center rounded-md border border-destructive/30 bg-destructive/10 px-2.5 py-1 text-[12px] font-medium text-destructive transition-colors hover:bg-destructive/20 disabled:opacity-50"
182 >175 >
183 {deleting ? "Deleting..." : "Delete"}176 {deleting ? "Deleting..." : "Delete"}
184 </button>177 </Button>
185 </div>178 </div>
186 ) : (179 ) : (
187 <button180 <Button
188 type="button"181 variant="outline"
182 size="sm"
189 onClick={() => setConfirmDelete(true)}183 onClick={() => setConfirmDelete(true)}
190 className="inline-flex items-center rounded-md border border-destructive/30 bg-background px-2.5 py-1 text-[12px] font-medium text-destructive transition-colors hover:bg-destructive/10"184 className="border-destructive/30 text-destructive hover:bg-destructive/10"
191 >185 >
192 Delete186 Delete
193 </button>187 </Button>
194 ))}188 ))}
195 </div>189 </div>
196 </div>190 </div>
packages/react/src/pages/sources.tsx+7−9
@@ -5,6 +5,8 @@ import { sourcesAtom, detectSource } from "../api/atoms"; · 4 unmodified lines
5import { useScope } from "../hooks/use-scope";5import { useScope } from "../hooks/use-scope";
6import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin";6import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin";
7import { McpInstallCard } from "../components/mcp-install-card";7import { McpInstallCard } from "../components/mcp-install-card";
8import { Button } from "../components/button";
9import { Input } from "../components/input";
8 10
9const KIND_TO_PLUGIN_KEY: Record<string, string> = {11const KIND_TO_PLUGIN_KEY: Record<string, string> = {
10 openapi: "openapi",12 openapi: "openapi",
@@ -79,11 +81,11 @@ export function SourcesPage(props: { sourcePlugins: readonly SourcePlugin[] }) { · 68 unmodified lines
79 {/* URL detection input */}81 {/* URL detection input */}
80 <div className="mt-5">82 <div className="mt-5">
81 <div className="flex gap-2">83 <div className="flex gap-2">
82 <input84 <Input
83 type="url"85 type="url"
84 value={url}86 value={url}
85 onChange={(e) => {87 onChange={(e) => {
86 setUrl(e.target.value);88 setUrl((e.target as HTMLInputElement).value);
87 setError(null);89 setError(null);
88 }}90 }}
89 onKeyDown={(e) => {91 onKeyDown={(e) => {
@@ -91,15 +93,11 @@ export function SourcesPage(props: { sourcePlugins: readonly SourcePlugin[] }) { · 1 unmodified line
91 }}93 }}
92 placeholder="Paste a URL to auto-detect source type..."94 placeholder="Paste a URL to auto-detect source type..."
93 disabled={detecting}95 disabled={detecting}
94 className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"96 className="flex-1"
95 />97 />
96 <button98 <Button onClick={handleDetect} disabled={detecting || !url.trim()}>
97 onClick={handleDetect}
98 disabled={detecting || !url.trim()}
99 className="inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none"
100 >
101 {detecting ? "Detecting..." : "Detect"}99 {detecting ? "Detecting..." : "Detect"}
102 </button>100 </Button>
103 </div>101 </div>
104 {error && <p className="mt-2 text-sm text-destructive">{error}</p>}102 {error && <p className="mt-2 text-sm text-destructive">{error}</p>}
105 <div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">103 <div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
packages/react/src/plugins/secret-header-auth.tsx+4−2
@@ -343,8 +343,10 @@ export function SecretHeaderAuthRow(props: { · 342 unmodified lines
343 343
344 <div className="flex flex-wrap gap-1">344 <div className="flex flex-wrap gap-1">
345 {presets.map((preset) => (345 {presets.map((preset) => (
346 <button346 <Button
347 key={preset.key}347 key={preset.key}
348 variant="ghost"
349 size="sm"
348 type="button"350 type="button"
349 onClick={() =>351 onClick={() =>
350 onChange({352 onChange({
@@ -360,7 +362,7 @@ export function SecretHeaderAuthRow(props: { · 9 unmodified lines
360 }`}362 }`}
361 >363 >
362 {preset.label}364 {preset.label}
363 </button>365 </Button>
364 ))}366 ))}
365 </div>367 </div>
366 368

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t