| author | |
| committer | |
| log | 3faeee49ac67dfd9534369f1e1c627c0356b75ac |
| tree | 03442461ac3cf257151c77f8b66ad2fa118d4312 |
| parent | 5e6cc2b89534a8e01772bf647b79a1f2da2f9664 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 690 insertions(+), 771 deletions(-)
apps/web/src/components/GitActionsControl.tsx+318-377| ... | ... | @@ -58,7 +58,7 @@ import { |
| 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) { |
| 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 | } |
apps/web/src/components/cloud/ConnectOnboardingDialog.tsx+51-54| ... | ... | @@ -15,18 +15,10 @@ import { useEnvironments, usePrimaryEnvironment } from "~/state/environments"; |
| 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() { |
| 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() { |
| 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&apos;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&apos;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 | } |
apps/web/src/components/onboarding/WelcomeWizard.tsx+55-55| ... | ... | @@ -71,8 +71,8 @@ import { Input } from "../ui/input"; |
| 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({ |
| 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 | } |
apps/web/src/components/settings/AddProviderInstanceDialog.tsx+185-194| ... | ... | @@ -15,21 +15,14 @@ import { cn } from "../../lib/utils"; |
| 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({ |
| 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 | } |
apps/web/src/components/settings/SnapShotSetupDialog.tsx+16-26| ... | ... | @@ -7,16 +7,8 @@ import { CircleCheckIcon } from "lucide-react"; |
| 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({ |
| 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({ |
| 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({ |
| 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 | } |
apps/web/src/components/ui/wizard-steps.tsx deleted-56| ... | ... | @@ -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 | } |
apps/web/src/components/ui/wizard.tsx+65-9| ... | ... | @@ -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({ |
| 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({ |
| 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> |