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 @@
11 {
2− "ignorePatterns": [".astro/"],
2+ "plugins": ["react"],
33 "rules": {
44 "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+ ],
518 },
19+ "ignorePatterns": [".astro/"],
620 }
apps/cloud/src/routes/billing.tsx+4−2
@@ -1,5 +1,6 @@
11 import { createFileRoute, Link } from "@tanstack/react-router";
22 import { useCustomer, useListPlans } from "autumn-js/react";
3+import { Button } from "@executor/react/components/button";
34
45 export const Route = createFileRoute("/billing")({
56 component: BillingPage,
@@ -87,13 +88,14 @@ function BillingPage() { · 81 unmodified lines
8788 </div>
8889 <div className="flex items-center gap-2">
8990 {activePlan && !isCanceling && (
90− <button
91+ <Button
92+ variant="ghost"
9193 type="button"
9294 onClick={() => openCustomerPortal()}
9395 className="rounded-md px-3 py-1.5 text-[0.75rem] font-medium text-destructive transition-colors hover:bg-destructive/10"
9496 >
9597 Cancel plan
96− </button>
98+ </Button>
9799 )}
98100 <Link
99101 to="/billing/plans"
apps/cloud/src/routes/billing_.plans.tsx+5−4
@@ -1,6 +1,7 @@
11 import { useState } from "react";
22 import { createFileRoute, Link } from "@tanstack/react-router";
33 import { useCustomer, useListPlans } from "autumn-js/react";
4+import { Button } from "@executor/react/components/button";
45
56 export const Route = createFileRoute("/billing_/plans")({
67 component: PlansPage,
@@ -149,16 +150,16 @@ function PlansPage() { · 142 unmodified lines
149150 {isCurrent ? "Current plan" : "Scheduled"}
150151 </div>
151152 ) : isCanceling ? (
152− <button
153+ <Button
153154 type="button"
154155 disabled={loadingPlan !== null}
155156 onClick={() => openCustomerPortal()}
156157 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"
157158 >
158159 Resume
159− </button>
160+ </Button>
160161 ) : (
161− <button
162+ <Button
162163 type="button"
163164 disabled={loadingPlan !== null}
164165 onClick={async () => {
@@ -174,7 +175,7 @@ function PlansPage() { · 9 unmodified lines
174175 ].join(" ")}
175176 >
176177 {loadingPlan === plan.id ? "Loading…" : label}
177− </button>
178+ </Button>
178179 )}
179180 </div>
180181
apps/cloud/src/web/shell.tsx+17−10
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; · 2 unmodified lines
33 import { useAtomValue, Result } from "@effect-atom/atom-react";
44 import { sourcesAtom } from "@executor/react/api/atoms";
55 import { useScope } from "@executor/react/api/scope-context";
6−
6+import { Button } from "@executor/react/components/button";
77 import { AUTH_PATHS } from "../auth/api";
88 import { useAuth } from "./auth";
99
@@ -109,9 +109,11 @@ function UserFooter() { · 99 unmodified lines
109109 )}
110110 </div>
111111 <form action={AUTH_PATHS.logout} method="post">
112− <button
112+ <Button
113+ variant="ghost"
114+ size="icon-xs"
113115 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"
115117 title="Sign out"
116118 >
117119 <svg viewBox="0 0 16 16" fill="none" className="size-3.5">
@@ -123,7 +125,7 @@ function UserFooter() { · 5 unmodified lines
123125 strokeLinejoin="round"
124126 />
125127 </svg>
126− </button>
128+ </Button>
127129 </form>
128130 </div>
129131 </div>
@@ -196,6 +198,7 @@ export function Shell() { · 66 unmodified lines
196198 {/* Mobile sidebar overlay */}
197199 {mobileSidebarOpen && (
198200 <div className="fixed inset-0 z-50 flex md:hidden">
201+ {/* oxlint-disable-next-line react/forbid-elements */}
199202 <button
200203 type="button"
201204 aria-label="Close navigation"
@@ -209,11 +212,13 @@ export function Shell() { · 7 unmodified lines
209212 executor
210213 </span>
211214 </Link>
212− <button
215+ <Button
216+ variant="ghost"
217+ size="icon-sm"
213218 type="button"
214219 aria-label="Close navigation"
215220 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"
217222 >
218223 <svg viewBox="0 0 16 16" className="size-3.5">
219224 <path
@@ -223,7 +228,7 @@ export function Shell() { · 3 unmodified lines
223228 strokeLinecap="round"
224229 />
225230 </svg>
226− </button>
231+ </Button>
227232 </div>
228233 <SidebarContent
229234 pathname={pathname}
@@ -238,11 +243,13 @@ export function Shell() { · 8 unmodified lines
238243 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">
239244 {/* Mobile top bar */}
240245 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">
241− <button
246+ <Button
247+ variant="outline"
248+ size="icon-sm"
242249 type="button"
243250 aria-label="Open navigation"
244251 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"
246253 >
247254 <svg viewBox="0 0 16 16" className="size-4">
248255 <path
@@ -252,7 +259,7 @@ export function Shell() { · 3 unmodified lines
252259 strokeLinecap="round"
253260 />
254261 </svg>
255− </button>
262+ </Button>
256263 <Link to="/" className="flex items-center gap-1.5">
257264 <span className="font-display text-base tracking-tight text-foreground">executor</span>
258265 </Link>
apps/local/src/web/shell.tsx+11−8
@@ -402,6 +402,7 @@ export function Shell() { · 401 unmodified lines
402402 {/* Mobile sidebar overlay */}
403403 {mobileSidebarOpen && (
404404 <div className="fixed inset-0 z-50 flex md:hidden">
405+ {/* oxlint-disable-next-line react/forbid-elements */}
405406 <button
406407 type="button"
407408 aria-label="Close navigation"
@@ -415,11 +416,12 @@ export function Shell() { · 7 unmodified lines
415416 executor
416417 </span>
417418 </Link>
418− <button
419− type="button"
419+ <Button
420+ variant="ghost"
421+ size="icon-sm"
420422 aria-label="Close navigation"
421423 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"
423425 >
424426 <svg viewBox="0 0 16 16" className="size-3.5">
425427 <path
@@ -429,7 +431,7 @@ export function Shell() { · 3 unmodified lines
429431 strokeLinecap="round"
430432 />
431433 </svg>
432− </button>
434+ </Button>
433435 </div>
434436 <SidebarContent
435437 pathname={pathname}
@@ -447,11 +449,12 @@ export function Shell() { · 11 unmodified lines
447449 <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">
448450 {/* Mobile top bar */}
449451 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">
450− <button
451− type="button"
452+ <Button
453+ variant="outline"
454+ size="icon-sm"
452455 aria-label="Open navigation"
453456 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"
455458 >
456459 <svg viewBox="0 0 16 16" className="size-4">
457460 <path
@@ -461,7 +464,7 @@ export function Shell() { · 3 unmodified lines
461464 strokeLinecap="round"
462465 />
463466 </svg>
464− </button>
467+ </Button>
465468 <Link to="/" className="flex items-center gap-1.5">
466469 <span className="font-display text-base tracking-tight text-foreground">executor</span>
467470 </Link>
packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx+10−8
@@ -481,7 +481,7 @@ export default function AddGoogleDiscoverySource(props: { · 480 unmodified lines
481481 } finally {
482482 setLoadingProbe(false);
483483 }
484− }, [discoveryUrl, doProbe, name]);
484+ }, [discoveryUrl, doProbe, name, scopeId]);
485485
486486 const autoProbed = useRef(false);
487487 useEffect(() => {
@@ -573,7 +573,7 @@ export default function AddGoogleDiscoverySource(props: { · 85 unmodified lines
573573 setError(e instanceof Error ? e.message : "Failed to add source");
574574 setAdding(false);
575575 }
576− }, [probe, doAdd, name, discoveryUrl, authKind, oauthAuth, props]);
576+ }, [probe, doAdd, name, discoveryUrl, authKind, oauthAuth, props, scopeId]);
577577
578578 const addDisabled =
579579 !probe || adding || (authKind === "oauth2" && (!canUseOAuth || oauthAuth === null));
@@ -598,11 +598,12 @@ export default function AddGoogleDiscoverySource(props: { · 18 unmodified lines
598598 {GOOGLE_DISCOVERY_TEMPLATES.map((template) => {
599599 const selected = template.id === selectedTemplateId;
600600 return (
601− <button
601+ <Button
602602 key={template.id}
603+ variant="ghost"
603604 type="button"
604605 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 ${
606607 selected
607608 ? "border-primary bg-primary/5 shadow-[0_0_0_1px_rgba(0,0,0,0.02)]"
608609 : "border-border bg-card hover:border-primary/30 hover:bg-card/80"
@@ -628,7 +629,7 @@ export default function AddGoogleDiscoverySource(props: { · 19 unmodified lines
628629 </p>
629630 <div className="h-px flex-1 bg-border/70" />
630631 </div>
631− </button>
632+ </Button>
632633 );
633634 })}
634635 </div>
@@ -737,12 +738,13 @@ export default function AddGoogleDiscoverySource(props: { · 102 unmodified lines
737738 </p>
738739 {canUseOAuth && (probe?.scopes.length ?? 0) > 0 && (
739740 <CollapsibleTrigger asChild>
740− <button
741+ <Button
742+ variant="link"
741743 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"
743745 >
744746 {showScopes ? "Hide scopes" : "View scopes"}
745− </button>
747+ </Button>
746748 </CollapsibleTrigger>
747749 )}
748750 </div>
packages/plugins/mcp/src/react/AddMcpSource.tsx+27−14
@@ -8,6 +8,7 @@ import { Label } from "@executor/react/components/label"; · 7 unmodified lines
88 import { Badge } from "@executor/react/components/badge";
99 import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";
1010 import { Spinner } from "@executor/react/components/spinner";
11+import { Textarea } from "@executor/react/components/textarea";
1112 import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth";
1213 import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";
1314 import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms";
@@ -420,7 +421,17 @@ export default function AddMcpSource(props: { · 406 unmodified lines
420421 error: e instanceof Error ? e.message : "Failed to add source",
421422 });
422423 }
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+ ]);
424435
425436 // ---- Stdio actions ----
426437
@@ -483,7 +494,8 @@ export default function AddMcpSource(props: { · 56 unmodified lines
483494
484495 {/* Transport toggle */}
485496 <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1">
486− <button
497+ <Button
498+ variant="ghost"
487499 type="button"
488500 onClick={() => setTransport("remote")}
489501 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
493505 }`}
494506 >
495507 Remote
496− </button>
497− <button
508+ </Button>
509+ <Button
510+ variant="ghost"
498511 type="button"
499512 onClick={() => setTransport("stdio")}
500513 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
504517 }`}
505518 >
506519 Stdio
507− </button>
520+ </Button>
508521 </div>
509522
510523 {transport === "remote" ? (
@@ -603,7 +616,7 @@ export default function AddMcpSource(props: { · 92 unmodified lines
603616 className="gap-1.5"
604617 >
605618 {!probe.requiresOAuth && (
606− <label
619+ <Label
607620 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
608621 remoteAuthMode === "none"
609622 ? "border-primary/50 bg-primary/[0.03]"
@@ -615,10 +628,10 @@ export default function AddMcpSource(props: { · 5 unmodified lines
615628 <span className="ml-auto text-[10px] text-muted-foreground">
616629 no auth header
617630 </span>
618− </label>
631+ </Label>
619632 )}
620633
621− <label
634+ <Label
622635 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
623636 remoteAuthMode === "header"
624637 ? "border-primary/50 bg-primary/[0.03]"
@@ -630,10 +643,10 @@ export default function AddMcpSource(props: { · 5 unmodified lines
630643 <span className="ml-auto text-[10px] text-muted-foreground">
631644 use a secret-backed auth header
632645 </span>
633− </label>
646+ </Label>
634647
635648 {probe.requiresOAuth && (
636− <label
649+ <Label
637650 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
638651 remoteAuthMode === "oauth2"
639652 ? "border-primary/50 bg-primary/[0.03]"
@@ -645,7 +658,7 @@ export default function AddMcpSource(props: { · 5 unmodified lines
645658 <span className="ml-auto text-[10px] text-muted-foreground">
646659 sign in with the server&apos;s OAuth flow
647660 </span>
648− </label>
661+ </Label>
649662 )}
650663 </RadioGroup>
651664
@@ -936,12 +949,12 @@ export default function AddMcpSource(props: { · 284 unmodified lines
936949 Environment variables{" "}
937950 <span className="text-muted-foreground font-normal">(optional)</span>
938951 </Label>
939− <textarea
952+ <Textarea
940953 value={stdioEnv}
941− onChange={(e) => setStdioEnv(e.target.value)}
954+ onChange={(e) => setStdioEnv((e.target as HTMLTextAreaElement).value)}
942955 placeholder={"KEY=value\nANOTHER=value"}
943956 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"
945958 />
946959 <p className="text-[12px] text-muted-foreground">One per line, KEY=value format.</p>
947960 </div>
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+8−8
@@ -296,7 +296,7 @@ export default function AddOpenApiSource(props: { · 295 unmodified lines
296296 {servers.map((s, i) => {
297297 const url = s.url ?? "";
298298 return (
299− <label
299+ <Label
300300 key={i}
301301 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
302302 baseUrl === url
@@ -306,7 +306,7 @@ export default function AddOpenApiSource(props: { · 3 unmodified lines
306306 >
307307 <RadioGroupItem value={url} />
308308 <span className="font-mono text-xs text-foreground truncate">{url}</span>
309− </label>
309+ </Label>
310310 );
311311 })}
312312 </RadioGroup>
@@ -345,7 +345,7 @@ export default function AddOpenApiSource(props: { · 32 unmodified lines
345345 className="gap-1.5"
346346 >
347347 {presets.map((preset, i) => (
348− <label
348+ <Label
349349 key={i}
350350 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
351351 presetIndex === i
@@ -361,10 +361,10 @@ export default function AddOpenApiSource(props: { · 9 unmodified lines
361361 {preset.secretHeaders.length > 1 ? "s" : ""}
362362 </span>
363363 )}
364− </label>
364+ </Label>
365365 ))}
366366
367− <label
367+ <Label
368368 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
369369 presetIndex === -2
370370 ? "border-primary/50 bg-primary/[0.03]"
@@ -376,9 +376,9 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines
376376 <span className="ml-auto text-[10px] text-muted-foreground">
377377 configure manually
378378 </span>
379− </label>
379+ </Label>
380380
381− <label
381+ <Label
382382 className={`flex items-center gap-2.5 rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
383383 presetIndex === -1
384384 ? "border-primary/50 bg-primary/[0.03]"
@@ -388,7 +388,7 @@ export default function AddOpenApiSource(props: { · 3 unmodified lines
388388 <RadioGroupItem value="-1" />
389389 <span className="text-xs font-medium text-foreground">None</span>
390390 <span className="ml-auto text-[10px] text-muted-foreground">skip auth</span>
391− </label>
391+ </Label>
392392 </RadioGroup>
393393 )}
394394
packages/react/src/components/code-block.tsx+15−11
@@ -3,6 +3,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime"; · 2 unmodified lines
33 import { toJsxRuntime } from "hast-util-to-jsx-runtime";
44 import { getHighlighter, resolveLang, THEME } from "../lib/shiki";
55 import { cn } from "../lib/utils";
6+import { Button } from "./button";
67
78 // ---------------------------------------------------------------------------
89 // Language detection
@@ -113,26 +114,28 @@ export function CodeBlock(props: { · 104 unmodified lines
113114 <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
114115 {title}
115116 </span>
116− <button
117− type="button"
117+ <Button
118+ variant="ghost"
119+ size="icon-xs"
118120 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"
120122 title="Copy"
121123 >
122124 {copied ? <CheckIcon /> : <CopyIcon />}
123− </button>
125+ </Button>
124126 </div>
125127 )}
126128 <div className="group relative">
127129 {!title && (
128− <button
129− type="button"
130+ <Button
131+ variant="ghost"
132+ size="icon-xs"
130133 onClick={handleCopy}
131134 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"
132135 title="Copy to clipboard"
133136 >
134137 {copied ? <CheckIcon /> : <CopyIcon />}
135− </button>
138+ </Button>
136139 )}
137140
138141 <div
@@ -148,13 +151,14 @@ export function CodeBlock(props: { · 9 unmodified lines
148151
149152 {isLong && !expanded && (
150153 <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− <button
152− type="button"
154+ <Button
155+ variant="outline"
156+ size="sm"
153157 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"
155159 >
156160 Show all ({lines.length} lines)
157− </button>
161+ </Button>
158162 </div>
159163 )}
160164 </div>
packages/react/src/components/expandable-code-block.tsx+5−3
@@ -1,6 +1,7 @@
11 import { useCallback, useEffect, useMemo, useState, startTransition } from "react";
22 import { getHighlighter, THEME } from "../lib/shiki";
33 import { cn } from "../lib/utils";
4+import { Button } from "./button";
45 import type { ThemedToken } from "shiki/core";
56
67 // ---------------------------------------------------------------------------
@@ -371,14 +372,15 @@ export function ExpandableCodeBlock(props: { · 364 unmodified lines
371372 return (
372373 <div className={cn("rounded-lg border border-border/40 bg-card/60 overflow-hidden", className)}>
373374 <div className="group relative">
374− <button
375− type="button"
375+ <Button
376+ variant="ghost"
377+ size="icon-xs"
376378 onClick={handleCopy}
377379 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"
378380 title="Copy to clipboard"
379381 >
380382 {copied ? <CheckIcon /> : <CopyIcon />}
381− </button>
383+ </Button>
382384
383385 <pre className="overflow-auto p-3 font-mono text-[0.75rem] leading-6 !bg-transparent">
384386 <code>
packages/react/src/components/input.tsx+1−0
@@ -4,6 +4,7 @@ import { cn } from "../lib/utils"; · 3 unmodified lines
44
55 function Input({ className, type, ...props }: React.ComponentProps<"input">) {
66 return (
7+ // oxlint-disable-next-line react/forbid-elements
78 <input
89 type={type}
910 data-slot="input"
packages/react/src/components/native-select.tsx+1−0
@@ -13,6 +13,7 @@ function NativeSelect({ · 12 unmodified lines
1313 className="group/native-select relative w-fit has-[select:disabled]:opacity-50"
1414 data-slot="native-select-wrapper"
1515 >
16+ {/* oxlint-disable-next-line react/forbid-elements */}
1617 <select
1718 data-slot="native-select"
1819 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
263263 const { toggleSidebar } = useSidebar();
264264
265265 return (
266+ // oxlint-disable-next-line react/forbid-elements
266267 <button
267268 data-sidebar="rail"
268269 data-slot="sidebar-rail"
packages/react/src/components/table.tsx+1−0
@@ -7,6 +7,7 @@ import { cn } from "../lib/utils"; · 6 unmodified lines
77 function Table({ className, ...props }: React.ComponentProps<"table">) {
88 return (
99 <div data-slot="table-container" className="relative w-full overflow-x-auto">
10+ {/* oxlint-disable-next-line react/forbid-elements */}
1011 <table
1112 data-slot="table"
1213 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
44
55 function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
66 return (
7+ // oxlint-disable-next-line react/forbid-elements
78 <textarea
89 data-slot="textarea"
910 className={cn(
packages/react/src/components/tool-detail.tsx+15−14
@@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; · 1 unmodified line
22 import { useAtomValue, Result } from "@effect-atom/atom-react";
33 import { toolSchemaAtom } from "../api/atoms";
44 import { ScopeId, ToolId } from "@executor/sdk";
5−
5+import { Button } from "./button";
66 import { Markdown } from "./markdown";
77 import { SchemaExplorer } from "./schema-explorer";
88 import { ExpandableCodeBlock } from "./expandable-code-block";
@@ -16,19 +16,20 @@ function CopyButton(props: { text: string; label?: string }) { · 7 unmodified lines
1616 const [copied, setCopied] = useState(false);
1717
1818 return (
19− <button
20− type="button"
19+ <Button
20+ variant="ghost"
21+ size="icon-xs"
2122 onClick={() => {
2223 void navigator.clipboard.writeText(props.text).then(() => {
2324 setCopied(true);
2425 setTimeout(() => setCopied(false), 1500);
2526 });
2627 }}
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"
2829 title={props.label ?? "Copy"}
2930 >
3031 {copied ? <Check className="size-3.5 shrink-0" /> : <Copy className="size-3.5 shrink-0" />}
31− </button>
32+ </Button>
3233 );
3334 }
3435
@@ -80,7 +81,7 @@ export function ToolDetail(props: { · 45 unmodified lines
8081 outputTypeScript: v.outputTypeScript ? `type Output = ${v.outputTypeScript}` : null,
8182 definitions,
8283 };
83− }, [toolContract, props.toolName]);
84+ }, [toolContract]);
8485
8586 const crumbs = breadcrumbParts(props.toolName);
8687 const displayName = friendlyName(props.toolName);
@@ -112,34 +113,34 @@ export function ToolDetail(props: { · 25 unmodified lines
112113
113114 {/* Tabs */}
114115 <div className="mt-3 flex gap-4" role="tablist">
115− <button
116− type="button"
116+ <Button
117+ variant="ghost"
117118 role="tab"
118119 aria-selected={tab === "schema"}
119120 onClick={() => setTab("schema")}
120121 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",
122123 tab === "schema"
123124 ? "border-primary text-foreground"
124125 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",
125126 ].join(" ")}
126127 >
127128 Schema
128− </button>
129− <button
130− type="button"
129+ </Button>
130+ <Button
131+ variant="ghost"
131132 role="tab"
132133 aria-selected={tab === "typescript"}
133134 onClick={() => setTab("typescript")}
134135 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",
136137 tab === "typescript"
137138 ? "border-primary text-foreground"
138139 : "border-transparent text-muted-foreground/50 hover:text-muted-foreground",
139140 ].join(" ")}
140141 >
141142 TypeScript
142− </button>
143+ </Button>
143144 </div>
144145 </div>
145146 </div>
packages/react/src/components/tool-tree.tsx+15−12
@@ -1,4 +1,6 @@
11 import { useEffect, useMemo, useRef, useState } from "react";
2+import { Button } from "./button";
3+import { Input } from "./input";
24
35 // ---------------------------------------------------------------------------
46 // Types
@@ -161,8 +163,8 @@ function TreeNodeView(props: { · 156 unmodified lines
161163
162164 return (
163165 <div>
164− <button
165− type="button"
166+ <Button
167+ variant="ghost"
166168 onClick={() => setOpen((v) => !v)}
167169 aria-expanded={open}
168170 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
184186 <span className="shrink-0 tabular-nums text-[10px] text-muted-foreground/25">
185187 {leafCount}
186188 </span>
187− </button>
189+ </Button>
188190
189191 {open && hasChildren && (
190192 <div className="relative flex flex-col gap-px">
@@ -232,9 +234,9 @@ function ToolLeafItem(props: { · 41 unmodified lines
232234 }, [props.active]);
233235
234236 return (
235− <button
237+ <Button
236238 ref={ref}
237− type="button"
239+ variant="ghost"
238240 onClick={props.onSelect}
239241 className={[
240242 "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
255257 <span className="flex-1 truncate font-mono text-[12px]">
256258 {highlightMatch(label, props.search)}
257259 </span>
258− </button>
260+ </Button>
259261 );
260262 }
261263
@@ -313,21 +315,22 @@ export function ToolTree(props: { · 51 unmodified lines
313315 strokeLinecap="round"
314316 />
315317 </svg>
316− <input
318+ <Input
317319 ref={searchRef}
318320 value={search}
319321 onChange={(e) => setSearch(e.target.value)}
320322 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"
322324 />
323325 {search.length > 0 ? (
324− <button
325− type="button"
326+ <Button
327+ variant="ghost"
328+ size="icon-xs"
326329 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"
328331 >
329332 ×
330− </button>
333+ </Button>
331334 ) : (
332335 <kbd className="shrink-0 rounded border border-border bg-muted px-1 py-px text-[10px] leading-none text-muted-foreground/50">
333336 /
packages/react/src/pages/source-detail.tsx+22−28
@@ -13,6 +13,7 @@ import { ToolDetail, ToolDetailEmpty } from "../components/tool-detail"; · 12 unmodified lines
1313 import type { ToolSummary } from "../components/tool-tree";
1414 import { useScope } from "../hooks/use-scope";
1515 import type { SourcePlugin } from "../plugins/source-plugin";
16+import { Button } from "../components/button";
1617
1718 export function SourceDetailPage(props: {
1819 namespace: string;
@@ -131,34 +132,26 @@ export function SourceDetailPage(props: { · 112 unmodified lines
131132
132133 <div className="flex shrink-0 items-center gap-2">
133134 {canEdit && editPlugin && !editing && (
134− <button
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− >
135+ <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
139136 Edit
140− </button>
137+ </Button>
141138 )}
142139
143140 {editing && (
144− <button
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− >
141+ <Button variant="outline" size="sm" onClick={() => setEditing(false)}>
149142 Back to tools
150− </button>
143+ </Button>
151144 )}
152145
153146 {canRefresh && !editing && (
154− <button
155− type="button"
147+ <Button
148+ variant="outline"
149+ size="sm"
156150 onClick={() => void handleRefresh()}
157151 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"
159152 >
160153 {refreshing ? "Refreshing..." : "Refresh"}
161− </button>
154+ </Button>
162155 )}
163156
164157 {canRemove &&
@@ -166,31 +159,32 @@ export function SourceDetailPage(props: { · 1 unmodified line
166159 (confirmDelete ? (
167160 <div className="flex items-center gap-2">
168161 <span className="text-[11px] font-medium text-destructive">Confirm?</span>
169− <button
170− type="button"
162+ <Button
163+ variant="outline"
164+ size="sm"
171165 onClick={() => setConfirmDelete(false)}
172166 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"
174167 >
175168 Cancel
176− </button>
177− <button
178− type="button"
169+ </Button>
170+ <Button
171+ variant="destructive"
172+ size="sm"
179173 onClick={() => void handleDelete()}
180174 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"
182175 >
183176 {deleting ? "Deleting..." : "Delete"}
184− </button>
177+ </Button>
185178 </div>
186179 ) : (
187− <button
188− type="button"
180+ <Button
181+ variant="outline"
182+ size="sm"
189183 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"
191185 >
192186 Delete
193− </button>
187+ </Button>
194188 ))}
195189 </div>
196190 </div>
packages/react/src/pages/sources.tsx+7−9
@@ -5,6 +5,8 @@ import { sourcesAtom, detectSource } from "../api/atoms"; · 4 unmodified lines
55 import { useScope } from "../hooks/use-scope";
66 import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin";
77 import { McpInstallCard } from "../components/mcp-install-card";
8+import { Button } from "../components/button";
9+import { Input } from "../components/input";
810
911 const KIND_TO_PLUGIN_KEY: Record<string, string> = {
1012 openapi: "openapi",
@@ -79,11 +81,11 @@ export function SourcesPage(props: { sourcePlugins: readonly SourcePlugin[] }) { · 68 unmodified lines
7981 {/* URL detection input */}
8082 <div className="mt-5">
8183 <div className="flex gap-2">
82− <input
84+ <Input
8385 type="url"
8486 value={url}
8587 onChange={(e) => {
86− setUrl(e.target.value);
88+ setUrl((e.target as HTMLInputElement).value);
8789 setError(null);
8890 }}
8991 onKeyDown={(e) => {
@@ -91,15 +93,11 @@ export function SourcesPage(props: { sourcePlugins: readonly SourcePlugin[] }) { · 1 unmodified line
9193 }}
9294 placeholder="Paste a URL to auto-detect source type..."
9395 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"
9597 />
96− <button
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− >
98+ <Button onClick={handleDetect} disabled={detecting || !url.trim()}>
10199 {detecting ? "Detecting..." : "Detect"}
102− </button>
100+ </Button>
103101 </div>
104102 {error && <p className="mt-2 text-sm text-destructive">{error}</p>}
105103 <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
343343
344344 <div className="flex flex-wrap gap-1">
345345 {presets.map((preset) => (
346− <button
346+ <Button
347347 key={preset.key}
348+ variant="ghost"
349+ size="sm"
348350 type="button"
349351 onClick={() =>
350352 onChange({
@@ -360,7 +362,7 @@ export function SecretHeaderAuthRow(props: { · 9 unmodified lines
360362 }`}
361363 >
362364 {preset.label}
363− </button>
365+ </Button>
364366 ))}
365367 </div>
366368

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