All files / client/src/hooks use-output-panel.ts

77.58% Statements 135/174
58.94% Branches 56/95
87.09% Functions 27/31
79.24% Lines 126/159

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377                              82x 82x 82x 82x 82x 82x     82x     5x 5x 5x 5x 5x       5x 1x       1x   1x             82x 43x 7x 7x 7x   7x 7x   7x 7x                     43x       43x 43x             82x 49x 49x                   82x   65x 5x 5x     5x   4x 4x 4x 4x 4x 4x     4x     4x         4x   60x   3x 3x 3x     3x   3x 3x 13x     3x 3x 3x 3x       3x       3x           3x 3x     3x   3x 3x     3x 3x     3x       3x                     3x 3x   57x           3x   3x                             82x   41x   18x 17x 17x 17x       41x                     41x                                                           82x   63x         1x       82x 49x 2x 49x   4x 4x 4x   4x       49x 49x 49x 49x 49x 49x 49x         82x 49x   13x 13x 49x   49x                     82x   49x         82x 50x 50x 50x   50x 26x 26x   26x 26x       26x                             50x 26x     50x 50x 50x       82x 5x 5x     82x                            
import { useState, useRef, useCallback, useEffect } from "react";
import type { ParserMessage } from "@shared/schema";
 
export function useOutputPanel(
  hasCompilationErrors: boolean,
  cliOutput: string,
  parserMessages: ParserMessage[],
  lastCompilationResult: "success" | "error" | null,
  parserMessagesContainerRef: React.RefObject<HTMLDivElement>,
  showCompilationOutput: boolean,
  setShowCompilationOutput: (value: boolean | ((prev: boolean) => boolean)) => void,
  setParserPanelDismissed: (value: boolean) => void,
  setActiveOutputTab: (tab: "compiler" | "messages" | "registry" | "debug") => void,
  code: string,
) {
  const outputPanelRef = useRef<any>(null);
  const outputTabsHeaderRef = useRef<HTMLDivElement | null>(null);
  const [outputPanelMinPercent, setOutputPanelMinPercent] = useState<number>(3);
  const [compilationPanelSize, setCompilationPanelSize] = useState(3);
  const [outputPanelManuallyResized, setOutputPanelManuallyResized] = useState(false);
  const outputPanelManuallyResizedRef = useRef(false);
 
  // Helper function to open the output panel (via double-click on tabs)
  const openOutputPanel = useCallback(
    (targetTab: "compiler" | "messages" | "registry" | "debug") => {
      // Mark as manually resized FIRST before showing panel (update both state and ref)
      outputPanelManuallyResizedRef.current = true;
      setOutputPanelManuallyResized(true);
      setShowCompilationOutput(true);
      setParserPanelDismissed(false);
      setActiveOutputTab(targetTab);
 
      // Resize panel to 50% directly without triggering compilationPanelSize state
      // This prevents the auto-sizing useEffect from interfering
      requestAnimationFrame(() => {
        Eif (
          outputPanelRef.current &&
          typeof outputPanelRef.current.resize === "function"
        ) {
          outputPanelRef.current.resize(50);
          // Update state after to reflect the manual size
          setCompilationPanelSize(50);
        }
      });
    },
    [setShowCompilationOutput, setParserPanelDismissed, setActiveOutputTab],
  );
 
  useEffect(() => {
    const handler = (ev: any) => {
      try {
        const newValue = Boolean(ev?.detail?.value);
        setShowCompilationOutput(newValue);
        // Reset manual resize flag when toggling panel visibility (update both ref and state)
        outputPanelManuallyResizedRef.current = false;
        setOutputPanelManuallyResized(false);
        // Persist to localStorage
        try {
          window.localStorage.setItem(
            "unoShowCompileOutput",
            newValue ? "1" : "0",
          );
        } catch {
          // localStorage may be unavailable (private browsing, etc.)
        }
      } catch {
        // ignore
      }
    };
    document.addEventListener(
      "showCompileOutputChange",
      handler as EventListener,
    );
    return () =>
      document.removeEventListener(
        "showCompileOutputChange",
        handler as EventListener,
      );
  }, [setShowCompilationOutput]);
 
  // Persist showCompilationOutput state to localStorage whenever it changes
  useEffect(() => {
    try {
      window.localStorage.setItem(
        "unoShowCompileOutput",
        showCompilationOutput ? "1" : "0",
      );
    } catch {
      // localStorage may be unavailable (private browsing, etc.)
    }
  }, [showCompilationOutput]);
 
  // Update compilation panel size based on error content and parser messages
  useEffect(() => {
    // Reset parserPanelDismissed when new errors occur (auto-reopen logic)
    if (hasCompilationErrors && cliOutput.trim().length > 0) {
      setParserPanelDismissed(false);
      setShowCompilationOutput(true);
 
      // Only auto-size if user hasn't manually resized
      if (!outputPanelManuallyResized) {
        // Auto-show and size panel for compiler errors
        const lines = cliOutput.split("\n").length;
        const totalChars = cliOutput.length;
        const HEADER_HEIGHT = 50;
        const PER_LINE = 20;
        const PADDING = 60;
        const AVAILABLE_HEIGHT = 800;
 
        const lineBasedPx =
          HEADER_HEIGHT +
          PADDING +
          Math.max(lines, Math.ceil(totalChars / 80)) * PER_LINE;
        const newSize = Math.min(
          75,
          Math.max(25, Math.ceil((lineBasedPx / AVAILABLE_HEIGHT) * 100)),
        );
 
        setCompilationPanelSize(newSize);
      }
    } else if (parserMessages.length > 0 && !hasCompilationErrors) {
      // Reset dismissal flag and show panel for new parser messages (auto-reopen)
      setParserPanelDismissed(false);
      setShowCompilationOutput(true);
      setActiveOutputTab("messages");
 
      // Only auto-size if user hasn't manually resized
      Eif (!outputPanelManuallyResized) {
        // Auto-show and size panel for parser messages (spec 3.2)
        const messageCount = parserMessages.length;
        const totalMessageLength = parserMessages.reduce(
          (sum, msg) => sum + (msg.message?.length || 0),
          0,
        );
        const HEADER_HEIGHT = 50;
        const PER_MESSAGE_BASE = 55;
        const PADDING = 60;
        const AVAILABLE_HEIGHT = 800;
 
        // SSOT formula (based on count + text length)
        const estimatedPx =
          HEADER_HEIGHT +
          PADDING +
          messageCount * PER_MESSAGE_BASE +
          Math.ceil(totalMessageLength / 100) * 15;
        const estimatedPercent = Math.min(
          75,
          Math.max(25, Math.ceil((estimatedPx / AVAILABLE_HEIGHT) * 100)),
        );
 
        // Measure rendered message container to ensure all containers stay visible
        const headerEl = outputTabsHeaderRef.current;
        const headerHeightPx = headerEl
          ? Math.ceil(headerEl.getBoundingClientRect().height || HEADER_HEIGHT)
          : HEADER_HEIGHT;
        let measuredPercent = estimatedPercent;
 
        try {
          const panelNode = headerEl?.closest("[data-panel]") as
            | HTMLElement
            | null;
          const groupNode = panelNode?.parentElement as HTMLElement | null;
          const groupHeightPx = Math.ceil(
            groupNode?.getBoundingClientRect().height || 0,
          );
          const messagesHeightPx = parserMessagesContainerRef.current
            ? Math.ceil(parserMessagesContainerRef.current.scrollHeight)
            : 0;
 
          Iif (groupHeightPx > 0) {
            const measuredPx = headerHeightPx + messagesHeightPx;
            measuredPercent = Math.min(
              75,
              Math.max(25, Math.ceil((measuredPx / groupHeightPx) * 100)),
            );
          }
        } catch {
          // Fallback to estimatedPercent
        }
 
        const newSize = Math.min(75, Math.max(25, Math.max(estimatedPercent, measuredPercent)));
        setCompilationPanelSize(newSize);
      }
    } else if (
      lastCompilationResult === "success" &&
      !hasCompilationErrors &&
      parserMessages.length === 0
    ) {
      // Only auto-minimize if user hasn't manually resized
      Eif (!outputPanelManuallyResized) {
        // Minimize panel when no errors and no messages (keep visible at 3%)
        setCompilationPanelSize(3);
      }
    }
  }, [
    cliOutput,
    hasCompilationErrors,
    lastCompilationResult,
    parserMessages.length,
    outputPanelManuallyResized,
    setParserPanelDismissed,
    setShowCompilationOutput,
    setActiveOutputTab,
  ]);
 
  // Apply panel size imperatively to ResizablePanel using absolute pixel floor
  const enforceOutputPanelFloor = useCallback(
    (forceResize: boolean = false) => {
      if (!showCompilationOutput) return;
      // ALWAYS skip auto-sizing if user manually resized the panel - use REF for current value (avoids stale closure)
      if (outputPanelManuallyResizedRef.current) return;
      const headerEl = outputTabsHeaderRef.current;
      const panelHandle = outputPanelRef.current;
      Eif (!headerEl || !panelHandle) return;
 
      const panelNode = headerEl.closest("[data-panel]") as HTMLElement | null;
      const groupNode = panelNode?.parentElement as HTMLElement | null;
      Iif (!panelNode || !groupNode) return;
 
      const headerRect = headerEl.getBoundingClientRect();
      const headerHeight = Math.ceil(headerRect.height);
      const groupHeight = Math.ceil(groupNode.getBoundingClientRect().height);
      if (!groupHeight || headerHeight <= 0) return;
 
      // Enforce absolute minimum height (px) equal to the header height (plus 0 gap target).
      // The panel is the bottom panel; keeping it at header height keeps the header near the bottom edge.
      const absoluteMinPx = headerHeight;
      const currentMinPx = parseInt(panelNode.style.minHeight || "0", 10);
      Iif (Number.isNaN(currentMinPx) || currentMinPx !== absoluteMinPx) {
        panelNode.style.minHeight = `${absoluteMinPx}px`;
      }
 
      // Convert absolute floor to percentage only for library API calls
      const minPercent = Math.max((absoluteMinPx / groupHeight) * 100, 3);
      const targetMinPercent = Math.min(75, minPercent);
 
      setOutputPanelMinPercent((prev) =>
        Math.abs(prev - targetMinPercent) > 0.01 ? targetMinPercent : prev,
      );
 
      if (
        typeof panelHandle.getSize === "function" &&
        typeof panelHandle.resize === "function"
      ) {
        const currentSize = panelHandle.getSize();
        if (typeof currentSize === "number") {
          const target = forceResize
            ? targetMinPercent // when forced (e.g., example load), snap to computed floor
            : Math.max(currentSize, targetMinPercent);
          if (Math.abs(currentSize - target) > 0.01) {
            panelHandle.resize(target);
          }
        }
      }
    },
    [showCompilationOutput],
  );
 
  useEffect(() => {
    // Only auto-resize if not manually resized by user (use ref for current value)
    if (
      !outputPanelManuallyResizedRef.current &&
      outputPanelRef.current &&
      typeof outputPanelRef.current.resize === "function"
    ) {
      outputPanelRef.current.resize(compilationPanelSize);
    }
  }, [compilationPanelSize, outputPanelManuallyResized]);
 
  useEffect(() => {
    const handleResize = () =>
      requestAnimationFrame(() => enforceOutputPanelFloor(false)); // Don't force resize on window resize
    const handleUiScale: EventListener = () => {
      // Double rAF to ensure CSS has fully applied and DOM has re-rendered
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          enforceOutputPanelFloor(true); // Force resize on scale change
          // Additional delayed enforcement for complex layout changes
          setTimeout(() => enforceOutputPanelFloor(true), 50);
        });
      });
    };
    window.addEventListener("resize", handleResize);
    window.addEventListener("uiFontScaleChange", handleUiScale);
    document.addEventListener("uiFontScaleChange", handleUiScale);
    return () => {
      window.removeEventListener("resize", handleResize);
      window.removeEventListener("uiFontScaleChange", handleUiScale);
      document.removeEventListener("uiFontScaleChange", handleUiScale);
    };
  }, [enforceOutputPanelFloor]);
 
  // ResizeObserver to continuously enforce floor when panel group size changes (e.g., when dragging divider)
  useEffect(() => {
    if (!showCompilationOutput) return;
 
    const headerEl = outputTabsHeaderRef.current;
    const panelNode = headerEl?.closest("[data-panel]") as HTMLElement | null;
    const groupNode = panelNode?.parentElement as HTMLElement | null;
 
    if (!groupNode) return;
 
    const observer = new ResizeObserver(() => {
      requestAnimationFrame(() => enforceOutputPanelFloor(false)); // Don't force on group resize
    });
 
    observer.observe(groupNode);
    return () => observer.disconnect();
  }, [showCompilationOutput, enforceOutputPanelFloor]);
 
  // Initial floor enforcement on first layout
  useEffect(() => {
    // Run after first paint to ensure DOM sizes are available
    requestAnimationFrame(() => enforceOutputPanelFloor(true));
  }, [enforceOutputPanelFloor]);
 
  // Re-enforce output panel floor when code changes (e.g., loading new example)
  // Use iterative correction loop until gap reaches 0, same approach as ResizeObserver
  useEffect(() => {
    let cancelled = false;
    let attempts = 0;
    const maxAttempts = 10;
 
    const correctUntilFlush = () => {
      Iif (cancelled || attempts >= maxAttempts) return;
      attempts++;
 
      const headerEl = outputTabsHeaderRef.current;
      Eif (!headerEl) return;
 
      const panelNode = headerEl.closest("[data-panel]") as HTMLElement | null;
      const groupNode = panelNode?.parentElement as HTMLElement | null;
      Iif (!panelNode || !groupNode) return;
 
      const headerRect = headerEl.getBoundingClientRect();
      const groupRect = groupNode.getBoundingClientRect();
      const gap = Math.round(groupRect.bottom - headerRect.bottom);
 
      enforceOutputPanelFloor(true);
 
      // If gap still exists, schedule another correction
      if (gap > 1) {
        requestAnimationFrame(correctUntilFlush);
      }
    };
 
    // Start after a brief delay to let DOM settle
    const timeoutId = setTimeout(() => {
      requestAnimationFrame(correctUntilFlush);
    }, 50);
 
    return () => {
      cancelled = true;
      clearTimeout(timeoutId);
    };
  }, [code, enforceOutputPanelFloor]);
 
  const handleOnResizeOutputPanel = useCallback(() => {
    outputPanelManuallyResizedRef.current = true;
    setOutputPanelManuallyResized(true);
  }, []);
 
  return {
    outputPanelRef,
    outputTabsHeaderRef,
    outputPanelMinPercent,
    compilationPanelSize,
    setCompilationPanelSize,
    outputPanelManuallyResized,
    setOutputPanelManuallyResized,
    outputPanelManuallyResizedRef,
    openOutputPanel,
    enforceOutputPanelFloor,
    handleOnResizeOutputPanel,
  };
}