* 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
Unified · Split
| @@ -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 | } |
| @@ -1,5 +1,6 @@ | |||
| 1 | import { createFileRoute, Link } from "@tanstack/react-router"; | 1 | import { createFileRoute, Link } from "@tanstack/react-router"; |
| 2 | import { useCustomer, useListPlans } from "autumn-js/react"; | 2 | import { useCustomer, useListPlans } from "autumn-js/react"; |
| 3 | import { Button } from "@executor/react/components/button"; | ||
| 3 | 4 | ||
| 4 | export const Route = createFileRoute("/billing")({ | 5 | export 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 | <button | 91 | <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 plan | 97 | Cancel plan |
| 96 | </button> | 98 | </Button> |
| 97 | )} | 99 | )} |
| 98 | <Link | 100 | <Link |
| 99 | to="/billing/plans" | 101 | to="/billing/plans" |
| @@ -1,6 +1,7 @@ | |||
| 1 | import { useState } from "react"; | 1 | import { useState } from "react"; |
| 2 | import { createFileRoute, Link } from "@tanstack/react-router"; | 2 | import { createFileRoute, Link } from "@tanstack/react-router"; |
| 3 | import { useCustomer, useListPlans } from "autumn-js/react"; | 3 | import { useCustomer, useListPlans } from "autumn-js/react"; |
| 4 | import { Button } from "@executor/react/components/button"; | ||
| 4 | 5 | ||
| 5 | export const Route = createFileRoute("/billing_/plans")({ | 6 | export 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 | <button | 153 | <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 | Resume | 159 | Resume |
| 159 | </button> | 160 | </Button> |
| 160 | ) : ( | 161 | ) : ( |
| 161 | <button | 162 | <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 | ||
| @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; · 2 unmodified lines | |||
| 3 | import { useAtomValue, Result } from "@effect-atom/atom-react"; | 3 | import { useAtomValue, Result } from "@effect-atom/atom-react"; |
| 4 | import { sourcesAtom } from "@executor/react/api/atoms"; | 4 | import { sourcesAtom } from "@executor/react/api/atoms"; |
| 5 | import { useScope } from "@executor/react/api/scope-context"; | 5 | import { useScope } from "@executor/react/api/scope-context"; |
| 6 | 6 | import { Button } from "@executor/react/components/button"; | |
| 7 | import { AUTH_PATHS } from "../auth/api"; | 7 | import { AUTH_PATHS } from "../auth/api"; |
| 8 | import { useAuth } from "./auth"; | 8 | import { 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 | <button | 112 | <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 | <button | 202 | <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 | executor | 212 | executor |
| 210 | </span> | 213 | </span> |
| 211 | </Link> | 214 | </Link> |
| 212 | <button | 215 | <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 | <path | 224 | <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 | <SidebarContent | 233 | <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 | <button | 246 | <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 | <path | 255 | <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> |
| @@ -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 | <button | 406 | <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 | executor | 416 | executor |
| 416 | </span> | 417 | </span> |
| 417 | </Link> | 418 | </Link> |
| 418 | <button | 419 | <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 | <path | 427 | <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 | <SidebarContent | 436 | <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 | <button | 452 | <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 | <path | 460 | <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> |
| @@ -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 | <button | 601 | <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 | selected | 607 | 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 | <button | 741 | <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> |
| @@ -8,6 +8,7 @@ import { Label } from "@executor/react/components/label"; · 7 unmodified lines | |||
| 8 | import { Badge } from "@executor/react/components/badge"; | 8 | import { Badge } from "@executor/react/components/badge"; |
| 9 | import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; | 9 | import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; |
| 10 | import { Spinner } from "@executor/react/components/spinner"; | 10 | import { Spinner } from "@executor/react/components/spinner"; |
| 11 | import { Textarea } from "@executor/react/components/textarea"; | ||
| 11 | import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth"; | 12 | import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth"; |
| 12 | import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; | 13 | import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; |
| 13 | import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms"; | 14 | import { 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 | <button | 497 | <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 | Remote | 507 | Remote |
| 496 | </button> | 508 | </Button> |
| 497 | <button | 509 | <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 | Stdio | 519 | 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 | <label | 619 | <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 header | 629 | no auth header |
| 617 | </span> | 630 | </span> |
| 618 | </label> | 631 | </Label> |
| 619 | )} | 632 | )} |
| 620 | 633 | ||
| 621 | <label | 634 | <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 header | 644 | use a secret-backed auth header |
| 632 | </span> | 645 | </span> |
| 633 | </label> | 646 | </Label> |
| 634 | 647 | ||
| 635 | {probe.requiresOAuth && ( | 648 | {probe.requiresOAuth && ( |
| 636 | <label | 649 | <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's OAuth flow | 659 | sign in with the server'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 | <textarea | 952 | <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> |
| @@ -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 | <label | 299 | <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 === url | 302 | 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 | <label | 348 | <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 === i | 351 | 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 | <label | 367 | <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 === -2 | 369 | 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 manually | 377 | configure manually |
| 378 | </span> | 378 | </span> |
| 379 | </label> | 379 | </Label> |
| 380 | 380 | ||
| 381 | <label | 381 | <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 === -1 | 383 | 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 | ||
| @@ -3,6 +3,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime"; · 2 unmodified lines | |||
| 3 | import { toJsxRuntime } from "hast-util-to-jsx-runtime"; | 3 | import { toJsxRuntime } from "hast-util-to-jsx-runtime"; |
| 4 | import { getHighlighter, resolveLang, THEME } from "../lib/shiki"; | 4 | import { getHighlighter, resolveLang, THEME } from "../lib/shiki"; |
| 5 | import { cn } from "../lib/utils"; | 5 | import { cn } from "../lib/utils"; |
| 6 | import { Button } from "./button"; | ||
| 6 | 7 | ||
| 7 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
| 8 | // Language detection | 9 | // 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 | <button | 117 | <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 | <button | 130 | <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 | <div | 141 | <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 | <button | 154 | <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> |
| @@ -1,6 +1,7 @@ | |||
| 1 | import { useCallback, useEffect, useMemo, useState, startTransition } from "react"; | 1 | import { useCallback, useEffect, useMemo, useState, startTransition } from "react"; |
| 2 | import { getHighlighter, THEME } from "../lib/shiki"; | 2 | import { getHighlighter, THEME } from "../lib/shiki"; |
| 3 | import { cn } from "../lib/utils"; | 3 | import { cn } from "../lib/utils"; |
| 4 | import { Button } from "./button"; | ||
| 4 | import type { ThemedToken } from "shiki/core"; | 5 | import 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 | <button | 375 | <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> |
| @@ -4,6 +4,7 @@ import { cn } from "../lib/utils"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { |
| 6 | return ( | 6 | return ( |
| 7 | // oxlint-disable-next-line react/forbid-elements | ||
| 7 | <input | 8 | <input |
| 8 | type={type} | 9 | type={type} |
| 9 | data-slot="input" | 10 | data-slot="input" |
| @@ -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 | <select | 17 | <select |
| 17 | data-slot="native-select" | 18 | data-slot="native-select" |
| 18 | data-size={size} | 19 | data-size={size} |
| @@ -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 | <button | 267 | <button |
| 267 | data-sidebar="rail" | 268 | data-sidebar="rail" |
| 268 | data-slot="sidebar-rail" | 269 | data-slot="sidebar-rail" |
| @@ -7,6 +7,7 @@ import { cn } from "../lib/utils"; · 6 unmodified lines | |||
| 7 | function Table({ className, ...props }: React.ComponentProps<"table">) { | 7 | function 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 | <table | 11 | <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)} |
| @@ -4,6 +4,7 @@ import { cn } from "../lib/utils"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { |
| 6 | return ( | 6 | return ( |
| 7 | // oxlint-disable-next-line react/forbid-elements | ||
| 7 | <textarea | 8 | <textarea |
| 8 | data-slot="textarea" | 9 | data-slot="textarea" |
| 9 | className={cn( | 10 | className={cn( |
| @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; · 1 unmodified line | |||
| 2 | import { useAtomValue, Result } from "@effect-atom/atom-react"; | 2 | import { useAtomValue, Result } from "@effect-atom/atom-react"; |
| 3 | import { toolSchemaAtom } from "../api/atoms"; | 3 | import { toolSchemaAtom } from "../api/atoms"; |
| 4 | import { ScopeId, ToolId } from "@executor/sdk"; | 4 | import { ScopeId, ToolId } from "@executor/sdk"; |
| 5 | 5 | import { Button } from "./button"; | |
| 6 | import { Markdown } from "./markdown"; | 6 | import { Markdown } from "./markdown"; |
| 7 | import { SchemaExplorer } from "./schema-explorer"; | 7 | import { SchemaExplorer } from "./schema-explorer"; |
| 8 | import { ExpandableCodeBlock } from "./expandable-code-block"; | 8 | import { 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 | <button | 19 | <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 | <button | 116 | <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 | Schema | 128 | Schema |
| 128 | </button> | 129 | </Button> |
| 129 | <button | 130 | <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 | TypeScript | 142 | TypeScript |
| 142 | </button> | 143 | </Button> |
| 143 | </div> | 144 | </div> |
| 144 | </div> | 145 | </div> |
| 145 | </div> | 146 | </div> |
| @@ -1,4 +1,6 @@ | |||
| 1 | import { useEffect, useMemo, useRef, useState } from "react"; | 1 | import { useEffect, useMemo, useRef, useState } from "react"; |
| 2 | import { Button } from "./button"; | ||
| 3 | import { Input } from "./input"; | ||
| 2 | 4 | ||
| 3 | // --------------------------------------------------------------------------- | 5 | // --------------------------------------------------------------------------- |
| 4 | // Types | 6 | // Types |
| @@ -161,8 +163,8 @@ function TreeNodeView(props: { · 156 unmodified lines | |||
| 161 | 163 | ||
| 162 | return ( | 164 | return ( |
| 163 | <div> | 165 | <div> |
| 164 | <button | 166 | <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 | <button | 237 | <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 | <input | 318 | <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 | <button | 326 | <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 | / |
| @@ -13,6 +13,7 @@ import { ToolDetail, ToolDetailEmpty } from "../components/tool-detail"; · 12 unmodified lines | |||
| 13 | import type { ToolSummary } from "../components/tool-tree"; | 13 | import type { ToolSummary } from "../components/tool-tree"; |
| 14 | import { useScope } from "../hooks/use-scope"; | 14 | import { useScope } from "../hooks/use-scope"; |
| 15 | import type { SourcePlugin } from "../plugins/source-plugin"; | 15 | import type { SourcePlugin } from "../plugins/source-plugin"; |
| 16 | import { Button } from "../components/button"; | ||
| 16 | 17 | ||
| 17 | export function SourceDetailPage(props: { | 18 | export 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 | <button | 135 | <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 | Edit | 136 | Edit |
| 140 | </button> | 137 | </Button> |
| 141 | )} | 138 | )} |
| 142 | 139 | ||
| 143 | {editing && ( | 140 | {editing && ( |
| 144 | <button | 141 | <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 tools | 142 | Back to tools |
| 150 | </button> | 143 | </Button> |
| 151 | )} | 144 | )} |
| 152 | 145 | ||
| 153 | {canRefresh && !editing && ( | 146 | {canRefresh && !editing && ( |
| 154 | <button | 147 | <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 | <button | 162 | <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 | Cancel | 168 | Cancel |
| 176 | </button> | 169 | </Button> |
| 177 | <button | 170 | <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 | <button | 180 | <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 | Delete | 186 | Delete |
| 193 | </button> | 187 | </Button> |
| 194 | ))} | 188 | ))} |
| 195 | </div> | 189 | </div> |
| 196 | </div> | 190 | </div> |
| @@ -5,6 +5,8 @@ import { sourcesAtom, detectSource } from "../api/atoms"; · 4 unmodified lines | |||
| 5 | import { useScope } from "../hooks/use-scope"; | 5 | import { useScope } from "../hooks/use-scope"; |
| 6 | import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin"; | 6 | import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin"; |
| 7 | import { McpInstallCard } from "../components/mcp-install-card"; | 7 | import { McpInstallCard } from "../components/mcp-install-card"; |
| 8 | import { Button } from "../components/button"; | ||
| 9 | import { Input } from "../components/input"; | ||
| 8 | 10 | ||
| 9 | const KIND_TO_PLUGIN_KEY: Record<string, string> = { | 11 | const 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 | <input | 84 | <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 | <button | 98 | <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"> |
| @@ -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 | <button | 346 | <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 | ||