Skip to content
Source
Sign inSign up

CommandPaletteAddProject.tsx

172 lines6.0 KB

1import {
2 type AddProjectRemoteSource,
3 type NewProjectPublishTarget,
4} from "@t3tools/client-runtime/operations/projects";
5import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
6import type { SourceControlProviderKind } from "@t3tools/contracts";
7import { FolderGit2Icon, LinkIcon } from "lucide-react";
8import { type ReactNode, useState } from "react";
9​
10import { sourceControlIcon } from "~/sourceControlPresentation";
11import { ITEM_ICON_CLASS } from "./CommandPalette.logic";
12import { CommandPaletteTitleIcon } from "./CommandPaletteResults";
13import { Button } from "./ui/button";
14import { Kbd, KbdGroup } from "./ui/kbd";
15import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
16import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
17​
18export function remoteProjectSourceIcon(
19 source: AddProjectRemoteSource,
20 className: string,
21): ReactNode {
22 if (source === "url") return <LinkIcon className={className} />;
23 const Icon = sourceControlIcon(sourceControlClients.get(source));
24 return <Icon className={className} />;
26​
27export function sourceControlHostIcon(
28 definition: NewProjectPublishTarget["definition"],
29 className: string,
30): ReactNode {
31 const Icon = sourceControlIcon(definition);
32 return <Icon className={className} />;
34​
35/** The input's trailing submit button for the add-project, clone, and new-project flows. */
36export function CommandPaletteSubmitButton(props: {
37 readonly id: string;
38 readonly label: ReactNode;
39 readonly shortcutLabel: string;
40 readonly ariaLabel: string;
41 readonly tooltip: ReactNode;
42 readonly disabled: boolean;
43 readonly onSubmit: () => void;
44}) {
45 return (
46 <Tooltip>
47 <TooltipTrigger
48 render={
49 <Button
50 id={props.id}
51 variant="secondary"
52 size="xs"
53 className="absolute inset-e-2.5 top-1/2 -translate-y-1/2"
54 aria-label={props.ariaLabel}
55 disabled={props.disabled}
56 onMouseDown={(event) => {
57 event.preventDefault();
58 }}
59 onClick={props.onSubmit}
60 />
61 }
62 >
63 <span>{props.label}</span>
64 <KbdGroup className="pointer-events-none -me-0.5">
65 <Kbd>{props.shortcutLabel}</Kbd>
66 </KbdGroup>
67 </TooltipTrigger>
68 <TooltipPopup side="top">{props.tooltip}</TooltipPopup>
69 </Tooltip>
70 );
72​
73/**
74 * Picks which host a new project publishes to. The row it sits in toggles on click, so the
75 * picker keeps its clicks to itself. The row also cancels pointerdown to keep focus in the
76 * input, which drops the mousedown Select opens on, so the picker opens on click instead.
77 */
78export function NewProjectPublishHostPicker(props: {
79 readonly disabled: boolean;
80 readonly targets: ReadonlyArray<NewProjectPublishTarget>;
81 readonly value: SourceControlProviderKind;
82 readonly onValueChange: (kind: SourceControlProviderKind) => void;
83}) {
84 const [open, setOpen] = useState(false);
85 return (
86 <span
87 className="flex"
88 onClick={(event) => {
89 event.stopPropagation();
90 // React bubbles the popup's clicks here through its portal; only the
91 // trigger opens the picker.
92 if (!event.currentTarget.contains(event.target as Node)) return;
93 if (!props.disabled) setOpen(true);
94 }}
95 >
96 <Select
97 disabled={props.disabled}
98 open={open}
99 onOpenChange={setOpen}
100 value={props.value}
101 onValueChange={(kind) => {
102 if (kind !== null) props.onValueChange(kind);
103 }}
104 >
105 <SelectTrigger size="xs" variant="ghost" aria-label="Host for the new repository">
106 <SelectValue>
107 {(kind: SourceControlProviderKind) => sourceControlClients.get(kind).label}
108 </SelectValue>
109 </SelectTrigger>
110 <SelectPopup align="end" alignItemWithTrigger={false}>
111 {props.targets.map((target) => (
112 <SelectItem key={target.definition.kind} value={target.definition.kind}>
113 <span className="inline-flex items-center gap-1.5">
114 {sourceControlHostIcon(target.definition, "size-3.5")}
115 {target.definition.label}
116 </span>
117 </SelectItem>
118 ))}
119 </SelectPopup>
120 </Select>
121 </span>
122 );
124​
125export function NewProjectSummary(props: {
126 readonly name: string;
127 readonly pathPreview: string;
128 readonly projectsRoot: string | null;
129 readonly environmentLabel: string | null;
130}) {
131 return (
132 <div className="p-2 pb-0">
133 <div className="flex min-h-8 items-center gap-2 rounded-sm px-2 py-1.5">
134 <CommandPaletteTitleIcon>
135 <FolderGit2Icon className={ITEM_ICON_CLASS} />
136 </CommandPaletteTitleIcon>
137 <span className="flex min-w-0 flex-1 flex-col">
138 <span className="truncate text-foreground text-sm">
139 {props.name.length > 0 ? props.name : "New project"}
140 </span>
141 <span className="truncate text-muted-foreground/85 text-xs">
142 {props.name.length > 0
143 ? `Creates ${props.pathPreview}`
144 : `Goes in ${props.projectsRoot}`}
145 {props.environmentLabel === null ? null : ` on ${props.environmentLabel}`}
146 </span>
147 </span>
148 </div>
149 </div>
150 );
152​
153export function RemoteRepositorySummary(props: {
154 readonly source: AddProjectRemoteSource;
155 readonly title: string;
156 readonly description: string;
157}) {
158 return (
159 <div className="shrink-0 p-2 pb-0">
160 <div className="px-2 py-1.5 text-meta text-dim-foreground">Repository</div>
161 <div className="flex min-h-8 items-center gap-2 rounded-control px-2 py-1.5">
162 <CommandPaletteTitleIcon>
163 {remoteProjectSourceIcon(props.source, ITEM_ICON_CLASS)}
164 </CommandPaletteTitleIcon>
165 <span className="flex min-w-0 flex-1 flex-col">
166 <span className="truncate text-strong-foreground text-sm">{props.title}</span>
167 <span className="truncate text-dim-foreground text-xs">{props.description}</span>
168 </span>
169 </div>
170 </div>
171 );