mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-23 21:34:00 +00:00
Show observers as a full-width list on mobile (#122)
This commit is contained in:
1 parent
e6e680192a
commit
441dcdee6c
2 files changed
+13
-13
No files matched your search
@@ -80,7 +80,7 @@ export function ObserverPage({ wsManager }: { wsManager: WsManager }) {
|
||||
</span>}
|
||||
/>
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1">
|
||||
<div className="flex min-h-0 w-full shrink-0 flex-col p-4 md:w-[260px] md:pr-0">
|
||||
<div className="flex min-h-0 w-full shrink-0 flex-col md:w-[260px] md:p-4 md:pr-0">
|
||||
<ObserverSidebar observers={observers} filtered={Boolean(search || status || type || broker || scope)} isPending={directory.isPending} isError={directory.isError}
|
||||
onRetry={() => void directory.refetch()} range={range} selectedId={id} onSelect={select} />
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { getTopObservers } from "../../api/client";
|
||||
import { useRegion } from "../../hooks/useRegion";
|
||||
import { formatCount } from "../../lib/formatters";
|
||||
import { Card } from "../stats/cards";
|
||||
import { Segmented } from "../stats/Segmented";
|
||||
import { RANGE_MS, type StatsRange } from "../stats/types";
|
||||
import { observerName as nameOf } from "./observer-filter";
|
||||
@@ -36,17 +35,18 @@ export function ObserverSidebar({ observers, filtered, isPending, isError, onRet
|
||||
}, [observers, sort, counts]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("tabs.Observers")}
|
||||
right={<Segmented size="sm" ariaLabel={t("observerPage.sort")} value={sort} onChange={(v) => setSort(v as Sort)}
|
||||
options={[{ value: "activity", label: t("observerPage.sortActivity") }, { value: "name", label: t("observerPage.sortName") }]} />}
|
||||
className="flex min-h-0 w-full flex-col"
|
||||
>
|
||||
// a card at md+, an edge-to-edge list like the Nodes tab below it
|
||||
<div className="flex min-h-0 w-full flex-col bg-bg-base md:rounded-lg md:border md:border-border md:bg-bg-surface md:p-3.5">
|
||||
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-border-subtle px-4 py-2 md:mb-2.5 md:border-0 md:p-0">
|
||||
<div className="font-mono text-[11px] font-semibold uppercase tracking-wider text-text-normal">{t("tabs.Observers")}</div>
|
||||
<Segmented size="sm" ariaLabel={t("observerPage.sort")} value={sort} onChange={(v) => setSort(v as Sort)}
|
||||
options={[{ value: "activity", label: t("observerPage.sortActivity") }, { value: "name", label: t("observerPage.sortName") }]} />
|
||||
</div>
|
||||
{isPending ? <div className="py-6 text-center font-mono text-[11px] text-text-dim">{t("common.loading")}</div>
|
||||
: isError ? <button type="button" onClick={onRetry} className="py-6 text-center font-mono text-[11px] text-danger">{t("common.loadFailed")} · {t("observerPage.retry")}</button>
|
||||
: rows.length === 0 ? <div className="py-6 text-center font-mono text-[11px] text-text-dim">{t(filtered ? "observerPage.noMatches" : "observerPage.none")}</div>
|
||||
: (
|
||||
<div role="listbox" aria-label={t("tabs.Observers")} className="-mx-1 flex min-h-0 flex-col gap-0.5 overflow-y-auto px-1">
|
||||
<div role="listbox" aria-label={t("tabs.Observers")} className="flex min-h-0 flex-col overflow-y-auto md:-mx-1 md:gap-0.5 md:px-1">
|
||||
{rows.map((o) => {
|
||||
const active = o.id === selectedId;
|
||||
const count = counts.get(o.id);
|
||||
@@ -57,14 +57,14 @@ export function ObserverSidebar({ observers, filtered, isPending, isError, onRet
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
onClick={() => onSelect(o.id)}
|
||||
className={`relative shrink-0 overflow-hidden rounded border-l-2 px-2.5 py-1.5 text-left transition-colors ${
|
||||
className={`relative shrink-0 overflow-hidden border-b border-l-2 border-b-border-subtle px-4 py-3 text-left md:rounded md:border-b-0 md:px-2.5 md:py-1.5 transition-colors ${
|
||||
active ? "border-primary bg-primary/10" : "border-transparent hover:bg-text-normal/3"
|
||||
}`}
|
||||
>
|
||||
{count != null && <div className="absolute inset-y-0 left-0 bg-secondary/15" style={{ width: `${(count / max) * 100}%` }} aria-hidden />}
|
||||
{count != null && <div className="absolute inset-y-0 left-0 bg-secondary/8 md:bg-secondary/15" style={{ width: `${(count / max) * 100}%` }} aria-hidden />}
|
||||
<div className="relative flex items-center gap-2">
|
||||
<span aria-hidden className={`h-1.5 w-1.5 shrink-0 rounded-full ${deriveObserverStatus(o) === "online" ? "bg-green" : "bg-text-dim/30"}`} />
|
||||
<span className={`min-w-0 flex-1 truncate font-mono text-[12px] ${active ? "text-text-bright" : "text-text-normal"}`}>{nameOf(o)}</span>
|
||||
<span className={`min-w-0 flex-1 truncate font-mono text-[13px] md:text-[12px] ${active ? "text-text-bright" : "text-text-normal"}`}>{nameOf(o)}</span>
|
||||
<span className="shrink-0 font-mono text-[11px] tabular-nums text-text-muted">{count != null ? formatCount(count) : "—"}</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -72,6 +72,6 @@ export function ObserverSidebar({ observers, filtered, isPending, isError, onRet
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user