Zum Inhalt springen
UI Kit
Accent

Bestellungen

Alles nach dem Kauf, von Bestellübersicht und Sendungsverlauf bis zu Rechnung, Rücksendung und Abo.

5 VariantsEine von 3 Components in Bestellungen und Retouren

Variants

Bestellübersicht

Alle Bestellungen mit Status, Filter als Reiter und Aktionen je Bestellung.

Bestelldetail

Lieferstufen, der ganze Sendungsverlauf, Artikel, Adressen und Summen.

Rechnung

Eine vollständige Rechnung mit Netto, MwSt. und Brutto, gesetzt für Papier. Beim Drucken verschwindet alles andere.

Rücksendung

Artikel und Gründe wählen, Erstattung und Rückweg bestimmen, die Summe rechnet mit.

Abo

Nächste Lieferung, Rhythmus, Menge, Aussetzen und Pausieren, mit bisherigen Lieferungen.

Details

Was Bestellungen mitbringt, aus Registry und Quellcode gelesen.

Bereich
E-Commerce
Variants
5
Version
1.0.1 · seit 1.0.0
Datei
components/ui/order.tsx
Bringt mit
utilskitbuttoniconrevealproduct-image
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 104 eigene Texte
Studio
Im Code anpassen
In Sammlungen
Shop-Checkout

Props

Die Schnittstelle OrderProps aus components/ui/order.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    OrderVariant

    order-historyorder-detailinvoicereturnssubscription

  • ordersoptional
    OrderRecord[]

    order-history

  • orderoptional
    OrderRecord

    order-detail and returns

  • invoiceoptional
    InvoiceData
  • subscriptionoptional
    SubscriptionData
  • returnReasonsoptional
    string[]

    returns

  • returnByoptional
    string
  • currencyoptional
    string

Eingebaute Texte

Diese Texte schreibt Bestellungen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 104 Texte anzeigen
ordersEnglisch OrdersDeutsch Bestellungen
ordersIntroEnglisch Track, return or buy again. Invoices are ready as soon as an order ships.Deutsch Verfolgen, zurückschicken oder nachbestellen. Rechnungen liegen bereit, sobald eine Bestellung verschickt ist.
allEnglisch AllDeutsch Alle
openEnglisch OpenDeutsch Offen
deliveredEnglisch DeliveredDeutsch Zugestellt
returnedEnglisch ReturnedDeutsch Retourniert
orderEnglisch OrderDeutsch Bestellung
placedEnglisch PlacedDeutsch Bestellt am
totalEnglisch TotalDeutsch Summe
statusEnglisch StatusDeutsch Status
processingEnglisch ProcessingDeutsch In Bearbeitung
shippedEnglisch On the wayDeutsch Unterwegs
cancelledEnglisch CancelledDeutsch Storniert
viewOrderEnglisch View orderDeutsch Bestellung ansehen
buyAgainEnglisch Buy againDeutsch Erneut kaufen
invoiceEnglisch InvoiceDeutsch Rechnung
moreEnglisch moreDeutsch weitere
noOrdersEnglisch No orders in this view.Deutsch In dieser Ansicht gibt es keine Bestellungen.
backToOrdersEnglisch All ordersDeutsch Alle Bestellungen
helpEnglisch Get helpDeutsch Hilfe erhalten
shipmentEnglisch ShipmentDeutsch Sendung
etaEnglisch ExpectedDeutsch Voraussichtlich
trackingNoEnglisch Tracking numberDeutsch Sendungsnummer
copyEnglisch Copy tracking numberDeutsch Sendungsnummer kopieren
copiedEnglisch CopiedDeutsch Kopiert
stagesEnglisch Ordered,Packed,On the way,DeliveredDeutsch Bestellt,Verpackt,Unterwegs,Zugestellt
historyEnglisch Tracking historyDeutsch Sendungsverlauf
itemsEnglisch ItemsDeutsch Artikel
deliveryAddressEnglisch Delivery addressDeutsch Lieferadresse
billingAddressEnglisch Billing addressDeutsch Rechnungsadresse
paymentEnglisch PaymentDeutsch Zahlung
summaryEnglisch SummaryDeutsch Übersicht
subtotalEnglisch SubtotalDeutsch Zwischensumme
shippingEnglisch ShippingDeutsch Versand
freeEnglisch FreeDeutsch Kostenlos
vatEnglisch VATDeutsch MwSt.
inclEnglisch incl.Deutsch inkl.
qtyEnglisch QtyDeutsch Menge
printEnglisch Print or save as PDFDeutsch Drucken oder als PDF sichern
printShortEnglisch PrintDeutsch Drucken
downloadEnglisch DownloadDeutsch Herunterladen
invoiceTitleEnglisch InvoiceDeutsch Rechnung
invoiceNoEnglisch Invoice no.Deutsch Rechnungsnr.
invoiceDateEnglisch Invoice dateDeutsch Rechnungsdatum
serviceDateEnglisch Delivery dateDeutsch Lieferdatum
orderNoEnglisch Order no.Deutsch Bestellnr.
customerNoEnglisch Customer no.Deutsch Kundennr.
billToEnglisch Bill toDeutsch Rechnung an
posEnglisch Pos.Deutsch Pos.
descriptionEnglisch DescriptionDeutsch Beschreibung
unitNetEnglisch Unit price netDeutsch Einzelpreis netto
amountNetEnglisch Amount netDeutsch Betrag netto
netTotalEnglisch Total netDeutsch Summe netto
grossTotalEnglisch TotalDeutsch Gesamtbetrag
vatIdEnglisch VAT IDDeutsch USt-IdNr.
bankEnglisch BankDeutsch Bank
contactEnglisch ContactDeutsch Kontakt
shippingLineEnglisch ShippingDeutsch Versand
returnsTitleEnglisch Return itemsDeutsch Artikel zurücksenden
returnsIntroEnglisch Pick what goes back. The label is free, the refund arrives within 5 days after we receive the parcel.Deutsch Wähle aus, was zurückgeht. Das Etikett ist kostenlos, das Geld ist innerhalb von 5 Tagen nach Eingang des Pakets zurück.
returnByEnglisch Return byDeutsch Rücksendung bis
reasonEnglisch ReasonDeutsch Grund
chooseReasonEnglisch Choose a reasonDeutsch Grund auswählen
refundToEnglisch Refund toDeutsch Erstattung auf
originalEnglisch Original paymentDeutsch Ursprüngliche Zahlart
originalNoteEnglisch Back to the card you paid withDeutsch Zurück auf die Karte, mit der du bezahlt hast
creditEnglisch Store creditDeutsch Guthaben
creditNoteEnglisch 10% extra, usable right awayDeutsch 10 % extra, sofort einlösbar
dropOffEnglisch How it goes backDeutsch So geht es zurück
shopEnglisch Parcel shopDeutsch Paketshop
shopNoteEnglisch Show a QR code, no printer neededDeutsch QR-Code vorzeigen, kein Drucker nötig
pickupEnglisch Pickup at your doorDeutsch Abholung an der Tür
pickupNoteEnglisch Tomorrow between 8 and 18Deutsch Morgen zwischen 8 und 18 Uhr
refundEnglisch Your refundDeutsch Deine Erstattung
bonusEnglisch Credit bonusDeutsch Guthaben-Bonus
labelEnglisch Return labelDeutsch Rücksendeetikett
requestEnglisch Request returnDeutsch Rücksendung anmelden
requestedEnglisch Return registered, label is on its wayDeutsch Angemeldet, das Etikett ist unterwegs
selectFirstEnglisch Pick at least one item to return.Deutsch Wähle mindestens einen Artikel für die Rücksendung.
itemsSelectedEnglisch items selectedDeutsch Artikel ausgewählt
itemSelectedEnglisch item selectedDeutsch Artikel ausgewählt
subTitleEnglisch SubscriptionDeutsch Abo
activeEnglisch ActiveDeutsch Aktiv
pausedEnglisch PausedDeutsch Pausiert
sinceEnglisch SinceDeutsch Seit
nextDeliveryEnglisch Next deliveryDeutsch Nächste Lieferung
everyEnglisch EveryDeutsch Alle
weeksEnglisch weeksDeutsch Wochen
perDeliveryEnglisch per deliveryDeutsch pro Lieferung
saveEnglisch You saveDeutsch Du sparst
deliverNowEnglisch Deliver nowDeutsch Jetzt liefern
skipEnglisch SkipDeutsch Aussetzen
skippedEnglisch SkippedDeutsch Ausgesetzt
unskipEnglisch Undo skipDeutsch Doch liefern
pauseEnglisch PauseDeutsch Pausieren
resumeEnglisch ResumeDeutsch Fortsetzen
upcomingEnglisch Upcoming deliveriesDeutsch Nächste Lieferungen
pastDeliveriesEnglisch Past deliveriesDeutsch Bisherige Lieferungen
manageEnglisch ManageDeutsch Verwalten
cancelEnglisch Cancel subscriptionDeutsch Abo kündigen
changeEnglisch ChangeDeutsch Ändern
frequencyEnglisch FrequencyDeutsch Rhythmus
quantityEnglisch Bags per deliveryDeutsch Tüten pro Lieferung
pausedNoteEnglisch No deliveries until you resume. Nothing is charged.Deutsch Bis zum Fortsetzen keine Lieferungen. Es wird nichts abgebucht.

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Bestellungen reagiert selbst auf die Tasten Pfeil links und Pfeil rechts.
  • Setzt aria-disabled, aria-label, aria-live und aria-selected, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen alert, radiogroup, tab, tablist und tabpanel.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 8 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 1.267 Zeilen
