+
+
+ { nodeIsFolder ? (
+
+ ) : (
+
+ ) }
+
+ { open && nodeIsFolder && (
+
+ { isLoading && (
+
+
+
+ ) }
+ { ! isLoading && ( children ?? [] ).length === 0 && (
+
+ { __( 'Empty', 'jetpack-backup-pkg' ) }
+
+ ) }
+ { ! isLoading &&
+ ( children ?? [] ).map( ( child, index ) => (
+
+ ) ) }
+
+ ) }
+
+ );
+}
+
+/**
+ * Recursively searches the rendered tree for a file at the given path.
+ *
+ * @param nodes - Nodes to search.
+ * @param path - File path to match, or null to short-circuit.
+ * @return The matching file node, or null.
+ */
+function findFileInTree( nodes: FileNode[], path: string | null ): FileNodeFile | null {
+ if ( ! path ) {
+ return null;
+ }
+ for ( const node of nodes ) {
+ if ( node.path === path && ! isFolder( node ) ) {
+ return node;
+ }
+ if ( isFolder( node ) && node.children ) {
+ const found = findFileInTree( node.children, path );
+ if ( found ) {
+ return found;
+ }
+ }
+ }
+ return null;
+}
diff --git a/projects/packages/backup/src/dashboard/components/file-browser/style.scss b/projects/packages/backup/src/dashboard/components/file-browser/style.scss
new file mode 100644
index 000000000000..8111937c8020
--- /dev/null
+++ b/projects/packages/backup/src/dashboard/components/file-browser/style.scss
@@ -0,0 +1,68 @@
+.jpb-file-browser {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ &__selection {
+ padding: 8px 0;
+ border-bottom: 1px solid #dcdcde;
+ }
+
+ &__layout {
+ // Single-column by default — the tree owns the full panel width so
+ // zebra-stripe rows can run edge-to-edge. The 280px sidebar slot is
+ // only carved out when a `FileInfoCard` is actually rendered.
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 16px;
+
+ &:has(> .jpb-file-info-card) {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 280px);
+ }
+ }
+
+ &__row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+
+ // Alt-row tint. Applied via a class set by `