Unified · Split
| @@ -58,7 +58,7 @@ import { · 57 unmodified lines | ||
| 58 | 58 | resolveQuickAction, |
| 59 | 59 | resolveThreadBranchUpdate, |
| 60 | 60 | } from "./GitActionsControl.logic"; |
| 61 | −import { AnimatedHeight } from "./AnimatedHeight"; | |
| 61 | +import { WizardPopup, WizardHeader, WizardSteps, WizardPanel, WizardFooter } from "./ui/wizard"; | |
| 62 | 62 | import { StartTruncatedPath } from "./StartTruncatedPath"; |
| 63 | 63 | import { Button } from "~/components/ui/button"; |
| 64 | 64 | import { Checkbox } from "~/components/ui/checkbox"; |
| @@ -563,419 +563,360 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { · 498 unmodified lines | ||
| 563 | 563 | |
| 564 | 564 | return ( |
| 565 | 565 | <Dialog open={props.open} onOpenChange={handleOpenChange}> |
| 566 | − <DialogPopup className="max-w-xl overflow-hidden"> | |
| 567 | − <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-transparent"> | |
| 568 | − <DialogHeader className="border-b border-border/70 bg-foreground/[0.025] dark:border-transparent dark:bg-transparent"> | |
| 569 | − <DialogTitle>Publish repository</DialogTitle> | |
| 570 | − <DialogDescription> | |
| 571 | − Pick where to host it, then point us at a repo to push to. | |
| 572 | − </DialogDescription> | |
| 573 | − <div className="grid grid-cols-3 gap-2"> | |
| 574 | − {publishWizardSteps.map((label, index) => { | |
| 575 | − const isComplete = index < publishWizardStep; | |
| 576 | − const isClickable = | |
| 577 | − publishWizardStep !== 2 && | |
| 578 | − index < publishWizardSteps.length - 1 && | |
| 579 | − index <= publishWizardStep; | |
| 566 | + <WizardPopup> | |
| 567 | + <WizardHeader | |
| 568 | + title="Publish repository" | |
| 569 | + description="Pick where to host it, then point us at a repo to push to." | |
| 570 | + > | |
| 571 | + <WizardSteps | |
| 572 | + steps={publishWizardSteps} | |
| 573 | + currentStep={publishWizardStep} | |
| 574 | + summaries={publishWizardStepSummaries} | |
| 575 | + showSummaries | |
| 576 | + isStepDisabled={(index) => | |
| 577 | + publishWizardStep === 2 || | |
| 578 | + index >= publishWizardSteps.length - 1 || | |
| 579 | + index > publishWizardStep | |
| 580 | + } | |
| 581 | + onStepChange={setPublishWizardStep} | |
| 582 | + /> | |
| 583 | + </WizardHeader> | |
| 584 | + | |
| 585 | + <WizardPanel> | |
| 586 | + <div className={cn("space-y-2", publishWizardStep !== 0 && "hidden")}> | |
| 587 | + <span id="publish-provider-cards-label" className="text-xs font-medium text-foreground"> | |
| 588 | + Provider | |
| 589 | + </span> | |
| 590 | + <RadioGroup | |
| 591 | + value={publishProvider} | |
| 592 | + onValueChange={(value) => { | |
| 593 | + setSelectedPublishProvider(value as PublishProviderKind); | |
| 594 | + setPublishRepositoryOverride(null); | |
| 595 | + }} | |
| 596 | + aria-labelledby="publish-provider-cards-label" | |
| 597 | + className="grid grid-cols-2 gap-2.5" | |
| 598 | + > | |
| 599 | + {sortedPublishProviderOptions.map((option) => { | |
| 600 | + const readiness = publishProviderReadiness[option.value]; | |
| 601 | + const isSelected = publishProvider === option.value && readiness.ready; | |
| 602 | + if (!readiness.ready) { | |
| 603 | + return ( | |
| 604 | + <div | |
| 605 | + key={option.value} | |
| 606 | + className="relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 dark:border-transparent dark:bg-white/[0.035]" | |
| 607 | + > | |
| 608 | + <option.Icon className="size-5 shrink-0 text-muted-foreground" aria-hidden /> | |
| 609 | + <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 610 | + {option.label} | |
| 611 | + </span> | |
| 612 | + <Tooltip> | |
| 613 | + <TooltipTrigger | |
| 614 | + render={ | |
| 615 | + <Button | |
| 616 | + variant="outline" | |
| 617 | + size="xs" | |
| 618 | + className="h-5 rounded-[.25rem] px-1.5 text-[10px] text-warning-foreground" | |
| 619 | + onClick={(event) => { | |
| 620 | + event.preventDefault(); | |
| 621 | + event.stopPropagation(); | |
| 622 | + openSourceControlSettings(); | |
| 623 | + }} | |
| 624 | + > | |
| 625 | + Setup Required | |
| 626 | + </Button> | |
| 627 | + } | |
| 628 | + /> | |
| 629 | + <TooltipPopup side="top" align="end" className="max-w-72"> | |
| 630 | + {readiness.hint ?? | |
| 631 | + "Open Settings -> Source Control to configure this provider."} | |
| 632 | + </TooltipPopup> | |
| 633 | + </Tooltip> | |
| 634 | + </div> | |
| 635 | + ); | |
| 636 | + } | |
| 637 | + | |
| 580 | 638 | return ( |
| 581 | − <button | |
| 582 | − key={label} | |
| 583 | − type="button" | |
| 584 | − onClick={isClickable ? () => setPublishWizardStep(index) : undefined} | |
| 585 | − disabled={!isClickable} | |
| 639 | + <RadioPrimitive.Root | |
| 640 | + key={option.value} | |
| 641 | + value={option.value} | |
| 586 | 642 | className={cn( |
| 587 | − "grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left", | |
| 588 | − index === publishWizardStep | |
| 589 | − ? "border-primary bg-primary/10 ring-1 ring-primary/25 dark:border-transparent" | |
| 590 | − : isComplete | |
| 591 | − ? "border-border bg-background dark:border-transparent dark:bg-white/[0.05]" | |
| 592 | − : "border-border bg-muted/40 dark:border-transparent dark:bg-white/[0.025]", | |
| 593 | − !isClickable && "cursor-default", | |
| 643 | + "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]", | |
| 644 | + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | |
| 645 | + isSelected | |
| 646 | + ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" | |
| 647 | + : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", | |
| 594 | 648 | )} |
| 595 | 649 | > |
| 596 | − <span | |
| 597 | − aria-hidden | |
| 598 | − className={cn( | |
| 599 | − "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border", | |
| 600 | − isComplete | |
| 601 | − ? "border-primary bg-primary text-primary-foreground" | |
| 602 | − : index === publishWizardStep | |
| 603 | − ? "border-primary bg-background" | |
| 604 | − : "border-muted-foreground/35 bg-background", | |
| 605 | − )} | |
| 606 | − > | |
| 607 | − {isComplete ? <CheckIcon className="size-3" /> : null} | |
| 608 | − </span> | |
| 609 | − <span className="text-[10px] font-medium uppercase text-muted-foreground"> | |
| 610 | − Step {index + 1} | |
| 650 | + <option.Icon className="size-5 shrink-0" aria-hidden /> | |
| 651 | + <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 652 | + {option.label} | |
| 611 | 653 | </span> |
| 612 | − <span className="truncate text-xs font-semibold text-foreground"> | |
| 613 | − {label} | |
| 614 | − {isComplete && publishWizardStepSummaries[index] | |
| 615 | − ? `: ${publishWizardStepSummaries[index]}` | |
| 616 | − : ""} | |
| 617 | − </span> | |
| 618 | − </button> | |
| 654 | + </RadioPrimitive.Root> | |
| 619 | 655 | ); |
| 620 | 656 | })} |
| 621 | − </div> | |
| 622 | − </DialogHeader> | |
| 623 | − | |
| 624 | − <DialogPanel className="space-y-5 border-b border-border/70 bg-muted/20 px-6 py-5 dark:border-transparent dark:bg-transparent"> | |
| 625 | − <AnimatedHeight> | |
| 626 | − <div className={cn("space-y-2", publishWizardStep !== 0 && "hidden")}> | |
| 627 | − <span | |
| 628 | − id="publish-provider-cards-label" | |
| 629 | − className="text-xs font-medium text-foreground" | |
| 630 | − > | |
| 631 | − Provider | |
| 657 | + </RadioGroup> | |
| 658 | + </div> | |
| 659 | + | |
| 660 | + <div className={cn("space-y-5", publishWizardStep !== 1 && "hidden")}> | |
| 661 | + <div className="space-y-2"> | |
| 662 | + <label | |
| 663 | + htmlFor="publish-repository-path" | |
| 664 | + className="text-xs font-medium text-foreground" | |
| 665 | + > | |
| 666 | + Repository | |
| 667 | + </label> | |
| 668 | + <div className="flex items-stretch overflow-hidden rounded-md border border-input bg-background focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"> | |
| 669 | + <span className="flex shrink-0 items-center gap-1.5 border-r border-input bg-muted/50 px-2.5 font-mono text-xs text-muted-foreground"> | |
| 670 | + <currentPublishProvider.Icon className="size-3.5" /> | |
| 671 | + {publishHost}/ | |
| 632 | 672 | </span> |
| 633 | − <RadioGroup | |
| 634 | − value={publishProvider} | |
| 635 | − onValueChange={(value) => { | |
| 636 | − setSelectedPublishProvider(value as PublishProviderKind); | |
| 637 | − setPublishRepositoryOverride(null); | |
| 673 | + <input | |
| 674 | + id="publish-repository-path" | |
| 675 | + name="publish-repository-path" | |
| 676 | + value={publishRepository} | |
| 677 | + onChange={(event) => { | |
| 678 | + setPublishRepositoryOverride(event.target.value); | |
| 638 | 679 | }} |
| 639 | − aria-labelledby="publish-provider-cards-label" | |
| 640 | − className="grid grid-cols-2 gap-2.5" | |
| 641 | − > | |
| 642 | − {sortedPublishProviderOptions.map((option) => { | |
| 643 | − const readiness = publishProviderReadiness[option.value]; | |
| 644 | − const isSelected = publishProvider === option.value && readiness.ready; | |
| 645 | − if (!readiness.ready) { | |
| 646 | − return ( | |
| 647 | − <div | |
| 648 | − key={option.value} | |
| 649 | − className="relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 dark:border-transparent dark:bg-white/[0.035]" | |
| 650 | − > | |
| 651 | − <option.Icon | |
| 652 | − className="size-5 shrink-0 text-muted-foreground" | |
| 653 | − aria-hidden | |
| 654 | − /> | |
| 655 | − <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 656 | − {option.label} | |
| 657 | − </span> | |
| 658 | − <Tooltip> | |
| 659 | − <TooltipTrigger | |
| 660 | − render={ | |
| 661 | − <Button | |
| 662 | − variant="outline" | |
| 663 | − size="xs" | |
| 664 | − className="h-5 rounded-[.25rem] px-1.5 text-[10px] text-warning-foreground" | |
| 665 | − onClick={(event) => { | |
| 666 | − event.preventDefault(); | |
| 667 | − event.stopPropagation(); | |
| 668 | − openSourceControlSettings(); | |
| 669 | − }} | |
| 670 | − > | |
| 671 | − Setup Required | |
| 672 | − </Button> | |
| 673 | − } | |
| 674 | − /> | |
| 675 | − <TooltipPopup side="top" align="end" className="max-w-72"> | |
| 676 | − {readiness.hint ?? | |
| 677 | − "Open Settings -> Source Control to configure this provider."} | |
| 678 | − </TooltipPopup> | |
| 679 | − </Tooltip> | |
| 680 | − </div> | |
| 681 | − ); | |
| 680 | + onKeyDown={(event) => { | |
| 681 | + if (event.key === "Enter") { | |
| 682 | + event.preventDefault(); | |
| 683 | + submitPublishRepository(); | |
| 682 | 684 | } |
| 685 | + }} | |
| 686 | + placeholder={publishPathPlaceholder} | |
| 687 | + disabled={publishRepositoryAction.isPending} | |
| 688 | + className="w-full bg-transparent px-3 py-2 font-mono text-sm placeholder:text-muted-foreground/60 focus:outline-none" | |
| 689 | + /> | |
| 690 | + </div> | |
| 691 | + </div> | |
| 683 | 692 | |
| 684 | − return ( | |
| 685 | − <RadioPrimitive.Root | |
| 686 | − key={option.value} | |
| 687 | − value={option.value} | |
| 688 | − className={cn( | |
| 689 | − "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]", | |
| 690 | − "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | |
| 691 | − isSelected | |
| 692 | − ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" | |
| 693 | − : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", | |
| 694 | − )} | |
| 695 | − > | |
| 696 | − <option.Icon className="size-5 shrink-0" aria-hidden /> | |
| 697 | − <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 693 | + <div className="space-y-2"> | |
| 694 | + <span | |
| 695 | + id="publish-visibility-cards-label" | |
| 696 | + className="text-xs font-medium text-foreground" | |
| 697 | + > | |
| 698 | + Visibility | |
| 699 | + </span> | |
| 700 | + <RadioGroup | |
| 701 | + value={publishVisibility} | |
| 702 | + onValueChange={(value) => | |
| 703 | + setPublishVisibility(value as SourceControlRepositoryVisibility) | |
| 704 | + } | |
| 705 | + aria-labelledby="publish-visibility-cards-label" | |
| 706 | + disabled={publishRepositoryAction.isPending} | |
| 707 | + className="grid grid-cols-2 gap-2.5" | |
| 708 | + > | |
| 709 | + {[ | |
| 710 | + { | |
| 711 | + value: "private" as const, | |
| 712 | + label: "Private", | |
| 713 | + description: "Only invited people", | |
| 714 | + Icon: LockIcon, | |
| 715 | + }, | |
| 716 | + { | |
| 717 | + value: "public" as const, | |
| 718 | + label: "Public", | |
| 719 | + description: "Anyone on the web", | |
| 720 | + Icon: GlobeIcon, | |
| 721 | + }, | |
| 722 | + ].map((option) => { | |
| 723 | + const isSelected = publishVisibility === option.value; | |
| 724 | + return ( | |
| 725 | + <RadioPrimitive.Root | |
| 726 | + key={option.value} | |
| 727 | + value={option.value} | |
| 728 | + className={cn( | |
| 729 | + "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]", | |
| 730 | + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | |
| 731 | + isSelected | |
| 732 | + ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" | |
| 733 | + : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", | |
| 734 | + )} | |
| 735 | + > | |
| 736 | + <option.Icon className="size-4 shrink-0 text-muted-foreground" aria-hidden /> | |
| 737 | + <span className="min-w-0 flex-1"> | |
| 738 | + <span className="block text-sm font-medium text-foreground"> | |
| 698 | 739 | {option.label} |
| 699 | 740 | </span> |
| 700 | − </RadioPrimitive.Root> | |
| 701 | − ); | |
| 702 | − })} | |
| 703 | − </RadioGroup> | |
| 704 | − </div> | |
| 741 | + <span className="block text-xs text-muted-foreground"> | |
| 742 | + {option.description} | |
| 743 | + </span> | |
| 744 | + </span> | |
| 745 | + </RadioPrimitive.Root> | |
| 746 | + ); | |
| 747 | + })} | |
| 748 | + </RadioGroup> | |
| 749 | + </div> | |
| 705 | 750 | |
| 706 | − <div className={cn("space-y-5", publishWizardStep !== 1 && "hidden")}> | |
| 707 | − <div className="space-y-2"> | |
| 708 | − <label | |
| 709 | − htmlFor="publish-repository-path" | |
| 710 | − className="text-xs font-medium text-foreground" | |
| 711 | − > | |
| 712 | − Repository | |
| 751 | + <div> | |
| 752 | + <button | |
| 753 | + type="button" | |
| 754 | + onClick={() => setPublishAdvancedOpen((prev) => !prev)} | |
| 755 | + aria-expanded={publishAdvancedOpen} | |
| 756 | + className="flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground" | |
| 757 | + > | |
| 758 | + <ChevronDownIcon | |
| 759 | + className={cn( | |
| 760 | + "size-3.5 transition-transform", | |
| 761 | + publishAdvancedOpen ? "" : "-rotate-90", | |
| 762 | + )} | |
| 763 | + /> | |
| 764 | + Advanced | |
| 765 | + </button> | |
| 766 | + {publishAdvancedOpen ? ( | |
| 767 | + <div className="mt-3 grid gap-3 sm:grid-cols-2"> | |
| 768 | + <label className="space-y-1.5" htmlFor="publish-remote-name"> | |
| 769 | + <span className="text-xs font-medium text-foreground">Remote</span> | |
| 770 | + <Input | |
| 771 | + id="publish-remote-name" | |
| 772 | + value={publishRemoteName} | |
| 773 | + onChange={(event) => setPublishRemoteName(event.target.value)} | |
| 774 | + placeholder="origin" | |
| 775 | + disabled={publishRepositoryAction.isPending} | |
| 776 | + /> | |
| 713 | 777 | </label> |
| 714 | − <div className="flex items-stretch overflow-hidden rounded-md border border-input bg-background focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"> | |
| 715 | − <span className="flex shrink-0 items-center gap-1.5 border-r border-input bg-muted/50 px-2.5 font-mono text-xs text-muted-foreground"> | |
| 716 | − <currentPublishProvider.Icon className="size-3.5" /> | |
| 717 | − {publishHost}/ | |
| 778 | + <div className="space-y-1.5"> | |
| 779 | + <span | |
| 780 | + id="publish-protocol-label" | |
| 781 | + className="text-xs font-medium text-foreground" | |
| 782 | + > | |
| 783 | + Protocol | |
| 718 | 784 | </span> |
| 719 | − <input | |
| 720 | − id="publish-repository-path" | |
| 721 | − name="publish-repository-path" | |
| 722 | − value={publishRepository} | |
| 723 | − onChange={(event) => { | |
| 724 | − setPublishRepositoryOverride(event.target.value); | |
| 725 | − }} | |
| 726 | − onKeyDown={(event) => { | |
| 727 | − if (event.key === "Enter") { | |
| 728 | − event.preventDefault(); | |
| 729 | − submitPublishRepository(); | |
| 785 | + <RadioGroup | |
| 786 | + className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5" | |
| 787 | + value={publishProtocol} | |
| 788 | + onValueChange={(protocol) => { | |
| 789 | + if (protocol === "ssh" || protocol === "https") { | |
| 790 | + setPublishProtocol(protocol); | |
| 730 | 791 | } |
| 731 | 792 | }} |
| 732 | − placeholder={publishPathPlaceholder} | |
| 793 | + aria-labelledby="publish-protocol-label" | |
| 733 | 794 | disabled={publishRepositoryAction.isPending} |
| 734 | − className="w-full bg-transparent px-3 py-2 font-mono text-sm placeholder:text-muted-foreground/60 focus:outline-none" | |
| 735 | − /> | |
| 736 | − </div> | |
| 737 | − </div> | |
| 738 | − | |
| 739 | − <div className="space-y-2"> | |
| 740 | − <span | |
| 741 | − id="publish-visibility-cards-label" | |
| 742 | − className="text-xs font-medium text-foreground" | |
| 743 | − > | |
| 744 | − Visibility | |
| 745 | − </span> | |
| 746 | − <RadioGroup | |
| 747 | − value={publishVisibility} | |
| 748 | − onValueChange={(value) => | |
| 749 | − setPublishVisibility(value as SourceControlRepositoryVisibility) | |
| 750 | − } | |
| 751 | − aria-labelledby="publish-visibility-cards-label" | |
| 752 | − disabled={publishRepositoryAction.isPending} | |
| 753 | − className="grid grid-cols-2 gap-2.5" | |
| 754 | − > | |
| 755 | − {[ | |
| 756 | − { | |
| 757 | − value: "private" as const, | |
| 758 | − label: "Private", | |
| 759 | − description: "Only invited people", | |
| 760 | − Icon: LockIcon, | |
| 761 | − }, | |
| 762 | − { | |
| 763 | − value: "public" as const, | |
| 764 | − label: "Public", | |
| 765 | − description: "Anyone on the web", | |
| 766 | − Icon: GlobeIcon, | |
| 767 | − }, | |
| 768 | − ].map((option) => { | |
| 769 | − const isSelected = publishVisibility === option.value; | |
| 770 | − return ( | |
| 795 | + > | |
| 796 | + {(["ssh", "https"] as const).map((protocol) => ( | |
| 771 | 797 | <RadioPrimitive.Root |
| 772 | − key={option.value} | |
| 773 | − value={option.value} | |
| 774 | − className={cn( | |
| 775 | − "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]", | |
| 776 | − "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | |
| 777 | − isSelected | |
| 778 | − ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" | |
| 779 | − : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", | |
| 780 | − )} | |
| 798 | + key={protocol} | |
| 799 | + value={protocol} | |
| 800 | + data-pressed={publishProtocol === protocol ? "" : undefined} | |
| 801 | + className={toggleVariants({ | |
| 802 | + variant: "segmented", | |
| 803 | + size: "segmented", | |
| 804 | + })} | |
| 781 | 805 | > |
| 782 | − <option.Icon | |
| 783 | − className="size-4 shrink-0 text-muted-foreground" | |
| 784 | − aria-hidden | |
| 785 | − /> | |
| 786 | − <span className="min-w-0 flex-1"> | |
| 787 | − <span className="block text-sm font-medium text-foreground"> | |
| 788 | − {option.label} | |
| 789 | − </span> | |
| 790 | − <span className="block text-xs text-muted-foreground"> | |
| 791 | − {option.description} | |
| 792 | − </span> | |
| 793 | − </span> | |
| 806 | + {protocol.toUpperCase()} | |
| 794 | 807 | </RadioPrimitive.Root> |
| 795 | − ); | |
| 796 | − })} | |
| 797 | − </RadioGroup> | |
| 798 | − </div> | |
| 799 | − | |
| 800 | − <div> | |
| 801 | − <button | |
| 802 | − type="button" | |
| 803 | − onClick={() => setPublishAdvancedOpen((prev) => !prev)} | |
| 804 | − aria-expanded={publishAdvancedOpen} | |
| 805 | − className="flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground" | |
| 806 | − > | |
| 807 | − <ChevronDownIcon | |
| 808 | − className={cn( | |
| 809 | − "size-3.5 transition-transform", | |
| 810 | − publishAdvancedOpen ? "" : "-rotate-90", | |
| 811 | − )} | |
| 812 | − /> | |
| 813 | − Advanced | |
| 814 | − </button> | |
| 815 | − {publishAdvancedOpen ? ( | |
| 816 | − <div className="mt-3 grid gap-3 sm:grid-cols-2"> | |
| 817 | − <label className="space-y-1.5" htmlFor="publish-remote-name"> | |
| 818 | − <span className="text-xs font-medium text-foreground">Remote</span> | |
| 819 | − <Input | |
| 820 | − id="publish-remote-name" | |
| 821 | − value={publishRemoteName} | |
| 822 | − onChange={(event) => setPublishRemoteName(event.target.value)} | |
| 823 | − placeholder="origin" | |
| 824 | − disabled={publishRepositoryAction.isPending} | |
| 825 | − /> | |
| 826 | − </label> | |
| 827 | − <div className="space-y-1.5"> | |
| 828 | − <span | |
| 829 | − id="publish-protocol-label" | |
| 830 | − className="text-xs font-medium text-foreground" | |
| 831 | − > | |
| 832 | − Protocol | |
| 833 | − </span> | |
| 834 | − <RadioGroup | |
| 835 | − className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5" | |
| 836 | − value={publishProtocol} | |
| 837 | − onValueChange={(protocol) => { | |
| 838 | − if (protocol === "ssh" || protocol === "https") { | |
| 839 | − setPublishProtocol(protocol); | |
| 840 | − } | |
| 841 | − }} | |
| 842 | − aria-labelledby="publish-protocol-label" | |
| 843 | − disabled={publishRepositoryAction.isPending} | |
| 844 | − > | |
| 845 | − {(["ssh", "https"] as const).map((protocol) => ( | |
| 846 | − <RadioPrimitive.Root | |
| 847 | − key={protocol} | |
| 848 | − value={protocol} | |
| 849 | − data-pressed={publishProtocol === protocol ? "" : undefined} | |
| 850 | − className={toggleVariants({ | |
| 851 | − variant: "segmented", | |
| 852 | − size: "segmented", | |
| 853 | − })} | |
| 854 | − > | |
| 855 | − {protocol.toUpperCase()} | |
| 856 | − </RadioPrimitive.Root> | |
| 857 | − ))} | |
| 858 | − </RadioGroup> | |
| 859 | − </div> | |
| 860 | − </div> | |
| 861 | − ) : null} | |
| 808 | + ))} | |
| 809 | + </RadioGroup> | |
| 810 | + </div> | |
| 862 | 811 | </div> |
| 812 | + ) : null} | |
| 813 | + </div> | |
| 863 | 814 | |
| 864 | − {publishRepositoryAction.isPending ? ( | |
| 865 | − <div | |
| 866 | − role="status" | |
| 867 | − aria-live="polite" | |
| 868 | − className="flex items-center gap-2 rounded-md border border-input bg-muted/40 px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]" | |
| 869 | − > | |
| 870 | − <Spinner className="size-3.5" aria-hidden /> | |
| 871 | − Publishing repository to {publishProviderLabel}... | |
| 872 | − </div> | |
| 873 | − ) : null} | |
| 874 | − {publishError && !publishRepositoryAction.isPending ? ( | |
| 875 | − <div | |
| 876 | − role="alert" | |
| 877 | − className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive" | |
| 878 | − > | |
| 879 | − <p className="font-medium">Publish failed</p> | |
| 880 | − <p className="mt-0.5 text-destructive/90">{publishError}</p> | |
| 881 | − </div> | |
| 882 | − ) : null} | |
| 815 | + {publishRepositoryAction.isPending ? ( | |
| 816 | + <div | |
| 817 | + role="status" | |
| 818 | + aria-live="polite" | |
| 819 | + className="flex items-center gap-2 rounded-md border border-input bg-muted/40 px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]" | |
| 820 | + > | |
| 821 | + <Spinner className="size-3.5" aria-hidden /> | |
| 822 | + Publishing repository to {publishProviderLabel}... | |
| 883 | 823 | </div> |
| 884 | − | |
| 885 | − <div className={cn("space-y-4", publishWizardStep !== 2 && "hidden")}> | |
| 886 | − {publishResult ? ( | |
| 887 | − <> | |
| 888 | − <div className="flex flex-col items-center gap-2 py-1 text-center"> | |
| 889 | − <span className="grid size-8 place-items-center rounded-full bg-success/15 text-success"> | |
| 890 | − <CheckIcon className="size-4" aria-hidden /> | |
| 891 | − </span> | |
| 892 | − <h3 className="text-sm font-semibold text-foreground"> | |
| 893 | − {publishResult.status === "pushed" | |
| 894 | − ? "Repository published" | |
| 895 | − : "Repository created"} | |
| 896 | − </h3> | |
| 897 | − <p className="max-w-xs text-pretty text-xs text-muted-foreground"> | |
| 898 | − {publishResult.status === "pushed" | |
| 899 | − ? `${publishResult.branch} is now live on ${publishProviderLabel}.` | |
| 900 | − : `Remote "${publishResult.remoteName}" is set up. Make a commit and push it to share your code.`} | |
| 901 | − </p> | |
| 902 | − </div> | |
| 903 | − <div className="flex items-center gap-2 rounded-lg border border-input bg-muted/40 px-3 py-2 dark:border-transparent dark:bg-white/[0.035]"> | |
| 904 | − <currentPublishProvider.Icon className="size-3.5 shrink-0 text-muted-foreground" /> | |
| 905 | − <span className="min-w-0 flex-1 truncate font-mono text-xs text-foreground"> | |
| 906 | − {publishResult.repository.nameWithOwner} | |
| 907 | − </span> | |
| 908 | − </div> | |
| 909 | − <Button | |
| 910 | − variant="outline" | |
| 911 | − size="sm" | |
| 912 | − className="w-full" | |
| 913 | − onClick={() => { | |
| 914 | − void openLink(publishResult.repository.url).catch(() => undefined); | |
| 915 | − }} | |
| 916 | − > | |
| 917 | − Open on {publishProviderLabel} | |
| 918 | − </Button> | |
| 919 | − </> | |
| 920 | − ) : ( | |
| 921 | − <div className="rounded-md border border-input bg-background px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]"> | |
| 922 | − Publish result unavailable. | |
| 923 | − </div> | |
| 924 | − )} | |
| 824 | + ) : null} | |
| 825 | + {publishError && !publishRepositoryAction.isPending ? ( | |
| 826 | + <div | |
| 827 | + role="alert" | |
| 828 | + className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive" | |
| 829 | + > | |
| 830 | + <p className="font-medium">Publish failed</p> | |
| 831 | + <p className="mt-0.5 text-destructive/90">{publishError}</p> | |
| 925 | 832 | </div> |
| 926 | − </AnimatedHeight> | |
| 927 | − </DialogPanel> | |
| 833 | + ) : null} | |
| 834 | + </div> | |
| 928 | 835 | |
| 929 | − <DialogFooter className="dark:border-transparent dark:bg-transparent"> | |
| 930 | − {publishWizardStep === 2 ? ( | |
| 931 | − <Button size="sm" onClick={() => handleOpenChange(false)}> | |
| 932 | − Done | |
| 933 | − </Button> | |
| 934 | − ) : ( | |
| 836 | + <div className={cn("space-y-4", publishWizardStep !== 2 && "hidden")}> | |
| 837 | + {publishResult ? ( | |
| 935 | 838 | <> |
| 839 | + <div className="flex flex-col items-center gap-2 py-1 text-center"> | |
| 840 | + <span className="grid size-8 place-items-center rounded-full bg-success/15 text-success"> | |
| 841 | + <CheckIcon className="size-4" aria-hidden /> | |
| 842 | + </span> | |
| 843 | + <h3 className="text-sm font-semibold text-foreground"> | |
| 844 | + {publishResult.status === "pushed" | |
| 845 | + ? "Repository published" | |
| 846 | + : "Repository created"} | |
| 847 | + </h3> | |
| 848 | + <p className="max-w-xs text-pretty text-xs text-muted-foreground"> | |
| 849 | + {publishResult.status === "pushed" | |
| 850 | + ? `${publishResult.branch} is now live on ${publishProviderLabel}.` | |
| 851 | + : `Remote "${publishResult.remoteName}" is set up. Make a commit and push it to share your code.`} | |
| 852 | + </p> | |
| 853 | + </div> | |
| 854 | + <div className="flex items-center gap-2 rounded-lg border border-input bg-muted/40 px-3 py-2 dark:border-transparent dark:bg-white/[0.035]"> | |
| 855 | + <currentPublishProvider.Icon className="size-3.5 shrink-0 text-muted-foreground" /> | |
| 856 | + <span className="min-w-0 flex-1 truncate font-mono text-xs text-foreground"> | |
| 857 | + {publishResult.repository.nameWithOwner} | |
| 858 | + </span> | |
| 859 | + </div> | |
| 936 | 860 | <Button |
| 937 | 861 | variant="outline" |
| 938 | 862 | size="sm" |
| 939 | − disabled={publishRepositoryAction.isPending} | |
| 863 | + className="w-full" | |
| 940 | 864 | onClick={() => { |
| 941 | − if (publishWizardStep === 0) { | |
| 942 | − handleOpenChange(false); | |
| 943 | − return; | |
| 944 | − } | |
| 945 | − setPublishWizardStep((step) => Math.max(0, step - 1)); | |
| 865 | + void openLink(publishResult.repository.url).catch(() => undefined); | |
| 946 | 866 | }} |
| 947 | 867 | > |
| 948 | − {publishWizardStep === 0 ? "Cancel" : "Back"} | |
| 868 | + Open on {publishProviderLabel} | |
| 949 | 869 | </Button> |
| 950 | − {publishWizardStep < 1 ? ( | |
| 951 | − <Button | |
| 952 | − size="sm" | |
| 953 | − disabled={!hasReadyPublishProvider || !selectedPublishProviderReadiness.ready} | |
| 954 | − onClick={() => setPublishWizardStep((step) => Math.min(1, step + 1))} | |
| 955 | − > | |
| 956 | − Next | |
| 957 | − </Button> | |
| 958 | − ) : ( | |
| 959 | − <Button | |
| 960 | − size="sm" | |
| 961 | − disabled={!canSubmitPublishRepository} | |
| 962 | − onClick={submitPublishRepository} | |
| 963 | − > | |
| 964 | − {publishRepositoryAction.isPending ? ( | |
| 965 | − <> | |
| 966 | − <Spinner className="size-3.5" aria-hidden /> | |
| 967 | − Publishing... | |
| 968 | − </> | |
| 969 | − ) : ( | |
| 970 | − "Publish" | |
| 971 | − )} | |
| 972 | − </Button> | |
| 973 | − )} | |
| 974 | 870 | </> |
| 871 | + ) : ( | |
| 872 | + <div className="rounded-md border border-input bg-background px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]"> | |
| 873 | + Publish result unavailable. | |
| 874 | + </div> | |
| 975 | 875 | )} |
| 976 | − </DialogFooter> | |
| 977 | − </div> | |
| 978 | − </DialogPopup> | |
| 876 | + </div> | |
| 877 | + </WizardPanel> | |
| 878 | + | |
| 879 | + <WizardFooter> | |
| 880 | + {publishWizardStep === 2 ? ( | |
| 881 | + <Button onClick={() => handleOpenChange(false)}>Done</Button> | |
| 882 | + ) : ( | |
| 883 | + <> | |
| 884 | + <Button | |
| 885 | + variant="outline" | |
| 886 | + disabled={publishRepositoryAction.isPending} | |
| 887 | + onClick={() => { | |
| 888 | + if (publishWizardStep === 0) { | |
| 889 | + handleOpenChange(false); | |
| 890 | + return; | |
| 891 | + } | |
| 892 | + setPublishWizardStep((step) => Math.max(0, step - 1)); | |
| 893 | + }} | |
| 894 | + > | |
| 895 | + {publishWizardStep === 0 ? "Cancel" : "Back"} | |
| 896 | + </Button> | |
| 897 | + {publishWizardStep < 1 ? ( | |
| 898 | + <Button | |
| 899 | + disabled={!hasReadyPublishProvider || !selectedPublishProviderReadiness.ready} | |
| 900 | + onClick={() => setPublishWizardStep((step) => Math.min(1, step + 1))} | |
| 901 | + > | |
| 902 | + Next | |
| 903 | + </Button> | |
| 904 | + ) : ( | |
| 905 | + <Button disabled={!canSubmitPublishRepository} onClick={submitPublishRepository}> | |
| 906 | + {publishRepositoryAction.isPending ? ( | |
| 907 | + <> | |
| 908 | + <Spinner className="size-3.5" aria-hidden /> | |
| 909 | + Publishing... | |
| 910 | + </> | |
| 911 | + ) : ( | |
| 912 | + "Publish" | |
| 913 | + )} | |
| 914 | + </Button> | |
| 915 | + )} | |
| 916 | + </> | |
| 917 | + )} | |
| 918 | + </WizardFooter> | |
| 919 | + </WizardPopup> | |
| 979 | 920 | </Dialog> |
| 980 | 921 | ); |
| 981 | 922 | } |
| @@ -15,18 +15,10 @@ import { useEnvironments, usePrimaryEnvironment } from "~/state/environments"; · 14 unmodified lines | ||
| 15 | 15 | import { CloudEnvironmentConnectRows } from "./CloudEnvironmentConnectList"; |
| 16 | 16 | import { Button } from "../ui/button"; |
| 17 | 17 | import { Checkbox } from "../ui/checkbox"; |
| 18 | −import { | |
| 19 | − Dialog, | |
| 20 | − DialogDescription, | |
| 21 | − DialogFooter, | |
| 22 | − DialogHeader, | |
| 23 | − DialogPanel, | |
| 24 | − DialogPopup, | |
| 25 | − DialogTitle, | |
| 26 | −} from "../ui/dialog"; | |
| 18 | +import { Dialog } from "../ui/dialog"; | |
| 27 | 19 | import { Switch } from "../ui/switch"; |
| 28 | 20 | import { toastManager } from "../ui/toast"; |
| 29 | −import { WizardSteps } from "../ui/wizard-steps"; | |
| 21 | +import { WizardSteps, WizardPopup, WizardHeader, WizardPanel, WizardFooter } from "../ui/wizard"; | |
| 30 | 22 | |
| 31 | 23 | /** |
| 32 | 24 | * Post-sign-in onboarding wizard for T3 Connect. Opens on every in-session |
| @@ -216,23 +208,29 @@ function ConfiguredConnectOnboardingDialog() { · 183 unmodified lines | ||
| 216 | 208 | if (!open && !isApplying) complete(); |
| 217 | 209 | }} |
| 218 | 210 | > |
| 219 | − <DialogPopup className="max-w-xl"> | |
| 220 | − <DialogHeader> | |
| 221 | − <DialogTitle>Set up T3 Connect</DialogTitle> | |
| 222 | − <DialogDescription> | |
| 223 | − Mesh your devices together — publish this environment and connect the rest, all in one | |
| 224 | − place. | |
| 225 | − </DialogDescription> | |
| 211 | + <WizardPopup> | |
| 212 | + <WizardHeader | |
| 213 | + title="Set up T3 Connect" | |
| 214 | + description={ | |
| 215 | + <> | |
| 216 | + Mesh your devices together — publish this environment and connect the rest, all in one | |
| 217 | + place. | |
| 218 | + </> | |
| 219 | + } | |
| 220 | + > | |
| 226 | 221 | {steps.length > 1 ? ( |
| 227 | 222 | <WizardSteps |
| 228 | − steps={steps.map((id) => ({ id, label: STEP_LABELS[id] }))} | |
| 229 | − currentStep={step} | |
| 230 | − disabled={isApplying} | |
| 231 | − onStepSelect={setStep} | |
| 223 | + steps={steps.map((id) => STEP_LABELS[id])} | |
| 224 | + currentStep={steps.indexOf(step)} | |
| 225 | + isStepDisabled={() => isApplying} | |
| 226 | + onStepChange={(index) => { | |
| 227 | + const next = steps[index]; | |
| 228 | + if (next) setStep(next); | |
| 229 | + }} | |
| 232 | 230 | /> |
| 233 | 231 | ) : null} |
| 234 | − </DialogHeader> | |
| 235 | − <DialogPanel> | |
| 232 | + </WizardHeader> | |
| 233 | + <WizardPanel> | |
| 236 | 234 | {step === "publish" ? ( |
| 237 | 235 | <PublishStep |
| 238 | 236 | exposeEnvironment={exposeEnvironment} |
| @@ -245,38 +243,37 @@ function ConfiguredConnectOnboardingDialog() { · 6 unmodified lines | ||
| 245 | 243 | ) : ( |
| 246 | 244 | <DevicesStep /> |
| 247 | 245 | )} |
| 248 | − </DialogPanel> | |
| 249 | − <DialogFooter variant="bare" className="sm:justify-between"> | |
| 250 | − <label className="flex cursor-pointer items-center gap-2 self-start text-xs text-muted-foreground sm:self-center"> | |
| 251 | − <Checkbox | |
| 252 | − checked={dontShowAgain} | |
| 253 | − onCheckedChange={(checked) => setDontShowAgain(checked === true)} | |
| 254 | − /> | |
| 255 | − Don't show this again | |
| 256 | − </label> | |
| 257 | − <div className="flex flex-col-reverse gap-2 sm:flex-row"> | |
| 258 | − {step === "publish" ? ( | |
| 259 | − <> | |
| 260 | − <Button variant="ghost" disabled={isApplying} onClick={() => setStep("devices")}> | |
| 261 | − Not now | |
| 262 | − </Button> | |
| 263 | − <Button | |
| 264 | − disabled={ | |
| 265 | − isApplying || (controller.linkState.isPending && linkStateData === null) | |
| 266 | − } | |
| 267 | − onClick={() => void applyPublishSelection()} | |
| 268 | − > | |
| 269 | − {isApplying ? "Enabling…" : "Continue"} | |
| 270 | − </Button> | |
| 271 | − </> | |
| 272 | − ) : ( | |
| 273 | − <Button disabled={isApplying} onClick={complete}> | |
| 274 | − Done | |
| 246 | + </WizardPanel> | |
| 247 | + <WizardFooter | |
| 248 | + leading={ | |
| 249 | + <label className="flex cursor-pointer items-center gap-2 self-start text-xs text-muted-foreground sm:self-center"> | |
| 250 | + <Checkbox | |
| 251 | + checked={dontShowAgain} | |
| 252 | + onCheckedChange={(checked) => setDontShowAgain(checked === true)} | |
| 253 | + /> | |
| 254 | + Don't show this again | |
| 255 | + </label> | |
| 256 | + } | |
| 257 | + > | |
| 258 | + {step === "publish" ? ( | |
| 259 | + <> | |
| 260 | + <Button variant="ghost" disabled={isApplying} onClick={() => setStep("devices")}> | |
| 261 | + Not now | |
| 275 | 262 | </Button> |
| 276 | − )} | |
| 277 | − </div> | |
| 278 | − </DialogFooter> | |
| 279 | − </DialogPopup> | |
| 263 | + <Button | |
| 264 | + disabled={isApplying || (controller.linkState.isPending && linkStateData === null)} | |
| 265 | + onClick={() => void applyPublishSelection()} | |
| 266 | + > | |
| 267 | + {isApplying ? "Enabling…" : "Continue"} | |
| 268 | + </Button> | |
| 269 | + </> | |
| 270 | + ) : ( | |
| 271 | + <Button disabled={isApplying} onClick={complete}> | |
| 272 | + Done | |
| 273 | + </Button> | |
| 274 | + )} | |
| 275 | + </WizardFooter> | |
| 276 | + </WizardPopup> | |
| 280 | 277 | </Dialog> |
| 281 | 278 | ); |
| 282 | 279 | } |
| @@ -71,8 +71,8 @@ import { Input } from "../ui/input"; · 70 unmodified lines | ||
| 71 | 71 | import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; |
| 72 | 72 | import { ScrollArea } from "../ui/scroll-area"; |
| 73 | 73 | import { Spinner } from "../ui/spinner"; |
| 74 | −import { WizardPanel, WizardSteps } from "../ui/wizard"; | |
| 75 | −import { Dialog, DialogHeader, DialogPopup, DialogTitle } from "../ui/dialog"; | |
| 74 | +import { WizardPanel, WizardSteps, WizardPopup, WizardHeader } from "../ui/wizard"; | |
| 75 | +import { Dialog } from "../ui/dialog"; | |
| 76 | 76 | import { toastManager } from "../ui/toast"; |
| 77 | 77 | import { cn } from "../../lib/utils"; |
| 78 | 78 | import { formatRelativeTime } from "../../timestampFormat"; |
| @@ -183,71 +183,71 @@ export function WelcomeWizard({ · 104 unmodified lines | ||
| 183 | 183 | |
| 184 | 184 | return ( |
| 185 | 185 | <Dialog open disablePointerDismissal onOpenChange={(_, event) => event.cancel()}> |
| 186 | − <DialogPopup | |
| 187 | − className="max-w-xl overflow-x-hidden overflow-y-auto" | |
| 186 | + <WizardPopup | |
| 188 | 187 | bottomStickOnMobile={false} |
| 189 | 188 | showCloseButton={false} |
| 190 | 189 | initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true} |
| 191 | 190 | > |
| 192 | − <DialogTitle className="sr-only">Set up T3 Code</DialogTitle> | |
| 193 | − <div className="flex min-h-0 flex-col"> | |
| 194 | − <DialogHeader className="gap-4"> | |
| 191 | + <WizardHeader | |
| 192 | + title="Set up T3 Code" | |
| 193 | + identity={ | |
| 195 | 194 | <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code"> |
| 196 | 195 | <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden /> |
| 197 | 196 | <span className="text-[1.4rem] font-medium tracking-tight text-muted-foreground"> |
| 198 | 197 | Code |
| 199 | 198 | </span> |
| 200 | 199 | </div> |
| 201 | − <WizardSteps | |
| 202 | − steps={ONBOARDING_STAGES} | |
| 203 | − currentStep={stageIndex} | |
| 204 | − isStepDisabled={(index) => isImporting || index >= stageIndex} | |
| 205 | − onStepChange={(index) => { | |
| 206 | − if (isImporting || index > stageIndex) return; | |
| 207 | − setStep(index === 0 ? "connection" : "agents"); | |
| 200 | + } | |
| 201 | + > | |
| 202 | + <WizardSteps | |
| 203 | + steps={ONBOARDING_STAGES} | |
| 204 | + currentStep={stageIndex} | |
| 205 | + isStepDisabled={(index) => isImporting || index >= stageIndex} | |
| 206 | + onStepChange={(index) => { | |
| 207 | + if (isImporting || index > stageIndex) return; | |
| 208 | + setStep(index === 0 ? "connection" : "agents"); | |
| 209 | + }} | |
| 210 | + /> | |
| 211 | + </WizardHeader> | |
| 212 | + | |
| 213 | + <WizardPanel holdHeight={isLoadingProjects}> | |
| 214 | + {step === "connection" ? ( | |
| 215 | + <ConnectionStep | |
| 216 | + expandPairingInitially={!localAvailable && !hasCloudPublicConfig()} | |
| 217 | + selectedIds={selectedIds} | |
| 218 | + autoSelectedComputers={autoSelectedComputers.current} | |
| 219 | + onSelectionChange={setSelection} | |
| 220 | + onToggleEnvironment={(environmentId, checked) => | |
| 221 | + setSelection((current) => { | |
| 222 | + const next = new Set(current ?? selectedIds); | |
| 223 | + if (checked) next.add(environmentId); | |
| 224 | + else next.delete(environmentId); | |
| 225 | + return next; | |
| 226 | + }) | |
| 227 | + } | |
| 228 | + onContinue={() => | |
| 229 | + startSetup( | |
| 230 | + environments | |
| 231 | + .filter((environment) => selectedIds.has(environment.environmentId)) | |
| 232 | + .map((environment) => environment.environmentId), | |
| 233 | + ) | |
| 234 | + } | |
| 235 | + onPaired={(environmentId) => { | |
| 236 | + setSelection(new Set([...selectedIds, environmentId])); | |
| 208 | 237 | }} |
| 209 | 238 | /> |
| 210 | − </DialogHeader> | |
| 211 | − | |
| 212 | − <WizardPanel className="min-w-0" holdHeight={isLoadingProjects}> | |
| 213 | − {step === "connection" ? ( | |
| 214 | − <ConnectionStep | |
| 215 | − expandPairingInitially={!localAvailable && !hasCloudPublicConfig()} | |
| 216 | − selectedIds={selectedIds} | |
| 217 | − autoSelectedComputers={autoSelectedComputers.current} | |
| 218 | − onSelectionChange={setSelection} | |
| 219 | − onToggleEnvironment={(environmentId, checked) => | |
| 220 | − setSelection((current) => { | |
| 221 | − const next = new Set(current ?? selectedIds); | |
| 222 | − if (checked) next.add(environmentId); | |
| 223 | − else next.delete(environmentId); | |
| 224 | − return next; | |
| 225 | − }) | |
| 226 | − } | |
| 227 | − onContinue={() => | |
| 228 | − startSetup( | |
| 229 | − environments | |
| 230 | − .filter((environment) => selectedIds.has(environment.environmentId)) | |
| 231 | − .map((environment) => environment.environmentId), | |
| 232 | − ) | |
| 233 | − } | |
| 234 | − onPaired={(environmentId) => { | |
| 235 | − setSelection(new Set([...selectedIds, environmentId])); | |
| 236 | − }} | |
| 237 | − /> | |
| 238 | − ) : step === "agents" ? ( | |
| 239 | − <AgentsStep environmentIds={setupIds} onContinue={() => setStep("import")} /> | |
| 240 | − ) : ( | |
| 241 | − <ImportStep | |
| 242 | − scans={scans} | |
| 243 | − isImporting={isImporting} | |
| 244 | − setIsImporting={setIsImporting} | |
| 245 | − onDone={finish} | |
| 246 | − /> | |
| 247 | − )} | |
| 248 | − </WizardPanel> | |
| 249 | − </div> | |
| 250 | − </DialogPopup> | |
| 239 | + ) : step === "agents" ? ( | |
| 240 | + <AgentsStep environmentIds={setupIds} onContinue={() => setStep("import")} /> | |
| 241 | + ) : ( | |
| 242 | + <ImportStep | |
| 243 | + scans={scans} | |
| 244 | + isImporting={isImporting} | |
| 245 | + setIsImporting={setIsImporting} | |
| 246 | + onDone={finish} | |
| 247 | + /> | |
| 248 | + )} | |
| 249 | + </WizardPanel> | |
| 250 | + </WizardPopup> | |
| 251 | 251 | </Dialog> |
| 252 | 252 | ); |
| 253 | 253 | } |
| @@ -15,21 +15,14 @@ import { cn } from "../../lib/utils"; · 14 unmodified lines | ||
| 15 | 15 | import { normalizeProviderAccentColor } from "../../providerInstances"; |
| 16 | 16 | import { Button } from "../ui/button"; |
| 17 | 17 | import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; |
| 18 | −import { | |
| 19 | − Dialog, | |
| 20 | − DialogDescription, | |
| 21 | − DialogFooter, | |
| 22 | − DialogHeader, | |
| 23 | − DialogPopup, | |
| 24 | − DialogTitle, | |
| 25 | −} from "../ui/dialog"; | |
| 18 | +import { Dialog } from "../ui/dialog"; | |
| 26 | 19 | import { Badge } from "../ui/badge"; |
| 27 | 20 | import { Input } from "../ui/input"; |
| 28 | 21 | import { RadioGroup } from "../ui/radio-group"; |
| 29 | 22 | import { toastManager } from "../ui/toast"; |
| 30 | 23 | import { DRIVER_OPTION_BY_VALUE, DRIVER_OPTIONS } from "./providerDriverMeta"; |
| 31 | 24 | import { ProviderSettingsForm, deriveProviderSettingsFields } from "./ProviderSettingsForm"; |
| 32 | −import { WizardPanel } from "../ui/wizard"; | |
| 25 | +import { WizardPanel, WizardPopup, WizardHeader, WizardFooter } from "../ui/wizard"; | |
| 33 | 26 | import { |
| 34 | 27 | ADD_PROVIDER_WIZARD_STEPS, |
| 35 | 28 | resolveWizardNavigation, |
| @@ -230,206 +223,204 @@ export function AddProviderInstanceDialog({ · 194 unmodified lines | ||
| 230 | 223 | |
| 231 | 224 | return ( |
| 232 | 225 | <Dialog open={open} onOpenChange={onOpenChange}> |
| 233 | − <DialogPopup className="max-w-xl overflow-hidden"> | |
| 234 | − <div className="flex min-h-0 flex-col overflow-hidden"> | |
| 235 | − <DialogHeader> | |
| 236 | − <DialogTitle>Add provider instance</DialogTitle> | |
| 237 | − <DialogDescription> | |
| 226 | + <WizardPopup> | |
| 227 | + <WizardHeader | |
| 228 | + title="Add provider instance" | |
| 229 | + description={ | |
| 230 | + <> | |
| 238 | 231 | Configure an additional provider instance on {environmentLabel} — for example, a |
| 239 | 232 | second Codex install pointed at a different workspace. |
| 240 | − </DialogDescription> | |
| 241 | − <AddProviderInstanceWizardSteps | |
| 242 | − currentStep={wizardStep} | |
| 243 | − summaries={wizardStepSummaries} | |
| 244 | − instanceIdError={instanceIdError} | |
| 245 | − onNavigation={applyWizardNavigation} | |
| 246 | − /> | |
| 247 | − </DialogHeader> | |
| 233 | + </> | |
| 234 | + } | |
| 235 | + > | |
| 236 | + <AddProviderInstanceWizardSteps | |
| 237 | + currentStep={wizardStep} | |
| 238 | + summaries={wizardStepSummaries} | |
| 239 | + instanceIdError={instanceIdError} | |
| 240 | + onNavigation={applyWizardNavigation} | |
| 241 | + /> | |
| 242 | + </WizardHeader> | |
| 248 | 243 | |
| 249 | − <WizardPanel> | |
| 250 | − <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}> | |
| 251 | − <div id="add-instance-driver-label" className="text-sm font-medium text-foreground"> | |
| 252 | − Driver | |
| 253 | − </div> | |
| 254 | − <RadioGroup | |
| 255 | − value={driver} | |
| 256 | − onValueChange={(value) => setDriver(ProviderDriverKind.make(value))} | |
| 257 | − aria-labelledby="add-instance-driver-label" | |
| 258 | − className="grid grid-cols-1 gap-2 sm:grid-cols-2" | |
| 259 | − > | |
| 260 | − {DRIVER_OPTIONS.map((option) => { | |
| 261 | − const IconComponent = option.icon; | |
| 262 | − return ( | |
| 263 | − <RadioPrimitive.Root | |
| 264 | − key={option.value} | |
| 265 | − value={option.value} | |
| 266 | − className="relative flex cursor-pointer items-center gap-3 rounded-lg bg-card px-3 py-3 text-left text-muted-foreground outline-none ring-1 ring-black/5 hover:bg-zinc-50 focus-visible:ring-2 focus-visible:ring-ring data-checked:bg-primary/8 data-checked:text-foreground data-checked:ring-2 data-checked:ring-primary data-checked:hover:bg-primary/8 dark:bg-white/3 dark:ring-white/5 dark:hover:bg-white/5 dark:data-checked:bg-primary/15 dark:data-checked:ring-primary dark:data-checked:hover:bg-primary/15" | |
| 267 | − > | |
| 268 | − <IconComponent className="size-4 shrink-0" aria-hidden /> | |
| 269 | − <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 270 | − {option.label} | |
| 271 | − </span> | |
| 272 | − <RadioPrimitive.Indicator | |
| 273 | − className="grid size-5 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground" | |
| 274 | − aria-hidden | |
| 275 | − > | |
| 276 | − <CheckIcon className="size-3.5 shrink-0" /> | |
| 277 | − </RadioPrimitive.Indicator> | |
| 278 | − {option.badgeLabel ? ( | |
| 279 | − <Badge variant="warning" size="sm"> | |
| 280 | − {option.badgeLabel} | |
| 281 | − </Badge> | |
| 282 | − ) : null} | |
| 283 | − </RadioPrimitive.Root> | |
| 284 | − ); | |
| 285 | − })} | |
| 286 | − {COMING_SOON_DRIVER_OPTIONS.map((option) => { | |
| 287 | − const IconComponent = option.icon; | |
| 288 | − return ( | |
| 289 | − <RadioPrimitive.Root | |
| 290 | − key={option.value} | |
| 291 | − value={option.value} | |
| 292 | − disabled | |
| 293 | − className={cn( | |
| 294 | − "relative flex cursor-not-allowed items-center gap-3 rounded-lg bg-card/60 px-3 py-3 text-left opacity-55 outline-none ring-1 ring-black/5 dark:bg-white/2 dark:ring-white/5", | |
| 295 | − )} | |
| 244 | + <WizardPanel> | |
| 245 | + <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}> | |
| 246 | + <div id="add-instance-driver-label" className="text-sm font-medium text-foreground"> | |
| 247 | + Driver | |
| 248 | + </div> | |
| 249 | + <RadioGroup | |
| 250 | + value={driver} | |
| 251 | + onValueChange={(value) => setDriver(ProviderDriverKind.make(value))} | |
| 252 | + aria-labelledby="add-instance-driver-label" | |
| 253 | + className="grid grid-cols-1 gap-2 sm:grid-cols-2" | |
| 254 | + > | |
| 255 | + {DRIVER_OPTIONS.map((option) => { | |
| 256 | + const IconComponent = option.icon; | |
| 257 | + return ( | |
| 258 | + <RadioPrimitive.Root | |
| 259 | + key={option.value} | |
| 260 | + value={option.value} | |
| 261 | + className="relative flex cursor-pointer items-center gap-3 rounded-lg bg-card px-3 py-3 text-left text-muted-foreground outline-none ring-1 ring-black/5 hover:bg-zinc-50 focus-visible:ring-2 focus-visible:ring-ring data-checked:bg-primary/8 data-checked:text-foreground data-checked:ring-2 data-checked:ring-primary data-checked:hover:bg-primary/8 dark:bg-white/3 dark:ring-white/5 dark:hover:bg-white/5 dark:data-checked:bg-primary/15 dark:data-checked:ring-primary dark:data-checked:hover:bg-primary/15" | |
| 262 | + > | |
| 263 | + <IconComponent className="size-4 shrink-0" aria-hidden /> | |
| 264 | + <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 265 | + {option.label} | |
| 266 | + </span> | |
| 267 | + <RadioPrimitive.Indicator | |
| 268 | + className="grid size-5 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground" | |
| 269 | + aria-hidden | |
| 296 | 270 | > |
| 297 | − <IconComponent | |
| 298 | − className="size-4 shrink-0 text-muted-foreground" | |
| 299 | − aria-hidden | |
| 300 | − /> | |
| 301 | − <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 302 | − {option.label} | |
| 303 | − </span> | |
| 271 | + <CheckIcon className="size-3.5 shrink-0" /> | |
| 272 | + </RadioPrimitive.Indicator> | |
| 273 | + {option.badgeLabel ? ( | |
| 304 | 274 | <Badge variant="warning" size="sm"> |
| 305 | − Coming Soon | |
| 275 | + {option.badgeLabel} | |
| 306 | 276 | </Badge> |
| 307 | − </RadioPrimitive.Root> | |
| 308 | − ); | |
| 309 | − })} | |
| 310 | − </RadioGroup> | |
| 311 | − </div> | |
| 277 | + ) : null} | |
| 278 | + </RadioPrimitive.Root> | |
| 279 | + ); | |
| 280 | + })} | |
| 281 | + {COMING_SOON_DRIVER_OPTIONS.map((option) => { | |
| 282 | + const IconComponent = option.icon; | |
| 283 | + return ( | |
| 284 | + <RadioPrimitive.Root | |
| 285 | + key={option.value} | |
| 286 | + value={option.value} | |
| 287 | + disabled | |
| 288 | + className={cn( | |
| 289 | + "relative flex cursor-not-allowed items-center gap-3 rounded-lg bg-card/60 px-3 py-3 text-left opacity-55 outline-none ring-1 ring-black/5 dark:bg-white/2 dark:ring-white/5", | |
| 290 | + )} | |
| 291 | + > | |
| 292 | + <IconComponent className="size-4 shrink-0 text-muted-foreground" aria-hidden /> | |
| 293 | + <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground"> | |
| 294 | + {option.label} | |
| 295 | + </span> | |
| 296 | + <Badge variant="warning" size="sm"> | |
| 297 | + Coming Soon | |
| 298 | + </Badge> | |
| 299 | + </RadioPrimitive.Root> | |
| 300 | + ); | |
| 301 | + })} | |
| 302 | + </RadioGroup> | |
| 303 | + </div> | |
| 312 | 304 | |
| 313 | − <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 314 | − <span className="text-xs font-medium text-foreground">Label</span> | |
| 315 | − <Input | |
| 316 | − className="bg-background" | |
| 317 | − placeholder="e.g. Work" | |
| 318 | − value={label} | |
| 319 | − onChange={(event) => setLabel(event.target.value)} | |
| 320 | − /> | |
| 305 | + <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 306 | + <span className="text-xs font-medium text-foreground">Label</span> | |
| 307 | + <Input | |
| 308 | + className="bg-background" | |
| 309 | + placeholder="e.g. Work" | |
| 310 | + value={label} | |
| 311 | + onChange={(event) => setLabel(event.target.value)} | |
| 312 | + /> | |
| 313 | + <span className="text-[11px] text-muted-foreground"> | |
| 314 | + Shown in the provider list. Optional. | |
| 315 | + </span> | |
| 316 | + </label> | |
| 317 | + | |
| 318 | + <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 319 | + <span className="text-xs font-medium text-foreground">Instance ID</span> | |
| 320 | + <Input | |
| 321 | + className="bg-background" | |
| 322 | + placeholder={`${driver}_work`} | |
| 323 | + value={instanceId} | |
| 324 | + onChange={(event) => { | |
| 325 | + setInstanceIdOverride(event.target.value); | |
| 326 | + }} | |
| 327 | + aria-invalid={showInstanceIdError} | |
| 328 | + /> | |
| 329 | + {showInstanceIdError ? ( | |
| 330 | + <span className="text-[11px] text-destructive">{instanceIdError}</span> | |
| 331 | + ) : ( | |
| 321 | 332 | <span className="text-[11px] text-muted-foreground"> |
| 322 | − Shown in the provider list. Optional. | |
| 333 | + Routing key used by threads and sessions. Letters, digits, '-', or '_'. | |
| 323 | 334 | </span> |
| 324 | − </label> | |
| 335 | + )} | |
| 336 | + </label> | |
| 325 | 337 | |
| 326 | − <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 327 | − <span className="text-xs font-medium text-foreground">Instance ID</span> | |
| 328 | − <Input | |
| 329 | − className="bg-background" | |
| 330 | − placeholder={`${driver}_work`} | |
| 331 | − value={instanceId} | |
| 332 | − onChange={(event) => { | |
| 333 | − setInstanceIdOverride(event.target.value); | |
| 334 | − }} | |
| 335 | − aria-invalid={showInstanceIdError} | |
| 338 | + <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 339 | + <span className="text-xs font-medium text-foreground">Accent color</span> | |
| 340 | + <div className="flex min-w-0 flex-wrap items-center gap-2"> | |
| 341 | + <input | |
| 342 | + type="color" | |
| 343 | + value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]} | |
| 344 | + onChange={(event) => setAccentColor(event.target.value)} | |
| 345 | + aria-label="Provider instance accent color" | |
| 346 | + className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5" | |
| 336 | 347 | /> |
| 337 | − {showInstanceIdError ? ( | |
| 338 | − <span className="text-[11px] text-destructive">{instanceIdError}</span> | |
| 339 | − ) : ( | |
| 340 | − <span className="text-[11px] text-muted-foreground"> | |
| 341 | − Routing key used by threads and sessions. Letters, digits, '-', or '_'. | |
| 342 | − </span> | |
| 343 | − )} | |
| 344 | − </label> | |
| 345 | − | |
| 346 | − <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}> | |
| 347 | − <span className="text-xs font-medium text-foreground">Accent color</span> | |
| 348 | − <div className="flex min-w-0 flex-wrap items-center gap-2"> | |
| 349 | − <input | |
| 350 | − type="color" | |
| 351 | − value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]} | |
| 352 | − onChange={(event) => setAccentColor(event.target.value)} | |
| 353 | − aria-label="Provider instance accent color" | |
| 354 | − className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5" | |
| 355 | − /> | |
| 356 | − <div className="flex flex-wrap gap-1.5"> | |
| 357 | − {PROVIDER_ACCENT_SWATCHES.map((swatch) => { | |
| 358 | − const selected = accentColor.toLowerCase() === swatch; | |
| 359 | − return ( | |
| 360 | − <button | |
| 361 | − key={swatch} | |
| 362 | − type="button" | |
| 363 | − className={cn( | |
| 364 | − "size-6 cursor-pointer rounded-full border transition", | |
| 365 | − selected | |
| 366 | − ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background" | |
| 367 | − : "border-black/10 hover:scale-105 dark:border-white/20", | |
| 368 | − )} | |
| 369 | − style={{ backgroundColor: swatch }} | |
| 370 | − onClick={() => setAccentColor(swatch)} | |
| 371 | − aria-label={`Use ${swatch} accent`} | |
| 372 | − /> | |
| 373 | − ); | |
| 374 | − })} | |
| 375 | − </div> | |
| 376 | − {accentColor ? ( | |
| 377 | − <Button | |
| 378 | − type="button" | |
| 379 | − size="xs" | |
| 380 | − variant="ghost" | |
| 381 | − className="text-muted-foreground" | |
| 382 | − onClick={() => setAccentColor("")} | |
| 383 | − > | |
| 384 | − Clear | |
| 385 | − </Button> | |
| 386 | − ) : null} | |
| 348 | + <div className="flex flex-wrap gap-1.5"> | |
| 349 | + {PROVIDER_ACCENT_SWATCHES.map((swatch) => { | |
| 350 | + const selected = accentColor.toLowerCase() === swatch; | |
| 351 | + return ( | |
| 352 | + <button | |
| 353 | + key={swatch} | |
| 354 | + type="button" | |
| 355 | + className={cn( | |
| 356 | + "size-6 cursor-pointer rounded-full border transition", | |
| 357 | + selected | |
| 358 | + ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background" | |
| 359 | + : "border-black/10 hover:scale-105 dark:border-white/20", | |
| 360 | + )} | |
| 361 | + style={{ backgroundColor: swatch }} | |
| 362 | + onClick={() => setAccentColor(swatch)} | |
| 363 | + aria-label={`Use ${swatch} accent`} | |
| 364 | + /> | |
| 365 | + ); | |
| 366 | + })} | |
| 387 | 367 | </div> |
| 388 | − <span className="text-[11px] text-muted-foreground"> | |
| 389 | − Optional marker shown in the picker. | |
| 390 | − </span> | |
| 368 | + {accentColor ? ( | |
| 369 | + <Button | |
| 370 | + type="button" | |
| 371 | + size="xs" | |
| 372 | + variant="ghost" | |
| 373 | + className="text-muted-foreground" | |
| 374 | + onClick={() => setAccentColor("")} | |
| 375 | + > | |
| 376 | + Clear | |
| 377 | + </Button> | |
| 378 | + ) : null} | |
| 391 | 379 | </div> |
| 380 | + <span className="text-[11px] text-muted-foreground"> | |
| 381 | + Optional marker shown in the picker. | |
| 382 | + </span> | |
| 383 | + </div> | |
| 392 | 384 | |
| 393 | − {driverSettingsFields.length > 0 ? ( | |
| 394 | − <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}> | |
| 395 | − <ProviderSettingsForm | |
| 396 | − definition={driverOption} | |
| 397 | − value={configDraft} | |
| 398 | − idPrefix={`add-provider-${driver}`} | |
| 399 | − variant="dialog" | |
| 400 | − onChange={setConfigDraft} | |
| 401 | − /> | |
| 402 | − </div> | |
| 403 | − ) : wizardStep === 2 ? ( | |
| 404 | − <div className="grid gap-2"> | |
| 405 | − <p className="text-sm text-muted-foreground"> | |
| 406 | − This driver has no required configuration. You can add the instance now. | |
| 407 | − </p> | |
| 408 | − </div> | |
| 409 | − ) : null} | |
| 410 | − </WizardPanel> | |
| 385 | + {driverSettingsFields.length > 0 ? ( | |
| 386 | + <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}> | |
| 387 | + <ProviderSettingsForm | |
| 388 | + definition={driverOption} | |
| 389 | + value={configDraft} | |
| 390 | + idPrefix={`add-provider-${driver}`} | |
| 391 | + variant="dialog" | |
| 392 | + onChange={setConfigDraft} | |
| 393 | + /> | |
| 394 | + </div> | |
| 395 | + ) : wizardStep === 2 ? ( | |
| 396 | + <div className="grid gap-2"> | |
| 397 | + <p className="text-sm text-muted-foreground"> | |
| 398 | + This driver has no required configuration. You can add the instance now. | |
| 399 | + </p> | |
| 400 | + </div> | |
| 401 | + ) : null} | |
| 402 | + </WizardPanel> | |
| 411 | 403 | |
| 412 | − <DialogFooter variant="bare"> | |
| 413 | − <Button | |
| 414 | − variant="outline" | |
| 415 | − onClick={() => { | |
| 416 | − if (wizardStep === 0) { | |
| 417 | − onOpenChange(false); | |
| 418 | − return; | |
| 419 | − } | |
| 420 | − setWizardStep((step) => Math.max(0, step - 1)); | |
| 421 | − }} | |
| 422 | − > | |
| 423 | − {wizardStep === 0 ? "Cancel" : "Back"} | |
| 424 | − </Button> | |
| 425 | − {wizardStep < ADD_PROVIDER_WIZARD_STEPS.length - 1 ? ( | |
| 426 | − <Button onClick={() => navigateToStep(wizardStep + 1)}>Next</Button> | |
| 427 | − ) : ( | |
| 428 | − <Button onClick={handleSave}>Add instance</Button> | |
| 429 | − )} | |
| 430 | − </DialogFooter> | |
| 431 | − </div> | |
| 432 | − </DialogPopup> | |
| 404 | + <WizardFooter> | |
| 405 | + <Button | |
| 406 | + variant="outline" | |
| 407 | + onClick={() => { | |
| 408 | + if (wizardStep === 0) { | |
| 409 | + onOpenChange(false); | |
| 410 | + return; | |
| 411 | + } | |
| 412 | + setWizardStep((step) => Math.max(0, step - 1)); | |
| 413 | + }} | |
| 414 | + > | |
| 415 | + {wizardStep === 0 ? "Cancel" : "Back"} | |
| 416 | + </Button> | |
| 417 | + {wizardStep < ADD_PROVIDER_WIZARD_STEPS.length - 1 ? ( | |
| 418 | + <Button onClick={() => navigateToStep(wizardStep + 1)}>Next</Button> | |
| 419 | + ) : ( | |
| 420 | + <Button onClick={handleSave}>Add instance</Button> | |
| 421 | + )} | |
| 422 | + </WizardFooter> | |
| 423 | + </WizardPopup> | |
| 433 | 424 | </Dialog> |
| 434 | 425 | ); |
| 435 | 426 | } |
| @@ -7,16 +7,8 @@ import { CircleCheckIcon } from "lucide-react"; · 6 unmodified lines | ||
| 7 | 7 | import { useEffect, useId, useState, type ReactNode } from "react"; |
| 8 | 8 | import { CaptureShortcutConfig } from "./CaptureShortcutConfig"; |
| 9 | 9 | import { Button } from "../ui/button"; |
| 10 | −import { | |
| 11 | − Dialog, | |
| 12 | − DialogDescription, | |
| 13 | − DialogFooter, | |
| 14 | − DialogHeader, | |
| 15 | − DialogPanel, | |
| 16 | − DialogPopup, | |
| 17 | − DialogTitle, | |
| 18 | −} from "../ui/dialog"; | |
| 19 | −import { WizardSteps } from "../ui/wizard-steps"; | |
| 10 | +import { Dialog, DialogDescription } from "../ui/dialog"; | |
| 11 | +import { WizardSteps, WizardPopup, WizardHeader, WizardPanel, WizardFooter } from "../ui/wizard"; | |
| 20 | 12 | import { |
| 21 | 13 | captureSetupAccessReady, |
| 22 | 14 | captureSetupBackend, |
| @@ -335,21 +327,19 @@ export function SnapShotSetupDialog({ · 312 unmodified lines | ||
| 335 | 327 | if (!open && !busy) void onClose(false); |
| 336 | 328 | }} |
| 337 | 329 | > |
| 338 | − <DialogPopup className="max-w-xl" showCloseButton={!busy}> | |
| 339 | − <DialogHeader> | |
| 340 | − <DialogTitle> | |
| 341 | − {desktop ? `Set up snapshots for ${desktop}` : "Set up snapshots"} | |
| 342 | − </DialogTitle> | |
| 330 | + <WizardPopup showCloseButton={!busy}> | |
| 331 | + <WizardHeader title={desktop ? `Set up snapshots for ${desktop}` : "Set up snapshots"}> | |
| 343 | 332 | <WizardSteps |
| 344 | − steps={SETUP_STEPS.map((item, index) => ({ ...item, disabled: index > stepIndex }))} | |
| 345 | − currentStep={step} | |
| 346 | − disabled={busy} | |
| 347 | − onStepSelect={(next) => { | |
| 348 | − if (next !== step) changeStep(next); | |
| 333 | + steps={SETUP_STEPS.map((item) => item.label)} | |
| 334 | + currentStep={stepIndex} | |
| 335 | + isStepDisabled={(index) => busy || index > stepIndex} | |
| 336 | + onStepChange={(index) => { | |
| 337 | + const next = SETUP_STEPS[index]; | |
| 338 | + if (next && next.id !== step) changeStep(next.id); | |
| 349 | 339 | }} |
| 350 | 340 | /> |
| 351 | − </DialogHeader> | |
| 352 | − <DialogPanel> | |
| 341 | + </WizardHeader> | |
| 342 | + <WizardPanel> | |
| 353 | 343 | <div className="space-y-4 text-sm"> |
| 354 | 344 | <div className="space-y-2" aria-live="polite"> |
| 355 | 345 | <h3 className="flex items-center gap-2 font-medium">{title}</h3> |
| @@ -479,8 +469,8 @@ export function SnapShotSetupDialog({ · 123 unmodified lines | ||
| 479 | 469 | </details> |
| 480 | 470 | ) : null} |
| 481 | 471 | </div> |
| 482 | − </DialogPanel> | |
| 483 | − <DialogFooter variant="bare"> | |
| 472 | + </WizardPanel> | |
| 473 | + <WizardFooter> | |
| 484 | 474 | {step !== "access" ? ( |
| 485 | 475 | <Button variant="ghost" disabled={busy} onClick={() => changeStep("access")}> |
| 486 | 476 | Back |
| @@ -566,8 +556,8 @@ export function SnapShotSetupDialog({ · 79 unmodified lines | ||
| 566 | 556 | {busy ? "Saving…" : shortcutChanged ? "Save and finish" : "Done"} |
| 567 | 557 | </Button> |
| 568 | 558 | ) : null} |
| 569 | − </DialogFooter> | |
| 570 | − </DialogPopup> | |
| 559 | + </WizardFooter> | |
| 560 | + </WizardPopup> | |
| 571 | 561 | </Dialog> |
| 572 | 562 | ); |
| 573 | 563 | } |
| @@ -1,56 +0,0 @@ | ||
| 1 | −import { CheckIcon } from "lucide-react"; | |
| 2 | −import { cn } from "~/lib/utils"; | |
| 3 | − | |
| 4 | −export function WizardSteps<Step extends string>({ | |
| 5 | − steps, | |
| 6 | − currentStep, | |
| 7 | − disabled = false, | |
| 8 | − onStepSelect, | |
| 9 | −}: { | |
| 10 | − steps: readonly { id: Step; label: string; disabled?: boolean }[]; | |
| 11 | − currentStep: Step; | |
| 12 | − disabled?: boolean; | |
| 13 | − onStepSelect: (step: Step) => void; | |
| 14 | −}) { | |
| 15 | − const currentIndex = steps.findIndex((step) => step.id === currentStep); | |
| 16 | − return ( | |
| 17 | − <ol className="grid auto-cols-fr grid-flow-col gap-2" aria-label="Setup steps"> | |
| 18 | − {steps.map((step, index) => ( | |
| 19 | − <li key={step.id} className="min-w-0"> | |
| 20 | − <button | |
| 21 | − type="button" | |
| 22 | − disabled={disabled || step.disabled} | |
| 23 | − aria-current={index === currentIndex ? "step" : undefined} | |
| 24 | − className={cn( | |
| 25 | − "grid w-full min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default", | |
| 26 | − index === currentIndex | |
| 27 | − ? "border-primary bg-primary/10 ring-1 ring-primary/25" | |
| 28 | − : index < currentIndex | |
| 29 | − ? "border-border bg-background" | |
| 30 | − : "border-border bg-muted/40", | |
| 31 | − )} | |
| 32 | − onClick={() => onStepSelect(step.id)} | |
| 33 | − > | |
| 34 | − <span | |
| 35 | − className={cn( | |
| 36 | − "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border", | |
| 37 | − index < currentIndex | |
| 38 | − ? "border-primary bg-primary text-primary-foreground" | |
| 39 | − : index === currentIndex | |
| 40 | − ? "border-primary bg-background" | |
| 41 | − : "border-muted-foreground/35 bg-background", | |
| 42 | − )} | |
| 43 | − aria-hidden | |
| 44 | − > | |
| 45 | − {index < currentIndex ? <CheckIcon className="size-3" /> : null} | |
| 46 | − </span> | |
| 47 | − <span className="text-[10px] font-medium uppercase text-muted-foreground"> | |
| 48 | − Step {index + 1} | |
| 49 | − </span> | |
| 50 | − <span className="truncate text-xs font-semibold text-foreground">{step.label}</span> | |
| 51 | − </button> | |
| 52 | − </li> | |
| 53 | − ))} | |
| 54 | − </ol> | |
| 55 | − ); | |
| 56 | −} | |
| @@ -1,19 +1,75 @@ | ||
| 1 | 1 | import { CheckIcon } from "lucide-react"; |
| 2 | −import type { ComponentProps } from "react"; | |
| 2 | +import type { ComponentProps, ReactNode } from "react"; | |
| 3 | 3 | |
| 4 | 4 | import { cn } from "../../lib/utils"; |
| 5 | 5 | import { AnimatedHeight } from "../AnimatedHeight"; |
| 6 | +import { DialogPopup, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "./dialog"; | |
| 7 | + | |
| 8 | +/** Compose a wizard from its header, panel, and footer; flow logic stays with the caller. */ | |
| 9 | +export function WizardPopup({ | |
| 10 | + children, | |
| 11 | + ...props | |
| 12 | +}: Omit<ComponentProps<typeof DialogPopup>, "className" | "style">) { | |
| 13 | + return ( | |
| 14 | + <DialogPopup {...props} className="max-w-xl overflow-x-hidden overflow-y-auto"> | |
| 15 | + <div className="flex min-h-0 flex-col">{children}</div> | |
| 16 | + </DialogPopup> | |
| 17 | + ); | |
| 18 | +} | |
| 19 | + | |
| 20 | +export function WizardHeader({ | |
| 21 | + title, | |
| 22 | + description, | |
| 23 | + identity, | |
| 24 | + children, | |
| 25 | +}: { | |
| 26 | + readonly title: ReactNode; | |
| 27 | + readonly description?: ReactNode; | |
| 28 | + /** Optional branding shown in place of the visible title. The title remains accessible. */ | |
| 29 | + readonly identity?: ReactNode; | |
| 30 | + readonly children?: ReactNode; | |
| 31 | +}) { | |
| 32 | + return ( | |
| 33 | + <DialogHeader> | |
| 34 | + <DialogTitle className={identity ? "sr-only" : undefined}>{title}</DialogTitle> | |
| 35 | + {identity} | |
| 36 | + {description ? <DialogDescription>{description}</DialogDescription> : null} | |
| 37 | + {children} | |
| 38 | + </DialogHeader> | |
| 39 | + ); | |
| 40 | +} | |
| 41 | + | |
| 42 | +export function WizardFooter({ | |
| 43 | + children, | |
| 44 | + leading, | |
| 45 | +}: { | |
| 46 | + readonly children: ReactNode; | |
| 47 | + readonly leading?: ReactNode; | |
| 48 | +}) { | |
| 49 | + return ( | |
| 50 | + <DialogFooter variant="bare" className={leading ? "sm:justify-between" : undefined}> | |
| 51 | + {leading} | |
| 52 | + {leading ? ( | |
| 53 | + <div className="flex flex-col-reverse gap-2 sm:flex-row">{children}</div> | |
| 54 | + ) : ( | |
| 55 | + children | |
| 56 | + )} | |
| 57 | + </DialogFooter> | |
| 58 | + ); | |
| 59 | +} | |
| 6 | 60 | |
| 7 | 61 | export function WizardSteps({ |
| 8 | 62 | steps, |
| 9 | 63 | currentStep, |
| 10 | 64 | summaries, |
| 65 | + showSummaries = false, | |
| 11 | 66 | onStepChange, |
| 12 | 67 | isStepDisabled, |
| 13 | 68 | }: { |
| 14 | 69 | readonly steps: readonly string[]; |
| 15 | 70 | readonly currentStep: number; |
| 16 | 71 | readonly summaries?: readonly (string | null)[]; |
| 72 | + readonly showSummaries?: boolean; | |
| 17 | 73 | readonly isStepDisabled?: (step: number) => boolean; |
| 18 | 74 | readonly onStepChange?: (step: number) => void; |
| 19 | 75 | }) { |
| @@ -61,6 +117,9 @@ export function WizardSteps({ · 41 unmodified lines | ||
| 61 | 117 | )} |
| 62 | 118 | > |
| 63 | 119 | {step} |
| 120 | + {showSummaries && index < currentStep && summaries?.[index] | |
| 121 | + ? `: ${summaries[index]}` | |
| 122 | + : null} | |
| 64 | 123 | </span> |
| 65 | 124 | </Step> |
| 66 | 125 | </li> |
| @@ -70,19 +129,16 @@ export function WizardSteps({ · 3 unmodified lines | ||
| 70 | 129 | } |
| 71 | 130 | |
| 72 | 131 | export function WizardPanel({ |
| 73 | − className, | |
| 74 | 132 | children, |
| 75 | 133 | holdHeight = false, |
| 76 | − ...props | |
| 77 | −}: ComponentProps<"div"> & { readonly holdHeight?: boolean }) { | |
| 134 | +}: { | |
| 135 | + readonly children: ReactNode; | |
| 136 | + readonly holdHeight?: boolean; | |
| 137 | +}) { | |
| 78 | 138 | return ( |
| 79 | 139 | <div |
| 80 | 140 | data-slot="dialog-panel" |
| 81 | − className={cn( | |
| 82 | − "space-y-4 bg-zinc-25/80 px-6 py-5 ring-1 ring-black/5 dark:bg-white/2 dark:ring-white/5", | |
| 83 | − className, | |
| 84 | − )} | |
| 85 | − {...props} | |
| 141 | + className="min-w-0 space-y-4 bg-zinc-25/80 px-6 py-5 ring-1 ring-black/5 dark:bg-white/2 dark:ring-white/5" | |
| 86 | 142 | > |
| 87 | 143 | <AnimatedHeight holdHeight={holdHeight}>{children}</AnimatedHeight> |
| 88 | 144 | </div> |