File size: 4,887 Bytes
dbb1bf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * 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}`);
}