# List

Die List stellt mehrere List-Items 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/components/list/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>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 List-Items 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](https://flow.mittwald.de/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](https://flow.mittwald.de/components/content/heading), wird diese über `aria-labelledby`
  zugeordnet.
- Gib jedem Item über `textValue` seinen Text mit. Erst damit findet der User
  ein Item über die Tastatur, indem er dessen Anfang tippt.

---

# 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.

Jedes Element renderst du über `List.Item`. Den Inhalt gestaltest du frei mit
einer eigenen View – oder du nutzt die vorgefertigte Layout-Lösung
[List.ItemView](https://flow.mittwald.de/components/list/list-item-view), die Avatar, Titel, Content und
Aktionen einheitlich anordnet.

## 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/components/list/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 `List.Item` 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/components/list/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](https://flow.mittwald.de/components/structure/table) darzustellen – dabei gelten die Guidelines
der Table.

```tsx
import { typedList } from "@mittwald/flow-react-components";
import {
  type Domain,
  domains,
} from "@/content/components/list/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>
  );
}
```

---

# 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/components/list/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/components/list/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](https://flow.mittwald.de/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](https://flow.mittwald.de/components/form-controls/checkbox). Bei **Einzelauswahl** werden
  sie als [RadioGroup](https://flow.mittwald.de/components/form-controls/radio-group) dargestellt.
- Jeder **aktive Filter** wird durch eine [Badge](https://flow.mittwald.de/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/components/list/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>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/components/list/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/components/list/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](https://flow.mittwald.de/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](https://flow.mittwald.de/components/overlays/modal), fokussiere das Suchfeld über `autoFocus`,
damit der User direkt tippen kann.

---

# Pagination

Standardmäßig zeigt die List maximal 10 List-Items; 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](https://flow.mittwald.de/components/content/skeleton) und
[SkeletonText](https://flow.mittwald.de/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/components/list/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](https://flow.mittwald.de/components/content/illustrated-message), die den User über
einen [Button](https://flow.mittwald.de/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/components/list/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.

---

# Kombiniere mit ...

## ActionGroup

Verwende `<ActionGroup />` innerhalb der List, um eine
[ActionGroup](https://flow.mittwald.de/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/components/list/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>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 |
| --- | --- | --- | --- |
| `accordion` | `boolean` | `false` | Makes list items expandable. The expanded content is placed in `<Content slot="bottom" />`. |
| `batchSize` | `number` | - | The number of items to be displayed on one page. |
| `children` | `ReactNode` | - | - |
| `defaultSelectedKeys` | `"all" \| Iterable<Key>` | - | The initial selected keys in the collection (uncontrolled). |
| `defaultViewMode` | `"list" \| "table" \| "tiles"` | `"list"` | The view mode the list starts in. A persisted view mode takes precedence. |
| `disabledKeys` | `Iterable<Key>` | - | The currently disabled keys in the collection (controlled). |
| `disallowEmptySelection` | `boolean` | - | Whether the collection allows empty selection. |
| `emptySearchResultView` | `ReactNode` | - | The view rendered when a search or filter returns no results. |
| `emptyView` | `ReactNode` | - | The view rendered when the list contains no items. |
| `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. |
| `hidePagination` | `boolean` | `false` | Hides the pagination controls below the list. |
| `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. |
| `loadingItemsCount` | `number` | - | The number of skeleton placeholder items rendered while data is loading. Defaults to the lists batch size. |
| `selectedKeys` | `"all" \| Iterable<Key>` | - | The currently selected keys in the collection (controlled). |
| `selectionBehavior` | `"toggle" \| "replace"` | - | Whether selecting an item replaces the current selection (`"replace"`) or adds to it (`"toggle"`). |
| `selectionMode` | `"multiple" \| "none" \| "single"` | - | The type of selection that is allowed in the collection. |
| `settingStorageKey` | `string` | - | The key the lists settings (view mode, search, filters, sorting) are persisted under. Requires a `<SettingsProvider />` — without a key nothing is persisted. |
| `settingsStorageDefaults` | `ListSettingsStorageDefaults` | - | Defaults for how the lists settings are persisted. |
| `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. |

### Events

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `onAction` | `ItemActionFn<never>` | - | Called with the items data when the user activates a list item. |
| `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. |

### 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. |

