authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-03-08 18:21:31-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-08 18:21:31-07:00
log17942787aa06070aa21ad6ae357e08f674383c4d
treead69df06efe040b4efeb19e4b5d65ba5af6c97d8
parent5815ba24e2478b2801fab98163e72dd476e5c280
signature Signed by PGP key B5690EEEBB952194

fix: settings in sidebar, better add project flow (#584)

* improvements * sidebar ui pt2 * back flow * fix back flow

1 files changed, 125 insertions(+), 61 deletions(-)

apps/web/src/components/Sidebar.tsx+125-61
......@@ -1,8 +1,11 @@
11import {
2 ArrowLeftIcon,
23 ChevronRightIcon,
34 FolderIcon,
45 GitPullRequestIcon,
6 PlusIcon,
57 RocketIcon,
8 SettingsIcon,
69 SquarePenIcon,
710 TerminalIcon,
811} from "lucide-react";
......@@ -17,7 +20,7 @@ import {
1720 type ResolvedKeybindingsConfig,
1821} from "@t3tools/contracts";
1922import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
20import { useNavigate, useParams } from "@tanstack/react-router";
23import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
2124import { useAppSettings } from "../appSettings";
2225import { isElectron } from "../env";
2326import { APP_STAGE_LABEL } from "../branding";
......@@ -278,6 +281,7 @@ export default function Sidebar() {
278281 (store) => store.clearProjectDraftThreadById,
279282 );
280283 const navigate = useNavigate();
284 const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" });
281285 const { settings: appSettings } = useAppSettings();
282286 const routeThreadId = useParams({
283287 strict: false,
......@@ -293,6 +297,8 @@ export default function Sidebar() {
293297 const [newCwd, setNewCwd] = useState("");
294298 const [isPickingFolder, setIsPickingFolder] = useState(false);
295299 const [isAddingProject, setIsAddingProject] = useState(false);
300 const [addProjectError, setAddProjectError] = useState<string | null>(null);
301 const addProjectInputRef = useRef<HTMLInputElement | null>(null);
296302 const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null);
297303 const [renamingTitle, setRenamingTitle] = useState("");
298304 const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState<
......@@ -486,6 +492,7 @@ export default function Sidebar() {
486492 const finishAddingProject = () => {
487493 setIsAddingProject(false);
488494 setNewCwd("");
495 setAddProjectError(null);
489496 setAddingProject(false);
490497 };
491498
......@@ -512,12 +519,9 @@ export default function Sidebar() {
512519 await handleNewThread(projectId).catch(() => undefined);
513520 } catch (error) {
514521 setIsAddingProject(false);
515 toastManager.add({
516 type: "error",
517 title: "Unable to add project",
518 description:
519 error instanceof Error ? error.message : "An error occurred while adding the project.",
520 });
522 setAddProjectError(
523 error instanceof Error ? error.message : "An error occurred while adding the project.",
524 );
521525 return;
522526 }
523527 finishAddingProject();
......@@ -541,6 +545,8 @@ export default function Sidebar() {
541545 }
542546 if (pickedPath) {
543547 await addProjectFromPath(pickedPath);
548 } else {
549 addProjectInputRef.current?.focus();
544550 }
545551 setIsPickingFolder(false);
546552 };
......@@ -1025,6 +1031,95 @@ export default function Sidebar() {
10251031
10261032 <SidebarContent className="gap-0">
10271033 <SidebarGroup className="px-2 py-2">
1034 <div className="mb-1 flex items-center justify-between px-2">
1035 <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
1036 Projects
1037 </span>
1038 <Tooltip>
1039 <TooltipTrigger
1040 render={
1041 <button
1042 type="button"
1043 aria-label="Add project"
1044 className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
1045 onClick={() => {
1046 setAddingProject((prev) => !prev);
1047 setAddProjectError(null);
1048 }}
1049 />
1050 }
1051 >
1052 <PlusIcon className="size-3.5" />
1053 </TooltipTrigger>
1054 <TooltipPopup side="right">Add project</TooltipPopup>
1055 </Tooltip>
1056 </div>
1057
1058 {addingProject && (
1059 <div className="mb-2 px-1">
1060 {isElectron && (
1061 <button
1062 type="button"
1063 className="mb-1.5 flex w-full items-center justify-center gap-2 rounded-md border border-border bg-secondary py-1.5 text-xs text-foreground/80 transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
1064 onClick={() => void handlePickFolder()}
1065 disabled={isPickingFolder || isAddingProject}
1066 >
1067 <FolderIcon className="size-3.5" />
1068 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
1069 </button>
1070 )}
1071 <div className="flex gap-1.5">
1072 <input
1073 ref={addProjectInputRef}
1074 className={`min-w-0 flex-1 rounded-md border bg-secondary px-2 py-1 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:outline-none ${
1075 addProjectError
1076 ? "border-red-500/70 focus:border-red-500"
1077 : "border-border focus:border-ring"
1078 }`}
1079 placeholder="/path/to/project"
1080 value={newCwd}
1081 onChange={(event) => {
1082 setNewCwd(event.target.value);
1083 setAddProjectError(null);
1084 }}
1085 onKeyDown={(event) => {
1086 if (event.key === "Enter") handleAddProject();
1087 if (event.key === "Escape") {
1088 setAddingProject(false);
1089 setAddProjectError(null);
1090 }
1091 }}
1092 autoFocus
1093 />
1094 <button
1095 type="button"
1096 className="shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90 disabled:opacity-60"
1097 onClick={handleAddProject}
1098 disabled={isAddingProject}
1099 >
1100 {isAddingProject ? "Adding..." : "Add"}
1101 </button>
1102 </div>
1103 {addProjectError && (
1104 <p className="mt-1 px-0.5 text-[11px] leading-tight text-red-400">
1105 {addProjectError}
1106 </p>
1107 )}
1108 <div className="mt-1.5 px-0.5">
1109 <button
1110 type="button"
1111 className="text-[11px] text-muted-foreground/50 transition-colors hover:text-muted-foreground"
1112 onClick={() => {
1113 setAddingProject(false);
1114 setAddProjectError(null);
1115 }}
1116 >
1117 Cancel
1118 </button>
1119 </div>
1120 </div>
1121 )}
1122
10281123 <SidebarMenu>
10291124 {projects.map((project) => {
10301125 const projectThreads = threads
......@@ -1288,68 +1383,37 @@ export default function Sidebar() {
12881383
12891384 {projects.length === 0 && !addingProject && (
12901385 <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">
1291 No projects yet.
1292 <br />
1293 Add one to get started.
1386 No projects yet
12941387 </div>
12951388 )}
12961389 </SidebarGroup>
12971390 </SidebarContent>
12981391
12991392 <SidebarSeparator />
1300 <SidebarFooter className="gap-0 p-3">
1301 {addingProject ? (
1302 <>
1303 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
1304 Add project
1305 </p>
1306 <input
1307 className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none"
1308 placeholder="/path/to/project"
1309 value={newCwd}
1310 onChange={(event) => setNewCwd(event.target.value)}
1311 onKeyDown={(event) => {
1312 if (event.key === "Enter") handleAddProject();
1313 if (event.key === "Escape") setAddingProject(false);
1314 }}
1315 />
1316 {isElectron && (
1317 <button
1318 type="button"
1319 className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
1320 onClick={() => void handlePickFolder()}
1321 disabled={isPickingFolder || isAddingProject}
1393 <SidebarFooter className="p-2">
1394 <SidebarMenu>
1395 <SidebarMenuItem>
1396 {isOnSettings ? (
1397 <SidebarMenuButton
1398 size="sm"
1399 className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
1400 onClick={() => window.history.back()}
13221401 >
1323 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
1324 </button>
1325 )}
1326 <div className="flex gap-2">
1327 <button
1328 type="button"
1329 className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
1330 onClick={handleAddProject}
1331 disabled={isAddingProject}
1332 >
1333 {isAddingProject ? "Adding..." : "Add"}
1334 </button>
1335 <button
1336 type="button"
1337 className="flex-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground/80 transition-colors duration-150 hover:bg-secondary"
1338 onClick={() => setAddingProject(false)}
1402 <ArrowLeftIcon className="size-3.5" />
1403 <span className="text-xs">Back</span>
1404 </SidebarMenuButton>
1405 ) : (
1406 <SidebarMenuButton
1407 size="sm"
1408 className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
1409 onClick={() => void navigate({ to: "/settings" })}
13391410 >
1340 Cancel
1341 </button>
1342 </div>
1343 </>
1344 ) : (
1345 <button
1346 type="button"
1347 className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground/70 transition-colors duration-150 hover:border-ring hover:text-muted-foreground"
1348 onClick={() => setAddingProject(true)}
1349 >
1350 + Add project
1351 </button>
1352 )}
1411 <SettingsIcon className="size-3.5" />
1412 <span className="text-xs">Settings</span>
1413 </SidebarMenuButton>
1414 )}
1415 </SidebarMenuItem>
1416 </SidebarMenu>
13531417 </SidebarFooter>
13541418 </>
13551419 );