# List

Die List stellt mehrere ListItems dar und bietet Sortierung, Filter und Suche.

```tsx
import {
  ActionGroup,
  AlertBadge,
  Avatar,
  Button,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={4}
      aria-label="Domains"
      defaultViewMode="list"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button color="success">Anlegen</Button>
      </ActionGroup>
      <DomainList.Search />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Typ"
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="asc"
        directionName="aufsteigend"
        defaultEnabled
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="desc"
        directionName="absteigend"
      />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
      <DomainList.Item
        textValue={(domain) => domain.domain}
        showTiles
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

---

# Best Practices

- Biete eine passende Ansicht an. Eine Rasteransicht eignet sich, wenn das Bild
  eines ListItems den Userflow bestimmt; bei mehreren Anwendungsfällen kann der
  User zwischen Ansichten wechseln.
- Wähle eine Standardsortierung nach dem häufigsten Anwendungsfall. Ein Beispiel
  ist „Neueste zuerst“ bei einer Änderungshistorie; biete nur Sortieroptionen
  mit echtem Mehrwert.
- Setze bei umfangreichen Lists Filter ein und gruppiere die Kategorien
  sinnvoll. Sinnvolle Gruppen sind zum Beispiel Typ, Größe oder Status.
- Platziere weiteren Seiteninhalt oberhalb der List. So verursacht das Nachladen
  über den „Mehr anzeigen“-Button keine Layout-Verschiebungen; die List nimmt
  die volle Breite des Contents einer
  [LayoutCard](/04-components/structure/layout-card) ein.
- Beschreibe die List zugänglich. Ist sie der einzige Hauptinhalt einer Seite,
  erhält sie ein `aria-label`; hat sie eine eigene
  [Heading](/04-components/content/heading), wird diese über `aria-labelledby`
  zugeordnet.

---

# Ansichten

Die List unterstützt drei Ansichten: **Liste**, **Raster** und **Tabelle**. Die
Default-Ansicht legst du über das Property `defaultViewMode` fest; sind mehrere
Ansichten verfügbar, wechselt der User über den Ansichts-Button zwischen ihnen.

## Listenansicht

Besonders geeignet, wenn viele Elemente übersichtlich, platzsparend und
ansprechend dargestellt werden sollen. Nutze `<List.Item />`, um die List in der
Listenansicht darzustellen.

```tsx
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={4}
      aria-label="Domains"
      hidePagination
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />

      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

## Rasteransicht

Sinnvoll, wenn die Anzahl der Elemente überschaubar ist oder die visuelle
Darstellung im Vordergrund steht – das ListItem sollte hier nur wenige
Informationen enthalten. Für die Rasteransicht wird ebenfalls das
`<List.Item />` verwendet: Aktiviere sie über `showTiles` und deaktiviere die
Listenansicht bei Bedarf mit `showList={false}`. Über `maxTileWidth` steuerst du
die maximale Breite der Kacheln.