components/ui/order.tsx
'use client';

import { useEffect, useId, useMemo, useRef, useState } from 'react';
import { cn, round, useInView, useLocale, useTexts, type Locale } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Reveal } from '@/components/ui/reveal';
import { Stagger } from '@/components/ui/motion';
import { NumberTicker } from '@/components/ui/number-ticker';
import { ProductMedia, Quantity, ShopMotion, Swap, TweenPrice, formatNumber, formatPercent, formatPrice, vatOf, type LineItem } from '@/components/ui/product-image';

export type OrderVariant = 'order-history' | 'order-detail' | 'invoice' | 'returns' | 'subscription';
export type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'returned' | 'cancelled';

export interface TrackingEvent {
  date: string;
  time?: string;
  text: string;
  location?: string;
}

export interface OrderRecord {
  number: string;
  /** Already formatted, e.g. "Oct 21, 2026" */
  date: string;
  status: OrderStatus;
  /** One line about the status, e.g. "Delivered on Fri, Oct 3" */
  statusNote?: string;
  items: LineItem[];
  shipping?: number;
  address?: string[];
  billing?: string[];
  payment?: string;
  tracking?: {
    carrier: string;
    code: string;
    eta?: string;
    /** Index of the reached stage: 0 ordered, 1 packed, 2 on the way, 3 delivered */
    stage: number;
    /** Newest first */
    events: TrackingEvent[];
  };
}

export interface InvoiceData {
  number: string;
  date: string;
  serviceDate: string;
  dueNote: string;
  orderNumber: string;
  customerNumber?: string;
  seller: { name: string; lines: string[]; vatId: string; register?: string; bank: string; iban: string; bic: string; contact: string[] };
  buyer: { name: string; lines: string[] };
  /** Unit prices gross, VAT is taken out per line */
  items: { description: string; detail?: string; quantity: number; price: number }[];
  shipping?: number;
  vatRate?: number;
  note?: string;
}

export interface SubscriptionData {
  name: string;
  variant: string;
  image: React.ReactNode;
  /** Price per delivery at quantity 1 */
  price: number;
  /** Saving against a single purchase, in percent */
  saving?: number;
  /** ISO date of the next delivery, e.g. 2026-10-28 */
  nextDate: string;
  /** Weeks between deliveries */
  every: number;
  everyOptions: number[];
  quantity: number;
  since: string;
  address: string[];
  payment: string;
  history: { date: string; number: string; total: number }[];
}

export interface OrderProps {
  variant?: OrderVariant;
  /** order-history */
  orders?: OrderRecord[];
  /** order-detail and returns */
  order?: OrderRecord;
  invoice?: InvoiceData;
  subscription?: SubscriptionData;
  /** returns */
  returnReasons?: string[];
  returnBy?: string;
  currency?: string;
  labels?: Partial<(typeof TEXTS)['en']>;
  className?: string;
}

const TEXTS = {
  en: {
    orders: 'Orders',
    ordersIntro: 'Track, return or buy again. Invoices are ready as soon as an order ships.',
    all: 'All',
    open: 'Open',
    delivered: 'Delivered',
    returned: 'Returned',
    order: 'Order',
    placed: 'Placed',
    total: 'Total',
    status: 'Status',
    processing: 'Processing',
    shipped: 'On the way',
    cancelled: 'Cancelled',
    viewOrder: 'View order',
    buyAgain: 'Buy again',
    invoice: 'Invoice',
    more: 'more',
    noOrders: 'No orders in this view.',
    backToOrders: 'All orders',
    help: 'Get help',
    shipment: 'Shipment',
    eta: 'Expected',
    trackingNo: 'Tracking number',
    copy: 'Copy tracking number',
    copied: 'Copied',
    stages: 'Ordered,Packed,On the way,Delivered',
    history: 'Tracking history',
    items: 'Items',
    deliveryAddress: 'Delivery address',
    billingAddress: 'Billing address',
    payment: 'Payment',
    summary: 'Summary',
    subtotal: 'Subtotal',
    shipping: 'Shipping',
    free: 'Free',
    vat: 'VAT',
    incl: 'incl.',
    qty: 'Qty',
    print: 'Print or save as PDF',
    printShort: 'Print',
    download: 'Download',
    invoiceTitle: 'Invoice',
    invoiceNo: 'Invoice no.',
    invoiceDate: 'Invoice date',
    serviceDate: 'Delivery date',
    orderNo: 'Order no.',
    customerNo: 'Customer no.',
    billTo: 'Bill to',
    pos: 'Pos.',
    description: 'Description',
    unitNet: 'Unit price net',
    amountNet: 'Amount net',
    netTotal: 'Total net',
    grossTotal: 'Total',
    vatId: 'VAT ID',
    bank: 'Bank',
    contact: 'Contact',
    shippingLine: 'Shipping',
    returnsTitle: 'Return items',
    returnsIntro: 'Pick what goes back. The label is free, the refund arrives within 5 days after we receive the parcel.',
    returnBy: 'Return by',
    reason: 'Reason',
    chooseReason: 'Choose a reason',
    refundTo: 'Refund to',
    original: 'Original payment',
    originalNote: 'Back to the card you paid with',
    credit: 'Store credit',
    creditNote: '10% extra, usable right away',
    dropOff: 'How it goes back',
    shop: 'Parcel shop',
    shopNote: 'Show a QR code, no printer needed',
    pickup: 'Pickup at your door',
    pickupNote: 'Tomorrow between 8 and 18',
    refund: 'Your refund',
    bonus: 'Credit bonus',
    label: 'Return label',
    request: 'Request return',
    requested: 'Return registered, label is on its way',
    selectFirst: 'Pick at least one item to return.',
    itemsSelected: 'items selected',
    itemSelected: 'item selected',
    subTitle: 'Subscription',
    active: 'Active',
    paused: 'Paused',
    since: 'Since',
    nextDelivery: 'Next delivery',
    every: 'Every',
    weeks: 'weeks',
    perDelivery: 'per delivery',
    save: 'You save',
    deliverNow: 'Deliver now',
    skip: 'Skip',
    skipped: 'Skipped',
    unskip: 'Undo skip',
    pause: 'Pause',
    resume: 'Resume',
    upcoming: 'Upcoming deliveries',
    pastDeliveries: 'Past deliveries',
    manage: 'Manage',
    cancel: 'Cancel subscription',
    change: 'Change',
    frequency: 'Frequency',
    quantity: 'Bags per delivery',
    pausedNote: 'No deliveries until you resume. Nothing is charged.',
  },
  de: {
    orders: 'Bestellungen',
    ordersIntro: 'Verfolgen, zurückschicken oder nachbestellen. Rechnungen liegen bereit, sobald eine Bestellung verschickt ist.',
    all: 'Alle',
    open: 'Offen',
    delivered: 'Zugestellt',
    returned: 'Retourniert',
    order: 'Bestellung',
    placed: 'Bestellt am',
    total: 'Summe',
    status: 'Status',
    processing: 'In Bearbeitung',
    shipped: 'Unterwegs',
    cancelled: 'Storniert',
    viewOrder: 'Bestellung ansehen',
    buyAgain: 'Erneut kaufen',
    invoice: 'Rechnung',
    more: 'weitere',
    noOrders: 'In dieser Ansicht gibt es keine Bestellungen.',
    backToOrders: 'Alle Bestellungen',
    help: 'Hilfe erhalten',
    shipment: 'Sendung',
    eta: 'Voraussichtlich',
    trackingNo: 'Sendungsnummer',
    copy: 'Sendungsnummer kopieren',
    copied: 'Kopiert',
    stages: 'Bestellt,Verpackt,Unterwegs,Zugestellt',
    history: 'Sendungsverlauf',
    items: 'Artikel',
    deliveryAddress: 'Lieferadresse',
    billingAddress: 'Rechnungsadresse',
    payment: 'Zahlung',
    summary: 'Übersicht',
    subtotal: 'Zwischensumme',
    shipping: 'Versand',
    free: 'Kostenlos',
    vat: 'MwSt.',
    incl: 'inkl.',
    qty: 'Menge',
    print: 'Drucken oder als PDF sichern',
    printShort: 'Drucken',
    download: 'Herunterladen',
    invoiceTitle: 'Rechnung',
    invoiceNo: 'Rechnungsnr.',
    invoiceDate: 'Rechnungsdatum',
    serviceDate: 'Lieferdatum',
    orderNo: 'Bestellnr.',
    customerNo: 'Kundennr.',
    billTo: 'Rechnung an',
    pos: 'Pos.',
    description: 'Beschreibung',
    unitNet: 'Einzelpreis netto',
    amountNet: 'Betrag netto',
    netTotal: 'Summe netto',
    grossTotal: 'Gesamtbetrag',
    vatId: 'USt-IdNr.',
    bank: 'Bank',
    contact: 'Kontakt',
    shippingLine: 'Versand',
    returnsTitle: 'Artikel zurücksenden',
    returnsIntro: 'Wähle aus, was zurückgeht. Das Etikett ist kostenlos, das Geld ist innerhalb von 5 Tagen nach Eingang des Pakets zurück.',
    returnBy: 'Rücksendung bis',
    reason: 'Grund',
    chooseReason: 'Grund auswählen',
    refundTo: 'Erstattung auf',
    original: 'Ursprüngliche Zahlart',
    originalNote: 'Zurück auf die Karte, mit der du bezahlt hast',
    credit: 'Guthaben',
    creditNote: '10 % extra, sofort einlösbar',
    dropOff: 'So geht es zurück',
    shop: 'Paketshop',
    shopNote: 'QR-Code vorzeigen, kein Drucker nötig',
    pickup: 'Abholung an der Tür',
    pickupNote: 'Morgen zwischen 8 und 18 Uhr',
    refund: 'Deine Erstattung',
    bonus: 'Guthaben-Bonus',
    label: 'Rücksendeetikett',
    request: 'Rücksendung anmelden',
    requested: 'Angemeldet, das Etikett ist unterwegs',
    selectFirst: 'Wähle mindestens einen Artikel für die Rücksendung.',
    itemsSelected: 'Artikel ausgewählt',
    itemSelected: 'Artikel ausgewählt',
    subTitle: 'Abo',
    active: 'Aktiv',
    paused: 'Pausiert',
    since: 'Seit',
    nextDelivery: 'Nächste Lieferung',
    every: 'Alle',
    weeks: 'Wochen',
    perDelivery: 'pro Lieferung',
    save: 'Du sparst',
    deliverNow: 'Jetzt liefern',
    skip: 'Aussetzen',
    skipped: 'Ausgesetzt',
    unskip: 'Doch liefern',
    pause: 'Pausieren',
    resume: 'Fortsetzen',
    upcoming: 'Nächste Lieferungen',
    pastDeliveries: 'Bisherige Lieferungen',
    manage: 'Verwalten',
    cancel: 'Abo kündigen',
    change: 'Ändern',
    frequency: 'Rhythmus',
    quantity: 'Tüten pro Lieferung',
    pausedNote: 'Bis zum Fortsetzen keine Lieferungen. Es wird nichts abgebucht.',
  },
};

