UMBRA/frontend/src/hooks/useTableVisibility.ts
Kyle Pope cb9f74a387 Entity pages enhancement: backend model extensions, shared components, Locations rebuild, panel animations
- Add migrations 019/020: extend Person (first/last name, nickname, is_favourite, company, job_title, mobile, category) and Location (is_frequent, contact_number, email)
- Update Person/Location models, schemas, and routers with new fields + name denormalisation
- Create shared component library: EntityTable, EntityDetailPanel, CategoryFilterBar, CopyableField, CategoryAutocomplete, useTableVisibility hook
- Rebuild LocationsPage: table layout with sortable columns, detail side panel, category filter bar, frequent pinned section
- Extend LocationForm with contact number, email, frequent toggle, category autocomplete
- Add animated panel transitions to ProjectDetail (55/45 split with cubic-bezier easing)
- Update TypeScript interfaces for Person and Location

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:10:26 +08:00

65 lines
1.8 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
export type VisibilityMode = 'all' | 'filtered' | 'essential';
/**
* Observes container width via ResizeObserver and returns a visibility mode
* for table columns. Adjusts thresholds when a side panel is open.
*/
export function useTableVisibility(
containerRef: React.RefObject<HTMLElement>,
panelOpen: boolean
): VisibilityMode {
const [mode, setMode] = useState<VisibilityMode>('all');
const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const calculate = (width: number): VisibilityMode => {
if (panelOpen) {
return width >= 600 ? 'filtered' : 'essential';
}
if (width >= 900) return 'all';
if (width >= 600) return 'filtered';
return 'essential';
};
const observer = new ResizeObserver((entries) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
const entry = entries[0];
if (entry) {
setMode(calculate(entry.contentRect.width));
}
}, 50);
});
observer.observe(el);
// Set initial value
setMode(calculate(el.getBoundingClientRect().width));
return () => {
observer.disconnect();
clearTimeout(timerRef.current);
};
}, [containerRef, panelOpen]);
// Recalculate when panelOpen changes
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const width = el.getBoundingClientRect().width;
if (panelOpen) {
setMode(width >= 600 ? 'filtered' : 'essential');
} else {
if (width >= 900) setMode('all');
else if (width >= 600) setMode('filtered');
else setMode('essential');
}
}, [panelOpen, containerRef]);
return mode;
}