Unified · Split
| @@ -2,7 +2,7 @@ · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { CheckIcon } from "lucide-react"; | 3 | import { CheckIcon } from "lucide-react"; |
| 4 | import { Radio as RadioPrimitive } from "@base-ui/react/radio"; | 4 | import { Radio as RadioPrimitive } from "@base-ui/react/radio"; |
| 5 | import { useCallback, useEffect, useMemo, useState } from "react"; | 5 | import { useCallback, useMemo, useState } from "react"; |
| 6 | import { | 6 | import { |
| 7 | ProviderInstanceId, | 7 | ProviderInstanceId, |
| 8 | ProviderDriverKind, | 8 | ProviderDriverKind, |
| @@ -114,6 +114,18 @@ interface AddProviderInstanceDialogProps { · 105 unmodified lines | |||
| 114 | } | 114 | } |
| 115 | 115 | ||
| 116 | export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { | 116 | export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { |
| 117 | return ( | ||
| 118 | <Dialog open={open} onOpenChange={onOpenChange}> | ||
| 119 | {open ? <AddProviderInstanceDialogContent onOpenChange={onOpenChange} /> : null} | ||
| 120 | </Dialog> | ||
| 121 | ); | ||
| 122 | } | ||
| 123 | |||
| 124 | function AddProviderInstanceDialogContent({ | ||
| 125 | onOpenChange, | ||
| 126 | }: { | ||
| 127 | readonly onOpenChange: (open: boolean) => void; | ||
| 128 | }) { | ||
| 117 | const settings = useSettings(); | 129 | const settings = useSettings(); |
| 118 | const { updateSettings } = useUpdateSettings(); | 130 | const { updateSettings } = useUpdateSettings(); |
| 119 | 131 | ||
| @@ -121,8 +133,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 1 unmodified line | |||
| 121 | const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND); | 133 | const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND); |
| 122 | const [label, setLabel] = useState(""); | 134 | const [label, setLabel] = useState(""); |
| 123 | const [accentColor, setAccentColor] = useState<string>(""); | 135 | const [accentColor, setAccentColor] = useState<string>(""); |
| 124 | const [instanceId, setInstanceId] = useState(""); | 136 | const [manualInstanceId, setManualInstanceId] = useState<string | null>(null); |
| 125 | const [instanceIdDirty, setInstanceIdDirty] = useState(false); | ||
| 126 | // Driver-specific config drafts keyed by driver so toggling between drivers | 137 | // Driver-specific config drafts keyed by driver so toggling between drivers |
| 127 | // during the same dialog session does not lose in-progress input. | 138 | // during the same dialog session does not lose in-progress input. |
| 128 | const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({}); | 139 | const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({}); |
| @@ -135,32 +146,12 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 6 unmodified lines | |||
| 135 | [settings.providerInstances], | 146 | [settings.providerInstances], |
| 136 | ); | 147 | ); |
| 137 | 148 | ||
| 138 | // Reset the form every time the dialog opens so each creation starts | ||
| 139 | // from a clean slate. | ||
| 140 | useEffect(() => { | ||
| 141 | if (!open) return; | ||
| 142 | setDriver(DEFAULT_DRIVER_KIND); | ||
| 143 | setLabel(""); | ||
| 144 | setAccentColor(""); | ||
| 145 | setInstanceId(""); | ||
| 146 | setWizardStep(0); | ||
| 147 | setInstanceIdDirty(false); | ||
| 148 | setConfigByDriver({}); | ||
| 149 | setHasAttemptedSubmit(false); | ||
| 150 | }, [open]); | ||
| 151 | |||
| 152 | // Auto-derive the instance id from driver + label until the user types | ||
| 153 | // in the Instance ID field directly (after which they own its value). | ||
| 154 | useEffect(() => { | ||
| 155 | if (instanceIdDirty) return; | ||
| 156 | setInstanceId(deriveInstanceId(driver, label)); | ||
| 157 | }, [driver, label, instanceIdDirty]); | ||
| 158 | |||
| 159 | const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION; | 149 | const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION; |
| 160 | const driverSettingsFields = useMemo( | 150 | const driverSettingsFields = useMemo( |
| 161 | () => deriveProviderSettingsFields(driverOption), | 151 | () => deriveProviderSettingsFields(driverOption), |
| 162 | [driverOption], | 152 | [driverOption], |
| 163 | ); | 153 | ); |
| 154 | const instanceId = manualInstanceId ?? deriveInstanceId(driver, label); | ||
| 164 | const instanceIdError = validateInstanceId(instanceId, existingIds); | 155 | const instanceIdError = validateInstanceId(instanceId, existingIds); |
| 165 | const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null; | 156 | const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null; |
| 166 | const previewLabel = label.trim() || `${driverOption.label} Workspace`; | 157 | const previewLabel = label.trim() || `${driverOption.label} Workspace`; |
| @@ -236,256 +227,250 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 69 unmodified lines | |||
| 236 | ]); | 227 | ]); |
| 237 | 228 | ||
| 238 | return ( | 229 | return ( |
| 239 | <Dialog open={open} onOpenChange={onOpenChange}> | 230 | <DialogPopup className="max-w-xl overflow-hidden"> |
| 240 | <DialogPopup className="max-w-xl overflow-hidden"> | 231 | <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl"> |
| 241 | <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl"> | 232 | <DialogHeader className="border-b border-border/70 bg-background"> |
| 242 | <DialogHeader className="border-b border-border/70 bg-background"> | 233 | <DialogTitle>Add provider instance</DialogTitle> |
| 243 | <DialogTitle>Add provider instance</DialogTitle> | 234 | <DialogDescription> |
| 244 | <DialogDescription> | 235 | Configure an additional provider instance — for example, a second Codex install pointed |
| 245 | Configure an additional provider instance — for example, a second Codex install | 236 | at a different workspace. |
| 246 | pointed at a different workspace. | 237 | </DialogDescription> |
| 247 | </DialogDescription> | 238 | <div className="grid grid-cols-3 gap-2"> |
| 248 | <div className="grid grid-cols-3 gap-2"> | 239 | {wizardSteps.map((step, index) => ( |
| 249 | {wizardSteps.map((step, index) => ( | 240 | <button |
| 250 | <button | 241 | key={step} |
| 251 | key={step} | 242 | type="button" |
| 252 | type="button" | 243 | className={cn( |
| 244 | "grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left", | ||
| 245 | index === wizardStep | ||
| 246 | ? "border-primary bg-primary/10 ring-1 ring-primary/25" | ||
| 247 | : index < wizardStep | ||
| 248 | ? "border-border bg-background" | ||
| 249 | : "border-border bg-muted/40", | ||
| 250 | )} | ||
| 251 | onClick={() => setWizardStep(index)} | ||
| 252 | > | ||
| 253 | <span | ||
| 253 | className={cn( | 254 | className={cn( |
| 254 | "grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left", | 255 | "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border", |
| 255 | index === wizardStep | 256 | index < wizardStep |
| 256 | ? "border-primary bg-primary/10 ring-1 ring-primary/25" | 257 | ? "border-primary bg-primary text-primary-foreground" |
| 257 | : index < wizardStep | 258 | : index === wizardStep |
| 258 | ? "border-border bg-background" | 259 | ? "border-primary bg-background" |
| 259 | : "border-border bg-muted/40", | 260 | : "border-muted-foreground/35 bg-background", |
| 260 | )} | 261 | )} |
| 261 | onClick={() => setWizardStep(index)} | 262 | aria-hidden |
| 262 | > | 263 | > |
| 263 | <span | 264 | {index < wizardStep ? <CheckIcon className="size-3" /> : null} |
| 264 | className={cn( | 265 | </span> |
| 265 | "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border", | 266 | <span className="text-[10px] font-medium uppercase text-muted-foreground"> |
| 266 | index < wizardStep | 267 | Step {index + 1} |
| 267 | ? "border-primary bg-primary text-primary-foreground" | 268 | </span> |
| 268 | : index === wizardStep | 269 | <span className="truncate text-xs font-semibold text-foreground"> |
| 269 | ? "border-primary bg-background" | 270 | {step} |
| 270 | : "border-muted-foreground/35 bg-background", | 271 | {index < wizardStep && wizardStepSummaries[index] |
| 271 | )} | 272 | ? `: ${wizardStepSummaries[index]}` |
| 272 | aria-hidden | 273 | : ""} |
| 273 | > | 274 | </span> |
| 274 | {index < wizardStep ? <CheckIcon className="size-3" /> : null} | 275 | </button> |
| 275 | </span> | 276 | ))} |
| 276 | <span className="text-[10px] font-medium uppercase text-muted-foreground"> | 277 | </div> |
| 277 | Step {index + 1} | 278 | </DialogHeader> |
| 278 | </span> | 279 | |
| 279 | <span className="truncate text-xs font-semibold text-foreground"> | 280 | <div |
| 280 | {step} | 281 | data-slot="dialog-panel" |
| 281 | {index < wizardStep && wizardStepSummaries[index] | 282 | className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5" |
| 282 | ? `: ${wizardStepSummaries[index]}` | 283 | > |
| 283 | : ""} | 284 | <AnimatedHeight> |
| 284 | </span> | 285 | <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}> |
| 285 | </button> | 286 | <span id="add-instance-driver-label" className="text-xs font-medium text-foreground"> |
| 286 | ))} | 287 | Driver |
| 288 | </span> | ||
| 289 | <RadioGroup | ||
| 290 | value={driver} | ||
| 291 | onValueChange={(value) => setDriver(ProviderDriverKind.make(value))} | ||
| 292 | aria-labelledby="add-instance-driver-label" | ||
| 293 | className="grid grid-cols-2 gap-2.5" | ||
| 294 | > | ||
| 295 | {DRIVER_OPTIONS.map((option) => { | ||
| 296 | const IconComponent = option.icon; | ||
| 297 | const isSelected = option.value === driver; | ||
| 298 | return ( | ||
| 299 | <RadioPrimitive.Root | ||
| 300 | key={option.value} | ||
| 301 | value={option.value} | ||
| 302 | className={cn( | ||
| 303 | "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]", | ||
| 304 | "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | ||
| 305 | isSelected | ||
| 306 | ? "border-primary bg-background shadow-sm ring-2 ring-primary/35" | ||
| 307 | : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50", | ||
| 308 | )} | ||
| 309 | > | ||
| 310 | <IconComponent className="size-5 shrink-0" aria-hidden /> | ||
| 311 | <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | ||
| 312 | {option.label} | ||
| 313 | </span> | ||
| 314 | {option.badgeLabel ? ( | ||
| 315 | <Badge variant="warning" size="sm"> | ||
| 316 | {option.badgeLabel} | ||
| 317 | </Badge> | ||
| 318 | ) : null} | ||
| 319 | </RadioPrimitive.Root> | ||
| 320 | ); | ||
| 321 | })} | ||
| 322 | {COMING_SOON_DRIVER_OPTIONS.map((option) => { | ||
| 323 | const IconComponent = option.icon; | ||
| 324 | return ( | ||
| 325 | <RadioPrimitive.Root | ||
| 326 | key={option.value} | ||
| 327 | value={option.value} | ||
| 328 | disabled | ||
| 329 | className={cn( | ||
| 330 | "relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 outline-none", | ||
| 331 | )} | ||
| 332 | > | ||
| 333 | <IconComponent | ||
| 334 | className="size-5 shrink-0 text-muted-foreground" | ||
| 335 | aria-hidden | ||
| 336 | /> | ||
| 337 | <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | ||
| 338 | {option.label} | ||
| 339 | </span> | ||
| 340 | <Badge variant="warning" size="sm"> | ||
| 341 | Coming Soon | ||
| 342 | </Badge> | ||
| 343 | </RadioPrimitive.Root> | ||
| 344 | ); | ||
| 345 | })} | ||
| 346 | </RadioGroup> | ||
| 287 | </div> | 347 | </div> |
| 288 | </DialogHeader> | ||
| 289 | 348 | ||
| 290 | <div | 349 | <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> |
| 291 | data-slot="dialog-panel" | 350 | <span className="text-xs font-medium text-foreground">Label</span> |
| 292 | className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5" | 351 | <Input |
| 293 | > | 352 | className="bg-background" |
| 294 | <AnimatedHeight> | 353 | placeholder="e.g. Work" |
| 295 | <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}> | 354 | value={label} |
| 296 | <span | 355 | onChange={(event) => setLabel(event.target.value)} |
| 297 | id="add-instance-driver-label" | 356 | /> |
| 298 | className="text-xs font-medium text-foreground" | 357 | <span className="text-[11px] text-muted-foreground"> |
| 299 | > | 358 | Shown in the provider list. Optional. |
| 300 | Driver | 359 | </span> |
| 360 | </label> | ||
| 361 | |||
| 362 | <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | ||
| 363 | <span className="text-xs font-medium text-foreground">Instance ID</span> | ||
| 364 | <Input | ||
| 365 | className="bg-background" | ||
| 366 | placeholder={`${driver}_work`} | ||
| 367 | value={instanceId} | ||
| 368 | onChange={(event) => { | ||
| 369 | setManualInstanceId(event.target.value); | ||
| 370 | }} | ||
| 371 | aria-invalid={showInstanceIdError} | ||
| 372 | /> | ||
| 373 | {showInstanceIdError ? ( | ||
| 374 | <span className="text-[11px] text-destructive">{instanceIdError}</span> | ||
| 375 | ) : ( | ||
| 376 | <span className="text-[11px] text-muted-foreground"> | ||
| 377 | Routing key used by threads and sessions. Letters, digits, '-', or '_'. | ||
| 301 | </span> | 378 | </span> |
| 302 | <RadioGroup | 379 | )} |
| 303 | value={driver} | 380 | </label> |
| 304 | onValueChange={(value) => setDriver(ProviderDriverKind.make(value))} | 381 | |
| 305 | aria-labelledby="add-instance-driver-label" | 382 | <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> |
| 306 | className="grid grid-cols-2 gap-2.5" | 383 | <span className="text-xs font-medium text-foreground">Accent color</span> |
| 307 | > | 384 | <div className="flex min-w-0 flex-wrap items-center gap-2"> |
| 308 | {DRIVER_OPTIONS.map((option) => { | 385 | <input |
| 309 | const IconComponent = option.icon; | 386 | type="color" |
| 310 | const isSelected = option.value === driver; | 387 | value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]} |
| 311 | return ( | 388 | onChange={(event) => setAccentColor(event.target.value)} |
| 312 | <RadioPrimitive.Root | 389 | aria-label="Provider instance accent color" |
| 313 | key={option.value} | 390 | className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5" |
| 314 | value={option.value} | 391 | /> |
| 315 | className={cn( | 392 | <div className="flex flex-wrap gap-1.5"> |
| 316 | "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]", | 393 | {PROVIDER_ACCENT_SWATCHES.map((swatch) => { |
| 317 | "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | 394 | const selected = accentColor.toLowerCase() === swatch; |
| 318 | isSelected | ||
| 319 | ? "border-primary bg-background shadow-sm ring-2 ring-primary/35" | ||
| 320 | : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50", | ||
| 321 | )} | ||
| 322 | > | ||
| 323 | <IconComponent className="size-5 shrink-0" aria-hidden /> | ||
| 324 | <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | ||
| 325 | {option.label} | ||
| 326 | </span> | ||
| 327 | {option.badgeLabel ? ( | ||
| 328 | <Badge variant="warning" size="sm"> | ||
| 329 | {option.badgeLabel} | ||
| 330 | </Badge> | ||
| 331 | ) : null} | ||
| 332 | </RadioPrimitive.Root> | ||
| 333 | ); | ||
| 334 | })} | ||
| 335 | {COMING_SOON_DRIVER_OPTIONS.map((option) => { | ||
| 336 | const IconComponent = option.icon; | ||
| 337 | return ( | 395 | return ( |
| 338 | <RadioPrimitive.Root | 396 | <button |
| 339 | key={option.value} | 397 | key={swatch} |
| 340 | value={option.value} | 398 | type="button" |
| 341 | disabled | ||
| 342 | className={cn( | 399 | className={cn( |
| 343 | "relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 outline-none", | 400 | "size-6 cursor-pointer rounded-full border transition", |
| 401 | selected | ||
| 402 | ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background" | ||
| 403 | : "border-black/10 hover:scale-105 dark:border-white/20", | ||
| 344 | )} | 404 | )} |
| 345 | > | 405 | style={{ backgroundColor: swatch }} |
| 346 | <IconComponent | 406 | onClick={() => setAccentColor(swatch)} |
| 347 | className="size-5 shrink-0 text-muted-foreground" | 407 | aria-label={`Use ${swatch} accent`} |
| 348 | aria-hidden | 408 | /> |
| 349 | /> | ||
| 350 | <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | ||
| 351 | {option.label} | ||
| 352 | </span> | ||
| 353 | <Badge variant="warning" size="sm"> | ||
| 354 | Coming Soon | ||
| 355 | </Badge> | ||
| 356 | </RadioPrimitive.Root> | ||
| 357 | ); | 409 | ); |
| 358 | })} | 410 | })} |
| 359 | </RadioGroup> | 411 | </div> |
| 412 | {accentColor ? ( | ||
| 413 | <Button | ||
| 414 | type="button" | ||
| 415 | size="sm" | ||
| 416 | variant="ghost" | ||
| 417 | className="h-7 px-2 text-xs text-muted-foreground" | ||
| 418 | onClick={() => setAccentColor("")} | ||
| 419 | > | ||
| 420 | Clear | ||
| 421 | </Button> | ||
| 422 | ) : null} | ||
| 360 | </div> | 423 | </div> |
| 424 | <span className="text-[11px] text-muted-foreground"> | ||
| 425 | Optional marker shown in the picker. | ||
| 426 | </span> | ||
| 427 | </div> | ||
| 361 | 428 | ||
| 362 | <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | 429 | {driverSettingsFields.length > 0 ? ( |
| 363 | <span className="text-xs font-medium text-foreground">Label</span> | 430 | <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}> |
| 364 | <Input | 431 | <ProviderSettingsForm |
| 365 | className="bg-background" | 432 | definition={driverOption} |
| 366 | placeholder="e.g. Work" | 433 | value={configDraft} |
| 367 | value={label} | 434 | idPrefix={`add-provider-${driver}`} |
| 368 | onChange={(event) => setLabel(event.target.value)} | 435 | variant="dialog" |
| 369 | /> | 436 | onChange={setConfigDraft} |
| 370 | <span className="text-[11px] text-muted-foreground"> | ||
| 371 | Shown in the provider list. Optional. | ||
| 372 | </span> | ||
| 373 | </label> | ||
| 374 | |||
| 375 | <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | ||
| 376 | <span className="text-xs font-medium text-foreground">Instance ID</span> | ||
| 377 | <Input | ||
| 378 | className="bg-background" | ||
| 379 | placeholder={`${driver}_work`} | ||
| 380 | value={instanceId} | ||
| 381 | onChange={(event) => { | ||
| 382 | setInstanceIdDirty(true); | ||
| 383 | setInstanceId(event.target.value); | ||
| 384 | }} | ||
| 385 | aria-invalid={showInstanceIdError} | ||
| 386 | /> | 437 | /> |
| 387 | {showInstanceIdError ? ( | ||
| 388 | <span className="text-[11px] text-destructive">{instanceIdError}</span> | ||
| 389 | ) : ( | ||
| 390 | <span className="text-[11px] text-muted-foreground"> | ||
| 391 | Routing key used by threads and sessions. Letters, digits, '-', or '_'. | ||
| 392 | </span> | ||
| 393 | )} | ||
| 394 | </label> | ||
| 395 | |||
| 396 | <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | ||
| 397 | <span className="text-xs font-medium text-foreground">Accent color</span> | ||
| 398 | <div className="flex min-w-0 flex-wrap items-center gap-2"> | ||
| 399 | <input | ||
| 400 | type="color" | ||
| 401 | value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]} | ||
| 402 | onChange={(event) => setAccentColor(event.target.value)} | ||
| 403 | aria-label="Provider instance accent color" | ||
| 404 | className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5" | ||
| 405 | /> | ||
| 406 | <div className="flex flex-wrap gap-1.5"> | ||
| 407 | {PROVIDER_ACCENT_SWATCHES.map((swatch) => { | ||
| 408 | const selected = accentColor.toLowerCase() === swatch; | ||
| 409 | return ( | ||
| 410 | <button | ||
| 411 | key={swatch} | ||
| 412 | type="button" | ||
| 413 | className={cn( | ||
| 414 | "size-6 cursor-pointer rounded-full border transition", | ||
| 415 | selected | ||
| 416 | ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background" | ||
| 417 | : "border-black/10 hover:scale-105 dark:border-white/20", | ||
| 418 | )} | ||
| 419 | style={{ backgroundColor: swatch }} | ||
| 420 | onClick={() => setAccentColor(swatch)} | ||
| 421 | aria-label={`Use ${swatch} accent`} | ||
| 422 | /> | ||
| 423 | ); | ||
| 424 | })} | ||
| 425 | </div> | ||
| 426 | {accentColor ? ( | ||
| 427 | <Button | ||
| 428 | type="button" | ||
| 429 | size="sm" | ||
| 430 | variant="ghost" | ||
| 431 | className="h-7 px-2 text-xs text-muted-foreground" | ||
| 432 | onClick={() => setAccentColor("")} | ||
| 433 | > | ||
| 434 | Clear | ||
| 435 | </Button> | ||
| 436 | ) : null} | ||
| 437 | </div> | ||
| 438 | <span className="text-[11px] text-muted-foreground"> | ||
| 439 | Optional marker shown in the picker. | ||
| 440 | </span> | ||
| 441 | </div> | 438 | </div> |
| 439 | ) : wizardStep === 2 ? ( | ||
| 440 | <div className="grid gap-2"> | ||
| 441 | <p className="text-sm text-muted-foreground"> | ||
| 442 | This driver has no required configuration. You can add the instance now. | ||
| 443 | </p> | ||
| 444 | </div> | ||
| 445 | ) : null} | ||
| 446 | </AnimatedHeight> | ||
| 447 | </div> | ||
| 442 | 448 | ||
| 443 | {driverSettingsFields.length > 0 ? ( | 449 | <DialogFooter className="border-t bg-background"> |
| 444 | <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}> | 450 | <Button |
| 445 | <ProviderSettingsForm | 451 | variant="outline" |
| 446 | definition={driverOption} | 452 | size="sm" |
| 447 | value={configDraft} | 453 | onClick={() => { |
| 448 | idPrefix={`add-provider-${driver}`} | 454 | if (wizardStep === 0) { |
| 449 | variant="dialog" | 455 | onOpenChange(false); |
| 450 | onChange={setConfigDraft} | 456 | return; |
| 451 | /> | 457 | } |
| 452 | </div> | 458 | setWizardStep((step) => Math.max(0, step - 1)); |
| 453 | ) : wizardStep === 2 ? ( | 459 | }} |
| 454 | <div className="grid gap-2"> | 460 | > |
| 455 | <p className="text-sm text-muted-foreground"> | 461 | {wizardStep === 0 ? "Cancel" : "Back"} |
| 456 | This driver has no required configuration. You can add the instance now. | 462 | </Button> |
| 457 | </p> | 463 | {wizardStep < wizardSteps.length - 1 ? ( |
| 458 | </div> | 464 | <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}> |
| 459 | ) : null} | 465 | Next |
| 460 | </AnimatedHeight> | ||
| 461 | </div> | ||
| 462 | |||
| 463 | <DialogFooter className="border-t bg-background"> | ||
| 464 | <Button | ||
| 465 | variant="outline" | ||
| 466 | size="sm" | ||
| 467 | onClick={() => { | ||
| 468 | if (wizardStep === 0) { | ||
| 469 | onOpenChange(false); | ||
| 470 | return; | ||
| 471 | } | ||
| 472 | setWizardStep((step) => Math.max(0, step - 1)); | ||
| 473 | }} | ||
| 474 | > | ||
| 475 | {wizardStep === 0 ? "Cancel" : "Back"} | ||
| 476 | </Button> | 466 | </Button> |
| 477 | {wizardStep < wizardSteps.length - 1 ? ( | 467 | ) : ( |
| 478 | <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}> | 468 | <Button size="sm" onClick={handleSave}> |
| 479 | Next | 469 | Add instance |
| 480 | </Button> | 470 | </Button> |
| 481 | ) : ( | 471 | )} |
| 482 | <Button size="sm" onClick={handleSave}> | 472 | </DialogFooter> |
| 483 | Add instance | 473 | </div> |
| 484 | </Button> | 474 | </DialogPopup> |
| 485 | )} | ||
| 486 | </DialogFooter> | ||
| 487 | </div> | ||
| 488 | </DialogPopup> | ||
| 489 | </Dialog> | ||
| 490 | ); | 475 | ); |
| 491 | } | 476 | } |