GenerAI / worldmonitor /src /utils /panel-grid.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 8)
dbb1bf9 verified
Raw
History Blame Contribute Delete
4.89 kB
/**
* Shared grid-geometry helpers for panel row/column spans.
*
* Both the live {@link Panel} component and the deferred-shell placeholder
* logic (`panel-mount-deferral`) reason about the same CSS grid
* (`.panels-grid` / `.map-bottom-grid`) and the same `span-N` / `col-span-N`
* classes. Keeping the column-count inference and the span clamp/class helpers
* in one module guarantees a deferred shell reserves exactly the footprint the
* real panel occupies after hydration — divergent copies would silently
* reintroduce the layout shift this reservation exists to prevent.
*/
/** Minimum track width assumed for `repeat(auto-fill, minmax(280px, 1fr))`. */
export const PANELS_GRID_MIN_TRACK_PX = 280;
/** Maximum row span supported by the CSS (`.panel.span-1` … `.panel.span-4`). */
export const MAX_PANEL_ROW_SPAN = 4;
/** Maximum column span supported by the CSS (`.panel.col-span-1` … `.col-span-3`). */
export const MAX_PANEL_COL_SPAN = 3;
function getPanelGrid(element: HTMLElement): HTMLElement | null {
return (element.closest('.panels-grid') || element.closest('.map-bottom-grid')) as HTMLElement | null;
}
function getPanelGridWidth(grid: HTMLElement): number {
const width = grid.getBoundingClientRect().width;
return Number.isFinite(width) ? width : 0;
}
/**
* Whether the owning grid's column count can be trusted right now. CSS
* `repeat(auto-fill/auto-fit, ...)` templates need a rendered width before
* they can be converted into a column count; a connected grid can briefly
* report width 0 during synchronous insertion/layout.
*/
export function isPanelGridColumnCountReady(element: HTMLElement): boolean {
const grid = getPanelGrid(element);
if (!grid || typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return true;
const style = window.getComputedStyle(grid);
const template = style.gridTemplateColumns;
if (!template || template === 'none' || !template.includes('repeat(')) return true;
if (/repeat\(\s*\d+\s*,/i.test(template)) return true;
if (/repeat\(\s*auto-(fill|fit)\s*,/i.test(template)) {
return getPanelGridWidth(grid) > 0;
}
return true;
}
/**
* Best-effort count of the rendered columns of the grid that owns `element`.
* Falls back to {@link MAX_PANEL_COL_SPAN} when the grid or its computed style
* is unavailable (e.g. server-side render).
*/
export function getGridColumnCount(element: HTMLElement): number {
const grid = getPanelGrid(element);
if (!grid || typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return MAX_PANEL_COL_SPAN;
const style = window.getComputedStyle(grid);
const template = style.gridTemplateColumns;
if (!template || template === 'none') return MAX_PANEL_COL_SPAN;
if (template.includes('repeat(')) {
const repeatCountMatch = template.match(/repeat\(\s*(\d+)\s*,/i);
if (repeatCountMatch) {
const parsed = Number.parseInt(repeatCountMatch[1] ?? '0', 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
// For repeat(auto-fill/auto-fit, minmax(...)), infer count from rendered width.
const autoRepeatMatch = template.match(/repeat\(\s*auto-(fill|fit)\s*,/i);
if (autoRepeatMatch) {
const gap = Number.parseFloat(style.columnGap || '0') || 0;
const width = getPanelGridWidth(grid);
if (width > 0) {
return Math.max(1, Math.floor((width + gap) / (PANELS_GRID_MIN_TRACK_PX + gap)));
}
return MAX_PANEL_COL_SPAN;
}
}
const columns = template.trim().split(/\s+/).filter(Boolean);
return columns.length > 0 ? columns.length : MAX_PANEL_COL_SPAN;
}
/** Largest column span the current grid can accommodate (1…{@link MAX_PANEL_COL_SPAN}). */
export function getMaxColSpan(element: HTMLElement): number {
return Math.max(1, Math.min(MAX_PANEL_COL_SPAN, getGridColumnCount(element)));
}
/** Clamp `span` into the `[1, maxSpan]` range. */
export function clampColSpan(span: number, maxSpan: number): number {
return Math.max(1, Math.min(maxSpan, span));
}
/**
* The explicit `col-span-N` class on `element`, or `undefined` when none is
* set (i.e. the element renders at its natural/default column span).
*/
export function getExplicitColSpanClass(element: HTMLElement): number | undefined {
if (element.classList.contains('col-span-3')) return 3;
if (element.classList.contains('col-span-2')) return 2;
if (element.classList.contains('col-span-1')) return 1;
return undefined;
}
/** Remove any `col-span-N` class. */
export function clearColSpanClass(element: HTMLElement): void {
element.classList.remove('col-span-1', 'col-span-2', 'col-span-3');
}
/** Replace any existing `col-span-N` class with `col-span-${span}`. */
export function setColSpanClass(element: HTMLElement, span: number): void {
clearColSpanClass(element);
element.classList.add(`col-span-${span}`);
}