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:
parent
98025d8fab
commit
ae0cd89e20
5 changed files with 542 additions and 213 deletions
64
frontend/src/lib/sidebarLayout.ts
Normal file
64
frontend/src/lib/sidebarLayout.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Per-user customization of the filter sidebar: order of the widget cards, which are
|
||||
// collapsed, and which are hidden. Persisted to localStorage and the server-side
|
||||
// `preferences` blob so it follows the account.
|
||||
|
||||
export type WidgetId = "show" | "sort" | "date" | "content" | "language" | "topic";
|
||||
|
||||
export const ALL_WIDGETS: WidgetId[] = [
|
||||
"show",
|
||||
"sort",
|
||||
"date",
|
||||
"content",
|
||||
"language",
|
||||
"topic",
|
||||
];
|
||||
|
||||
export const WIDGET_TITLES: Record<WidgetId, string> = {
|
||||
show: "Show",
|
||||
sort: "Sort",
|
||||
date: "Upload date",
|
||||
content: "Content type",
|
||||
language: "Language",
|
||||
topic: "Topic",
|
||||
};
|
||||
|
||||
export interface SidebarLayout {
|
||||
order: WidgetId[];
|
||||
collapsed: Partial<Record<WidgetId, boolean>>;
|
||||
hidden: Partial<Record<WidgetId, boolean>>;
|
||||
}
|
||||
|
||||
export const DEFAULT_LAYOUT: SidebarLayout = {
|
||||
order: [...ALL_WIDGETS],
|
||||
collapsed: {},
|
||||
hidden: {},
|
||||
};
|
||||
|
||||
const KEY = "subfeed.sidebarLayout";
|
||||
|
||||
// Tolerate stale/partial data: keep only known widgets and append any that are missing
|
||||
// (e.g. a widget added in a later version) so nothing silently disappears.
|
||||
export function normalizeLayout(raw: unknown): SidebarLayout {
|
||||
const r = (raw ?? {}) as Partial<SidebarLayout>;
|
||||
const order: WidgetId[] = Array.isArray(r.order)
|
||||
? (r.order.filter((x) => ALL_WIDGETS.includes(x as WidgetId)) as WidgetId[])
|
||||
: [];
|
||||
for (const w of ALL_WIDGETS) if (!order.includes(w)) order.push(w);
|
||||
return {
|
||||
order,
|
||||
collapsed: { ...(r.collapsed ?? {}) },
|
||||
hidden: { ...(r.hidden ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
export function loadLayout(): SidebarLayout {
|
||||
try {
|
||||
return normalizeLayout(JSON.parse(localStorage.getItem(KEY) || "{}"));
|
||||
} catch {
|
||||
return DEFAULT_LAYOUT;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLayoutLocal(l: SidebarLayout): void {
|
||||
localStorage.setItem(KEY, JSON.stringify(l));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue