/* Hearth — the parts every screen shares.
   A Card is the only surface in the product: one hairline, --radius-2xl, one
   elevation step, never nested. Nothing here holds data or assigns a status
   tone: StatusBadge does the tone, metrics.js does the arithmetic, enums.js
   decides which transitions exist. Reported in the README as local additions to
   the design system's inventory. */
(() => {
  const {
    Button, Input, Select, Checkbox, Switch, Textarea, Field, FieldLabel, FieldDescription, FieldError,
    Dialog, DialogPopup, DialogHeader, DialogTitle, DialogDescription, DialogPanel, DialogFooter,
    AlertDialog, AlertDialogPopup, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter,
    RadioGroup, MenuItem, MenuGroupLabel, Badge, Avatar, Slider,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* Money is pounds in this product, so the formatter lives here rather than in
     lib/ — currency is a product decision, and lib/ stays domain-free. */
  const poundsFormatter = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", maximumFractionDigits: 0 });
  const poundsExactFormatter = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", minimumFractionDigits: 2 });
  const formatPounds = (minor) => poundsFormatter.format((minor || 0) / 100);
  const formatPoundsExact = (minor) => poundsExactFormatter.format((minor || 0) / 100);
  const formatKwh = (kwh) => (Math.round(kwh * 10) / 10).toFixed(1);
  const formatTemp = (c) => (c == null ? "—" : (Math.round(c * 10) / 10).toFixed(1));
  const formatSince = (ts) => (ts ? new Date(ts).toLocaleDateString("en-GB", { month: "long", year: "numeric" }) : "—");
  const formatWatts = (w) => (w >= 1000 ? (w / 1000).toFixed(2) + " kW" : Math.round(w) + " W");

  /* ── Structure ─────────────────────────────────────────────────────────── */
  function Card({ title, note, actions, flush, tint, fill, children }) {
    return (
      <section className={"hh-card" + (flush ? " hh-card--flush" : "") + (tint ? " hh-card--tint" : "") + (fill ? " hh-card--fill" : "")}>
        {title || actions ? (
          <div className="hh-card__head">
            <div style={{ minWidth: 0 }}>
              <h2 className="hh-card__title">{title}</h2>
              {note ? <p className="hh-card__note">{note}</p> : null}
            </div>
            {actions ? <div className="hh-actions">{actions}</div> : null}
          </div>
        ) : null}
        {children}
      </section>
    );
  }

  /* A card in a band of its own, for a screen that wants one full-width region. */
  const Band = ({ children }) => <div className="hh-band">{children}</div>;

  /* Equal-weight metric tiles, so they may auto-fit (§5.1). Every one carries a
     qualifier that decomposes its own figure's set. */
  function Figures({ items }) {
    return (
      <div className="hh-figs">
        {items.map((item) => (
          <div key={item.label}>
            <span className="hh-fig__label">{item.label}</span>
            <span className="hh-fig">{item.value}{item.unit ? <span className="hh-fig__unit"> {item.unit}</span> : null}</span>
            <span className="hh-fig__qualifier">{item.qualifier}</span>
          </div>
        ))}
      </div>
    );
  }

  function Pairs({ items }) {
    return (
      <div className="hh-pairs">
        {items.map((item) => (
          <div className="hh-pair" key={item.label}>
            <span className="hh-pair__label">{item.label}</span>
            <span className="hh-pair__value">{item.value}</span>
          </div>
        ))}
      </div>
    );
  }

  function Rows({ children }) { return <div className="hh-rows">{children}</div>; }

  function Row({ title, note, meta, value, recessed, children }) {
    return (
      <div className={"hh-row" + (recessed ? " hh-row--recessed" : "")}>
        <div className="hh-row__text">
          <span className="hh-row__title">{title}</span>
          {note ? <span className="hh-row__note">{note}</span> : null}
          {meta ? <span className="hh-row__note">{meta}</span> : null}
        </div>
        {value != null ? <span className="hh-row__value">{value}</span> : null}
        {children}
      </div>
    );
  }

  /* ── The two signature geometries ──────────────────────────────────────── */
  function Bar({ percent, tone, label }) {
    return (
      <span className="hh-bar" role="img" aria-label={label}>
        <span className={"hh-bar__fill" + (tone ? " hh-bar__fill--" + tone : "")} style={{ width: Math.max(1.5, Math.min(100, percent)) + "%" }} />
      </span>
    );
  }

  /* A day series built from the same proportion: the dark part is what was drawn
     inside a cheap window, the soft part the rest. */
  function DayBars({ days, valueOf, softOf, tickOf, labelOf: labelFor, selected, onSelect }) {
    const max = Math.max.apply(null, days.map(valueOf));
    return (
      <div className="hh-bars">
        {days.map((day, index) => {
          const value = valueOf(day);
          const soft = softOf ? softOf(day) : 0;
          const height = max ? (value / max) * 100 : 0;
          const softPart = value ? (soft / value) * 100 : 0;
          const on = selected === index;
          return (
            <button type="button" key={day.at} className="hh-bars__col" data-on={on ? "true" : "false"}
              aria-label={labelFor(day)} aria-pressed={on} onClick={() => onSelect && onSelect(on ? null : index)}>
              <span className="hh-bars__stack" style={{ height: "80px" }}>
                <span className="hh-bars__part" style={{ height: Math.max(2, height - (height * softPart) / 100) + "%" }} />
                {soft ? <span className="hh-bars__part hh-bars__part--soft" style={{ height: (height * softPart) / 100 + "%" }} /> : null}
              </span>
              <span className="hh-bars__tick">{tickOf(day, index)}</span>
            </button>
          );
        })}
      </div>
    );
  }

  /* 24 cells, one per hour. levelOf returns 0–3, or a tone name for a window. */
  function DayStrip({ levelOf, toneOf, now, label, axis = true }) {
    const nowHour = new Date(now).getHours();
    return (
      <div>
        <div className="hh-strip" role="img" aria-label={label}>
          {Array.from({ length: 24 }, (_, hour) => (
            <span key={hour} className="hh-strip__cell"
              data-level={levelOf ? levelOf(hour) : undefined}
              data-tone={toneOf ? toneOf(hour) : undefined}
              data-now={hour === nowHour ? "true" : undefined}
              title={String(hour).padStart(2, "0") + ":00"} />
          ))}
        </div>
        {axis ? (
          <div className="hh-strip__axis" aria-hidden="true">
            <span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span>
          </div>
        ) : null}
      </div>
    );
  }

  function Legend({ items }) {
    return (
      <div className="hh-legend">
        {items.map((item) => (
          <span className="hh-legend__key" key={item.label}>
            <span className={"hh-legend__swatch" + (item.kind ? " hh-legend__swatch--" + item.kind : "")} />
            {item.label}
          </span>
        ))}
      </div>
    );
  }

  /* ── People and time ───────────────────────────────────────────────────── */
  function PersonCell({ name, meta, avatar, size = 30, onOpen, badge }) {
    return (
      <span className="hh-person">
        <Avatar size={size} src={avatar || undefined} alt={avatar ? "Photograph of " + name : ""} fallback={initialsOf(name)} />
        <span className="hh-person__text">
          <span className="hh-person__name">{onOpen ? <button type="button" className="hh-linkbtn" onClick={onOpen}>{name}</button> : name}</span>
          {meta ? <span className="hh-person__meta">{meta}</span> : null}
        </span>
        {badge || null}
      </span>
    );
  }

  /* One format rule per column (§8.3). `problem` is a property of the column,
     declared by the caller: whether a time in the past is a problem. */
  function Clock({ at, now, problem, dangerous }) {
    if (!at) return <span className="hh-muted">Never</span>;
    const past = at < now;
    const cls = past && problem ? (dangerous ? "hh-danger" : "hh-warn") : "";
    return <span className={cls} title={formatAbsolute(at)}>{formatWhen(at, now)}</span>;
  }

  function Battery({ level }) {
    if (level == null) return <span className="hh-muted">Mains</span>;
    const tone = level <= 0 ? "danger" : level <= HOUSE_POLICY.lowBatteryPercent ? "warn" : null;
    return (
      <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-2)", minWidth: "5.5rem" }}>
        <span style={{ width: 34, display: "inline-block" }}><Bar percent={level} tone={tone} label={level + "% battery"} /></span>
        <span className={tone === "danger" ? "hh-danger" : tone === "warn" ? "hh-warn" : "hh-muted"} style={{ fontSize: "var(--text-xs)", fontVariantNumeric: "tabular-nums" }}>{level}%</span>
      </span>
    );
  }

  /* A verdict headline. The tone rides on a dotted badge above the sentence, not
     on the sentence's colour — so the strongest thing on the card is what it
     says, and the colour is one 6px dot doing one job. */
  function VerdictLine({ verdict, size }) {
    const spec = VERDICT_TONES.find((t) => t.id === verdict.tone) || VERDICT_TONES[2];
    return (
      <>
        <span className="hh-vdot" data-tone={verdict.tone}>{spec.label}</span>
        <p className="hh-card__title" style={{ marginTop: "var(--space-2)", fontSize: size || "var(--text-2xl)" }}>{verdict.line}</p>
      </>
    );
  }

  /* Signal is three bars off ONE derived band, so a bar, a word and a count can
     never disagree about what −68 dBm means. */
  function Signal({ dbm, compact }) {
    const band = signalBand(dbm);
    if (!band) return <span className="hh-muted">No answer</span>;
    const spec = SIGNAL_BANDS.find((b) => b.value === band);
    return (
      <span className="hh-signal" data-band={band} title={dbm + " dBm"}>
        <span className="hh-signal__bars" role="img" aria-label={spec.label + " signal, " + dbm + " dBm"}><i /><i /><i /></span>
        {compact ? null : <span className="hh-signal__text">{spec.label}</span>}
      </span>
    );
  }

  /* The live view. A template has no picture to show, so the plate names what the
     camera is pointed at rather than pretending — and it carries the overlay a
     real view would: whether it is live, the clock, and which camera this is. */
  function CameraView({ camera, now, tall, onClick, selected }) {
    const state = camera.blind ? "blind" : camera.privacy ? "shuttered" : "live";
    const label = state === "live" ? "Live" : state === "shuttered" ? "Shutter closed" : statusValue(DEVICE_STATUS, camera.device ? camera.device.status : "offline").label;
    const inner = (
      <>
        <span className="hh-view__top">
          <span className="hh-view__chip" data-live={state === "live" ? "true" : "false"}>{label}</span>
          {tall ? <span className="hh-view__clock">{formatAbsolute(now)}</span> : null}
        </span>
        <span className="hh-view__body"><Icon name={state === "live" ? "video" : "video-off"} size={tall ? 22 : 15} /><span>{camera.view}</span></span>
        <span className="hh-view__foot"><span className="hh-view__name">{camera.name}</span>{tall ? <span className="hh-view__res">{camera.resolution} · {roomName(camera.device ? camera.device.room : null)}</span> : null}</span>
      </>
    );
    if (!onClick) return <div className="hh-view" data-state={state} data-tall={tall ? "true" : "false"}>{inner}</div>;
    return (
      <button type="button" className="hh-view hh-view--pick" data-state={state} data-tall={tall ? "true" : "false"}
        aria-pressed={selected ? "true" : "false"} onClick={onClick}>{inner}</button>
    );
  }

  /* ── Actions ───────────────────────────────────────────────────────────── */
  function Transitions({ transitions, onRun, size = "sm" }) {
    if (!transitions.length) return null;
    return (
      <span className="hh-actions__row">
        {transitions.map((t) => {
          const button = (
            <Button key={t.id} size={size} disabled={!!t.disabled}
              variant={t.destructive ? "destructive-subtle" : t.primary ? "default" : "outline"}
              onClick={() => onRun(t)}>{t.label}</Button>
          );
          return t.disabled && t.disabledReason
            ? <FloatingTip key={t.id} label={t.disabledReason} side="bottom">{button}</FloatingTip>
            : button;
        })}
      </span>
    );
  }

  /* A row carries the transition its state asks for — the primary one — as a
     button. Everything else goes in the row menu: a list is not where an
     irreversible action belongs. */
  function RowActions({ transitions, onRun, label, extra }) {
    const primary = transitions.filter((t) => t.primary);
    const rest = transitions.filter((t) => !t.primary).concat(extra || []);
    if (!primary.length && !rest.length) return <span className="hh-muted">—</span>;
    return (
      <span className="hh-actions__row">
        {primary.length ? <Transitions transitions={primary} onRun={onRun} /> : null}
        {rest.length ? (
          <Floating align="end" width="15rem" label={label}
            trigger={<Button variant="ghost" size="icon-sm" aria-label={label}><Icon name="ellipsis" /></Button>}>
            {(close) => rest.map((t) => (
              <MenuItem key={t.id} disabled={!!t.disabled} variant={t.destructive ? "destructive" : "default"} onClick={() => { close(); onRun(t); }}>{t.label}</MenuItem>
            ))}
          </Floating>
        ) : null}
      </span>
    );
  }

  /* A device's control is whatever that kind of device actually has: a switch
     for anything that turns on, a lock button for a lock, nothing at all for a
     meter or a sensor. Switch means it applies immediately (§3). */
  function DeviceControl({ device, viewer, onToggle, onRun }) {
    if (DEVICE_FLAGS.recessed(device)) return <span className="hh-footnote">{device.status === "updating" ? "Updating — nothing to do" : "Out of the house"}</span>;
    if (DEVICE_FLAGS.needsYou(device)) {
      const transitions = deviceTransitions(device, viewer).filter((t) => t.primary);
      return <Transitions transitions={transitions} onRun={(t) => onRun(device, t)} />;
    }
    if (device.kind === "meter" || device.kind === "sensor") return <span className="hh-footnote">Reporting</span>;
    if (device.kind === "lock") {
      const may = ROLE_CAN_UNLOCK[viewer.role];
      const button = (
        <Button size="sm" variant={device.on ? "outline" : "default"} disabled={!may}
          onClick={() => onRun(device, { id: device.on ? "unlock" : "lock", label: device.on ? "Unlock" : "Lock" })}>
          {device.on ? "Unlock" : "Lock"}
        </Button>
      );
      return may ? button : <FloatingTip label={"Locks are an adult's to move. You are signed in as " + labelOf(HOUSEHOLD_ROLES, viewer.role).toLowerCase() + "."} side="bottom">{button}</FloatingTip>;
    }
    return <Switch checked={!!device.on} aria-label={(device.on ? "Turn off " : "Turn on ") + device.name} onCheckedChange={() => onToggle(device)} />;
  }

  /* ── List furniture: same order on every list screen (§5) ───────────────── */
  function FilterBar({ query, onQuery, placeholder, selects, onClear, dirty }) {
    return (
      <div className="hh-filters">
        <div className="hh-filters__text">
          <Input size="sm" value={query} placeholder={placeholder} aria-label={placeholder} startAddon={<Icon name="search" />} onChange={(e) => onQuery(e.target.value)} />
        </div>
        {(selects || []).map((s) => (
          <span className="hh-filters__select" key={s.id}>
            <Select size="sm" items={s.items} value={s.value} aria-label={s.label} onValueChange={s.onChange} />
          </span>
        ))}
        {dirty ? <Button variant="ghost" size="sm" onClick={onClear}>Clear filters</Button> : null}
      </div>
    );
  }


  function ExportMenu({ scope, onExport, size = "sm" }) {
    return (
      <Floating align="end" width="13rem" label="Export"
        trigger={<Button variant="outline" size={size}><Icon name="download" />Export<Icon name="chevron-down" /></Button>}>
        {(close) => (
          <>
            <MenuGroupLabel>{scope}</MenuGroupLabel>
            {["CSV", "PDF"].map((format) => (
              <MenuItem key={format} onClick={() => { close(); onExport(format); }}>{format}</MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  const TableWrap = ({ children }) => <div className="hh-tablewrap">{children}</div>;

  function TableFoot({ shown, total, noun, children }) {
    return (
      <div className="hh-tablefoot">
        <span className="hh-footnote">Showing {shown} of {total} {noun}</span>
        {children ? <span className="hh-actions__row">{children}</span> : null}
      </div>
    );
  }

  function NoResults({ noun, onClear }) {
    return (
      <div style={{ padding: "var(--space-6) var(--card-pad)" }}>
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
            <EmptyTitle>No {noun} match those filters</EmptyTitle>
            <EmptyDescription>Nothing in this list matches every filter at once. Clearing them brings the whole list back.</EmptyDescription>
          </EmptyHeader>
          <EmptyContent><Button onClick={onClear}>Clear filters</Button></EmptyContent>
        </Empty>
      </div>
    );
  }

  /* ── Dialogs ───────────────────────────────────────────────────────────── */
  function ConfirmDialog({ request, onCancel, onConfirm }) {
    return (
      <AlertDialog open={!!request} onOpenChange={(open) => { if (!open) onCancel(); }}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>{request ? request.title : ""}</AlertDialogTitle>
            <AlertDialogDescription>{request ? request.description : ""}</AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <Button variant="outline" onClick={onCancel}>Cancel</Button>
            <Button variant="destructive" onClick={onConfirm}>{request ? request.confirmLabel : ""}</Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    );
  }

  /* A staged dialog — its own commit model, so an instant screen may open one. */
  /* A staged dialog — its own commit model, so an instant screen may open one.
     A request may carry one set of fields, or `steps`: a named stage each with
     its own fields and its own validation, which is what pairing needs. Nothing
     is committed until the last stage is submitted. */
  function FormDialog({ request, onCancel, onSubmit }) {
    const [values, setValues] = React.useState({});
    const [touched, setTouched] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    const [stage, setStage] = React.useState(0);
    React.useEffect(() => { setValues(request ? request.initial || {} : {}); setTouched(false); setSubmitted(false); setStage(0); }, [request]);
    if (!request) return null;
    const stages = request.steps || [{ title: request.title, description: request.description, fields: request.fields, validate: request.validate }];
    const current = stages[Math.min(stage, stages.length - 1)];
    const last = stage >= stages.length - 1;
    const errors = current.validate ? current.validate(values) : {};
    const valid = Object.keys(errors).length === 0;
    const set = (id, value) => { setTouched(true); setValues((v) => Object.assign({}, v, { [id]: value })); };
    const advance = () => { setSubmitted(true); if (!valid) return; if (last) return onSubmit(values); setStage((n) => n + 1); setSubmitted(false); };
    return (
      <Dialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>{request.title}</DialogTitle>
            <DialogDescription>{current.description}</DialogDescription>
          </DialogHeader>
          <DialogPanel>
            {stages.length > 1 ? (
              <p className="hh-footnote" style={{ marginBottom: "var(--space-4)" }}>Step {stage + 1} of {stages.length} · {current.title}</p>
            ) : null}
            <div className="hh-fields hh-fields--one">
              {current.fields.filter((f) => !f.when || f.when(values)).map((field) => (
                <Field key={field.id}>
                  <FieldLabel htmlFor={"hh-f-" + field.id}>{field.label}</FieldLabel>
                  {field.kind === "radio" ? (
                    <RadioGroup name={"hh-f-" + field.id} items={field.options} value={values[field.id] || ""} onValueChange={(v) => set(field.id, v)} />
                  ) : field.kind === "select" ? (
                    <Select items={field.options} value={values[field.id] || ""} aria-label={field.label} onValueChange={(v) => set(field.id, v)} />
                  ) : field.kind === "text" ? (
                    <Textarea id={"hh-f-" + field.id} rows={3} placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  ) : field.kind === "checkbox" ? (
                    <span className="hh-person">
                      <Checkbox id={"hh-f-" + field.id} checked={!!values[field.id]} onCheckedChange={(v) => set(field.id, v)} />
                      <span className="hh-pair__value">{field.checkboxLabel}</span>
                    </span>
                  ) : field.kind === "note" ? (
                    <p className="hh-card__note" style={{ margin: 0 }}>{typeof field.body === "function" ? field.body(values) : field.body}</p>
                  ) : (
                    <Input id={"hh-f-" + field.id} placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  )}
                  {submitted && errors[field.id] ? <FieldError>{errors[field.id]}</FieldError> : field.hint ? <FieldDescription>{field.hint}</FieldDescription> : null}
                </Field>
              ))}
            </div>
          </DialogPanel>
          <DialogFooter>
            {stage > 0 ? <Button variant="outline" onClick={() => { setStage((n) => n - 1); setSubmitted(false); }}>Back</Button> : touched ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
            <Button disabled={!touched && stages.length === 1} onClick={advance}>{last ? request.submitLabel : current.nextLabel || "Next"}</Button>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    );
  }

  function FormBar({ dirty, note, onCancel, onSave, label }) {
    return (
      <div className="hh-formbar">
        <span className="hh-formbar__note">{note}</span>
        {dirty ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
        <Button disabled={!dirty} onClick={onSave}>{label}</Button>
      </div>
    );
  }

  function RouteNotBuilt({ path, onNavigate }) {
    return (
      <div style={{ padding: "var(--space-8) var(--gutter-page)" }}>
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="signpost" size={18} /></EmptyMedia>
            <EmptyTitle>Nothing is registered at {path}</EmptyTitle>
            <EmptyDescription>Hearth registers ten destinations and the screens behind them. The route table decides what exists, so this address has no screen behind it.</EmptyDescription>
          </EmptyHeader>
          <EmptyContent><Button onClick={() => onNavigate("/")}>Go to Today</Button></EmptyContent>
        </Empty>
      </div>
    );
  }

  /* Sorting: one hook, one header control, one comparator. */
  function useSort(key, dir) {
    const [sort, setSort] = React.useState({ key: key, dir: dir || "desc" });
    const onSort = (next) => setSort((s) => (s.key === next ? { key: next, dir: s.dir === "asc" ? "desc" : "asc" } : { key: next, dir: "desc" }));
    return { sort: sort, onSort: onSort };
  }

  function SortHead({ label, sortKey, sort, onSort }) {
    const on = sort.key === sortKey;
    return (
      <button type="button" className="hh-sort" data-on={on ? "true" : "false"}
        aria-label={"Sort by " + label + (on && sort.dir === "asc" ? ", highest first" : ", lowest first")}
        onClick={() => onSort(sortKey)}>
        {label}
        <Icon name={on ? (sort.dir === "asc" ? "arrow-up" : "arrow-down") : "chevrons-up-down"} size={12} />
      </button>
    );
  }

  function sortRows(rows, sort, accessors) {
    const get = accessors[sort.key];
    if (!get) return rows;
    return rows.slice().sort((a, b) => {
      const av = get(a), bv = get(b);
      if (typeof av === "string" || typeof bv === "string") {
        const cmp = String(av == null ? "" : av).localeCompare(String(bv == null ? "" : bv));
        return sort.dir === "asc" ? cmp : -cmp;
      }
      const an = av == null ? -Infinity : av, bn = bv == null ? -Infinity : bv;
      return sort.dir === "asc" ? an - bn : bn - an;
    });
  }

  Object.assign(window, {
    formatPounds, formatPoundsExact, formatKwh, formatTemp, formatWatts, formatSince,
    Card, Band, Figures, Pairs, Rows, Row, Bar, DayBars, DayStrip, Legend,
    PersonCell, Clock, Battery, Signal, CameraView, VerdictLine, Transitions, RowActions, DeviceControl,
    FilterBar, ExportMenu, TableWrap, TableFoot, NoResults,
    ConfirmDialog, FormDialog, FormBar, RouteNotBuilt, useSort, SortHead, sortRows,
  });
})();
