Unify text inputs onto the form.tsx inputCls look (surface bg, rounded-lg, 2px accent RING focus), replacing the settings-family border-focus style app-wide: - deleted 3 duplicate local `const inputCls` (KeyGate/SettingsPanel/SetupWizard) → import the shared one; a 4th (Welcome, py-2.5) kept local but re-styled to match. - ~30 inline settings inputs: default full-size fields → inputCls; size variants (pager/ table-filter/chat textarea/plex/glass-card) keep their width/radius/padding but swap bg-card→bg-surface and border-focus→ring-focus. glass-card/bg-transparent bgs left as-is. - refreshed the form.tsx doc-comment (the old two-style split is gone). The ring is the more visible / a11y-correct focus cue, matching R9's focus foundation.
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
import { type Column } from "./DataTable";
|
|
import { type SortState } from "../lib/columnSort";
|
|
|
|
// A sort control for views without clickable column headers (the channel manager's card layout):
|
|
// a dropdown of the sortable columns + an asc/desc toggle, mirroring the feed toolbar's sort. It's
|
|
// driven by the SAME `SortState` the DataTable header uses, so table and cards stay in sync. The
|
|
// dropdown's options come straight from the columns' `sortable`/`sortValue` metadata — no new list.
|
|
export default function ColumnSortControl<T>({
|
|
columns,
|
|
sort,
|
|
onChange,
|
|
label,
|
|
defaultLabel,
|
|
ascLabel,
|
|
descLabel,
|
|
}: {
|
|
columns: Column<T>[];
|
|
sort: SortState;
|
|
onChange: (next: SortState) => void;
|
|
/** Accessible name for the dropdown (e.g. "Sort"). */
|
|
label: string;
|
|
/** The "no sort / original order" option's text. */
|
|
defaultLabel: string;
|
|
ascLabel: string;
|
|
descLabel: string;
|
|
}) {
|
|
const sortable = columns.filter((c) => c.sortable && c.sortValue);
|
|
// A stored sort can name a column this table no longer offers (renamed, or `sortable` dropped).
|
|
// `sortRows` already leaves the rows unsorted then, so treat it as no sort here too — otherwise the
|
|
// select falls back to showing "default" while a direction arrow claims a sort that isn't applied.
|
|
const active = sortable.some((c) => c.key === sort?.key) ? sort : null;
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<select
|
|
value={active?.key ?? ""}
|
|
onChange={(e) => {
|
|
const key = e.target.value;
|
|
// Keep the direction when re-picking the same column; a new column starts ascending.
|
|
onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null);
|
|
}}
|
|
aria-label={label}
|
|
className="bg-surface border border-border rounded-lg px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
|
|
>
|
|
<option value="">{defaultLabel}</option>
|
|
{sortable.map((c) => (
|
|
<option key={c.key} value={c.key}>
|
|
{c.header}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{active && (
|
|
<button
|
|
onClick={() => onChange({ key: active.key, dir: active.dir === "asc" ? "desc" : "asc" })}
|
|
title={active.dir === "asc" ? ascLabel : descLabel}
|
|
aria-label={active.dir === "asc" ? ascLabel : descLabel}
|
|
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
|
|
>
|
|
{active.dir === "asc" ? (
|
|
<ArrowUp className="w-4 h-4" />
|
|
) : (
|
|
<ArrowDown className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|