Files
govoplan-search/webui/src/components/searchOverlayLayout.ts
T

85 lines
2.5 KiB
TypeScript

import type { SearchContextContribution } from "@govoplan/core-webui";
export type SearchOverlayAnchor = {
top: number;
left: number;
width: number;
height: number;
};
export type SearchOverlayLayout = {
top: number;
left: number;
width: number;
maxHeight: number;
inputOffset: number;
inputWidth: number;
inputHeight: number;
resultsHeight: number;
};
const DESKTOP_PANEL_WIDTH = 820;
const DESKTOP_MARGIN = 16;
const MOBILE_MARGIN = 12;
const MOBILE_BREAKPOINT = 900;
const RESULTS_GAP = 8;
export function calculateSearchOverlayLayout(
anchor: SearchOverlayAnchor,
viewportWidth: number,
viewportHeight: number
): SearchOverlayLayout {
const mobile = viewportWidth < MOBILE_BREAKPOINT;
const margin = mobile ? MOBILE_MARGIN : DESKTOP_MARGIN;
const width = Math.max(1, Math.min(DESKTOP_PANEL_WIDTH, viewportWidth - margin * 2));
const left = Math.max(margin, (viewportWidth - width) / 2);
const inputWidth = width;
const inputOffset = 0;
const top = Math.max(0, anchor.top);
const inputHeight = Math.max(1, anchor.height);
const availableResultsHeight = viewportHeight - top - inputHeight - RESULTS_GAP - margin;
const resultsHeight = Math.max(96, Math.min(620, availableResultsHeight));
return {
top,
left,
width,
maxHeight: inputHeight + RESULTS_GAP + resultsHeight,
inputOffset,
inputWidth,
inputHeight,
resultsHeight
};
}
export function selectSearchContext(
contexts: readonly SearchContextContribution[],
pathname: string
): SearchContextContribution | null {
const matches = contexts.flatMap((context) =>
context.pathPrefixes
.filter((prefix) => pathMatchesPrefix(pathname, prefix))
.map((prefix) => ({ context, prefixLength: normalizePath(prefix).length }))
);
matches.sort((left, right) =>
right.prefixLength - left.prefixLength
|| (left.context.order ?? 100) - (right.context.order ?? 100)
|| left.context.id.localeCompare(right.context.id)
);
return matches[0]?.context ?? null;
}
function pathMatchesPrefix(pathname: string, prefix: string): boolean {
const normalizedPath = normalizePath(pathname);
const normalizedPrefix = normalizePath(prefix);
return normalizedPrefix === "/"
|| normalizedPath === normalizedPrefix
|| normalizedPath.startsWith(`${normalizedPrefix}/`);
}
function normalizePath(value: string): string {
const normalized = `/${String(value || "").trim().replace(/^\/+|\/+$/g, "")}`;
return normalized === "/" ? normalized : normalized.replace(/\/+$/g, "");
}