mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-10 06:45:28 -05:00
fix(frontend): typed inputs in new run modal (#10799)
## Changes 🏗️ ### Make the **Credentials Inputs** show up on the new Run Agent Modal <img width="450" height="784" alt="Screenshot 2025-09-02 at 00 54 19" src="https://github.com/user-attachments/assets/26ad8242-a1bc-45f6-9149-a3d207683679" /> ### Fixes on other modals... <img width="450" height="579" alt="Screenshot 2025-09-02 at 00 04 40" src="https://github.com/user-attachments/assets/fa2f9ed9-207b-4599-9e60-3e37c4be6ea9" /> <img width="450" height="579" alt="Screenshot 2025-09-02 at 00 04 44" src="https://github.com/user-attachments/assets/92e062a7-f161-423e-b6c9-f998fbdef102" /> <img width="450" height="634" alt="Screenshot 2025-09-02 at 00 47 06" src="https://github.com/user-attachments/assets/93009809-0df0-44c5-b2d2-a9aa0f501312" /> - always use buttons/inputs in small size ( _due to the tight space_ ) - use from the design system - always use pretty scrollbars - Configure [`tailwind-scrollbars`](https://github.com/adoxography/tailwind-scrollbar) for pretty scrollbars - prevent content in dialog to overflow when scrollable ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run the app locally with the new agent run modal flag enabled - [x] Check the above #### For configuration changes: None
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
"shepherd.js": "14.5.1",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"uuid": "11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
|
||||
32
autogpt_platform/frontend/pnpm-lock.yaml
generated
32
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -200,6 +200,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: 2.6.0
|
||||
version: 2.6.0
|
||||
tailwind-scrollbar:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17)
|
||||
tailwindcss-animate:
|
||||
specifier: 1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
@@ -2930,6 +2933,9 @@ packages:
|
||||
'@types/phoenix@1.6.6':
|
||||
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
@@ -5909,6 +5915,11 @@ packages:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
prism-react-renderer@2.4.1:
|
||||
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -6612,6 +6623,12 @@ packages:
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
tailwind-scrollbar@4.0.2:
|
||||
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 4.x
|
||||
|
||||
tailwindcss-animate@1.0.7:
|
||||
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
|
||||
peerDependencies:
|
||||
@@ -10211,6 +10228,8 @@ snapshots:
|
||||
|
||||
'@types/phoenix@1.6.6': {}
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/react-dom@18.3.5(@types/react@18.3.17)':
|
||||
@@ -13581,6 +13600,12 @@ snapshots:
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
prism-react-renderer@2.4.1(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/prismjs': 1.26.5
|
||||
clsx: 2.1.1
|
||||
react: 18.3.1
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
@@ -14465,6 +14490,13 @@ snapshots:
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
prism-react-renderer: 2.4.1(react@18.3.1)
|
||||
tailwindcss: 3.4.17
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
tailwindcss: 3.4.17
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Form, FormDescription, FormField } from "@/components/ui/form";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
@@ -54,6 +46,9 @@ export function APIKeyCredentialsModal({
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{
|
||||
maxWidth: "25rem",
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
{schemaDescription && (
|
||||
@@ -61,79 +56,65 @@ export function APIKeyCredentialsModal({
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
{schema.credentials_scopes && (
|
||||
<FormDescription>
|
||||
Required scope(s) for this block:{" "}
|
||||
{schema.credentials_scopes?.map((s, i, a) => (
|
||||
<span key={i}>
|
||||
<code>{s}</code>
|
||||
{i < a.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormControl>
|
||||
<Input
|
||||
id="apiKey"
|
||||
label="API Key"
|
||||
hideLabel
|
||||
type="password"
|
||||
placeholder="Enter API key..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<>
|
||||
<Input
|
||||
id="apiKey"
|
||||
label="API Key"
|
||||
type="password"
|
||||
placeholder="Enter API key..."
|
||||
size="small"
|
||||
hint={
|
||||
schema.credentials_scopes ? (
|
||||
<FormDescription>
|
||||
Required scope(s) for this block:{" "}
|
||||
{schema.credentials_scopes?.map((s, i, a) => (
|
||||
<span key={i}>
|
||||
<code className="text-xs font-bold">{s}</code>
|
||||
{i < a.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</FormDescription>
|
||||
) : null
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="title"
|
||||
label="Name"
|
||||
hideLabel
|
||||
type="text"
|
||||
placeholder="Enter a name for this API key..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
id="title"
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder="Enter a name for this API key..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiresAt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Expiration Date (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="expiresAt"
|
||||
label="Expiration Date"
|
||||
hideLabel
|
||||
type="datetime-local"
|
||||
placeholder="Select expiration date..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
id="expiresAt"
|
||||
label="Expiration Date"
|
||||
type="datetime-local"
|
||||
placeholder="Select expiration date..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
<Button type="submit" size="small" className="min-w-68">
|
||||
Save & use this API key
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -335,30 +335,39 @@ export const CredentialsInput: FC<{
|
||||
// Show credentials creation UI when no relevant credentials exist
|
||||
if (!hasRelevantCredentials) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
{fieldHeader}
|
||||
|
||||
<div className={cn("flex flex-row space-x-2", className)}>
|
||||
{supportsOAuth2 && (
|
||||
<Button onClick={handleOAuthLogin}>
|
||||
<Button onClick={handleOAuthLogin} size="small">
|
||||
<ProviderIcon className="mr-2 h-4 w-4" />
|
||||
{"Sign in with " + providerName}
|
||||
</Button>
|
||||
)}
|
||||
{supportsApiKey && (
|
||||
<Button onClick={() => setAPICredentialsModalOpen(true)}>
|
||||
<Button
|
||||
onClick={() => setAPICredentialsModalOpen(true)}
|
||||
size="small"
|
||||
>
|
||||
<ProviderIcon className="mr-2 h-4 w-4" />
|
||||
Enter API key
|
||||
</Button>
|
||||
)}
|
||||
{supportsUserPassword && (
|
||||
<Button onClick={() => setUserPasswordCredentialsModalOpen(true)}>
|
||||
<Button
|
||||
onClick={() => setUserPasswordCredentialsModalOpen(true)}
|
||||
size="small"
|
||||
>
|
||||
<ProviderIcon className="mr-2 h-4 w-4" />
|
||||
Enter username and password
|
||||
</Button>
|
||||
)}
|
||||
{supportsHostScoped && credentials.discriminatorValue && (
|
||||
<Button onClick={() => setHostScopedCredentialsModalOpen(true)}>
|
||||
<Button
|
||||
onClick={() => setHostScopedCredentialsModalOpen(true)}
|
||||
size="small"
|
||||
>
|
||||
<ProviderIcon className="mr-2 h-4 w-4" />
|
||||
{`Enter sensitive headers for ${getHostFromUrl(credentials.discriminatorValue)}`}
|
||||
</Button>
|
||||
|
||||
@@ -7,12 +7,9 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import useCredentials from "@/hooks/useCredentials";
|
||||
import {
|
||||
@@ -20,6 +17,7 @@ import {
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { getHostFromUrl } from "@/lib/utils/url";
|
||||
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
|
||||
|
||||
type Props = {
|
||||
schema: BlockIOCredentialsSubSchema;
|
||||
@@ -139,6 +137,9 @@ export function HostScopedCredentialsModal({
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{
|
||||
maxWidth: "25rem",
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
{schema.description && (
|
||||
@@ -146,81 +147,74 @@ export function HostScopedCredentialsModal({
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host Pattern</FormLabel>
|
||||
<FormDescription>
|
||||
{currentHost
|
||||
<Input
|
||||
id="host"
|
||||
label="Host Pattern"
|
||||
type="text"
|
||||
size="small"
|
||||
readOnly={!!currentHost}
|
||||
hint={
|
||||
currentHost
|
||||
? "Auto-populated from the URL field. Headers will be applied to requests to this host."
|
||||
: "Enter the host/domain to match against request URLs (e.g., api.example.com)."}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="host"
|
||||
label="Host Pattern"
|
||||
hideLabel
|
||||
type="text"
|
||||
readOnly={!!currentHost}
|
||||
placeholder={
|
||||
currentHost
|
||||
? undefined
|
||||
: "Enter host (e.g., api.example.com)"
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
: "Enter the host/domain to match against request URLs (e.g., api.example.com)."
|
||||
}
|
||||
placeholder={
|
||||
currentHost
|
||||
? undefined
|
||||
: "Enter host (e.g., api.example.com)"
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Headers</FormLabel>
|
||||
<FormDescription>
|
||||
<FormDescription className="max-w-md">
|
||||
Add sensitive headers (like Authorization, X-API-Key) that
|
||||
should be automatically included in requests to the specified
|
||||
host.
|
||||
</FormDescription>
|
||||
|
||||
{headerPairs.map((pair, index) => (
|
||||
<div key={index} className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id={`header-${index}-key`}
|
||||
label="Header Name"
|
||||
hideLabel
|
||||
placeholder="Header name (e.g., Authorization)"
|
||||
value={pair.key}
|
||||
onChange={(e) =>
|
||||
updateHeaderPair(index, "key", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id={`header-${index}-value`}
|
||||
label="Header Value"
|
||||
hideLabel
|
||||
type="password"
|
||||
placeholder="Header value (e.g., Bearer token123)"
|
||||
value={pair.value}
|
||||
onChange={(e) =>
|
||||
updateHeaderPair(index, "value", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div key={index} className="flex w-full items-center gap-4">
|
||||
<Input
|
||||
id={`header-${index}-key`}
|
||||
label="Header Name"
|
||||
placeholder="Header name (e.g., Authorization)"
|
||||
size="small"
|
||||
value={pair.key}
|
||||
className="flex-1"
|
||||
onChange={(e) =>
|
||||
updateHeaderPair(index, "key", e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id={`header-${index}-value`}
|
||||
label="Header Value"
|
||||
size="small"
|
||||
type="password"
|
||||
className="flex-2"
|
||||
placeholder="Header value (e.g., Bearer token123)"
|
||||
value={pair.value}
|
||||
onChange={(e) =>
|
||||
updateHeaderPair(index, "value", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => removeHeaderPair(index)}
|
||||
disabled={headerPairs.length === 1}
|
||||
>
|
||||
Remove
|
||||
<TrashIcon className="size-4" /> Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@@ -230,15 +224,16 @@ export function HostScopedCredentialsModal({
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={addHeaderPair}
|
||||
className="w-full"
|
||||
>
|
||||
Add Another Header
|
||||
<PlusIcon className="size-4" /> Add Another Header
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Save & use these credentials
|
||||
</Button>
|
||||
<div className="pt-8">
|
||||
<Button type="submit" className="w-full" size="small">
|
||||
Save & use these credentials
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { Form, FormField } from "@/components/ui/form";
|
||||
import useCredentials from "@/hooks/useCredentials";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
@@ -81,76 +69,72 @@ export function PasswordCredentialsModal({
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
title={`Add new username & password for ${providerName}`}
|
||||
controlled={{
|
||||
isOpen: open,
|
||||
set: (isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{
|
||||
maxWidth: "25rem",
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Add new username & password for {providerName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Dialog.Content>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-2 pt-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter username..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
id="username"
|
||||
label="Username"
|
||||
type="text"
|
||||
placeholder="Enter username..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter password..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Enter password..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter a name for this user login..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
id="title"
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder="Enter a name for this user login..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
<Button type="submit" size="small" className="min-w-68">
|
||||
Save & use this user login
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export function RunAgentInputs({
|
||||
id={`${baseId}-number`}
|
||||
label={schema.title ?? placeholder ?? "Number"}
|
||||
hideLabel
|
||||
size="small"
|
||||
type="number"
|
||||
value={value ?? ""}
|
||||
placeholder={placeholder || "Enter number"}
|
||||
@@ -73,6 +74,7 @@ export function RunAgentInputs({
|
||||
id={`${baseId}-textarea`}
|
||||
label={schema.title ?? placeholder ?? "Text"}
|
||||
hideLabel
|
||||
size="small"
|
||||
type="textarea"
|
||||
rows={3}
|
||||
value={value ?? ""}
|
||||
@@ -105,6 +107,7 @@ export function RunAgentInputs({
|
||||
id={`${baseId}-date`}
|
||||
label={schema.title ?? placeholder ?? "Date"}
|
||||
hideLabel
|
||||
size="small"
|
||||
type="date"
|
||||
value={value ? format(value as Date, "yyyy-MM-dd") : ""}
|
||||
onChange={(e) => {
|
||||
@@ -133,6 +136,7 @@ export function RunAgentInputs({
|
||||
id={`${baseId}-datetime`}
|
||||
label={schema.title ?? placeholder ?? "Date time"}
|
||||
hideLabel
|
||||
size="small"
|
||||
type="datetime-local"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
@@ -167,6 +171,7 @@ export function RunAgentInputs({
|
||||
label={schema.title ?? placeholder ?? "Select"}
|
||||
hideLabel
|
||||
value={value ?? ""}
|
||||
size="small"
|
||||
onValueChange={(val: string) => onChange(val)}
|
||||
placeholder={placeholder || "Select an option"}
|
||||
options={schema.enum
|
||||
@@ -189,6 +194,7 @@ export function RunAgentInputs({
|
||||
items={allKeys.map((key) => ({
|
||||
value: key,
|
||||
label: _schema.properties[key]?.title ?? key,
|
||||
size: "small",
|
||||
}))}
|
||||
selectedValues={selectedValues}
|
||||
onChange={(values: string[]) =>
|
||||
@@ -212,6 +218,7 @@ export function RunAgentInputs({
|
||||
id={`${baseId}-text`}
|
||||
label={schema.title ?? placeholder ?? "Text"}
|
||||
hideLabel
|
||||
size="small"
|
||||
type="text"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
|
||||
@@ -26,28 +26,45 @@ interface Props {
|
||||
|
||||
export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
const {
|
||||
// UI state
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
showScheduleView,
|
||||
|
||||
// Run mode
|
||||
defaultRunType,
|
||||
|
||||
// Form: regular inputs
|
||||
inputValues,
|
||||
setInputValues,
|
||||
|
||||
// Form: credentials
|
||||
inputCredentials,
|
||||
setInputCredentials,
|
||||
|
||||
// Preset/trigger labels
|
||||
presetName,
|
||||
presetDescription,
|
||||
setPresetName,
|
||||
setPresetDescription,
|
||||
|
||||
// Scheduling
|
||||
scheduleName,
|
||||
cronExpression,
|
||||
|
||||
// Validation/readiness
|
||||
allRequiredInputsAreSet,
|
||||
// agentInputFields, // Available if needed for future use
|
||||
|
||||
// Schemas
|
||||
agentInputFields,
|
||||
agentCredentialsInputFields,
|
||||
hasInputFields,
|
||||
|
||||
// Async states
|
||||
isExecuting,
|
||||
isCreatingSchedule,
|
||||
isSettingUpTrigger,
|
||||
|
||||
// Actions
|
||||
handleRun,
|
||||
handleSchedule,
|
||||
handleShowSchedule,
|
||||
@@ -58,6 +75,10 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
|
||||
const [isScheduleFormValid, setIsScheduleFormValid] = useState(true);
|
||||
|
||||
const hasAnySetupFields =
|
||||
Object.keys(agentInputFields || {}).length > 0 ||
|
||||
Object.keys(agentCredentialsInputFields || {}).length > 0;
|
||||
|
||||
function handleInputChange(key: string, value: string) {
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
@@ -97,7 +118,7 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
>
|
||||
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full flex-col pb-4">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0">
|
||||
<ModalHeader agent={agent} />
|
||||
@@ -105,13 +126,10 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto overflow-x-hidden pr-1"
|
||||
style={{ scrollbarGutter: "stable" }}
|
||||
>
|
||||
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
|
||||
{/* Setup Section */}
|
||||
<div className="mt-10">
|
||||
{hasInputFields ? (
|
||||
{hasAnySetupFields ? (
|
||||
<RunAgentModalContextProvider
|
||||
value={{
|
||||
agent,
|
||||
@@ -191,30 +209,31 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
<AgentDetails agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Actions - sticky inside dialog scroll */}
|
||||
<Dialog.Footer className="sticky bottom-0 z-10 bg-white">
|
||||
{showScheduleView ? (
|
||||
<ScheduleActions
|
||||
onSchedule={handleSchedule}
|
||||
isCreatingSchedule={isCreatingSchedule}
|
||||
allRequiredInputsAreSet={
|
||||
allRequiredInputsAreSet &&
|
||||
!!scheduleName.trim() &&
|
||||
isScheduleFormValid
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
allRequiredInputsAreSet={allRequiredInputsAreSet}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
<Dialog.Footer
|
||||
className="fixed bottom-1 left-0 z-10 w-full bg-white p-4"
|
||||
style={{ boxShadow: "0px -8px 10px white" }}
|
||||
>
|
||||
{showScheduleView ? (
|
||||
<ScheduleActions
|
||||
onSchedule={handleSchedule}
|
||||
isCreatingSchedule={isCreatingSchedule}
|
||||
allRequiredInputsAreSet={
|
||||
allRequiredInputsAreSet &&
|
||||
!!scheduleName.trim() &&
|
||||
isScheduleFormValid
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
allRequiredInputsAreSet={allRequiredInputsAreSet}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
inputValues: Record<string, any>;
|
||||
onInputChange: (key: string, value: string) => void;
|
||||
variant?: "default" | "schedule";
|
||||
}
|
||||
|
||||
export function AgentInputFields({
|
||||
agent,
|
||||
inputValues,
|
||||
onInputChange,
|
||||
variant = "default",
|
||||
}: Props) {
|
||||
const hasInputFields =
|
||||
agent.input_schema &&
|
||||
typeof agent.input_schema === "object" &&
|
||||
"properties" in agent.input_schema;
|
||||
|
||||
if (!hasInputFields) {
|
||||
const emptyStateClass =
|
||||
variant === "schedule"
|
||||
? "rounded-lg bg-neutral-50 p-4 text-sm text-neutral-500"
|
||||
: "p-4 text-sm text-neutral-500";
|
||||
|
||||
return (
|
||||
<div className={emptyStateClass}>
|
||||
No input fields required for this agent
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries((agent.input_schema as any).properties || {}).map(
|
||||
([key, schema]: [string, any]) => (
|
||||
<Input
|
||||
key={key}
|
||||
id={key}
|
||||
label={schema.title || key}
|
||||
value={inputValues[key] || ""}
|
||||
onChange={(e) => onInputChange(key, e.target.value)}
|
||||
placeholder={schema.description}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export function DefaultRunView() {
|
||||
<Input
|
||||
id="trigger_name"
|
||||
label="Trigger Name"
|
||||
size="small"
|
||||
hideLabel
|
||||
value={presetName}
|
||||
placeholder="Enter trigger name"
|
||||
@@ -50,6 +51,7 @@ export function DefaultRunView() {
|
||||
<Input
|
||||
id="trigger_description"
|
||||
label="Trigger Description"
|
||||
size="small"
|
||||
hideLabel
|
||||
value={presetDescription}
|
||||
placeholder="Enter trigger description"
|
||||
|
||||
@@ -326,32 +326,47 @@ export function useAgentRunModal(
|
||||
}, [agentInputFields]);
|
||||
|
||||
return {
|
||||
// UI state
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
showScheduleView,
|
||||
|
||||
// Run mode
|
||||
defaultRunType,
|
||||
|
||||
// Form: regular inputs
|
||||
inputValues,
|
||||
setInputValues,
|
||||
|
||||
// Form: credentials
|
||||
inputCredentials,
|
||||
setInputCredentials,
|
||||
|
||||
// Preset/trigger labels
|
||||
presetName,
|
||||
presetDescription,
|
||||
setPresetName,
|
||||
setPresetDescription,
|
||||
|
||||
// Scheduling
|
||||
scheduleName,
|
||||
cronExpression,
|
||||
|
||||
// Validation/readiness
|
||||
allRequiredInputsAreSet,
|
||||
missingInputs,
|
||||
// Expose credential readiness for any UI hints if needed
|
||||
// but enforcement is already applied in allRequiredInputsAreSet
|
||||
// allCredentialsAreSet,
|
||||
// missingCredentials,
|
||||
|
||||
// Schemas for rendering
|
||||
agentInputFields,
|
||||
agentCredentialsInputFields,
|
||||
hasInputFields,
|
||||
|
||||
// Async states
|
||||
isExecuting: executeGraphMutation.isPending,
|
||||
isCreatingSchedule: createScheduleMutation.isPending,
|
||||
isSettingUpTrigger: setupTriggerMutation.isPending,
|
||||
|
||||
// Actions
|
||||
handleRun,
|
||||
handleSchedule,
|
||||
handleShowSchedule,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@plugin 'tailwind-scrollbar';
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 98%; /* neutral-50#FAFAFA */
|
||||
|
||||
@@ -332,13 +332,7 @@ export const CustomNode = React.memo(
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={handleSSOLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button type="button" onClick={handleSSOLogin} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
"Loading..."
|
||||
) : (
|
||||
|
||||
@@ -178,7 +178,11 @@ export function Input({
|
||||
<Text variant="body-medium" as="span" className="text-black">
|
||||
{label}
|
||||
</Text>
|
||||
{hint}
|
||||
{hint ? (
|
||||
<Text variant="small" as="span" className="!text-zinc-400">
|
||||
{hint}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
{inputWithError}
|
||||
</label>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Button } from "../../../../atoms/Button/Button";
|
||||
import { StepHeader } from "../StepHeader";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAgentSelectStep } from "./useAgentSelectStep";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
onSelect: (agentId: string, agentVersion: number) => void;
|
||||
@@ -110,7 +112,10 @@ export function AgentSelectStep({
|
||||
<div className="flex-grow overflow-hidden p-4 sm:p-6">
|
||||
<h3 className="sr-only">List of agents</h3>
|
||||
<div
|
||||
className="h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]"
|
||||
className={cn(
|
||||
scrollbarStyles,
|
||||
"h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]",
|
||||
)}
|
||||
role="region"
|
||||
aria-labelledby="agentListHeading"
|
||||
>
|
||||
|
||||
@@ -4,12 +4,14 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function BaseFooter({
|
||||
children,
|
||||
testId = "modal-footer",
|
||||
className = "",
|
||||
style,
|
||||
}: Props) {
|
||||
const ctx = useDialogCtx();
|
||||
|
||||
@@ -17,6 +19,7 @@ export function BaseFooter({
|
||||
<div
|
||||
className={`flex justify-end gap-4 pt-6 ${className}`}
|
||||
data-testid={testId}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import * as RXDialog from "@radix-ui/react-dialog";
|
||||
import { CSSProperties, PropsWithChildren } from "react";
|
||||
import {
|
||||
CSSProperties,
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { DialogCtx } from "../useDialogCtx";
|
||||
import { modalStyles } from "./styles";
|
||||
import styles from "./styles.module.css";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
|
||||
type BaseProps = DialogCtx & PropsWithChildren;
|
||||
|
||||
@@ -22,6 +27,25 @@ export function DialogWrap({
|
||||
isForceOpen,
|
||||
handleClose,
|
||||
}: Props) {
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function update() {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
setHasVerticalScrollbar(el.scrollHeight > el.clientHeight + 1);
|
||||
}
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
if (scrollRef.current) ro.observe(scrollRef.current);
|
||||
window.addEventListener("resize", update);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RXDialog.Portal>
|
||||
<RXDialog.Overlay className={modalStyles.overlay} />
|
||||
@@ -51,17 +75,23 @@ export function DialogWrap({
|
||||
)}
|
||||
|
||||
{isForceOpen && !handleClose ? null : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
className="absolute -right-2 top-2 z-50 hover:border-transparent hover:bg-transparent"
|
||||
className="absolute right-4 top-4 z-50 hover:border-transparent hover:bg-transparent focus:border-none focus:outline-none"
|
||||
>
|
||||
<X className={modalStyles.icon} />
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={`overflow-y-auto ${styles.scrollableContent}`}>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={cn("overflow-y-auto overflow-x-hidden", scrollbarStyles)}
|
||||
style={{
|
||||
scrollbarGutter: "stable",
|
||||
marginRight: hasVerticalScrollbar ? "-14px" : "0px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</RXDialog.Content>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PropsWithChildren } from "react";
|
||||
import { Drawer } from "vaul";
|
||||
import { DialogCtx } from "../useDialogCtx";
|
||||
import { drawerStyles, modalStyles } from "./styles";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type BaseProps = DialogCtx & PropsWithChildren;
|
||||
|
||||
@@ -62,9 +61,7 @@ export function DrawerWrap({
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<div className={`overflow-auto ${styles.scrollableContent}`}>
|
||||
{children}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
);
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/* Scrollable content area for dialogs */
|
||||
.scrollableContent {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(209, 213, 219) white;
|
||||
}
|
||||
|
||||
/* Webkit scrollbar styles */
|
||||
.scrollableContent::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-track {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb {
|
||||
background: rgb(209, 213, 219); /* gray-300 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(156, 163, 175); /* gray-400 */
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.scrollableContent {
|
||||
scrollbar-color: rgb(107, 114, 128) rgb(31, 41, 55);
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-track {
|
||||
background: rgb(31, 41, 55);
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb {
|
||||
background: rgb(107, 114, 128); /* gray-500 */
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(75, 85, 99); /* gray-600 */
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ const commonStyles = {
|
||||
overlay:
|
||||
"fixed inset-0 z-50 bg-stone-500/20 dark:bg-black/50 backdrop-blur-md animate-fade-in",
|
||||
content:
|
||||
"bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full overflow-hidden",
|
||||
"overflow-y-hidden bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full",
|
||||
};
|
||||
|
||||
// Modal specific styles
|
||||
export const modalStyles = {
|
||||
...commonStyles,
|
||||
content: `${commonStyles.content} p-6 border border-stone-200 overflow-y-auto min-w-[40vw] max-w-[60vw] max-h-[95vh] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-fadein`,
|
||||
content: `${commonStyles.content} p-6 border border-stone-200 min-w-[40vw] max-w-[60vw] max-h-[95vh] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-fadein`,
|
||||
iconWrap:
|
||||
"absolute top-2 right-3 bg-transparent p-2 rounded-full transition-colors duration-300 ease-in-out outline-none border-none",
|
||||
icon: "w-4 h-4 text-stone-800",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const scrollbarStyles =
|
||||
"scrollbar-thin scrollbar-thumb-zinc-300 scrollbar-track-transparent";
|
||||
@@ -256,7 +256,7 @@ const MultiSelectorList = forwardRef<
|
||||
<CommandList
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors",
|
||||
"absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground scrollbar-thumb-rounded-lg dark:scrollbar-thumb-muted",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import tailwindcssAnimate from "tailwindcss-animate";
|
||||
import scrollbar from "tailwind-scrollbar";
|
||||
import { colors } from "./src/components/styles/colors";
|
||||
|
||||
const config = {
|
||||
@@ -157,7 +158,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [tailwindcssAnimate],
|
||||
plugins: [tailwindcssAnimate, scrollbar({ nocompatible: true })],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user