type T = (typeof TEXTS)['en'];

/* ------------------------------------------------------------------ */
/* Shared parts                                                        */
/* ------------------------------------------------------------------ */

const STATUS: Record<OrderStatus, { icon: IconName; cls: string }> = {
  processing: { icon: 'clock', cls: 'bg-[color-mix(in_oklab,var(--primary)_12%,transparent)] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]' },
  shipped: { icon: 'truck', cls: 'bg-[color-mix(in_oklab,var(--primary)_12%,transparent)] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]' },
  delivered: { icon: 'check-circle', cls: 'bg-[color-mix(in_oklch,var(--success)_14%,transparent)] text-[color-mix(in_oklch,var(--success)_75%,var(--foreground))]' },
  returned: { icon: 'undo', cls: 'bg-muted text-muted-foreground' },
  cancelled: { icon: 'x-circle', cls: 'bg-[color-mix(in_oklch,var(--destructive)_12%,transparent)] text-[color-mix(in_oklch,var(--destructive)_80%,var(--foreground))]' },
};

const statusLabel = (t: T, s: OrderStatus) => ({ processing: t.processing, shipped: t.shipped, delivered: t.delivered, returned: t.returned, cancelled: t.cancelled })[s];

function StatusBadge({ status, t }: { status: OrderStatus; t: T }) {
  const s = STATUS[status];
  return (
    <span className={cn('inline-flex h-7 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-[12px] font-medium', s.cls)}>
      <Icon name={s.icon} size={13} />
      {statusLabel(t, status)}
    </span>
  );
}

/**
 * A marker that glides to the active item of a row (tabs, segments). Items
 * carry data-glide. Until it is measured, every item draws its own state.
 */
function useGlide<T extends HTMLElement>(index: number) {
  const ref = useRef<T>(null);
  const [box, setBox] = useState<{ x: number; w: number; ready: boolean } | null>(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const update = () => {
      const item = el.querySelectorAll<HTMLElement>('[data-glide]')[index];
      if (!item) return setBox(null);
      setBox((b) => ({ x: item.offsetLeft, w: item.offsetWidth, ready: !!b }));
    };
    update();
    const ro = new ResizeObserver(update);
    ro.observe(el);
    return () => ro.disconnect();
  }, [index]);
  return [ref, box] as const;
}

const sumItems = (items: LineItem[]) => round(items.reduce((a, i) => a + i.price * i.quantity, 0));

function Card({ className, children }: { className?: string; children: React.ReactNode }) {
  return <div className={cn('rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)]', className)}>{children}</div>;
}

function Address({ title, lines, icon }: { title: string; lines?: string[]; icon: IconName }) {
  if (!lines?.length) return null;
  return (
    <div className="flex gap-3.5 py-4">
      <Icon name={icon} size={17} className="-mt-px shrink-0 text-muted-foreground" />
      <div>
        <p className="text-[12px] text-muted-foreground">{title}</p>
        <p className="mt-0.5 text-[14px] leading-relaxed text-foreground">
          {lines.map((l) => (
            <span key={l} className="block">
              {l}
            </span>
          ))}
        </p>
      </div>
    </div>
  );
}

