Просмотр исходного кода

feat(ui): implement consistent loading UI delays across components and add uxTimings module

Code_Uwe 1 неделя назад
Родитель
Сommit
713e7ff397

+ 6 - 5
components/app-shell/SessionIndicator.jsx

@@ -5,18 +5,19 @@ import { Loader2 } from "lucide-react";
 
 
 import { useAuth } from "@/components/auth/authContext";
 import { useAuth } from "@/components/auth/authContext";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import {
+	SESSION_INDICATOR_DELAY_MS,
+	SESSION_INDICATOR_MIN_VISIBLE_MS,
+} from "@/lib/frontend/ui/uxTimings";
 
 
 export default function SessionIndicator() {
 export default function SessionIndicator() {
 	const { status, isValidating } = useAuth();
 	const { status, isValidating } = useAuth();
 
 
 	const isActive = status === "loading" || Boolean(isValidating);
 	const isActive = status === "loading" || Boolean(isValidating);
 
 
-	// Debounce policy:
-	// - Show only if it lasts longer than 200ms.
-	// - Once shown, keep it visible for at least 250ms to avoid blinking.
 	const visible = useDebouncedVisibility(isActive, {
 	const visible = useDebouncedVisibility(isActive, {
-		delayMs: 200,
-		minVisibleMs: 250,
+		delayMs: SESSION_INDICATOR_DELAY_MS,
+		minVisibleMs: SESSION_INDICATOR_MIN_VISIBLE_MS,
 	});
 	});
 
 
 	if (!visible) return null;
 	if (!visible) return null;

+ 2 - 6
components/app-shell/ThemeToggleButton.jsx

@@ -11,12 +11,12 @@ import {
 	TooltipProvider,
 	TooltipProvider,
 	TooltipTrigger,
 	TooltipTrigger,
 } from "@/components/ui/tooltip";
 } from "@/components/ui/tooltip";
+import { TOOLTIP_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 
 export default function ThemeToggleButton() {
 export default function ThemeToggleButton() {
 	const { setTheme } = useTheme();
 	const { setTheme } = useTheme();
 
 
 	function toggleTheme() {
 	function toggleTheme() {
-		// Read current theme from the DOM class to avoid hydration-unsafe theme reads.
 		const isDark =
 		const isDark =
 			typeof document !== "undefined" &&
 			typeof document !== "undefined" &&
 			document.documentElement.classList.contains("dark");
 			document.documentElement.classList.contains("dark");
@@ -25,7 +25,7 @@ export default function ThemeToggleButton() {
 	}
 	}
 
 
 	return (
 	return (
-		<TooltipProvider delayDuration={200}>
+		<TooltipProvider delayDuration={TOOLTIP_DELAY_MS}>
 			<Tooltip>
 			<Tooltip>
 				<TooltipTrigger asChild>
 				<TooltipTrigger asChild>
 					<Button
 					<Button
@@ -35,12 +35,8 @@ export default function ThemeToggleButton() {
 						onClick={toggleTheme}
 						onClick={toggleTheme}
 						aria-label="Design umschalten"
 						aria-label="Design umschalten"
 					>
 					>
-						{/* Light mode: show Moon (switch to dark) */}
 						<Moon className="h-4 w-4 dark:hidden" aria-hidden="true" />
 						<Moon className="h-4 w-4 dark:hidden" aria-hidden="true" />
-
-						{/* Dark mode: show Sun (switch to light) */}
 						<Sun className="hidden h-4 w-4 dark:block" aria-hidden="true" />
 						<Sun className="hidden h-4 w-4 dark:block" aria-hidden="true" />
-
 						<span className="sr-only">Design umschalten</span>
 						<span className="sr-only">Design umschalten</span>
 					</Button>
 					</Button>
 				</TooltipTrigger>
 				</TooltipTrigger>

+ 2 - 3
components/explorer/levels/DaysExplorer.jsx

@@ -11,6 +11,7 @@ import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
@@ -23,8 +24,6 @@ import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 
 
-const LOADING_DELAY_MS = 300;
-
 export default function DaysExplorer({ branch, year, month }) {
 export default function DaysExplorer({ branch, year, month }) {
 	const daysLoadFn = React.useCallback(
 	const daysLoadFn = React.useCallback(
 		() => getDays(branch, year, month),
 		() => getDays(branch, year, month),
@@ -47,7 +46,7 @@ export default function DaysExplorer({ branch, year, month }) {
 	);
 	);
 
 
 	const showLoadingUi = useDebouncedVisibility(daysQuery.status === "loading", {
 	const showLoadingUi = useDebouncedVisibility(daysQuery.status === "loading", {
-		delayMs: LOADING_DELAY_MS,
+		delayMs: LOADING_UI_DELAY_MS,
 		minVisibleMs: 0,
 		minVisibleMs: 0,
 	});
 	});
 
 

+ 2 - 3
components/explorer/levels/FilesExplorer.jsx

@@ -15,6 +15,7 @@ import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";
 import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";
 
 
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
@@ -37,8 +38,6 @@ import {
 	TableCaption,
 	TableCaption,
 } from "@/components/ui/table";
 } from "@/components/ui/table";
 
 
-const LOADING_DELAY_MS = 300;
-
 export default function FilesExplorer({ branch, year, month, day }) {
 export default function FilesExplorer({ branch, year, month, day }) {
 	const filesLoadFn = React.useCallback(
 	const filesLoadFn = React.useCallback(
 		() => getFiles(branch, year, month, day),
 		() => getFiles(branch, year, month, day),
@@ -69,7 +68,7 @@ export default function FilesExplorer({ branch, year, month, day }) {
 	const showLoadingUi = useDebouncedVisibility(
 	const showLoadingUi = useDebouncedVisibility(
 		filesQuery.status === "loading",
 		filesQuery.status === "loading",
 		{
 		{
-			delayMs: LOADING_DELAY_MS,
+			delayMs: LOADING_UI_DELAY_MS,
 			minVisibleMs: 0,
 			minVisibleMs: 0,
 		},
 		},
 	);
 	);

+ 2 - 3
components/explorer/levels/MonthsExplorer.jsx

@@ -12,6 +12,7 @@ import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
@@ -24,8 +25,6 @@ import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 
 
-const LOADING_DELAY_MS = 300;
-
 export default function MonthsExplorer({ branch, year }) {
 export default function MonthsExplorer({ branch, year }) {
 	const monthsLoadFn = React.useCallback(
 	const monthsLoadFn = React.useCallback(
 		() => getMonths(branch, year),
 		() => getMonths(branch, year),
@@ -44,7 +43,7 @@ export default function MonthsExplorer({ branch, year }) {
 	const showLoadingUi = useDebouncedVisibility(
 	const showLoadingUi = useDebouncedVisibility(
 		monthsQuery.status === "loading",
 		monthsQuery.status === "loading",
 		{
 		{
-			delayMs: LOADING_DELAY_MS,
+			delayMs: LOADING_UI_DELAY_MS,
 			minVisibleMs: 0,
 			minVisibleMs: 0,
 		},
 		},
 	);
 	);

+ 2 - 3
components/explorer/levels/YearsExplorer.jsx

@@ -11,6 +11,7 @@ import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
 import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
@@ -23,8 +24,6 @@ import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import ForbiddenView from "@/components/system/ForbiddenView";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 
 
-const LOADING_DELAY_MS = 300;
-
 export default function YearsExplorer({ branch }) {
 export default function YearsExplorer({ branch }) {
 	const loadFn = React.useCallback(() => getYears(branch), [branch]);
 	const loadFn = React.useCallback(() => getYears(branch), [branch]);
 	const { status, data, error, retry } = useExplorerQuery(loadFn, [loadFn]);
 	const { status, data, error, retry } = useExplorerQuery(loadFn, [loadFn]);
@@ -32,7 +31,7 @@ export default function YearsExplorer({ branch }) {
 	const mapped = React.useMemo(() => mapExplorerError(error), [error]);
 	const mapped = React.useMemo(() => mapExplorerError(error), [error]);
 
 
 	const showLoadingUi = useDebouncedVisibility(status === "loading", {
 	const showLoadingUi = useDebouncedVisibility(status === "loading", {
-		delayMs: LOADING_DELAY_MS,
+		delayMs: LOADING_UI_DELAY_MS,
 		minVisibleMs: 0,
 		minVisibleMs: 0,
 	});
 	});
 
 

+ 2 - 5
components/search/SearchResults.jsx

@@ -15,12 +15,11 @@ import {
 	SEARCH_RESULTS_SORT,
 	SEARCH_RESULTS_SORT,
 } from "@/lib/frontend/search/resultsSorting";
 } from "@/lib/frontend/search/resultsSorting";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
 import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
+import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 
 import SearchResultsToolbar from "@/components/search/SearchResultsToolbar";
 import SearchResultsToolbar from "@/components/search/SearchResultsToolbar";
 import SearchResultsTable from "@/components/search/SearchResultsTable";
 import SearchResultsTable from "@/components/search/SearchResultsTable";
 
 
-const LOADING_DELAY_MS = 300;
-
 export default function SearchResults({
 export default function SearchResults({
 	branch,
 	branch,
 	status,
 	status,
@@ -37,7 +36,7 @@ export default function SearchResults({
 	const [sortMode, setSortMode] = React.useState(SEARCH_RESULTS_SORT.RELEVANCE);
 	const [sortMode, setSortMode] = React.useState(SEARCH_RESULTS_SORT.RELEVANCE);
 
 
 	const showLoadingUi = useDebouncedVisibility(status === "loading", {
 	const showLoadingUi = useDebouncedVisibility(status === "loading", {
-		delayMs: LOADING_DELAY_MS,
+		delayMs: LOADING_UI_DELAY_MS,
 		minVisibleMs: 0,
 		minVisibleMs: 0,
 	});
 	});
 
 
@@ -86,8 +85,6 @@ export default function SearchResults({
 		);
 		);
 	}
 	}
 
 
-	// Debounced loading UI:
-	// - If loading is very fast, do not show skeletons (prevents flicker).
 	if (showLoadingUi) {
 	if (showLoadingUi) {
 		return <ExplorerLoading variant="table" count={8} />;
 		return <ExplorerLoading variant="table" count={8} />;
 	}
 	}

+ 6 - 0
lib/frontend/ui/uxTimings.js

@@ -0,0 +1,6 @@
+export const SESSION_INDICATOR_DELAY_MS = 200;
+export const SESSION_INDICATOR_MIN_VISIBLE_MS = 250;
+
+export const LOADING_UI_DELAY_MS = 300;
+
+export const TOOLTIP_DELAY_MS = 200;