```tsx
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={4}
      aria-label="Domains"
      hidePagination
      defaultViewMode="tiles"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Item
        textValue={(domain) => domain.domain}
        showTiles
        showList={false}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

## Tabellenansicht

Ideal für Daten, die schnell erfassbar sein müssen, während die optische
Gestaltung zweitrangig ist. Nutze `<List.Table />`, um die List als
[Table](/04-components/structure/table) darzustellen – dabei gelten die
Guidelines der Table.

```tsx
import { typedList } from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={4}
      defaultViewMode="table"
      aria-label="Domains"
      hidePagination
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
    </DomainList.List>
  );
}
```

---

# ListItems

Ein ListItem repräsentiert ein spezifisches Element einer Kategorie (z. B. eine
Domain, E-Mail-Adresse oder ein Projekt) und zeigt nur die Informationen, die
zum Verständnis nötig sind. In der Listenansicht besteht es typischerweise aus:

- **Avatar:** Der [Avatar](/04-components/content/avatar) steht am Anfang. Ein
  [Icon](/04-components/content/icon) spiegelt die Kategorie wider (z. B. ein
  Domain-Icon); ein hochgeladenes [Image](/04-components/content/image) wird
  angezeigt, andernfalls erscheinen Initialen.
- **Titel und Untertitel:** Der Titel gibt den Namen wieder, der Untertitel
  ergänzt weitere Informationen nach dem Muster „Beschreibung – 1. Information
  – 2. Information“.
- **Content Slots:** Zusätzliche Bereiche (Top und Bottom Content), die flexibel
  befüllt werden können.
- **Aktionen:** Interaktionen erfolgen über ein
  [ContextMenu](/04-components/actions/context-menu) oder direkt als
  [Buttons](/04-components/actions/button) im ListItem.

In der Rasteransicht ist die Darstellung kompakter: Der Avatar wird größer und
eckig, Top Content sowie die Accordion-Funktion entfallen.

## Mit Link

Ein ListItem bietet das Property `href`, um das Element zu verlinken.

```tsx
import {
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  MenuItem,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  return (
    <List.List
      batchSize={2}
      hidePagination
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <List.StaticData data={domains} />
      <List.Item
        href={() => "#"}
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <List.ItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
            </ContextMenu>
          </List.ItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

## Mit Accordion

Das Accordion-Verhalten wird über die `accordion`-Property aktiviert. Dadurch
lässt sich ein ListItem per Klick ein- oder ausklappen. Der erweiterte Inhalt
wird in `<Content slot="bottom" />` platziert.

```tsx
import {
  Avatar,
  Content,
  Heading,
  IconDomain,
  ListItemView,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  return (
    <List.List
      batchSize={2}
      hidePagination
      accordion
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <List.StaticData data={domains} />
      <List.Item textValue={(domain) => domain.domain}>
        {(domain) => (
          <ListItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>
            <Text>{domain.type}</Text>
            <Content slot="bottom">Mehr Inhalt</Content>
          </ListItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

## Mit Checkboxen

[Checkboxen](/04-components/form-controls/checkbox) in einem ListItem werden
automatisch am Anfang der Zeile angeordnet. Ihre Funktionalität wird nicht von
der List gesteuert und muss individuell implementiert werden. Achte darauf, dass
die gesamte Zeile zur Auswahl genutzt werden kann – nutze dafür `onAction` der
List.

```tsx
import {
  Avatar,
  Checkbox,
  Heading,
  IconDomain,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";
import { useState } from "react";

export default () => {
  const List = typedList<Domain>();

  const [selectedDomains, setSelectedDomains] = useState<
    Domain[]
  >([]);

  const onSelected = (
    domain: Domain,
    selected: boolean,
  ) => {
    if (selected) {
      setSelectedDomains((prev) => [...prev, domain]);
    } else {
      setSelectedDomains((prev) =>
        prev.filter((d) => d.id !== domain.id),
      );
    }
  };

  const isSelected = (domain: Domain) => {
    return (
      selectedDomains.find((d) => d.id === domain.id) !==
      undefined
    );
  };

  return (
    <List.List
      hidePagination
      batchSize={2}
      aria-label="Domains"
      onAction={(domain) => {
        onSelected(domain, !isSelected(domain));
      }}
      getItemId={(domain) => domain.id}
    >
      <List.StaticData data={domains} />
      <List.Item
        showTiles
        textValue={(domain) => domain.hostname}
      >
        {(domain) => (
          <List.ItemView>
            <Checkbox
              isSelected={isSelected(domain)}
              onChange={(value) =>
                onSelected(domain, value)
              }
              aria-label={`${domain.hostname} auswählen`}
            />
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>
            <Text>{domain.type}</Text>
          </List.ItemView>
        )}
      </List.Item>

      <List.Table>
        <List.TableHeader>
          <List.TableColumn>
            <Checkbox
              aria-label="Alle auswählen"
              onChange={(v) =>
                setSelectedDomains(v ? domains : [])
              }
            />
          </List.TableColumn>
          <List.TableColumn>Domain</List.TableColumn>
        </List.TableHeader>
        <List.TableBody>
          <List.TableRow>
            <List.TableCell>
              {(domain) => (
                <Checkbox
                  isSelected={isSelected(domain)}
                  onChange={(value) =>
                    onSelected(domain, value)
                  }
                  aria-label={`${domain.hostname} auswählen`}
                />
              )}
            </List.TableCell>
            <List.TableCell>
              {(domain) => domain.hostname}
            </List.TableCell>
          </List.TableRow>
        </List.TableBody>
      </List.Table>
    </List.List>
  );
}
```

## Mit Content Slots

In einem ListItem kann zusätzlicher `<Content />` (Top und Bottom Content)
platziert werden. Die Position wird über das `slot`-Property gesteuert.

```tsx
import {
  Avatar,
  Content,
  ContextMenu,
  Heading,
  IconDomain,
  MenuItem,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  return (
    <List.List
      batchSize={2}
      hidePagination
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <List.StaticData data={domains} />
      <List.Item
        showTiles
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <List.ItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>

            <Content slot="top">Top Content</Content>
            <Content slot="bottom">Bottom Content</Content>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
            </ContextMenu>
          </List.ItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

## Mit ColumnLayout

Dem ListItem können die
[ColumnLayout](/04-components/structure/column-layout)-Properties `s`, `m` und
`l` mitgegeben werden, um Seitenverhältnis und Umbruchverhalten von Header und
Content zu steuern.

```tsx
import {
  Avatar,
  Content,
  ContextMenu,
  Heading,
  IconEmail,
  Label,
  MenuItem,
  ProgressBar,
  typedList,
} from "@mittwald/flow-react-components";

export default () => {
  const List = typedList<{ mail: string }>();

  return (
    <List.List
      batchSize={2}
      aria-label="E-Mail-Adressen"
      hidePagination
    >
      <List.StaticData
        data={[
          { mail: "john@doe.com" },
          { mail: "max@mustermann.de" },
        ]}
      />
      <List.Item textValue={(mail) => mail.mail}>
        {(mail) => (
          <List.ItemView l={[3, 1]} m={[2, 1]} s={[1]}>
            <Avatar>
              <IconEmail />
            </Avatar>
            <Heading>{mail.mail}</Heading>

            <Content>
              <ProgressBar size="s" value={50}>
                <Label>Speicherplatz</Label>
              </ProgressBar>
            </Content>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
            </ContextMenu>
          </List.ItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

Da für die Spalten auch `null` gesetzt werden kann, lässt sich nicht zwingend
benötigter Content in kleineren Ansichten ausblenden. In diesem Fall werden auch
die entsprechenden Content Slots nicht angezeigt.

```tsx
import {
  Avatar,
  Content,
  ContextMenu,
  Heading,
  IconEmail,
  Label,
  MenuItem,
  ProgressBar,
  typedList,
} from "@mittwald/flow-react-components";

export default () => {
  const List = typedList<{ mail: string }>();

  return (
    <div style={{ width: 400 }}>
      <List.List
        batchSize={2}
        aria-label="E-Mail-Adressen"
        hidePagination
      >
        <List.StaticData
          data={[
            { mail: "john@doe.com" },
            { mail: "max@mustermann.de" },
          ]}
        />
        <List.Item textValue={(mail) => mail.mail}>
          {(mail) => (
            <List.ItemView
              l={[3, 1]}
              m={[2, 1]}
              s={[1, null]}
            >
              <Avatar>
                <IconEmail />
              </Avatar>
              <Heading>{mail.mail}</Heading>

              <Content>
                <ProgressBar size="s" value={50}>
                  <Label>Speicherplatz</Label>
                </ProgressBar>
              </Content>

              <ContextMenu>
                <MenuItem>Details anzeigen</MenuItem>
              </ContextMenu>
            </List.ItemView>
          )}
        </List.Item>
      </List.List>
    </div>
  );
}
```

## Mit ActionGroup

Verwende eine ActionGroup innerhalb des `<Content />`, um
[Buttons](/04-components/actions/button) im ListItem zu platzieren.

```tsx
import {
  ActionGroup,
  Avatar,
  Button,
  Content,
  Heading,
  IconDomain,
  typedList,
  IconEdit,
  IconDelete,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  return (
    <List.List
      batchSize={2}
      hidePagination
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <List.StaticData data={domains} />
      <List.Item textValue={(domain) => domain.domain}>
        {(domain) => (
          <List.ItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>

            <Content>
              <ActionGroup>
                <Button
                  aria-label="Bearbeiten"
                  variant="plain"
                  color="secondary"
                >
                  <IconEdit />
                </Button>
                <Button
                  aria-label="Löschen"
                  variant="plain"
                  color="secondary"
                >
                  <IconDelete />
                </Button>
              </ActionGroup>
            </Content>
          </List.ItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

---

# Sortierung

Ist die Standardsortierung aktiv, zeigt der Sortierungs-Button nur „Sortierung“
an; wählt der User eine Option, wird der Button-Text entsprechend angepasst.
Lege eine Sortiermethode über `<List.Sorting />` an; mit `customSortingFn` und
einem vorangestellten `$` im `property` definierst du eine eigene Sortierung.

Benenne die Sortierung so, dass Kriterium und Reihenfolge sofort ersichtlich
sind.

  

**✅ Do**

Benenne die Sortierung so, dass eindeutig ersichtlich ist, wonach und in
    welcher Reihenfolge sortiert wird.

```tsx
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={2}
      aria-label="Domains"
      getItemId={(domain) => domain.id}
      hidePagination
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="asc"
        directionName="aufsteigend"
        defaultEnabled
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="desc"
        directionName="absteigend"
      />
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

  

**⛔️ Don't**

Verzichte auf Sortierformulierungen, die nicht eindeutig verständlich sind
    oder keine klare Reihenfolge vermitteln.

```tsx
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={2}
      aria-label="Domains"
      getItemId={(domain) => domain.id}
      hidePagination
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Sorting
        property="domain"
        name="Name"
        defaultEnabled
      />
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

## Sorting Properties

| Property          | Typ                   | Beschreibung                                                                                                                          |
| ----------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `customSortingFn` | `SortingFn<T>`        | Möglichkeit, eine eigene Sortierfunktion zu definieren                                                                                |
| `defaultEnabled`  | `boolean \| "hidden"` | Bestimmt, ob die Sortierung als Default gesetzt wird; bei "hidden" ist die Option nicht sichtbar, wird aber im Hintergrund angewendet |
| `direction`       | `"asc" \| "desc"`     | Auf- oder absteigende Sortierung                                                                                                      |
| `name`            | `string`              | Der Anzeigename der Sortier-Option                                                                                                    |
| `directionName`   | `string`              | Der Anzeigename der Sortierrichtung                                                                                                   |
| `property`        | `string`              | Das für die Sortierung verwendete Property                                                                                            |

---

# Filter

Ein Klick auf den Filter-Button öffnet ein
[ContextMenu](/04-components/actions/context-menu), in dem Filter aktiviert oder
deaktiviert werden können.

- Standardmäßig erlaubt ein Filter die **Mehrfachauswahl**, damit User nach
  mehreren Kriterien filtern können; die Optionen erscheinen dann als
  [Checkbox](/04-components/form-controls/checkbox). Bei **Einzelauswahl**
  werden sie als [RadioGroup](/04-components/form-controls/radio-group)
  dargestellt.
- Jeder **aktive Filter** wird durch eine [Badge](/04-components/status/badge)
  visualisiert, die per Klick entfernt werden kann. Sind mindestens zwei Filter
  aktiv, erscheint zusätzlich ein „Filter zurücksetzen“-Button.
- Mehrere Filter **derselben Kategorie** (z. B. Status, Art, Größe) werden in
  einem eigenen, passend benannten Filter-Button zusammengefasst. Filter ohne
  Kategorie gruppierst du unter einem allgemeinen „Filter“-Button.

Lege Filter über `<List.Filter />` an. Über `priority` bestimmst du, ob ein
Filter immer sichtbar ist (`primary`) oder erst im „Alle Filter“-Modal erscheint
(`secondary`); „Alle Filter“ wird automatisch angezeigt, sobald es secondary
Filter gibt. Die Anzeige des Filter-Werts lässt sich über eine Funktion anpassen
(z. B. für Übersetzungen), und mit einem eigenen `matcher` plus vorangestelltem
`$` im `property` filterst du nach Werten, die nicht in der List vorkommen.

```tsx
import {
  ActionGroup,
  AlertBadge,
  Avatar,
  Button,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={4}
      aria-label="Domains"
      defaultViewMode="list"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button color="success">Anlegen</Button>
      </ActionGroup>
      <DomainList.Search />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Typ"
      />
      <DomainList.Filter
        property="tld"
        mode="some"
        name="TLD"
        priority="secondary"
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="asc"
        defaultEnabled
        directionName="aufsteigend"
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="desc"
        directionName="absteigend"
      />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
      <DomainList.Item
        textValue={(domain) => domain.domain}
        showTiles
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

Aktive Filter-Badges sollten selbsterklärend sein. Bei mehrdeutigen Begriffen
gib zusätzlichen Kontext an.

  

**✅ Do**

Intuitiv verständliche Filter benötigen keinen zusätzlichen Kontext.
    Erklärungsbedürftige Filter sollten mit weiterem Text versehen werden.

```tsx
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={5}
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Typ"
        values={["Domain", "Subdomain"]}
        defaultSelected={["Domain"]}
      />
      <DomainList.Filter
        property="verified"
        mode="some"
        name="Verifizierung"
        matcher={(filterValue, propertyValue) =>
          filterValue === "Verifiziert"
            ? propertyValue
            : !propertyValue
        }
        defaultSelected={["Unverifiziert"]}
        values={["Verifiziert", "Unverifiziert"]}
      />
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

  

**⛔️ Don't**

Bei intuitiven Filtern sollte auf zusätzlichen Text verzichtet werden. Meist
    genügt ein einzelnes beschreibendes Wort.

```tsx
import {
  AlertBadge,
  Avatar,
  ContextMenu,
  Heading,
  IconDomain,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={5}
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Typ"
        matcher={(filterValue, propertyValue) =>
          filterValue === "Type Domain"
            ? propertyValue === "Domain"
            : propertyValue === "Subdomain"
        }
        values={["Type Domain", "Type Subdomain"]}
        defaultSelected={["Type Domain"]}
      />
      <DomainList.Filter
        property="verified"
        mode="some"
        name="Verifizierung"
        matcher={(filterValue, propertyValue) =>
          filterValue === "Verifizierung Verifiziert"
            ? propertyValue
            : !propertyValue
        }
        defaultSelected={["Verifizierung Unverifiziert"]}
        values={[
          "Verifizierung Verifiziert",
          "Verifizierung Unverifiziert",
        ]}
      />
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

## Date Range Filter

Mit `mode="dateRange"` definierst du einen Filter, der die Auswahl eines
Zeitraums ermöglicht. So lassen sich Einträge gezielt zwischen einem Start- und
Enddatum eingrenzen.

```tsx
import { typedList } from "@mittwald/flow-react-components";
import {
  type CalendarDate,
  getLocalTimeZone,
  today,
} from "@internationalized/date";

export default () => {
  const InvoiceList = typedList<{
    id: string;
    date: CalendarDate;
  }>();

  return (
    <InvoiceList.List
      aria-label="Rechnungen"
      defaultViewMode="table"
      getItemId={(domain) => domain.id}
    >
      <InvoiceList.StaticData
        data={[
          {
            id: "RG100000",
            date: today(getLocalTimeZone()),
          },
          {
            id: "RG100001",
            date: today(getLocalTimeZone()).subtract({
              days: 7,
            }),
          },
          {
            id: "RG100002",
            date: today(getLocalTimeZone()).subtract({
              days: 14,
            }),
          },
        ]}
      />
      <InvoiceList.Filter
        property="date"
        mode="dateRange"
        name="Datum"
        dateRangeOptions={{
          maxValue: today(getLocalTimeZone()),
        }}
      />
      <InvoiceList.Table>
        <InvoiceList.TableHeader>
          <InvoiceList.TableColumn>
            Rechnung
          </InvoiceList.TableColumn>
          <InvoiceList.TableColumn>
            Datum
          </InvoiceList.TableColumn>
        </InvoiceList.TableHeader>

        <InvoiceList.TableBody>
          <InvoiceList.TableRow>
            <InvoiceList.TableCell>
              {(invoice) => invoice.id}
            </InvoiceList.TableCell>
            <InvoiceList.TableCell>
              {(invoice) =>
                `${invoice.date.day}.${invoice.date.month}.${invoice.date.year}`
              }
            </InvoiceList.TableCell>
          </InvoiceList.TableRow>
        </InvoiceList.TableBody>
      </InvoiceList.Table>
    </InvoiceList.List>
  );
}
```

## Filter Properties

| Property          | Typ                               | Beschreibung                                                                |
| ----------------- | --------------------------------- | --------------------------------------------------------------------------- |
| `defaultSelected` | `string[]`                        | Array der als Default gesetzten Filter                                      |
| `matcher`         | `FilterMatcher<T, TProp, string>` | Definiert eine eigene Filterlogik für die Listenelemente                    |
| `mode`            | `"all" \| "some" \| "one"`        | Bestimmt, wie mehrere ausgewählte Filterwerte miteinander kombiniert werden |
| `name`            | `string`                          | Der Anzeigename des Filters                                                 |
| `property`        | `string`                          | Das für die Filterung verwendete Property                                   |
| `values`          | `string[]`                        | Die Optionen für den Filter                                                 |

---

# Suche

Verwende `<List.Search />` innerhalb der List, um ein
[SearchField](/04-components/form-controls/search-field) anzuzeigen.
Standardmäßig startet die Suche automatisch; soll sie erst auf Enter auslösen,
setze `autoSubmit` auf `false`. Öffnet sich die List in einem
[Modal](/04-components/overlays/modal), fokussiere das Suchfeld über
`autoFocus`, damit der User direkt tippen kann.

---

# Pagination

Standardmäßig zeigt die List maximal 10 ListItems; weitere lädt der User über
den „Mehr anzeigen“-Button nach. Über `batchSize` änderst du die Anzahl, über
`hidePagination` schaltest du die Pagination ab. Halte die Zahl niedrig: Über
Suche, Filter und Sortierung findet der User gezielt, und viele gleichzeitig
geladene Einträge kosten Performance.

## Infinite Scroll

Für sehr lange Listen, in denen ohne konkretes Suchziel gestöbert wird, kann
statt des „Mehr anzeigen“-Buttons **Infinite Scroll** aktiviert werden: Die
nächste Seite lädt automatisch, sobald das Ende der Liste in den sichtbaren
Bereich scrollt.

```tsx
<List.List infiniteScroll batchSize={20} aria-label="Domains">
  {/* ... */}
</List.List>
```

- Infinite Scroll ist **opt-in** und sollte nicht der Default sein. Für kurze
  oder gezielt durchsuchte Listen ist der „Mehr anzeigen“-Button meist die
  bessere Wahl.
- Der Mechanismus funktioniert unabhängig davon, ob die Daten statisch,
  asynchron oder über Hooks geladen werden, und respektiert `manualPagination`.
- Während des Nachladens wird ein Ladeindikator am Ende der Liste angezeigt.

---

# Lade- und Leeransichten

## Loading View

Während die Daten initial geladen werden, zeigt die List eine Loading View aus
Skeleton-Platzhaltern an. Ohne weitere Angabe wird ein generisches Skeleton
verwendet. Über das `loadingView`-Property eines `<List.Item />` – oder eines
`<TableCell />` in der Tabellenansicht – lässt sich diese Ansicht anpassen. Sie
gilt in allen Ansichten (List, Tiles, Table) und auch für einzelne Items, die
nach dem initialen Laden noch suspenden – etwa weil ihr Inhalt eigene Daten
nachlädt. Gestalte sie mit [Skeleton](/04-components/content/skeleton) und
[SkeletonText](/04-components/content/skeleton-text) so, dass sie dem Inhalt in
Aufbau und Größe nahekommt, damit der Übergang ohne Layout-Sprung wirkt.

```tsx
import {
  Avatar,
  Heading,
  IconDomain,
  Skeleton,
  SkeletonText,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  return (
    <List.List aria-label="Domains">
      {/* The loader waits a moment before resolving, so the loading view is
          briefly visible before the data appears. */}
      <List.LoaderAsync>
        {() =>
          new Promise<{ data: Domain[] }>((resolve) => {
            setTimeout(
              () => resolve({ data: domains.slice(0, 3) }),
              2000,
            );
          })
        }
      </List.LoaderAsync>
      <List.Item
        textValue={(domain) => domain.hostname}
        loadingView={
          <List.ItemView>
            <Avatar>
              <Skeleton />
            </Avatar>
            <Heading>
              <SkeletonText width="12em" />
            </Heading>
            <SkeletonText width="6em" />
          </List.ItemView>
        }
      >
        {(domain) => (
          <List.ItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>
            <Text>{domain.type}</Text>
          </List.ItemView>
        )}
      </List.Item>
    </List.List>
  );
}
```

## Empty View

Über `emptyView` zeigst du eine eigene Ansicht an, wenn die List keine Einträge
enthält – in der Regel eine
[IllustratedMessage](/04-components/content/illustrated-message), die den User
über einen [Button](/04-components/actions/button) einlädt, das erste Element zu
erstellen. Liefert eine Suche oder ein Filter kein Ergebnis, zeigt
`emptySearchResultView` einen entsprechenden Hinweis. Ist das jeweilige Property
nicht gesetzt, verwendet die List eine vordefinierte Ansicht.

```tsx
import {
  Heading,
  IconDomain,
  IllustratedMessage,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import { type Domain } from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const List = typedList<Domain>();

  const emptyView = (
    <IllustratedMessage>
      <IconDomain />
      <Heading>Keine Domains gefunden</Heading>
      <Text>Füge neue Domains hinzu, um zu starten.</Text>
    </IllustratedMessage>
  );

  return (
    <List.List aria-label="Domains" emptyView={emptyView}>
      <List.StaticData data={[]} />
      <List.Item>{() => null}</List.Item>
    </List.List>
  );
}
```

## Initiale Suspense-Boundary

Beim initialen Laden umschließt die List das Laden der Daten standardmäßig mit
einer eigenen [Suspense](https://react.dev/reference/react/Suspense)-Boundary
und zeigt währenddessen ihre Loading View. Über `disableInitialSuspenseBoundary`
an der Datenquelle (`<List.LoaderAsync />`, `<List.LoaderAsyncResource />`,
`<List.LoaderHooks />`) steuerst du dieses Verhalten:

| Wert              | Verhalten                                                                                                                                                                                    |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `false` (Default) | Die List rendert beim initialen Laden ihre eigene Loading View (Skeleton).                                                                                                                   |
| `true`            | Die List rendert beim initialen Laden keine eigene Suspense-Boundary. Das Suspending wird an die nächste übergeordnete Boundary weitergereicht; die List erscheint erst mit geladenen Daten. |

Belasse den Wert bei `false`, wenn die List den Hauptinhalt darstellt oder keine
übergeordnete Ladeanzeige existiert. Setze ihn auf `true`, wenn die List in eine
Seite eingebettet ist, die bereits einen eigenen Ladezustand anzeigt – so wird
die List atomar dargestellt und der Layout-Shift zwischen Loading und Empty View
vermieden. Zeigt deine Anwendung durchgängig eigene Ladezustände, kannst du
`true` über den `<ComponentDefaultsProvider />` als Standard festlegen.

---

# Daten laden

Die List kann ihre Daten statisch oder asynchron laden.

## Statische Daten

Für statische Daten wird `<List.StaticData />` verwendet. Diese Variante
benötigt keine zusätzliche Logik für Nachladen oder Filtern.

```tsx
<List.StaticData data={dataArray} />
```

## Asynchrone Daten

Mit `<List.LoaderAsync />` werden Daten dynamisch aus einer API oder anderen
asynchronen Quellen nachgeladen.

```tsx
<List.LoaderAsync>
  {async (options) => {
    const response = await fetchDataFromAPI(options);
    return {
      data: response.items,
      itemTotalCount: response.totalCount,
    };
  }}
</List.LoaderAsync>
```

Die Loader-Funktion erhält ein `options`-Objekt und muss `data` sowie – für die
Pagination – `itemTotalCount` zurückgeben.

| Property       | Typ                                                                    | Beschreibung                                                                                             |
| -------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `filtering`    | `{ [key: string]: { mode: "all" \| "some" \| "one"; values: any[] } }` | Enthält Filter für die Daten. Jedes Key-Value-Paar repräsentiert eine Filterbedingung für ein Datenfeld. |
| `searchString` | `string`                                                               | Der eingegebene Suchbegriff.                                                                             |
| `pagination`   | `{ offset: number; limit: number }`                                    | Enthält Offset (Startpunkt) und Limit (maximale Anzahl an Datensätzen).                                  |
| `sorting`      | `{ [key: string]: "asc" \| "desc" }`                                   | Gibt an, nach welchen Datenfeldern sortiert werden soll.                                                 |

## Laden über Hooks

Mit `<List.LoaderHooks />` werden Daten über React Hooks (z. B. TanStack Query
oder SWR) nachgeladen. Der Einsatz von Suspense ist hierbei erforderlich.

```tsx
import { useSuspenseQuery } from "@tanstack/react-query";

<List.LoaderHooks>
  {(options) => {
    const response = useSuspenseQuery({
      queryKey: ["api", options],
      queryFn: () => fetchDataFromAPI(options),
    });
    return {
      data: response.items,
      itemTotalCount: response.totalCount,
    };
  }}
</List.LoaderHooks>;
```

Beim asynchronen Laden lassen sich Pagination (`manualPagination`), Sortierung
(`manualSorting`), Filterung (`manualFiltering`) und Suche serverseitig
verarbeiten.

---

# Responsive Layout

Auf kleinen Bildschirmen passt sich der Header der List an: Ansicht und
Sortierung werden in einem Icon-Button zusammengefasst, ebenso die Filter. Auf
besonders kleinen Bildschirmen wandern diese Elemente zusammen mit der Suche
eine Zeile nach unten.

Der Inhalt der ListItems bricht bei kleineren Bildschirmgrößen um. Top Content
kann über [ColumnLayouts](/04-components/structure/column-layout) auf kleinen
Bildschirmen ausgeblendet werden – dabei dürfen nur Informationen entfallen, die
der User nicht benötigt, um das ListItem zu verstehen.

**ℹ️ Info**

```tsx
import {
  ActionGroup,
  AlertBadge,
  Avatar,
  Button,
  ContextMenu,
  Heading,
  IconDomain,
  IconDownload,
  IconSubdomain,
  MenuItem,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={2}
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button
          color="secondary"
          variant="soft"
          slot="secondary"
        >
          <IconDownload />
        </Button>
        <Button color="success">Anlegen</Button>
      </ActionGroup>
      <DomainList.Search />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Typ"
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="asc"
        defaultEnabled
        directionName="aufsteigend"
      />
      <DomainList.Sorting
        property="hostname"
        name="Alphabetisch"
        direction="desc"
        directionName="absteigend"
      />
      <DomainList.Sorting
        property="type"
        name="Typ"
        direction="asc"
      />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

---

# Kombiniere mit ...

## ActionGroup

Verwende `<ActionGroup />` innerhalb der List, um eine
[ActionGroup](/04-components/actions/action-group) mit Aktionen anzuzeigen, die
sich direkt auf die Liste beziehen.

```tsx
import {
  ActionGroup,
  Avatar,
  Button,
  Heading,
  IconDomain,
  Text,
  typedList,
} from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List
      batchSize={2}
      hidePagination
      aria-label="Domains"
      getItemId={(domain) => domain.id}
    >
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button color="success">Anlegen</Button>
      </ActionGroup>
      <DomainList.Item
        textValue={(domain) => domain.domain}
      >
        {(domain) => (
          <DomainList.ItemView>
            <Avatar>
              <IconDomain />
            </Avatar>
            <Heading>{domain.hostname}</Heading>
            <Text>{domain.type}</Text>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}
```

## Summary

Verwende eine `<ListSummary />`, um eine Zusammenfassung anzuzeigen,
beispielsweise die Gesamtsumme der Beträge. Über das `position`-Property legst
du fest, ob die Summary oberhalb oder unterhalb der List erscheint.

```tsx
import {
  Flex,
  Heading,
  ListItemView,
  ListSummary,
  Text,
  typedList,
} from "@mittwald/flow-react-components";

export default () => {
  const InvoiceList = typedList<{
    id: string;
    amount: string;
  }>();

  return (
    <InvoiceList.List
      batchSize={2}
      hidePagination
      aria-label="Rechnungen"
      getItemId={(invoice) => invoice.id}
    >
      <ListSummary position="bottom">
        <Flex justify="end">
          <Text>
            <strong>Gesamt: 37,00 €</strong>
          </Text>
        </Flex>
      </ListSummary>
      <InvoiceList.StaticData
        data={[
          {
            id: "Rechnung 1",
            amount: "25,00 €",
          },
          {
            id: "Rechnung 2",
            amount: "12,00 €",
          },
        ]}
      />

      <InvoiceList.Item textValue={(invoice) => invoice.id}>
        {(invoice) => (
          <ListItemView>
            <Heading>{invoice.id}</Heading>
            <Text>{invoice.amount}</Text>
          </ListItemView>
        )}
      </InvoiceList.Item>
    </InvoiceList.List>
  );
}
```

---

# Properties

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `batchSize` | `number` | - | The number of items to be displayed on one page. |
| `infiniteScroll` | `boolean` | `false` | Automatically loads the next batch of items when the user scrolls to the end of the list, instead of showing a "Show more" button. |
| `hidePagination` | `boolean` | `false` | Hides the pagination controls below the list. |
| `emptySearchResultView` | `ReactNode` | - | The view rendered when a search or filter returns no results. |
| `emptyView` | `ReactNode` | - | The view rendered when the list contains no items. |
| `children` | `ReactNode` | - | - |
| `wrapWith` | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | - | A React element the component is wrapped with. The element is cloned and receives the component as its only child — useful to render the component inside a link, a tooltip trigger or any other wrapper without changing the surrounding markup. |
| `ref` | `Ref<HTMLSpanElement>` | - | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see [React Docs](https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom) |
| `key` | `Key` | - | - |
| `disallowEmptySelection` | `boolean` | - | Whether the collection allows empty selection. |
| `disabledKeys` | `Iterable<Key>` | - | The currently disabled keys in the collection (controlled). |
| `selectionMode` | `SelectionMode` | - | The type of selection that is allowed in the collection. |
| `selectedKeys` | `"all" \| Iterable<Key>` | - | The currently selected keys in the collection (controlled). |
| `defaultSelectedKeys` | `"all" \| Iterable<Key>` | - | The initial selected keys in the collection (uncontrolled). |
| `selectionBehavior` | `SelectionBehavior` | - | Whether selecting an item replaces the current selection (`"replace"`) or adds to it (`"toggle"`). |
| `accordion` | `boolean` | `false` | Makes list items expandable. The expanded content is placed in `<Content slot="bottom" />`. |
| `settingStorageKey` | `string` | - | The key the lists settings (view mode, search, filters, sorting) are persisted under. Requires a `<SettingsProvider />` — without a key nothing is persisted. |
| `loadingItemsCount` | `number` | - | The number of skeleton placeholder items rendered while data is loading. Defaults to the lists batch size. |
| `getItemId` | `GetItemId<never>` | - | Derives a stable ID from an items data. Used to deduplicate items across loaded batches and as the row ID in the table view. |
| `defaultViewMode` | `ListViewMode` | `"list"` | The view mode the list starts in. A persisted view mode takes precedence. |
| `settingsStorageDefaults` | `ListSettingsStorageDefaults` | - | Defaults for how the lists settings are persisted. |

### Events

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `onChange` | `OnListChanged<never, unknown>` | - | Called with the list model whenever its state changes. |
| `onSelectionChange` | `((keys: Selection) => void)` | - | Handler that is called when the selection changes. |
| `onAction` | `ItemActionFn<never>` | - | Called with the items data when the user activates a list item. |

### Accessibility

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `aria-label` | `string` | - | An accessible label for the list. |
| `aria-labelledby` | `string` | - | The ID of the element labelling the list. |

