Skip to content

Commit 504aa24

Browse files
authored
fix(file_browser): Tailwind v4 opacity fix, selection toggle, hover colors, and cleanup (#9259)
1 parent aefb859 commit 504aa24

1 file changed

Lines changed: 81 additions & 81 deletions

File tree

frontend/src/plugins/impl/FileBrowserPlugin.tsx

Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Copyright 2026 Marimo. All rights reserved. */
22

3-
import { CornerLeftUp } from "lucide-react";
3+
import { type LucideIcon, CornerLeftUp } from "lucide-react";
44
import { type JSX, useEffect, useState } from "react";
55
import { z } from "zod";
66
import {
@@ -128,6 +128,43 @@ interface FileBrowserProps extends Data, PluginFunctions {
128128
host: HTMLElement;
129129
}
130130

131+
interface CheckboxOrIconProps {
132+
isSelected: boolean;
133+
canSelect: boolean;
134+
Icon: LucideIcon;
135+
onSelect: () => void;
136+
}
137+
138+
function CheckboxOrIcon({
139+
isSelected,
140+
canSelect,
141+
Icon,
142+
onSelect,
143+
}: CheckboxOrIconProps) {
144+
if (canSelect) {
145+
return (
146+
<>
147+
<Checkbox
148+
checked={isSelected}
149+
onClick={(e) => {
150+
onSelect();
151+
e.stopPropagation();
152+
}}
153+
className={cn({ "hidden group-hover:flex": !isSelected })}
154+
/>
155+
<Icon
156+
size={16}
157+
className={cn("mr-2", {
158+
hidden: isSelected,
159+
"group-hover:hidden": !isSelected,
160+
})}
161+
/>
162+
</>
163+
);
164+
}
165+
return <Icon size={16} className="mr-2" />;
166+
}
167+
131168
/**
132169
* File browser component.
133170
*
@@ -145,7 +182,6 @@ export const FileBrowser = ({
145182
host,
146183
}: FileBrowserProps): JSX.Element | null => {
147184
const [path, setPath] = useInternalStateWithSync(initialPath);
148-
const [selectAllLabel, setSelectAllLabel] = useState("Select all");
149185
const [isUpdatingPath, setIsUpdatingPath] = useState(false);
150186
const [showLoadingOverlay, setShowLoadingOverlay] = useState(false);
151187

@@ -158,7 +194,6 @@ export const FileBrowser = ({
158194
const { data, error, isPending } = useAsyncData(() => {
159195
return list_directory({ path: path });
160196
}, [path, randomId]);
161-
const spinnerLabel = "Listing files...";
162197

163198
useEffect(() => {
164199
if (!isPending) {
@@ -175,25 +210,29 @@ export const FileBrowser = ({
175210
};
176211
}, [isPending]);
177212

213+
const files = data?.files ?? [];
214+
const selectedPaths = new Set(value.map((x) => x.path));
215+
const canSelectDirectories =
216+
selectionMode === "directory" || selectionMode === "all";
217+
const canSelectFiles = selectionMode === "file" || selectionMode === "all";
218+
219+
const selectable = files.filter(
220+
(f) =>
221+
(canSelectDirectories && f.is_directory) ||
222+
(canSelectFiles && !f.is_directory),
223+
);
224+
const allSelected =
225+
selectable.length > 0 && selectable.every((f) => selectedPaths.has(f.path));
226+
178227
if (!data && error) {
179228
return <Banner kind="danger">{error.message}</Banner>;
180229
}
181230

182-
let { files } = data || {};
183-
if (files === undefined) {
184-
files = [];
185-
}
186-
187231
const pathBuilder = PathBuilder.guessDeliminator(initialPath);
188232
const delimiter = pathBuilder.deliminator;
189233

190-
const selectedPaths = new Set(value.map((x) => x.path));
191234
const selectedFiles = value.map((x) => <li key={x.id}>{x.path}</li>);
192235

193-
const canSelectDirectories =
194-
selectionMode === "directory" || selectionMode === "all";
195-
const canSelectFiles = selectionMode === "file" || selectionMode === "all";
196-
197236
function setNewPath(newPath: string) {
198237
// Prevent updating path while updating
199238
if (isUpdatingPath) {
@@ -230,9 +269,7 @@ export const FileBrowser = ({
230269
return;
231270
}
232271

233-
// Update path and reset select all label
234272
setPath(newPath);
235-
setSelectAllLabel("Select all");
236273
setIsUpdatingPath(false);
237274
}
238275

@@ -264,28 +301,18 @@ export const FileBrowser = ({
264301
}) {
265302
const fileInfo = createFileInfo({ path, name, isDirectory });
266303

267-
if (multiple) {
268-
if (selectedPaths.has(path)) {
269-
setValue(value.filter((x) => x.path !== path));
270-
setSelectAllLabel("Select all");
271-
} else {
272-
setValue([...value, fileInfo]);
273-
}
304+
if (selectedPaths.has(path)) {
305+
setValue(value.filter((x) => x.path !== path));
274306
} else {
275-
setValue([fileInfo]);
307+
setValue(multiple ? [...value, fileInfo] : [fileInfo]);
276308
}
277309
}
278310

279311
function deselectAllFiles() {
280312
setValue(value.filter((x) => Paths.dirname(x.path) !== path));
281-
setSelectAllLabel("Select all");
282313
}
283314

284315
function selectAllFiles() {
285-
if (!files) {
286-
return;
287-
}
288-
289316
const filesInView: FileInfo[] = [];
290317

291318
for (const file of files) {
@@ -304,7 +331,6 @@ export const FileBrowser = ({
304331
}
305332

306333
setValue([...value, ...filesInView]);
307-
setSelectAllLabel("Deselect all");
308334
}
309335

310336
// Create rows for directories and files
@@ -313,7 +339,7 @@ export const FileBrowser = ({
313339
// Parent directory ".." row button
314340
fileRows.push(
315341
<TableRow
316-
className="hover:bg-primary hover:bg-opacity-25 select-none"
342+
className="hover:bg-accent select-none"
317343
key={"Parent directory"}
318344
onClick={() => setNewPath(PARENT_DIRECTORY)}
319345
>
@@ -344,50 +370,13 @@ export const FileBrowser = ({
344370
const Icon = FILE_TYPE_ICONS[fileType];
345371

346372
const isSelected = selectedPaths.has(filePath);
347-
const renderCheckboxOrIcon = () => {
348-
if (
349-
(canSelectDirectories && file.is_directory) ||
350-
(canSelectFiles && !file.is_directory)
351-
) {
352-
return (
353-
<>
354-
<Checkbox
355-
checked={isSelected}
356-
onClick={(e) => {
357-
handleSelection({
358-
path: filePath,
359-
name: file.name,
360-
isDirectory: file.is_directory,
361-
});
362-
e.stopPropagation();
363-
}}
364-
className={cn("", {
365-
"hidden group-hover:flex": !isSelected,
366-
})}
367-
/>
368-
<Icon
369-
size={16}
370-
className={cn("mr-2", {
371-
hidden: isSelected,
372-
"group-hover:hidden": !isSelected,
373-
})}
374-
/>
375-
</>
376-
);
377-
}
378-
379-
return <Icon size={16} className="mr-2" />;
380-
};
381373

382374
fileRows.push(
383375
<TableRow
384376
key={file.id}
385-
className={cn(
386-
"hover:bg-primary hover:bg-opacity-25 group select-none",
387-
{
388-
"bg-primary bg-opacity-25": isSelected,
389-
},
390-
)}
377+
className={cn("hover:bg-accent group select-none", {
378+
"bg-primary/25 hover:bg-primary/35": isSelected,
379+
})}
391380
onClick={() =>
392381
handleClick({
393382
path: filePath,
@@ -397,7 +386,21 @@ export const FileBrowser = ({
397386
}
398387
>
399388
<TableCell className="w-[50px] pl-4">
400-
{renderCheckboxOrIcon()}
389+
<CheckboxOrIcon
390+
isSelected={isSelected}
391+
canSelect={
392+
(canSelectDirectories && file.is_directory) ||
393+
(canSelectFiles && !file.is_directory)
394+
}
395+
Icon={Icon}
396+
onSelect={() =>
397+
handleSelection({
398+
path: filePath,
399+
name: file.name,
400+
isDirectory: file.is_directory,
401+
})
402+
}
403+
/>
401404
</TableCell>
402405
<TableCell>{file.name}</TableCell>
403406
</TableRow>,
@@ -423,8 +426,9 @@ export const FileBrowser = ({
423426
: PluralWords.of("file");
424427

425428
const renderHeader = () => {
426-
label = label ?? `Select ${selectionKindLabel.join(" and ", 2)}...`;
427-
const labelText = <Label>{renderHTML({ html: label })}</Label>;
429+
const displayLabel =
430+
label ?? `Select ${selectionKindLabel.join(" and ", 2)}...`;
431+
const labelText = <Label>{renderHTML({ html: displayLabel })}</Label>;
428432

429433
if (multiple) {
430434
return (
@@ -434,13 +438,9 @@ export const FileBrowser = ({
434438
<Button
435439
size="xs"
436440
variant="link"
437-
onClick={
438-
selectAllLabel === "Select all"
439-
? () => selectAllFiles()
440-
: () => deselectAllFiles()
441-
}
441+
onClick={allSelected ? deselectAllFiles : selectAllFiles}
442442
>
443-
{renderHTML({ html: selectAllLabel })}
443+
{allSelected ? "Deselect all" : "Select all"}
444444
</Button>
445445
</div>
446446
</div>
@@ -461,7 +461,7 @@ export const FileBrowser = ({
461461
onChange={(e) => setNewPath(e.target.value)}
462462
>
463463
{parentDirectories.map((dir) => (
464-
<option value={dir} key={dir} selected={dir === path}>
464+
<option value={dir} key={dir}>
465465
{dir}
466466
</option>
467467
))}
@@ -487,7 +487,7 @@ export const FileBrowser = ({
487487
role="status"
488488
>
489489
<Spinner size="small" />
490-
<span>{spinnerLabel}</span>
490+
<span>Listing files...</span>
491491
</div>
492492
)}
493493
<Table className="cursor-pointer table-fixed">

0 commit comments

Comments
 (0)