Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/features/observers/ObserverPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function ObserverPage({ wsManager }: { wsManager: WsManager }) {
brokerFilter={broker} onBrokerChange={setBroker} brokerOptions={brokers?.map((b) => b.name) ?? []}
scopeFilter={scope} onScopeChange={setScope} scopeOptions={scopeOptions}
trailing={<span className="ml-auto flex items-center gap-2 font-mono text-[11px] text-text-muted"><span className="hidden sm:inline">{t("observerPage.range")}</span>
<Segmented ariaLabel={t("observerPage.range")} size="sm" value={range} options={[{ value: "24h", label: t("stats.ranges.24h") }, { value: "7d", label: t("stats.ranges.7d") }, { value: "30d", label: t("stats.ranges.30d") }]} onChange={v => setParams(observerDestination(params, id, observerRange(v)))} />
<Segmented ariaLabel={t("observerPage.range")} size="xs" value={range} options={[{ value: "24h", label: t("stats.ranges.24h") }, { value: "7d", label: t("stats.ranges.7d") }, { value: "30d", label: t("stats.ranges.30d") }]} onChange={v => setParams(observerDestination(params, id, observerRange(v)))} />
</span>}
/>
<div className="relative flex min-h-0 min-w-0 flex-1">
Expand Down
31 changes: 19 additions & 12 deletions src/features/routes/RouteTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Timestamp } from "../../components/Timestamp";
import { DataTable, type Column } from "../../components/DataTable";
import { LoadingPill } from "../../components/LoadingPill";
import { MultiSelectDropdown } from "../../components/MultiSelectDropdown";
import { useIsMobile } from "../../hooks/useMediaQuery";
import { RouteDetailPanel } from "./RouteDetailPanel";
import { ResolvedHopBlock } from "../packets/PathData";
import { formatHex } from "../../lib/formatters";
Expand Down Expand Up @@ -257,6 +258,7 @@ export function RouteTable(actions: RouteActions) {
loadMore();
}, [search, serverIata, iatas, hasMore, isPaging, rows, loadedCount, loadMore]);

const isMobile = useIsMobile();
const panelOpen = Boolean(pathKey && routeIata || selectedRoute);
const canSearch = !!(from.trim() && to.trim() && searchIatas.length >= 1);
// clear any selection when the visible list changes out from under it (search submit/clear)
Expand All @@ -279,44 +281,49 @@ export function RouteTable(actions: RouteActions) {
{/* stacks into two rows on mobile (the inputs would otherwise wrap around the arrow); one row at md+ */}
<div className={`${panelOpen ? "hidden md:flex" : "flex"} flex-col md:flex-row md:flex-wrap md:items-center gap-1.5 gap-y-1.5 px-4 py-2 border-b border-border-subtle bg-bg-base shrink-0`}>
<div className="flex items-center gap-1.5">
<span className="text-text-muted text-[11px] uppercase tracking-wider mr-1 shrink-0">Find path</span>
<span className="hidden md:inline text-text-muted text-[11px] uppercase tracking-wider mr-1 shrink-0">Find path</span>
<input
className={`${inputClass} flex-1 min-w-0 md:flex-none md:w-24`}
className={`${inputClass} h-7 flex-1 min-w-0 md:flex-none md:w-24`}
placeholder="from hash"
aria-label="From hash"
value={from}
onChange={(e) => setFrom(e.target.value)}
onKeyDown={onKeyDown}
/>
<span className="text-text-dim text-xs shrink-0" aria-hidden>→</span>
<input
className={`${inputClass} flex-1 min-w-0 md:flex-none md:w-24`}
className={`${inputClass} h-7 flex-1 min-w-0 md:flex-none md:w-24`}
placeholder="to hash"
aria-label="To hash"
value={to}
onChange={(e) => setTo(e.target.value)}
onKeyDown={onKeyDown}
/>
</div>
<div className="flex items-center gap-1.5">
<MultiSelectDropdown
label="IATA"
options={iataOptions}
selected={searchIatas}
onChange={setSearchIatas}
align="left"
/>
<div className="flex-1 min-w-0 md:flex-none">
<MultiSelectDropdown
label="IATA"
options={iataOptions}
selected={searchIatas}
onChange={setSearchIatas}
align="left"
fullWidth={isMobile}
/>
</div>
<button
type="button"
onClick={submitSearch}
disabled={!canSearch}
className="text-[11px] font-mono px-2 py-1 rounded-sm border border-border bg-bg-surface text-text-normal hover:border-primary-dim disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
className="h-7 text-[11px] font-mono px-3 rounded-sm border border-border bg-bg-surface text-text-normal hover:border-primary-dim disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
>
Search
</button>
{search && (
<button
type="button"
onClick={clearSearch}
className="text-[11px] font-mono px-2 py-1 rounded-sm text-text-dim hover:text-text-normal cursor-pointer transition-colors"
className="h-7 text-[11px] font-mono px-2 rounded-sm text-text-dim hover:text-text-normal cursor-pointer transition-colors"
>
Clear
</button>
Expand Down
5 changes: 3 additions & 2 deletions src/features/stats/Segmented.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ interface SegmentedProps {
value: string;
onChange: (value: string) => void;
ariaLabel: string;
size?: "sm" | "md";
size?: "xs" | "sm" | "md";
className?: string;
}

// A contained "pill group" segmented control bound to the palette: the active pill gets a primary
// tint + inset ring, inactive pills are muted. Used for the Stats sub-tabs (md, with icons) and the
// time-range selector (sm). Active state is conveyed with aria-pressed, not color alone.
export function Segmented({ options, value, onChange, ariaLabel, size = "sm", className }: SegmentedProps) {
const pad = size === "md" ? "px-3 py-1.5 text-xs" : "px-2.5 py-1 text-[11px]";
// xs matches the 28px filter-bar controls it sits beside
const pad = size === "md" ? "px-3 py-1.5 text-xs" : size === "xs" ? "px-2.5 py-[3px] text-[11px] leading-4" : "px-2.5 py-1 text-[11px]";
return (
<div
role="group"
Expand Down
Loading