# Object List (https://v2.s3.dimah.dev/docs/react/ui/components/object-list)

`ObjectList` renders a folder catalog. Pass the object returned by
[`useObjectList`](https://v2.s3.dimah.dev/docs/react/hooks/object-list) as `list`. The component
does not fetch and does not call the hooks you give it.

## Preview [#preview]

<DemoPreview name="object-list-demo.tsx">
  <ObjectListDemo />
</DemoPreview>

The preview lists records in memory with `memoryObjectSource`. Six files
load at a time. Search, sort, switch to the grid, select rows with
Shift-click or Ctrl+A, or drop a file onto the frame. Swap the source for
your API, or for `dbObjectSource`, without changing the component.

## Install [#install]

Complete [UI Setup](https://v2.s3.dimah.dev/docs/react/ui) first: the stylesheet and one
`<Toaster />`. Then add this component:

<Tabs items={["npm package", "shadcn registry"]}>

<Tab value="npm package">

```tsx
import { ObjectList } from "@dimah-s3/ui";
```

</Tab>

<Tab value="shadcn registry">

```npm
npx shadcn@latest add @dimah-s3/object-list
```

</Tab>

</Tabs>

---

## Usage [#usage]

Pass `list`, then the hooks you already have. Each one switches a feature on.

```tsx
"use client";

import {
  useDelete,
  useDownload,
  useObjectList,
  useObjectListSelection,
  useUpload,
  type ObjectListSource,
} from "@dimah-s3/react";
import { ObjectList, UploadTrigger } from "@dimah-s3/ui";

type LibraryFile = { id: string; key: string; name: string; size: number };

export function Library({ source }: { source: ObjectListSource<LibraryFile> }) {
  const list = useObjectList(source);
  const selection = useObjectListSelection(list);
  const upload = useUpload({ route: "docs", maxFiles: 8, queue: true });
  const download = useDownload({ route: "docs" });
  const remove = useDelete({ route: "docs" });

  return (
    <ObjectList
      list={list}
      selection={selection}
      upload={upload}
      download={download}
      delete={remove}
      toolbar={<UploadTrigger size="sm" />}
    />
  );
}
```

| Prop        | Adds                                                                          |
| ----------- | ----------------------------------------------------------------------------- |
| `selection` | Checkboxes, the selection bar, Shift ranges, Ctrl+A / Cmd+A, Escape, Space    |
| `upload`    | Drop and paste into the open folder, progress under the header                |
| `download`  | Download in the file menu. `useDownload` or `useFetchDownload`                |
| `share`     | Copy link through a second `useDownload`, such as `disposition: "inline"`     |
| `delete`    | Delete in the file menu with a confirmation, and bulk Delete on the selection |
| `toasts`    | Toasts for those hooks. On by default; see [Toasts](#toasts)                  |

Rows read `list.getFile(item)`: the name, object key, size, type, date, and
thumbnail you return from `getFile` on the source. The second line shows the
size and the date. A file with `previewUrl` shows the image; the name stays
the accessible label.

The list is the [`Upload`](https://v2.s3.dimah.dev/docs/react/ui/components/upload),
[`Download`](https://v2.s3.dimah.dev/docs/react/ui/components/download), and
[`Delete`](https://v2.s3.dimah.dev/docs/react/ui/components/delete) root for its hooks. So
`UploadTrigger`, `UploadDropzone`, `DownloadTrigger`, and `DeleteTrigger`
work inside it without their own root.

## Compose [#compose]

`children` replaces the default layout. Every part reads the nearest
`ObjectList`, so you only arrange them. Props on `ObjectList` configure the
data and the entries; children arrange the frame.

```tsx
import {
  ObjectList,
  ObjectListContent,
  ObjectListHeader,
  ObjectListSearch,
  ObjectListSortMenu,
  ObjectListUploadStatus,
  ObjectListUsage,
  ObjectListViewToggle,
  UploadTrigger,
} from "@dimah-s3/ui";

<ObjectList
  list={list}
  selection={selection}
  upload={upload}
  delete={remove}
  height="28rem"
>
  <ObjectListHeader>
    <ObjectListSearch />
    <ObjectListSortMenu />
    <ObjectListViewToggle />
    <UploadTrigger size="sm" />
  </ObjectListHeader>
  <ObjectListUsage />
  <ObjectListUploadStatus />
  <ObjectListContent />
</ObjectList>;
```

| Part                     | Renders                                                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `ObjectListHeader`       | Select-all, the path, a reload spinner, New folder, and `children`. The selection bar while selecting                     |
| `ObjectListBreadcrumb`   | The path. It shortens to fit, and a menu lists the folders above the open one                                             |
| `ObjectListSearch`       | Debounced search for `list.search`. In the header, a button that opens a field across the bar                             |
| `ObjectListSortMenu`     | Name, size, and date order for `list.sort`. Pass `options` for your fields                                                |
| `ObjectListViewToggle`   | List and grid buttons                                                                                                     |
| `ObjectListSelectAll`    | The select-all checkbox                                                                                                   |
| `ObjectListSelectionBar` | Count, `actions`, bulk Delete, and Clear                                                                                  |
| `ObjectListBulkDelete`   | Bulk Delete for a bar of your own                                                                                         |
| `NewFolderControl`       | New folder, when the source has `createFolder`. In the header it opens across the bar. `ObjectListHeader` already has one |
| `ObjectListUsage`        | Bytes and files from `list.usage`, with a meter for its caps                                                              |
| `ObjectListUploadStatus` | Upload progress and errors                                                                                                |
| `ObjectListContent`      | Rows or tiles, loading, error, and empty states, and the next page                                                        |
| `ObjectListEmpty`        | The empty state, with your own text or a call to action                                                                   |
| `ObjectListItemMenu`     | Download, Copy link, your actions, and Delete for a file. Upload to this folder for a folder                              |

## Header [#header]

`ObjectListHeader` lays out by its own width, so it fits a sidebar as well as
a page:

- The path keeps room for itself. When the controls do not fit beside it,
  they move to a second row, aligned to the end.
- The path shortens to fit: `Files › … › Parent › Current`, then
  `Files › … › Current`, then `… › Current`. The … menu lists every folder
  above the open one.
- Under 36rem, the labels of New folder, Sort, bulk Delete, and the default
  `UploadTrigger` hide, leaving square icon buttons with the same names.
- `ObjectListSearch` is a search button. A click opens a field across the
  whole bar; it stays open while it has text. Escape clears the text, then
  closes it, and Close search does both. Pass `collapsible={false}` to keep
  the field in the bar.
- New folder opens the same way. A click replaces the bar with the name
  field, the create button, and Cancel. Enter creates the folder; Escape or
  Cancel closes the field and brings the path and the other controls back.

Render the controls as direct children of the header, so an open search or
new folder can take the bar from them.

`useObjectListContext()` returns the list, selection, hooks, and view, for
parts of your own. Inside an entry, `useObjectListEntry()` returns it.

## Entries [#entries]

Change part of every entry with a `render*` prop. Each receives the entry:
`entry.kind` is `"file"` or `"folder"`, `entry.item` is your record, and
`entry.file` is what `getFile` returned.

```tsx
<ObjectList
  list={list}
  onOpenItem={(file) => openPreview(file)}
  renderMeta={(entry) =>
    entry.kind === "file" && entry.item.shared ? <Badge>Shared</Badge> : null
  }
  renderDescription={(entry) =>
    entry.kind === "file"
      ? `${entry.item.owner} · ${formatSize(entry.item.size)}`
      : null
  }
  renderActions={(entry) =>
    entry.kind === "file" ? (
      <ObjectListItemMenu
        actions={[{ label: "Rename", onClick: () => rename(entry.item) }]}
      />
    ) : null
  }
/>
```

- `onOpenItem` runs when a file is clicked or activated with Enter. Folders always open in place.
- `renderActions` replaces the trailing controls. Without it, files get `ObjectListItemMenu` when the list has `download`, `share`, or `delete`, and folders get it when files can be uploaded into them. Return `null` to hide it.
- `renderDescription` replaces the second line. `renderMeta` sits beside the name.
- `renderEntry` replaces the whole row or tile. Return `ObjectListRow` or `ObjectListTile` with your own `className`, `media`, or `actions` to keep selection, keyboard, and opening.

Each entry sets `data-slot="object-list-entry"`, `data-kind`, `data-view`, and `data-selected` for styling.

## Views [#views]

`defaultView="grid"` starts in the grid. Pass `view` and `onViewChange` to
keep it in your own state, for example in the URL. `ObjectListViewToggle`
switches it. The grid shows `previewUrl` thumbnails at full width and a
larger glyph for other files.

## Search and sort [#search-and-sort]

`list.setSearch` and `list.setSort` reload the first page. The query receives
`search` (trimmed) and `sort`; the source decides what they mean.
`memoryObjectSource` matches names and sorts by name, size, date, or
`sortValue`. A query of your own passes them to your API. While the new page
loads, the current rows stay on screen.

```tsx
<ObjectListSortMenu
  options={[
    { field: "name", label: "Name" },
    { field: "owner", label: "Owner" },
    { field: "updatedAt", label: "Modified", direction: "desc" },
  ]}
/>
```

Opening another folder clears the search. With a search and no match, the
empty state says nothing matches.

## Selection [#selection]

Pass `useObjectListSelection(list)` as `selection`. By default only files can
be selected. Click a checkbox, Shift-click to extend from the last one, or
Ctrl-click / Cmd-click a row. With focus inside the list, Space toggles the
focused entry, Ctrl+A or Cmd+A selects every loaded entry, and Escape
clears. With more pages to load, select-all takes the rows on screen and
says so.

The selection bar shows the count, `selectionActions`, bulk Delete when the
list has `delete`, and Clear. Controls in `selectionActions` read the
selection you already hold:

```tsx
const selection = useObjectListSelection(list);

<ObjectList
  list={list}
  selection={selection}
  selectionActions={
    <Button size="sm" onClick={() => share(selection.selectedItems)}>
      Share
    </Button>
  }
/>;
```

To delete folders, let them be selected and say which keys they hold.
Without `resolveFolderKeys`, selected folders stay out of the delete.

```tsx
const selection = useObjectListSelection(list, { isSelectable: () => true });

<ObjectList
  list={list}
  selection={selection}
  delete={remove}
  resolveFolderKeys={(folder) => api.keysUnder(folder.id)}
/>;
```

## Delete [#delete]

The file menus and the selection bar share one confirmation. `removeMany`
sends the keys in batches of 100. Deleted files leave the rows and the
selection at once, then the list reloads the pages on screen. Selected
folders stay selected while they still exist.

`onDeleted` receives the `DeleteResult` of each confirmed delete: the keys
that were `deleted`, those that `failed`, and those `beforeDelete` blocked. Leave
`useDelete`'s `onSuccess` for your own work.

After a delete, focus moves to the entry that took the place of the first
deleted one, else the one before it, else the frame.

## Keyboard [#keyboard]

| Key                     | Action                                     |
| ----------------------- | ------------------------------------------ |
| Arrow keys, Home, End   | Move between entries (by row in the grid)  |
| Enter                   | Open the folder, or call `onOpenItem`      |
| Space                   | Toggle the focused entry. Shift: the range |
| Ctrl+A / Cmd+A          | Select every loaded entry                  |
| Escape                  | Clear the selection                        |
| Backspace / Alt+ArrowUp | Open the parent folder                     |

Fields, menus, and dialogs inside the list keep their own keys.

## Upload into folders [#upload-into-folders]

Pass a `useUpload` as `upload`. The frame takes drops and pastes into the
open folder. It is a drop target, not a dropzone: clicking it does not open
the picker, so the hook needs no `noClick` or `noKeyboard`. For the picker,
put an `UploadTrigger` in the header, the toolbar, or the empty state. It
uses the list's hook without its own `Upload`.

```tsx
<ObjectList
  list={list}
  upload={upload}
  empty={
    <ObjectListEmpty>
      <UploadTrigger size="sm" />
    </ObjectListEmpty>
  }
/>
```

Where a file goes comes from the source. When it has `uploadOptions(parent)`,
every file uploaded into the list sends the options for its folder:

- drops and pastes on the frame use the open folder;
- a folder row takes drops for that folder and announces
  `Drop to upload into {name}` while a drag is over it, and its menu has
  "Upload to this folder" as the keyboard path;
- `UploadTrigger` and `UploadDropzone` inside the list use the open folder.
  Their own `call` merges over it.

`dbObjectSource` sets `uploadOptions` for its folders. With your own source,
return the folder as metadata, read it as `clientMetadata` in the server's
upload hook, and store it on your file row:

```tsx
const list = useObjectList({
  ...source,
  uploadOptions: (parent) => ({ metadata: { folderId: parent ?? "" } }),
});
```

Without `uploadOptions`, files use the hook's own options, and folder rows
take no drops. Each time the queue settles with new uploads, the list
reloads once. While the hook is disabled, the frame and folder rows ignore
drops and pastes, and "Upload to this folder" is disabled.

## Toasts [#toasts]

With `toasts` on (the default), the list shows toasts for `upload`,
`download`, and `delete`. They step aside for an `UploadToasts`,
`DownloadToasts`, or `DeleteToasts` you mount for the same hook, so a hook
never reports twice. Mount one `<Toaster />` either way.

Copy link and the bulk delete report their own result. With
`toasts={false}`, they, and every delete result, speak through a polite live
region instead. The drop overlay follows only drags over the list itself.

## File menu [#file-menu]

`ObjectListItemMenu` downloads, copies a link, and deletes one file. Inside a
list every prop is optional: the object key, name, and size come from the
entry, and the hooks come from the list. Delete asks the list's confirmation.

Copy link signs one URL through `share` when the list has it, otherwise
through a navigate-mode `download`, then writes it to the clipboard. Signing
does not start a download. Pass `expiresIn` to ask for a shorter TTL than
the route cap; the message shows the TTL that was signed. Pass `actions`, or
`DropdownMenuItem` children, for rows such as rename. They render after Copy
link and before Delete.

Outside a list the menu has Copy link, with `share`, and your own rows:

```tsx
<ObjectListItemMenu
  objectKey={file.key}
  fileName={file.name}
  share={share}
  actions={[{ label: "Rename", onClick: () => rename(file) }]}
/>
```

## Frame [#frame]

Pass `height` to keep the header in place and scroll the entries inside a
fixed frame. A number is pixels; a string is a CSS length, such as `"18rem"`
or `"100%"`. Pass `maxHeight` when the frame should grow with the folder up
to that cap.

With `loadMore="auto"` (the default), the next page loads as the end of the
entries comes into view, and a frame that is not full keeps loading until it
is. `loadMore="button"` shows Load more instead. A failed page shows Retry,
which repeats that page.

## Storage [#storage]

Return `usage` from the query and `useObjectList` keeps it on `list.usage`.
`ObjectListUsage` shows the bytes and the file count. When the usage carries
`maxBytes` or `maxFiles`, it adds a meter that follows whichever cap is
closer to full and turns destructive at the cap. Without `usage`, nothing is
shown.

The caps come from the source: `memoryObjectSource({ limits })`, or the
`db({ quota })` caps through `dbObjectSource`. `dbObjectSource` sends the
scope total, not the open folder's.

## Folders [#folders]

`ObjectList` does not invent folders. The first page of the query returns
them.

- `memoryObjectSource` builds them from a `/`-separated path with
  `getFolder`.
- `dbObjectSource` lists the folders stored by `db()`, uploads into the open
  folder, and creates folders with New folder.
- With your own source, return folders from your query on the first page, and
  add `createFolder` to enable New folder.

New folder, in the header, opens a name field across the bar. Enter creates
the folder through the source, and Escape or Cancel closes the field. An
invalid or taken name shows a message under the field that describes it.

## Props [#props]

```ts
import type { ObjectListProps } from "@dimah-s3/ui";
```

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/list/object-list.tsx"
  name="ObjectListProps"
/>

### File menu [#file-menu-1]

```ts
import type { ObjectListItemMenuProps } from "@dimah-s3/ui";
```

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/list/object-list-item-menu.tsx"
  name="ObjectListItemMenuProps"
/>
