feat(ui): customizable sidebar — collapsible, reorderable, toggleable widgets

Each filter group is now a card with a collapse chevron. An Edit mode (pencil)
reveals drag handles (@dnd-kit) to reorder and eye toggles to show/hide widgets,
plus Reset to defaults. Layout (order/collapsed/hidden) persists to localStorage
and the server preferences blob, adopted on login.
This commit is contained in:
npeter83 2026-06-11 19:21:48 +02:00
parent a595a33e36
commit 01960d3bb7
5 changed files with 542 additions and 213 deletions

View file

@ -9,6 +9,12 @@ import {
type ThemePrefs,
} from "./lib/theme";
import { hasFilterParams, paramsToFilters, syncUrl } from "./lib/urlState";
import {
loadLayout,
normalizeLayout,
saveLayoutLocal,
type SidebarLayout,
} from "./lib/sidebarLayout";
import Login from "./components/Login";
import Header from "./components/Header";
import Sidebar from "./components/Sidebar";
@ -47,6 +53,7 @@ export default function App() {
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
const [filters, setFiltersState] = useState<FeedFilters>(loadInitialFilters);
const [view, setView] = useState<"grid" | "list">("grid");
const [sidebarLayout, setSidebarLayoutState] = useState<SidebarLayout>(loadLayout);
function setFilters(next: FeedFilters) {
setFiltersState(next);
@ -54,6 +61,12 @@ export default function App() {
syncUrl(next);
}
function setSidebarLayout(next: SidebarLayout) {
setSidebarLayoutState(next);
saveLayoutLocal(next);
api.savePrefs({ sidebarLayout: next }).catch(() => {});
}
useEffect(() => applyTheme(theme), [theme]);
// Reflect the initial filters (from localStorage or URL) into the address bar
@ -72,6 +85,11 @@ export default function App() {
saveLocalTheme(merged);
}
if (prefs.view === "grid" || prefs.view === "list") setView(prefs.view);
if (prefs.sidebarLayout) {
const l = normalizeLayout(prefs.sidebarLayout);
setSidebarLayoutState(l);
saveLayoutLocal(l);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [meQuery.data?.id]);
@ -108,7 +126,12 @@ export default function App() {
setView={changeView}
/>
<div className="flex flex-1 min-h-0">
<Sidebar filters={filters} setFilters={setFilters} />
<Sidebar
filters={filters}
setFilters={setFilters}
layout={sidebarLayout}
setLayout={setSidebarLayout}
/>
<main className="flex-1 min-w-0 overflow-y-auto">
<Feed filters={filters} setFilters={setFilters} view={view} />
</main>