/** Dates for the subscription: fixed to UTC so server and browser print the same */
function addWeeks(iso: string, weeks: number) {
  const d = new Date(`${iso}T12:00:00Z`);
  d.setUTCDate(d.getUTCDate() + weeks * 7);
  return d.toISOString().slice(0, 10);
}
function formatDate(locale: Locale, iso: string, long = false) {
  return new Intl.DateTimeFormat(locale === 'de' ? 'de-DE' : 'en-US', long ? { weekday: 'long', day: 'numeric', month: 'long', timeZone: 'UTC' } : { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T12:00:00Z`));
}

/* ------------------------------------------------------------------ */
/* order-history                                                       */
/* ------------------------------------------------------------------ */

function History(props: OrderProps & { t: T }) {
  const { t, orders = [], currency } = props;
  const locale = useLocale();
  const tabs: { id: string; label: string; match: (o: OrderRecord) => boolean }[] = [
    { id: 'all', label: t.all, match: () => true },
    { id: 'open', label: t.open, match: (o) => o.status === 'processing' || o.status === 'shipped' },
    { id: 'delivered', label: t.delivered, match: (o) => o.status === 'delivered' },
    { id: 'returned', label: t.returned, match: (o) => o.status === 'returned' || o.status === 'cancelled' },
  ];
  const [tab, setTab] = useState('all');
  const [switched, setSwitched] = useState(false);
  const list = orders.filter(tabs.find((x) => x.id === tab)!.match);
  const [tabRow, mark] = useGlide<HTMLDivElement>(tabs.findIndex((x) => x.id === tab));
  const pick = (id: string) => {
    setTab(id);
    setSwitched(true);
  };
  return (
    <section className={cn('pb-20 pt-10 sm:pt-14 lg:pb-28', props.className)}>
      <div className="kit-container max-w-[60rem] lg:max-w-[64rem]">
        <h1 className="text-h2 font-semibold text-foreground">{t.orders}</h1>
        <p className="mt-3 max-w-[34rem] text-pretty text-[15px] text-muted-foreground">{t.ordersIntro}</p>

        <div className="-mx-6 mt-8 border-b border-border sm:mx-0">
          <div
            ref={tabRow}
            role="tablist"
            aria-label={t.orders}
            onKeyDown={(e) => {
              if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
              const i = (tabs.findIndex((x) => x.id === tab) + (e.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length;
              pick(tabs[i].id);
              e.currentTarget.querySelectorAll<HTMLElement>('[role=tab]')[i]?.focus();
            }}
            className="kit-no-scrollbar relative flex gap-5 overflow-x-auto scroll-px-6 px-6 max-sm:kit-fade-x sm:gap-6 sm:px-0"
          >
            {tabs.map((x) => {
              const on = tab === x.id;
              const n = orders.filter(x.match).length;
              return (
                <button key={x.id} data-glide role="tab" type="button" aria-selected={on} tabIndex={on ? 0 : -1} onClick={() => pick(x.id)} className={cn('relative flex h-11 shrink-0 items-center gap-2 text-[14px] font-medium transition-colors', on ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>
                  {x.label}
                  <span className={cn('grid h-5 min-w-5 place-items-center rounded-full px-1.5 text-[11px] tabular-nums transition-colors duration-300', on ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground')}>{n}</span>
                  {!mark && <span aria-hidden className={cn('absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-foreground', on ? 'opacity-100' : 'opacity-0')} />}
                </button>
              );
            })}
            {/* The underline glides from tab to tab */}
            {mark && <span aria-hidden className={cn('absolute bottom-0 left-0 h-0.5 rounded-full bg-foreground', mark.ready && 'transition-[transform,width] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none')} style={{ width: mark.w, transform: `translateX(${mark.x}px)` }} />}
          </div>
        </div>

        <ShopMotion />
        {/* A new tab lets its orders rise in one after another */}
        <ul key={tab} className="mt-8 space-y-6" role="tabpanel">
          {list.map((o, n) => {
            const total = sumItems(o.items) + (o.shipping ?? 0);
            const shown = o.items.slice(0, 2);
            return (
              <Reveal as="li" key={o.number} delay={n * 70} className={cn(switched && 'animate-[kitshop-in_0.5s_var(--ease-kit)_both]')} style={switched ? { animationDelay: `${n * 70}ms` } : undefined}>
                <Card className="overflow-hidden transition-[box-shadow,transform] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:shadow-[var(--highlight),var(--shadow-md)] motion-reduce:transition-none">
                  <div className="grid grid-cols-2 gap-x-6 gap-y-3 border-b border-border bg-surface px-5 py-4 sm:flex sm:items-center sm:gap-10 sm:px-6">
                    <div>
                      <p className="text-[12px] text-muted-foreground">{t.order}</p>
                      <p className="text-[14px] font-semibold tabular-nums text-foreground">{o.number}</p>
                    </div>
                    <div>
                      <p className="text-[12px] text-muted-foreground">{t.placed}</p>
                      <p className="text-[14px] text-foreground">{o.date}</p>
                    </div>
                    <div>
                      <p className="text-[12px] text-muted-foreground">{t.total}</p>
                      <p className="text-[14px] font-medium tabular-nums text-foreground">{formatPrice(locale, total, currency)}</p>
                    </div>
                    <div className="sm:ml-auto">
                      <StatusBadge status={o.status} t={t} />
                    </div>
                  </div>
                  <div className="grid gap-5 px-5 py-5 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center sm:px-6">
                    <div className="min-w-0">
                      {o.statusNote && <p className="mb-4 text-[14px] font-medium text-foreground">{o.statusNote}</p>}
                      <ul className="space-y-3">
                        {shown.map((i) => (
                          <li key={i.id} className="flex items-center gap-3.5">
                            <ProductMedia className="size-14 shrink-0 rounded-lg">{i.image}</ProductMedia>
                            <div className="min-w-0">
                              <p className="truncate text-[14px] text-foreground">{i.name}</p>
                              <p className="truncate text-[12px] text-muted-foreground">
                                {i.variant}
                                {i.quantity > 1 && ` · ${i.quantity}×`}
                              </p>
                            </div>
                          </li>
                        ))}
                      </ul>
                      {o.items.length > shown.length && (
                        <p className="mt-3 text-[12px] text-muted-foreground">
                          + {o.items.length - shown.length} {t.more}
                        </p>
                      )}
                    </div>
                    <div className="flex flex-wrap gap-2 sm:w-48 sm:flex-col">
                      <Button href="#" variant="primary" size="sm" className="h-10 flex-1 sm:flex-none">
                        {t.viewOrder}
                      </Button>
                      <Button variant="outline" size="sm" iconLeft="refresh" className="h-10 flex-1 sm:flex-none">
                        {t.buyAgain}
                      </Button>
                      {o.status !== 'processing' && o.status !== 'cancelled' && (
                        <Button href="#" variant="ghost" size="sm" iconLeft="download" className="h-10 flex-1 text-muted-foreground sm:flex-none">
                          {t.invoice}
                        </Button>
                      )}
                    </div>
                  </div>
                </Card>
              </Reveal>
            );
          })}
          {!list.length && <li className="rounded-2xl border border-dashed border-border-strong py-16 text-center text-[14px] text-muted-foreground">{t.noOrders}</li>}
        </ul>
      </div>
    </section>
  );
}

/* ------------------------------------------------------------------ */
/* order-detail                                                        */
/* ------------------------------------------------------------------ */

function Totals({ items, shipping = 0, currency, t, vatRate = 19 }: { items: LineItem[]; shipping?: number; currency?: string; t: T; vatRate?: number }) {
  const locale = useLocale();
  const sub = sumItems(items);
  const total = round(sub + shipping);
  const fp = (n: number) => formatPrice(locale, n, currency);
  return (
    <dl className="space-y-2.5 text-[14px]">
      <div className="flex justify-between">
        <dt className="text-muted-foreground">{t.subtotal}</dt>
        <dd className="tabular-nums text-foreground">{fp(sub)}</dd>
      </div>
      <div className="flex justify-between">
        <dt className="text-muted-foreground">{t.shipping}</dt>
        <dd className={cn('tabular-nums', shipping ? 'text-foreground' : 'text-[color-mix(in_oklab,var(--success)_70%,var(--foreground))] dark:text-success')}>{shipping ? fp(shipping) : t.free}</dd>
      </div>
      <div className="flex items-baseline justify-between border-t border-border pt-3">
        <dt className="font-semibold text-foreground">{t.total}</dt>
        <dd className="text-right">
          <span className="text-[18px] font-semibold tabular-nums text-foreground">{fp(total)}</span>
          <span className="block text-[12px] text-muted-foreground">
            {t.incl} {fp(vatOf(total, vatRate))} {t.vat} ({formatPercent(locale, vatRate)})
          </span>
        </dd>
      </div>
    </dl>
  );
}

function Detail(props: OrderProps & { t: T }) {
  const { t, order, currency } = props;
  const locale = useLocale();
  const [copied, setCopied] = useState(false);
  // The stage bars and the event line fill once the tracking card is in view
  const [trackRef, trackIn] = useInView<HTMLDivElement>('0px 0px -15% 0px');
  if (!order) return null;
  const tr = order.tracking;
  const stages = t.stages.split(',');
  return (
    <section className={cn('pb-20 pt-8 sm:pt-12 lg:pb-28', props.className)}>
      <div className="kit-container">
        <a href="#" className="group mb-6 inline-flex h-10 items-center gap-1.5 text-[14px] text-muted-foreground hover:text-foreground">
          <Icon name="arrow-left" size={15} className="transition-transform group-hover:-translate-x-0.5" />
          {t.backToOrders}
        </a>
        <div className="flex flex-col gap-5 border-b border-border pb-8 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <div className="flex flex-wrap items-center gap-3">
              <h1 className="text-[30px] font-semibold tracking-[-0.03em] text-foreground sm:text-[36px]">
                {t.order} {order.number}
              </h1>
              <StatusBadge status={order.status} t={t} />
            </div>
            <p className="mt-2 text-[14px] text-muted-foreground">
              {t.placed} {order.date}
            </p>
          </div>
          <div className="flex gap-2">
            <Button variant="outline" iconLeft="download">
              {t.invoice}
            </Button>
            <Button variant="ghost" iconLeft="help-circle">
              {t.help}
            </Button>
          </div>
        </div>

        <div className="mt-8 grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-10">
          <div className="min-w-0 space-y-8">
            {tr && (
              <div ref={trackRef}>
              <Card className="p-5 sm:p-7">
                <div className="flex flex-wrap items-start justify-between gap-4">
                  <div>
                    <p className="text-[13px] text-muted-foreground">{t.eta}</p>
                    <p className="mt-0.5 text-[22px] font-semibold tracking-[-0.02em] text-foreground">{tr.eta}</p>
                  </div>
                  <div className="sm:text-right">
                    <p className="text-[12px] text-muted-foreground">
                      {tr.carrier} · {t.trackingNo}
                    </p>
                    <button
                      type="button"
                      onClick={() => { navigator.clipboard?.writeText(tr.code).catch(() => {}); setCopied(true); setTimeout(() => setCopied(false), 1600); }}
                      aria-label={t.copy}
                      className="group -ml-2 mt-0.5 inline-flex h-9 items-center gap-2 rounded-md px-2 text-[14px] font-medium tabular-nums text-foreground hover:bg-accent sm:-mr-2 sm:ml-0"
                    >
                      {tr.code}
                      <Swap value={copied}>
                        <Icon name={copied ? 'check' : 'copy'} size={14} className={copied ? 'text-success' : 'text-muted-foreground group-hover:text-foreground'} />
                      </Swap>
                    </button>
                    <span className="sr-only" aria-live="polite">
                      {copied ? t.copied : ''}
                    </span>
                  </div>
                </div>

                <ol className="mt-7 grid grid-cols-4 gap-1.5" aria-label={t.shipment}>
                  {stages.map((s, n) => {
                    const done = n <= tr.stage;
                    return (
                      <li key={s}>
                        <span className="relative block h-1.5 overflow-hidden rounded-full bg-track">
                          {done && (
                            <span
                              className="absolute inset-0 origin-left rounded-full bg-primary transition-transform duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none"
                              style={{ transform: `scaleX(${trackIn ? 1 : 0})`, transitionDelay: trackIn ? `${n * 260}ms` : '0ms' }}
                            />
                          )}
                          {n === tr.stage + 1 && <span className="absolute inset-y-0 left-0 w-1/3 animate-kit-pulse rounded-full bg-[color-mix(in_oklab,var(--primary)_45%,transparent)] motion-reduce:animate-none" />}
                        </span>
                        <span className={cn('mt-2 block text-[12px] sm:text-[13px]', n === tr.stage ? 'font-medium text-foreground' : done ? 'text-foreground' : 'text-subtle-foreground')}>{s}</span>
                      </li>
                    );
                  })}
                </ol>

                <h2 className="mt-9 text-[14px] font-semibold text-foreground">{t.history}</h2>
                <ol className="relative mt-4">
                  {tr.events.map((e, n) => (
                    <li
                      key={n}
                      className={cn('relative grid grid-cols-[1.25rem_minmax(0,1fr)] gap-x-4 pb-6 transition-[opacity,transform] duration-500 ease-[var(--ease-kit)] last:pb-0 motion-reduce:transition-none', trackIn ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0')}
                      style={{ transitionDelay: trackIn ? `${600 + n * 90}ms` : '0ms' }}
                    >
                      {n < tr.events.length - 1 && <span aria-hidden className="absolute left-[9.5px] top-5 bottom-0 w-px bg-border" />}
                      <span aria-hidden className={cn('relative mt-1 grid size-5 place-items-center rounded-full', n === 0 ? 'bg-[color-mix(in_oklab,var(--primary)_18%,transparent)]' : '')}>
                        <span className={cn('size-2 rounded-full', n === 0 ? 'bg-primary' : 'bg-border-strong')} />
                      </span>
                      <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between sm:gap-6">
                        <div>
                          <p className={cn('text-[14px]', n === 0 ? 'font-medium text-foreground' : 'text-foreground')}>{e.text}</p>
                          {e.location && <p className="text-[13px] text-muted-foreground">{e.location}</p>}
                        </div>
                        <p className="shrink-0 text-[13px] tabular-nums text-muted-foreground sm:text-right">
                          {e.date}
                          {e.time && <span className="sm:block"> {e.time}</span>}
                        </p>
                      </div>
                    </li>
                  ))}
                </ol>
              </Card>
              </div>
            )}

            <Card className="px-5 sm:px-7">
              <h2 className="pb-1 pt-5 text-[16px] font-semibold text-foreground sm:pt-6">
                {t.items} <span className="font-normal text-muted-foreground">({order.items.reduce((a, i) => a + i.quantity, 0)})</span>
              </h2>
              <Stagger as="ul" step={80} className="divide-y divide-border">
                {order.items.map((i) => (
                  <li key={i.id} className="group/it flex items-center gap-4 py-4">
                    <ProductMedia className="size-16 shrink-0 rounded-lg sm:size-20 [&>*]:transition-transform [&>*]:duration-500 group-hover/it:[&>*]:scale-[1.06] motion-reduce:[&>*]:transition-none">{i.image}</ProductMedia>
                    <div className="min-w-0 flex-1">
                      <a href={i.href ?? '#'} className="block truncate text-[15px] font-medium text-foreground hover:underline hover:underline-offset-4">
                        {i.name}
                      </a>
                      <p className="text-[13px] text-muted-foreground sm:truncate">
                        {i.variant} · {t.qty} {i.quantity}
                      </p>
                      <button type="button" className="mt-1 inline-flex h-8 items-center gap-1.5 text-[13px] font-medium text-foreground underline decoration-border-strong underline-offset-4 sm:hidden">
                        {t.buyAgain}
                      </button>
                    </div>
                    <p className="text-[14px] font-medium tabular-nums text-foreground">{formatPrice(locale, i.price * i.quantity, currency)}</p>
                    <Button variant="outline" size="sm" iconLeft="refresh" className="h-10 max-sm:hidden">
                      {t.buyAgain}
                    </Button>
                  </li>
                ))}
              </Stagger>
            </Card>
          </div>

          <div className="space-y-6 lg:sticky lg:top-8 lg:self-start">
            <Card className="divide-y divide-border px-5 sm:px-6">
              <Address title={t.deliveryAddress} lines={order.address} icon="map-pin" />
              <Address title={t.billingAddress} lines={order.billing ?? order.address} icon="file" />
              {order.payment && <Address title={t.payment} lines={[order.payment]} icon="credit-card" />}
            </Card>
            <Card className="p-5 sm:p-6">
              <h2 className="mb-4 text-[16px] font-semibold text-foreground">{t.summary}</h2>
              <Totals items={order.items} shipping={order.shipping} currency={currency} t={t} />
            </Card>
          </div>
        </div>
      </div>
    </section>
  );
}

/* ------------------------------------------------------------------ */
/* invoice                                                             */
/* ------------------------------------------------------------------ */

function Invoice(props: OrderProps & { t: T }) {
  const { t, invoice: inv, currency } = props;
  const locale = useLocale();
  const [rows, rowsIn] = useInView<HTMLTableSectionElement>();
  if (!inv) return null;
  const rate = inv.vatRate ?? 19;
  const net = (gross: number) => round(gross / (1 + rate / 100));
  const fp = (n: number) => formatPrice(locale, n, currency);
  const lines = [...inv.items, ...(inv.shipping ? [{ description: t.shippingLine, quantity: 1, price: inv.shipping }] : [])];
  // Rows rise one after another when the table scrolls in
  const rowStyle = (n: number): React.CSSProperties => ({ transitionDelay: rowsIn ? `${n * 70}ms` : '0ms' });
  const rowCls = cn('transition-[opacity,transform] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', rowsIn ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0');
  const gross = round(lines.reduce((a, l) => a + l.price * l.quantity, 0));
  const grossFormat: Intl.NumberFormatOptions = { style: 'currency', currency: currency ?? 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 };
  const netTotal = round(lines.reduce((a, l) => a + net(l.price) * l.quantity, 0));
  const vat = round(gross - netTotal);
  const meta: [string, string][] = [
    [t.invoiceNo, inv.number],
    [t.invoiceDate, inv.date],
    [t.serviceDate, inv.serviceDate],
    [t.orderNo, inv.orderNumber],
    ...(inv.customerNumber ? ([[t.customerNo, inv.customerNumber]] as [string, string][]) : []),
  ];
  return (
    <section className={cn('bg-surface pb-20 pt-8 sm:pt-12 lg:pb-28 print:bg-transparent print:p-0', props.className)}>
      <div className="mx-auto w-full max-w-[52rem] px-4 sm:px-6 print:max-w-none print:px-0">
        <div className="mb-5 flex flex-wrap items-center justify-between gap-3 print:hidden">
          <a href="#" className="group inline-flex h-10 items-center gap-1.5 text-[14px] text-muted-foreground hover:text-foreground">
            <Icon name="arrow-left" size={15} className="transition-transform group-hover:-translate-x-0.5" />
            {t.backToOrders}
          </a>
          <div className="flex gap-2">
            <Button variant="outline" size="sm" iconLeft="download" className="h-10">
              {t.download}
            </Button>
            <Button size="sm" iconLeft="printer" className="h-10" onClick={() => window.print()}>
              <span className="sm:hidden">{t.printShort}</span>
              <span className="max-sm:hidden">{t.print}</span>
            </Button>
          </div>
        </div>

        {/* The sheet settles in like paper laid on a desk */}
        <Reveal effect="scale">
        <article className="rounded-xl border border-border bg-card px-5 py-8 text-foreground shadow-[var(--highlight),var(--shadow-lg)] sm:px-12 sm:py-12 print:rounded-none print:border-0 print:bg-white print:px-0 print:py-0 print:text-black print:shadow-none">
          <header className="flex flex-col gap-8 sm:flex-row sm:justify-between">
            <div>
              <p className="text-[22px] font-semibold tracking-[-0.03em]">{inv.seller.name}</p>
              <p className="mt-1 text-[12px] leading-relaxed text-muted-foreground print:text-neutral-600">{inv.seller.lines.join(' · ')}</p>
            </div>
            <div className="sm:text-right">
              <h1 className="text-[28px] font-semibold tracking-[-0.03em]">{t.invoiceTitle}</h1>
              <p className="text-[14px] tabular-nums text-muted-foreground print:text-neutral-600">{inv.number}</p>
            </div>
          </header>

          <div className="mt-10 grid gap-8 sm:grid-cols-2">
            <div>
              <p className="text-[11px] font-medium uppercase tracking-[0.1em] text-subtle-foreground print:text-neutral-500">{t.billTo}</p>
              <p className="mt-2 text-[14px] leading-relaxed">
                <span className="block font-medium">{inv.buyer.name}</span>
                {inv.buyer.lines.map((l) => (
                  <span key={l} className="block">
                    {l}
                  </span>
                ))}
              </p>
            </div>
            <dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-1.5 text-[13px] sm:justify-self-end">
              {meta.map(([k, v]) => (
                <div key={k} className="contents">
                  <dt className="text-muted-foreground print:text-neutral-600">{k}</dt>
                  <dd className="text-right font-medium tabular-nums">{v}</dd>
                </div>
              ))}
            </dl>
          </div>

          {/* Table on wide screens and paper, stacked rows on phones */}
          <table className="mt-10 w-full border-collapse text-[13px] max-sm:hidden print:table">
            <thead>
              <tr className="border-b-2 border-foreground text-left text-[11px] uppercase tracking-[0.08em] text-muted-foreground print:border-black print:text-neutral-600">
                <th className="w-10 py-2.5 font-medium">{t.pos}</th>
                <th className="py-2.5 font-medium">{t.description}</th>
                <th className="py-2.5 text-right font-medium">{t.qty}</th>
                <th className="py-2.5 text-right font-medium">{t.unitNet}</th>
                <th className="py-2.5 text-right font-medium">{t.vat}</th>
                <th className="py-2.5 text-right font-medium">{t.amountNet}</th>
              </tr>
            </thead>
            <tbody ref={rows}>
              {lines.map((l, n) => (
                <tr key={n} className={cn('border-b border-border align-top print:border-neutral-300', rowCls)} style={rowStyle(n)}>
                  <td className="py-3.5 tabular-nums text-muted-foreground">{n + 1}</td>
                  <td className="py-3.5 pr-4">
                    <span className="font-medium">{l.description}</span>
                    {'detail' in l && l.detail && <span className="block text-[12px] text-muted-foreground">{l.detail}</span>}
                  </td>
                  <td className="py-3.5 text-right tabular-nums">{l.quantity}</td>
                  <td className="py-3.5 text-right tabular-nums">{fp(net(l.price))}</td>
                  <td className="py-3.5 text-right tabular-nums">{formatPercent(locale, rate)}</td>
                  <td className="py-3.5 text-right tabular-nums">{fp(round(net(l.price) * l.quantity))}</td>
                </tr>
              ))}
            </tbody>
          </table>
          <Stagger as="ul" step={70} className="mt-8 divide-y divide-border border-y border-border sm:hidden print:hidden">
            {lines.map((l, n) => (
              <li key={n} className="flex justify-between gap-4 py-3.5 text-[13px]">
                <div>
                  <p className="font-medium">{l.description}</p>
                  <p className="text-muted-foreground tabular-nums">
                    {l.quantity} × {fp(net(l.price))} · {formatPercent(locale, rate)} {t.vat}
                  </p>
                </div>
                <p className="tabular-nums">{fp(round(net(l.price) * l.quantity))}</p>
              </li>
            ))}
          </Stagger>

          <div className="mt-6 flex justify-end">
            <dl className="w-full space-y-2 text-[13px] sm:max-w-[18rem]">
              <div className="flex justify-between">
                <dt className="text-muted-foreground">{t.netTotal}</dt>
                <dd className="tabular-nums">{fp(netTotal)}</dd>
              </div>
              <div className="flex justify-between">
                <dt className="text-muted-foreground">
                  {t.vat} {formatPercent(locale, rate)}
                </dt>
                <dd className="tabular-nums">{fp(vat)}</dd>
              </div>
              <div className="flex items-baseline justify-between border-t-2 border-foreground pt-2.5 print:border-black">
                <dt className="text-[14px] font-semibold">{t.grossTotal}</dt>
                <NumberTicker as="dd" value={gross} format={grossFormat} duration={1400} className="text-[18px] font-semibold" />
              </div>
            </dl>
          </div>

          <p className="mt-10 rounded-lg bg-surface-2 px-4 py-3 text-[13px] print:bg-transparent print:px-0">
            <Icon name="check-circle" size={14} className="mr-1.5 inline text-success print:hidden" />
            {inv.dueNote}
          </p>
          {inv.note && <p className="mt-4 text-pretty text-[13px] text-muted-foreground print:text-neutral-600">{inv.note}</p>}

          <footer className="mt-12 grid gap-6 border-t border-border pt-6 text-[11px] leading-relaxed text-muted-foreground sm:grid-cols-3 print:border-neutral-300 print:text-neutral-600">
            <div>
              <p className="font-medium text-foreground print:text-black">{inv.seller.name}</p>
              {inv.seller.lines.map((l) => (
                <p key={l}>{l}</p>
              ))}
              {inv.seller.register && <p>{inv.seller.register}</p>}
            </div>
            <div>
              <p className="font-medium text-foreground print:text-black">{t.contact}</p>
              {inv.seller.contact.map((l) => (
                <p key={l}>{l}</p>
              ))}
              <p>
                {t.vatId} {inv.seller.vatId}
              </p>
            </div>
            <div>
              <p className="font-medium text-foreground print:text-black">{t.bank}</p>
              <p>{inv.seller.bank}</p>
              <p className="tabular-nums">IBAN {inv.seller.iban}</p>
              <p>BIC {inv.seller.bic}</p>
            </div>
          </footer>
        </article>
        </Reveal>
      </div>
    </section>
  );
}

/* ------------------------------------------------------------------ */
/* returns                                                             */
/* ------------------------------------------------------------------ */

function Choice({ name, value, on, onChange, title, note, icon, right }: { name: string; value: string; on: boolean; onChange: () => void; title: string; note: string; icon: IconName; right?: React.ReactNode }) {
  return (
    <label className={cn('flex cursor-pointer items-start gap-3.5 rounded-xl border p-4 transition-[border-color,box-shadow] has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--ring)]', on ? 'border-primary shadow-[inset_0_0_0_1px_var(--primary)]' : 'border-border-strong hover:border-foreground/40')}>
      <input type="radio" name={name} value={value} checked={on} onChange={onChange} className="sr-only" />
      <span className={cn('grid size-9 shrink-0 place-items-center rounded-lg', on ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground')}>
        <Icon name={icon} size={17} />
      </span>
      <span className="min-w-0 flex-1">
        <span className="flex items-baseline justify-between gap-2">
          <span className="text-[14px] font-medium text-foreground">{title}</span>
          {right}
        </span>
        <span className="mt-0.5 block text-[13px] text-muted-foreground">{note}</span>
      </span>
    </label>
  );
}

function Returns(props: OrderProps & { t: T }) {
  const { t, order, currency, returnReasons = [] } = props;
  const locale = useLocale();
  const items = order?.items ?? [];
  const [sel, setSel] = useState<Record<string, { qty: number; reason: string }>>(items[0] ? { [items[0].id]: { qty: 1, reason: returnReasons[0] ?? '' } } : {});
  const [refund, setRefund] = useState<'original' | 'credit'>('original');
  const [drop, setDrop] = useState<'shop' | 'pickup'>('shop');
  const [tried, setTried] = useState(0);
  const [sent, setSent] = useState(false);
  const fp = (n: number) => formatPrice(locale, n, currency);
  const base = round(items.reduce((a, i) => a + (sel[i.id] ? i.price * sel[i.id].qty : 0), 0));
  const bonus = refund === 'credit' ? round(base * 0.1) : 0;
  const count = Object.values(sel).reduce((a, s) => a + s.qty, 0);
  const reasonId = useId();
  if (!order) return null;
  const toggle = (i: LineItem) => setSel((s) => {
    const next = { ...s };
    if (next[i.id]) delete next[i.id];
    else next[i.id] = { qty: 1, reason: '' };
    return next;
  });
  return (
    <section className={cn('pb-20 pt-10 sm:pt-14 lg:pb-28', props.className)}>
      <div className="kit-container">
        <div className="mb-10 max-w-[40rem]">
          <p className="text-[13px] text-muted-foreground">
            {t.order} {order.number} · {order.date}
          </p>
          <h1 className="mt-2 text-h2 font-semibold text-foreground">{t.returnsTitle}</h1>
          <p className="mt-3 text-pretty text-[15px] text-muted-foreground">{t.returnsIntro}</p>
        </div>
        <form
          noValidate
          onSubmit={(e) => { e.preventDefault(); setTried((n) => n + 1); if (count > 0) setSent(true); }}
          className="grid grid-cols-1 gap-10 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-14"
        >
          <div className="min-w-0 space-y-10">
            <fieldset className="min-w-0">
              <legend className="mb-4 text-[16px] font-semibold text-foreground">{t.items}</legend>
              <ul className="space-y-3">
                {items.map((i) => {
                  const s = sel[i.id];
                  return (
                    <li key={i.id} className={cn('rounded-xl border transition-[border-color,box-shadow]', s ? 'border-primary shadow-[inset_0_0_0_1px_var(--primary)]' : 'border-border-strong')}>
                      <label className="flex cursor-pointer items-center gap-4 p-4 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--ring)] rounded-xl">
                        <input type="checkbox" checked={!!s} onChange={() => toggle(i)} className="peer sr-only" />
                        <span aria-hidden className={cn('grid size-5 shrink-0 place-items-center rounded-[6px] border transition-colors', s ? 'border-primary bg-primary text-primary-foreground' : 'border-border-strong bg-background')}>
                          {s && <Icon name="check" size={13} strokeWidth={2.4} className="animate-[kitshop-pop_0.35s_var(--ease-kit)]" />}
                        </span>
                        <ProductMedia className={cn('size-16 shrink-0 rounded-lg transition-[opacity,filter] duration-300', !s && 'opacity-70 saturate-50')}>{i.image}</ProductMedia>
                        <span className="min-w-0 flex-1">
                          <span className="block text-[14px] font-medium leading-snug text-foreground">{i.name}</span>
                          <span className="mt-0.5 block text-[13px] text-muted-foreground">
                            {i.variant} · {t.qty} {i.quantity}
                          </span>
                        </span>
                        <span className="text-[14px] tabular-nums text-foreground">{fp(i.price)}</span>
                      </label>
                      {s && (
                        <div className="kit-rise grid gap-3 border-t border-border px-4 pb-4 pt-4 [animation-duration:0.4s] sm:grid-cols-[auto_minmax(0,1fr)] sm:items-end">
                          {i.quantity > 1 && (
                            <div>
                              <p className="mb-1.5 text-[13px] font-medium text-foreground">{t.qty}</p>
                              <Quantity value={s.qty} max={i.quantity} onChange={(q) => setSel((x) => ({ ...x, [i.id]: { ...x[i.id], qty: q } }))} />
                            </div>
                          )}
                          <div className={cn(i.quantity <= 1 && 'sm:col-span-2')}>
                            <label htmlFor={`${reasonId}-${i.id}`} className="mb-1.5 block text-[13px] font-medium text-foreground">
                              {t.reason}
                            </label>
                            <div className="relative">
                              <select
                                id={`${reasonId}-${i.id}`}
                                value={s.reason}
                                onChange={(e) => setSel((x) => ({ ...x, [i.id]: { ...x[i.id], reason: e.target.value } }))}
                                className="h-11 w-full appearance-none rounded-lg border border-input bg-background px-3.5 pr-10 text-[14px] text-foreground shadow-[var(--shadow-sm)] outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-[color-mix(in_oklab,var(--primary)_16%,transparent)]"
                              >
                                <option value="" disabled>
                                  {t.chooseReason}
                                </option>
                                {returnReasons.map((r) => (
                                  <option key={r}>{r}</option>
                                ))}
                              </select>
                              <Icon name="chevrons-up-down" size={15} className="pointer-events-none absolute right-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
                            </div>
                          </div>
                        </div>
                      )}
                    </li>
                  );
                })}
              </ul>
            </fieldset>

            <fieldset className="min-w-0">
              <legend className="mb-4 text-[16px] font-semibold text-foreground">{t.refundTo}</legend>
              <div className="grid gap-3 sm:grid-cols-2">
                <Choice name="refund" value="original" on={refund === 'original'} onChange={() => setRefund('original')} title={t.original} note={t.originalNote} icon="credit-card" />
                <Choice name="refund" value="credit" on={refund === 'credit'} onChange={() => setRefund('credit')} title={t.credit} note={t.creditNote} icon="gift" right={<span className="text-[12px] font-semibold text-success">+{formatPercent(locale, 10)}</span>} />
              </div>
            </fieldset>

            <fieldset className="min-w-0">
              <legend className="mb-4 text-[16px] font-semibold text-foreground">{t.dropOff}</legend>
              <div className="grid gap-3 sm:grid-cols-2">
                <Choice name="drop" value="shop" on={drop === 'shop'} onChange={() => setDrop('shop')} title={t.shop} note={t.shopNote} icon="qr-code" />
                <Choice name="drop" value="pickup" on={drop === 'pickup'} onChange={() => setDrop('pickup')} title={t.pickup} note={t.pickupNote} icon="truck" />
              </div>
            </fieldset>
          </div>

          <aside className="lg:sticky lg:top-8 lg:self-start">
            <Card className="p-5 sm:p-6">
              <h2 className="text-[16px] font-semibold text-foreground">{t.refund}</h2>
              <p className="mt-1 text-[13px] text-muted-foreground">
                {count} {count === 1 ? t.itemSelected : t.itemsSelected}
              </p>
              <dl className="mt-5 space-y-2.5 text-[14px]">
                <div className="flex justify-between">
                  <dt className="text-muted-foreground">{t.subtotal}</dt>
                  <dd className="text-foreground">
                    <TweenPrice value={base} currency={currency} />
                  </dd>
                </div>
                {bonus > 0 && (
                  <div className="flex animate-[kitshop-in_0.4s_var(--ease-kit)] justify-between">
                    <dt className="text-muted-foreground">{t.bonus}</dt>
                    <dd className="text-[color-mix(in_oklab,var(--success)_70%,var(--foreground))] dark:text-success">
                      + <TweenPrice value={bonus} currency={currency} />
                    </dd>
                  </div>
                )}
                <div className="flex justify-between">
                  <dt className="text-muted-foreground">{t.label}</dt>
                  <dd className="text-[color-mix(in_oklab,var(--success)_70%,var(--foreground))] dark:text-success">{t.free}</dd>
                </div>
                <div className="flex items-baseline justify-between border-t border-border pt-3">
                  <dt className="font-semibold text-foreground">{t.total}</dt>
                  <dd className="text-[24px] font-semibold tracking-[-0.02em] text-foreground" aria-live="polite">
                    <TweenPrice value={round(base + bonus)} currency={currency} />
                  </dd>
                </div>
              </dl>
              {props.returnBy && (
                <p className="mt-5 flex items-center gap-2 rounded-lg bg-surface-2 px-3.5 py-2.5 text-[13px] text-foreground">
                  <Icon name="calendar" size={15} className="text-muted-foreground" />
                  {t.returnBy} {props.returnBy}
                </p>
              )}
              <ShopMotion />
              {tried > 0 && count === 0 && (
                <p key={tried} role="alert" className="mt-4 flex animate-[kitshop-shake_0.42s_var(--ease-kit)] items-start gap-2 text-[13px] text-destructive">
                  <Icon name="alert-circle" size={15} className="mt-px" />
                  {t.selectFirst}
                </p>
              )}
              <Button type="submit" size="lg" aria-disabled={(sent && count > 0) || undefined} className={cn('mt-5 w-full overflow-hidden', sent && count > 0 && 'bg-success text-background hover:bg-success')}>
                <Swap value={sent && count > 0} className="min-w-0 gap-2">
                  <span className="truncate">{sent && count > 0 ? t.requested : t.request}</span>
                  <Icon name={sent && count > 0 ? 'check' : 'arrow-right'} size={17} className="shrink-0" />
                </Swap>
              </Button>
            </Card>
          </aside>
        </form>
      </div>
    </section>
  );
}

/* ------------------------------------------------------------------ */
/* subscription                                                        */
/* ------------------------------------------------------------------ */

function Subscription(props: OrderProps & { t: T }) {
  const { t, subscription: s, currency } = props;
  const locale = useLocale();
  const [every, setEvery] = useState(s?.every ?? 2);
  const [qty, setQty] = useState(s?.quantity ?? 1);
  const [paused, setPaused] = useState(false);
  const [skips, setSkips] = useState<Record<string, boolean>>({});
  const upcoming = useMemo(() => (s ? Array.from({ length: 4 }, (_, n) => addWeeks(s.nextDate, n * every)) : []), [s, every]);
  if (!s) return null;
  const fp = (n: number) => formatPrice(locale, n, currency);
  const next = upcoming.find((d) => !skips[d]) ?? upcoming[0];
  const perDelivery = round(s.price * qty);
  return (
    <section className={cn('pb-20 pt-10 sm:pt-14 lg:pb-28', props.className)}>
      <div className="kit-container">
        <div className="mb-8 flex flex-wrap items-end justify-between gap-4">
          <div>
            <p className="text-[13px] text-muted-foreground">{t.subTitle}</p>
            <h1 className="mt-1 text-h2 font-semibold text-foreground">{s.name}</h1>
          </div>
          <span className={cn('inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-[12px] font-medium', paused ? 'bg-muted text-muted-foreground' : 'bg-[color-mix(in_oklch,var(--success)_14%,transparent)] text-[color-mix(in_oklch,var(--success)_75%,var(--foreground))]')}>
            <span className={cn('size-1.5 rounded-full', paused ? 'bg-muted-foreground' : 'animate-kit-pulse bg-success')} />
            {paused ? t.paused : t.active} · {t.since} {s.since}
          </span>
        </div>

        <div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-10">
          <div className="min-w-0 space-y-8">
            <Card className="kit-glow overflow-hidden">
              <div className="grid sm:grid-cols-[14rem_minmax(0,1fr)]">
                <ProductMedia className="aspect-[4/3] rounded-none sm:aspect-auto sm:h-full">{s.image}</ProductMedia>
                <div className="p-5 sm:p-7">
                  <p className="text-[13px] text-muted-foreground">{t.nextDelivery}</p>
                  <ShopMotion />
                  {/* A new date (other rhythm, skipped delivery) slides in */}
                  <p key={next} className={cn('mt-1 animate-[kitshop-in_0.45s_var(--ease-kit)] text-[26px] font-semibold tracking-[-0.025em] text-foreground transition-colors duration-300 sm:text-[30px]', paused && 'text-muted-foreground line-through decoration-2')}>{formatDate(locale, next, true)}</p>
                  {paused && <p className="mt-1 text-[13px] text-muted-foreground">{t.pausedNote}</p>}
                  <p className="mt-3 text-[14px] text-muted-foreground">
                    {qty} × {s.variant} · <TweenPrice value={perDelivery} currency={currency} className="font-medium text-foreground" /> {t.perDelivery}
                    {s.saving ? (
                      <span className="ml-2 inline-flex h-6 items-center rounded-full bg-[color-mix(in_oklab,var(--primary)_12%,transparent)] px-2 text-[11px] font-semibold text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">
                        {t.save} {formatPercent(locale, s.saving)}
                      </span>
                    ) : null}
                  </p>
                  <div className="mt-6 grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
                    <Button size="sm" className="col-span-2 h-11 sm:h-10" iconLeft="bolt" disabled={paused}>
                      {t.deliverNow}
                    </Button>
                    <Button size="sm" variant="outline" className="h-11 sm:h-10" iconLeft="skip-forward" disabled={paused} onClick={() => setSkips((x) => ({ ...x, [next]: true }))}>
                      {t.skip}
                    </Button>
                    <Button size="sm" variant="outline" className="h-11 sm:h-10" iconLeft={paused ? 'play' : 'pause'} onClick={() => setPaused(!paused)}>
                      {paused ? t.resume : t.pause}
                    </Button>
                  </div>
                </div>
              </div>
            </Card>

            <div className="grid gap-6 sm:grid-cols-2">
              <fieldset className="min-w-0">
                <legend className="mb-3 text-[14px] font-semibold text-foreground">{t.frequency}</legend>
                <div className="relative isolate grid grid-cols-3 gap-1 rounded-xl bg-muted p-1" role="radiogroup">
                  {/* The light pill glides to the picked rhythm: equal columns, so one third plus the gap per step */}
                  <span
                    aria-hidden
                    className="absolute bottom-1 left-1 top-1 -z-10 w-[calc((100%-1rem)/3)] rounded-lg bg-background shadow-[var(--shadow-sm)] transition-transform duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none"
                    style={{ transform: `translateX(calc(${Math.max(0, s.everyOptions.indexOf(every))} * (100% + 0.25rem)))` }}
                  />
                  {s.everyOptions.map((w) => (
                    <label key={w} className={cn('grid h-11 cursor-pointer place-items-center rounded-lg text-[13px] font-medium transition-colors duration-300 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-[var(--ring)]', every === w ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>
                      <input type="radio" name="every" checked={every === w} onChange={() => { setEvery(w); setSkips({}); }} className="sr-only" />
                      {t.every} {w} {t.weeks}
                    </label>
                  ))}
                </div>
              </fieldset>
              <div>
                <p className="mb-3 text-[14px] font-semibold text-foreground">{t.quantity}</p>
                <div className="flex items-center gap-4">
                  <Quantity value={qty} onChange={setQty} max={6} />
                  <p className="text-[13px] tabular-nums text-muted-foreground">
                    {formatNumber(locale, qty * 250)} g · <TweenPrice value={perDelivery} currency={currency} />
                  </p>
                </div>
              </div>
            </div>

            <Reveal>
              <h2 className="mb-4 text-[16px] font-semibold text-foreground">{t.upcoming}</h2>
              {/* A new rhythm deals out the new dates one by one */}
              <ol key={every} className={cn('overflow-hidden rounded-xl border border-border transition-opacity duration-300', paused && 'opacity-50')}>
                {upcoming.map((d, n) => {
                  const skipped = !!skips[d];
                  return (
                    <li key={d} className={cn('flex min-h-16 items-center gap-4 px-4 py-3 transition-colors duration-300', n > 0 && 'border-t border-border', d === next && !paused && 'bg-surface')}>
                      <span
                        className={cn('grid size-10 shrink-0 animate-[kitshop-pop_0.45s_var(--ease-kit)_both] place-items-center rounded-lg text-center leading-none transition-colors duration-300', d === next && !paused ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground')}
                        style={{ animationDelay: `${n * 70}ms` }}
                      >
                        <span className="text-[15px] font-semibold tabular-nums">{new Intl.DateTimeFormat(locale === 'de' ? 'de-DE' : 'en-US', { day: 'numeric', timeZone: 'UTC' }).format(new Date(`${d}T12:00:00Z`))}</span>
                      </span>
                      <div className="min-w-0 flex-1">
                        <p className={cn('text-[14px] font-medium', skipped ? 'text-muted-foreground line-through' : 'text-foreground')}>{formatDate(locale, d)}</p>
                        <p className="text-[12px] text-muted-foreground">{skipped ? t.skipped : `${qty} × ${s.variant} · ${fp(perDelivery)}`}</p>
                      </div>
                      <button
                        type="button"
                        disabled={paused}
                        onClick={() => setSkips((x) => ({ ...x, [d]: !x[d] }))}
                        className="h-9 shrink-0 rounded-md px-2.5 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none"
                      >
                        {skipped ? t.unskip : t.skip}
                      </button>
                    </li>
                  );
                })}
              </ol>
            </Reveal>
          </div>

          <div className="space-y-6 lg:sticky lg:top-8 lg:self-start">
            <Card className="divide-y divide-border px-5 sm:px-6">
              <Address title={t.deliveryAddress} lines={s.address} icon="map-pin" />
              <Address title={t.payment} lines={[s.payment]} icon="credit-card" />
            </Card>
            <Card className="px-5 pb-2 pt-5 sm:px-6">
              <h2 className="text-[14px] font-semibold text-foreground">{t.pastDeliveries}</h2>
              <ul className="mt-2 divide-y divide-border">
                {s.history.map((h) => (
                  <li key={h.number} className="flex items-center justify-between gap-3 py-3 text-[13px]">
                    <div>
                      <p className="text-foreground">{h.date}</p>
                      <p className="tabular-nums text-muted-foreground">{h.number}</p>
                    </div>
                    <div className="flex items-center gap-2">
                      <span className="tabular-nums text-foreground">{fp(h.total)}</span>
                      <a href="#" aria-label={`${t.invoice} ${h.number}`} className="grid size-9 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground">
                        <Icon name="download" size={15} />
                      </a>
                    </div>
                  </li>
                ))}
              </ul>
            </Card>
            <Button variant="destructive" className="h-11 w-full" iconLeft="x-circle">
              {t.cancel}
            </Button>
          </div>
        </div>
      </div>
    </section>
  );
}

/**
 * After the purchase: order history, an order with its tracking, a printable
 * invoice, a return flow and a subscription to manage.
 */
export function Order(props: OrderProps) {
  const t = useTexts(TEXTS, props.labels);
  const { variant = 'order-history' } = props;
  if (variant === 'order-detail') return <Detail {...props} t={t} />;
  if (variant === 'invoice') return <Invoice {...props} t={t} />;
  if (variant === 'returns') return <Returns {...props} t={t} />;
  if (variant === 'subscription') return <Subscription {...props} t={t} />;
  return <History {...props} t={t} />;
}

Häufige Fragen

Wie installiere ich Bestellungen?

Mit aiw add @ai-website/order. Der Befehl legt die Datei components/ui/order.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal und product-image. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Bestellungen?

5 Varianten, Bestellübersicht, Bestelldetail, Rechnung, Rücksendung und Abo. Du wählst sie mit der Prop variant, zum Beispiel variant="order-detail".

Kann ich die Texte von Bestellungen übersetzen?

Ja. Bestellungen bringt 104 eigene Texte auf Deutsch und Englisch mit, etwa „Bestellungen“, „Verfolgen, zurückschicken oder nachbestellen. Rechnungen liegen bereit, sobald eine Bestellung verschickt ist.“ und „Alle“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Bestellungen einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Bestellungen mit aiw_components und installieren sie mit aiw_add @ai-website/order. Nenn im Prompt die Variante, etwa variant="order-history", dann schreibt der Assistent deine Inhalte direkt in die Props.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert