authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-29 12:36:54+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 10:36:54+00:00
log08e4932f0465327968368b22be343b58a07d476a
tree9f8b11dd6eee820f68a7b56d8d6898cce88d25e9
parent1b4830ff06bf80e505dbf669deef189af85f77d7
signature Signed by PGP key B5690EEEBB952194

Simplify files panel header (#4828)


4 files changed, 97 insertions(+), 40 deletions(-)

apps/web/src/components/RightPanelTabs.tsx+1
......@@ -358,6 +358,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
358358 <div
359359 className={cn(
360360 "workspace-topbar gap-1 pl-2",
361 !ownsDesktopTitleBar && "[--workspace-topbar-height:--spacing(11)]",
361362 props.mode === "inline" ? "pr-28" : "pr-3",
362363 ownsDesktopTitleBar && "wco:pr-[calc(var(--workspace-native-controls-inset)+6rem)]",
363364 props.mode === "inline" && props.maximized && COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
apps/web/src/components/files/FileBrowserPanel.tsx+72-34
......@@ -3,12 +3,15 @@ import type {
33 ContextMenuOpenContext as TreeContextMenuOpenContext,
44} from "@pierre/trees";
55import type { EnvironmentId, ProjectEntry } from "@t3tools/contracts";
6import { FileTree, useFileTree } from "@pierre/trees/react";
6import { FileTree, useFileTree, useFileTreeSearch } from "@pierre/trees/react";
77import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
8import { RefreshCw, Search } from "lucide-react";
8import { RotateCw } from "lucide-react";
99import { useEffect, useMemo, useRef } from "react";
1010
11import { Button } from "~/components/ui/button";
12import { InputGroup, InputGroupInput } from "~/components/ui/input-group";
1113import { toastManager } from "~/components/ui/toast";
14import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
1215import { useComposerHandleContext } from "~/composerHandleContext";
1316import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
1417import { useTheme } from "~/hooks/useTheme";
......@@ -42,6 +45,55 @@ function treePath(entry: ProjectEntry): string {
4245 return entry.kind === "directory" ? `${entry.path}/` : entry.path;
4346}
4447
48function RefreshFilesButton(props: { isPending: boolean; onRefresh: () => void }) {
49 return (
50 <Tooltip>
51 <TooltipTrigger
52 render={
53 <Button
54 type="button"
55 variant="ghost"
56 size="icon-xs"
57 aria-label="Refresh workspace files"
58 onClick={props.onRefresh}
59 />
60 }
61 >
62 <RotateCw className={cn(props.isPending && "animate-spin")} />
63 </TooltipTrigger>
64 <TooltipPopup>{props.isPending ? "Refreshing…" : "Refresh files"}</TooltipPopup>
65 </Tooltip>
66 );
67}
68
69function FileSearchField(props: {
70 ariaLabel: string;
71 name: string;
72 onClose: () => void;
73 onValueChange: (value: string) => void;
74 value: string;
75}) {
76 return (
77 <InputGroup variant="ghost" className="h-7 min-w-0 flex-1 rounded-md">
78 <InputGroupInput
79 type="search"
80 name={props.name}
81 size="sm"
82 value={props.value}
83 aria-label={props.ariaLabel}
84 placeholder="Search files"
85 spellCheck={false}
86 onChange={(event) => props.onValueChange(event.target.value)}
87 onKeyDown={(event) => {
88 if (event.key !== "Escape") return;
89 props.onClose();
90 event.currentTarget.blur();
91 }}
92 />
93 </InputGroup>
94 );
95}
96
4597export default function FileBrowserPanel({
4698 environmentId,
4799 cwd,
......@@ -176,9 +228,17 @@ export default function FileBrowserPanel({
176228 }
177229 },
178230 paths: [],
179 search: true,
231 search: false,
180232 unsafeCSS: TREE_UNSAFE_CSS,
181233 });
234 const search = useFileTreeSearch(model);
235 const handleSearchValueChange = (value: string) => {
236 if (value.trim().length === 0) {
237 search.close();
238 return;
239 }
240 search.setValue(value);
241 };
182242
183243 useEffect(() => {
184244 if (previousTreePathsRef.current === treePaths) return;
......@@ -187,11 +247,6 @@ export default function FileBrowserPanel({
187247 model.resetPaths(treePaths);
188248 }, [entryKinds, model, treePaths]);
189249
190 const fileCount = useMemo(
191 () => entries.reduce((count, entry) => count + (entry.kind === "file" ? 1 : 0), 0),
192 [entries],
193 );
194
195250 // Tag tree drags with the composer mention payload. The row is read from
196251 // the composed event path (the tree's shadow root is open), so this does
197252 // not depend on running after the tree's own dragstart handler; the drag
......@@ -223,32 +278,15 @@ export default function FileBrowserPanel({
223278 className="flex min-h-0 flex-1 flex-col bg-background"
224279 data-file-browser-panel={`${environmentId}:${cwd}`}
225280 >
226 <div className="flex h-9 shrink-0 items-center gap-2 border-b border-border/60 px-3">
227 <div className="min-w-0 flex-1">
228 <div className="truncate text-xs font-medium text-foreground">{projectName}</div>
229 <div className="truncate text-[10px] leading-none text-muted-foreground">
230 {entriesQuery.isPending && entriesQuery.data === null
231 ? "Indexing…"
232 : `${fileCount.toLocaleString()} files`}
233 {entriesQuery.data?.truncated ? " · partial" : ""}
234 </div>
235 </div>
236 <button
237 type="button"
238 className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
239 aria-label="Search workspace files"
240 onClick={() => model.openSearch()}
241 >
242 <Search className="size-3.5" />
243 </button>
244 <button
245 type="button"
246 className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
247 aria-label="Refresh workspace files"
248 onClick={entriesQuery.refresh}
249 >
250 <RefreshCw className={cn("size-3.5", entriesQuery.isPending && "animate-spin")} />
251 </button>
281 <div className="surface-subheader gap-1 px-2" data-surface-subheader>
282 <RefreshFilesButton isPending={entriesQuery.isPending} onRefresh={entriesQuery.refresh} />
283 <FileSearchField
284 name="project-files-search"
285 ariaLabel={`Search ${projectName} files`}
286 value={search.value}
287 onValueChange={handleSearchValueChange}
288 onClose={search.close}
289 />
252290 </div>
253291 {entriesQuery.error && entriesQuery.data === null ? (
254292 <div className="p-4 text-xs leading-relaxed text-destructive">{entriesQuery.error}</div>
apps/web/src/components/preview/PreviewChromeRow.tsx+1-1
......@@ -166,7 +166,7 @@ export function PreviewChromeRow({
166166 </Tooltip>
167167 </div>
168168
169 <InputGroup className="group/address h-7 flex-1 rounded-md border-transparent bg-transparent shadow-none before:shadow-none hover:bg-muted/40 focus-within:bg-background">
169 <InputGroup variant="ghost" className="group/address h-7 flex-1 rounded-md">
170170 <Tooltip>
171171 <TooltipTrigger
172172 render={
apps/web/src/components/ui/input-group.tsx+23-5
......@@ -7,13 +7,31 @@ import { cn } from "~/lib/utils";
77import { Input, type InputProps } from "~/components/ui/input";
88import { Textarea, type TextareaProps } from "~/components/ui/textarea";
99
10function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10const inputGroupVariants = cva(
11 "relative inline-flex w-full min-w-0 items-center rounded-lg border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
12 {
13 defaultVariants: {
14 variant: "default",
15 },
16 variants: {
17 variant: {
18 default:
19 "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)]",
20 ghost:
21 "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background",
22 },
23 },
24 },
25);
26
27function InputGroup({
28 className,
29 variant,
30 ...props
31}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupVariants>) {
1132 return (
1233 <div
13 className={cn(
14 "relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
15 className,
16 )}
34 className={cn(inputGroupVariants({ variant }), className)}
1735 data-slot="input-group"
1836 role="group"
1937 {...props}