85 lines
2.5 KiB
TypeScript
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, "");
|
|
}
|