UNPKG

react-bracket-ui

Version:

A modern, feature-rich React component library for displaying single-elimination tournament brackets with drag-drop, zoom/pan, and error validation

1,017 lines (1,012 loc) 35.3 kB
// src/components/Bracket.tsx import React3, { useState as useState3, useMemo, useCallback, useRef as useRef2, useEffect as useEffect2 } from "react"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; // src/utils/bracketValidation.ts function validateBracket(matches) { const errors = []; if (!matches || matches.length === 0) { return errors; } const matchIds = /* @__PURE__ */ new Set(); matches.forEach((match) => { if (matchIds.has(match.id)) { errors.push({ matchId: match.id, message: "Duplicate match ID detected", type: "error" }); } matchIds.add(match.id); }); matches.forEach((match) => { if (match.nextMatchId && !matchIds.has(match.nextMatchId)) { errors.push({ matchId: match.id, message: `Invalid nextMatchId: ${match.nextMatchId}`, type: "error" }); } }); matches.forEach((match) => { if (match.participant1 && !match.participant1.id) { errors.push({ matchId: match.id, message: "Participant 1 missing ID", type: "warning" }); } if (match.participant2 && !match.participant2.id) { errors.push({ matchId: match.id, message: "Participant 2 missing ID", type: "warning" }); } }); matches.forEach((match) => { if (match.winner) { const participant1Id = match.participant1?.id; const participant2Id = match.participant2?.id; if (match.winner !== participant1Id && match.winner !== participant2Id) { errors.push({ matchId: match.id, message: "Winner ID does not match any participant", type: "error" }); } } }); return errors; } function hasMatchError(matchId, errors) { return errors.some((error) => error.matchId === matchId); } function getMatchErrorMessage(matchId, errors) { const matchErrors = errors.filter((error) => error.matchId === matchId); return matchErrors.map((e) => e.message).join(", "); } // src/components/MatchComponent.tsx import { memo, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; var MatchComponent = ({ match, hasError, errorMessage, enableDragDrop, onDragStart, onDragEnd, onDragOver, onDrop, colors, matchWidth, matchHeight, forwardedRef, onScheduleChange, schedule }) => { const isParticipant1Winner = match.winner === match.participant1?.id; const isParticipant2Winner = match.winner === match.participant2?.id; const matchStyles = { border: hasError ? `2px solid ${colors.error}` : `1px solid #ccc`, borderRadius: "6px", padding: "8px", backgroundColor: hasError ? "#ffebee" : "white", display: "flex", flexDirection: "column", minWidth: `${matchWidth}px`, minHeight: `${matchHeight}px`, height: `${matchHeight}px`, boxShadow: hasError ? `0 0 0 3px ${colors.error}33, 0 0 12px ${colors.error}` : "0 1px 3px rgba(0,0,0,0.1)", transition: "all 0.2s ease", position: "relative", overflow: "visible" // Đảm bảo tooltip không bị cắt }; const participantStyles = (participant, isWinner, slot) => ({ padding: "8px 12px", borderBottom: slot === "participant1" ? "1px solid #eee" : "none", display: "flex", justifyContent: "space-between", alignItems: "center", fontWeight: isWinner ? "bold" : "normal", backgroundColor: isWinner ? colors.winner : "transparent", cursor: enableDragDrop && participant ? "grab" : "default", transition: "background-color 0.2s ease", borderRadius: "4px", minHeight: "32px" }); const renderParticipant = (participant, isWinner, slot) => { if (!participant) { const style = participantStyles(null, false, slot); console.log(`[MatchComponent] Placeholder for match ${match.id} slot ${slot}:`, style); return /* @__PURE__ */ jsx( "div", { className: enableDragDrop ? "draggable-participant" : void 0, style: { ...style, fontStyle: "italic", color: "#999", cursor: enableDragDrop ? "pointer" : "default", backgroundColor: enableDragDrop ? "#f5f5f5" : "transparent" }, onDragOver: enableDragDrop ? onDragOver : void 0, onDrop: enableDragDrop ? (e) => { e.preventDefault(); onDrop(match.id, slot); } : void 0, children: "TBD" } ); } return /* @__PURE__ */ jsxs( "div", { className: enableDragDrop ? "draggable-participant" : void 0, draggable: enableDragDrop, onDragStart: enableDragDrop ? (e) => { e.dataTransfer.effectAllowed = "move"; onDragStart(match.id, participant.id, slot); } : void 0, onDragEnd: enableDragDrop && onDragEnd ? onDragEnd : void 0, onDragOver: enableDragDrop ? onDragOver : void 0, onDrop: enableDragDrop ? (e) => { e.preventDefault(); onDrop(match.id, slot); } : void 0, style: participantStyles(participant, isWinner, slot), onMouseEnter: (e) => { if (enableDragDrop && participant) { e.currentTarget.style.backgroundColor = isWinner ? colors.winner : "#f0f0f0"; } }, onMouseLeave: (e) => { if (enableDragDrop && participant) { e.currentTarget.style.backgroundColor = isWinner ? colors.winner : "transparent"; } }, children: [ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: participant.name }), typeof participant.score === "number" && /* @__PURE__ */ jsx("span", { style: { fontWeight: "bold", marginLeft: "8px", fontSize: "14px", color: colors.primary }, children: participant.score }) ] } ); }; console.log(`[MatchComponent] Render match ${match.id} style:`, matchStyles, { participant1: match.participant1, participant2: match.participant2 }); let formattedSchedule = ""; if (schedule) { let dateObj = null; if (typeof schedule === "string") { const parsed = Date.parse(schedule); if (!isNaN(parsed)) dateObj = new Date(parsed); } else if (schedule instanceof Date) { dateObj = schedule; } if (dateObj) { const pad = (n) => n < 10 ? "0" + n : n; const h = pad(dateObj.getHours()); const m = pad(dateObj.getMinutes()); const d = pad(dateObj.getDate()); const mo = pad(dateObj.getMonth() + 1); const y = dateObj.getFullYear(); formattedSchedule = `${h}:${m}, ${d}/${mo}/${y}`; } else if (typeof schedule === "string") { formattedSchedule = schedule; } } const [hovered, setHovered] = useState(false); return /* @__PURE__ */ jsxs( "div", { className: (enableDragDrop ? "match-container " : "") + "relative", style: { ...matchStyles, zIndex: hasError && hovered ? 1050 : void 0 }, ref: forwardedRef, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [ /* @__PURE__ */ jsxs("div", { style: { fontSize: "11px", color: hasError ? colors.error : "#666", fontWeight: hasError ? "bold" : "normal", display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-between", position: "relative", gap: 8 }, children: [ /* @__PURE__ */ jsxs("span", { style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: [ "Match ", match.matchNumber || match.id ] }), /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }, children: /* @__PURE__ */ jsx( "input", { type: "datetime-local", value: (() => { if (!schedule) return ""; let d = null; if (typeof schedule === "string") { const parsed = Date.parse(schedule); if (!isNaN(parsed)) d = new Date(parsed); } else if (schedule instanceof Date) { d = schedule; } if (!d) return ""; const pad = (n) => n < 10 ? "0" + n : n; return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; })(), onChange: (e) => { if (typeof match.id !== "undefined" && typeof e.target.value === "string" && e.target.value && typeof onScheduleChange === "function") { onScheduleChange(match.id, e.target.value); } }, style: { fontSize: 12, color: "#888", fontWeight: 400, border: "1px solid #ccc", borderRadius: 4, padding: "2px 4px", background: "#fafbfc", minWidth: 120, maxWidth: 150 }, title: "Ch\u1EC9nh s\u1EEDa l\u1ECBch thi \u0111\u1EA5u" } ) }) ] }), renderParticipant(match.participant1, isParticipant1Winner, "participant1"), renderParticipant(match.participant2, isParticipant2Winner, "participant2"), hasError && hovered && errorMessage && /* @__PURE__ */ jsx( "div", { className: "absolute left-1/2 -translate-x-1/2 top-full mt-2 z-[1051] bg-red-600 text-white px-3 py-2 rounded shadow-lg text-sm", style: { pointerEvents: "none", boxShadow: "0 0 0 2px #d32f2f, 0 4px 16px #d32f2faa", background: "#d32f2f", minWidth: 0, maxWidth: matchWidth - 16, wordBreak: "break-word", whiteSpace: "pre-line", textAlign: "left" }, children: errorMessage } ) ] } ); }; var MatchComponent_default = memo(MatchComponent); // src/components/BracketRound.tsx import { useRef, useEffect } from "react"; import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var BracketRound = ({ round, matches, totalRounds, renderMatch, matchHeight, gap, roundName, showRoundNames, primaryColor, matchWidth, onMatchPositionsChange, zoomScale = 1 }) => { const matchRefs = useRef(/* @__PURE__ */ new Map()); const containerRef = useRef(null); useEffect(() => { if (!onMatchPositionsChange || !containerRef.current) return; const positions = []; const containerTop = containerRef.current.getBoundingClientRect().top; matches.forEach((match) => { const element = matchRefs.current.get(match.id); if (element) { const rect = element.getBoundingClientRect(); console.log(`[BracketRound] round ${round} match ${match.id} measured top:`, (rect.top - containerTop) / zoomScale, "height:", rect.height / zoomScale); positions.push({ matchId: match.id, top: (rect.top - containerTop) / zoomScale, height: rect.height / zoomScale }); } }); onMatchPositionsChange(positions); }, [matches, onMatchPositionsChange, zoomScale]); const matchSpacing = round === 1 ? gap : (Math.pow(2, round - 1) - 1) * matchHeight + Math.pow(2, round - 1) * gap; console.log(`[BracketRound] round ${round} matchSpacing:`, matchSpacing, "fixed matchHeight:", matchHeight, "gap:", gap); const topPadding = round === 1 ? 0 : (matchHeight + gap) * (Math.pow(2, round - 1) - 1) / 2; console.log(`[BracketRound] round ${round} topPadding:`, topPadding); return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", minWidth: `${matchWidth + 20}px` }, children: [ showRoundNames && roundName && /* @__PURE__ */ jsx2("h4", { style: { margin: "0 0 12px 0", color: primaryColor, fontSize: "14px", fontWeight: "600", textAlign: "center", height: "30px" }, children: roundName }), /* @__PURE__ */ jsx2( "div", { ref: containerRef, style: { display: "flex", flexDirection: "column", paddingTop: `${topPadding}px` }, children: matches.map((match, index) => /* @__PURE__ */ jsx2( "div", { ref: (el) => { if (el) { matchRefs.current.set(match.id, el); } else { matchRefs.current.delete(match.id); } }, style: { marginBottom: index < matches.length - 1 ? `${matchSpacing}px` : "0", visibility: match.isVirtual ? "hidden" : "visible" }, children: renderMatch(match, index) }, match.id )) } ) ] }); }; var BracketRound_default = BracketRound; // src/components/BracketConnectors.tsx import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime"; var BracketConnectors = ({ leftRoundMatches, rightRoundMatches, leftRound, rightRound, matchHeight, gap, color, showRoundNames, matchPositions, zoomScale = 1 }) => { const connectorWidth = 50; const getActualMatchHeight = (matches) => { if (matches.length === 0) return matchHeight; let totalHeight2 = 0; let count = 0; matches.forEach((match) => { const pos = matchPositions.get(match.id); if (pos && pos.height > 0) { totalHeight2 += pos.height; count++; console.log(`[BracketConnectors] getActualMatchHeight match ${match.id} height:`, pos.height); } }); return count > 0 ? totalHeight2 / count : matchHeight; }; const actualLeftMatchHeight = getActualMatchHeight(leftRoundMatches); const actualRightMatchHeight = getActualMatchHeight(rightRoundMatches); const leftMatchSpacing = leftRound === 1 ? gap : (Math.pow(2, leftRound - 1) - 1) * actualLeftMatchHeight + Math.pow(2, leftRound - 1) * gap; console.log(`[BracketConnectors] leftRound ${leftRound} leftMatchSpacing:`, leftMatchSpacing, "actualLeftMatchHeight:", actualLeftMatchHeight, "gap:", gap); const leftTopPadding = leftRound === 1 ? 0 : (actualLeftMatchHeight + gap) * (Math.pow(2, leftRound - 1) - 1) / 2; console.log(`[BracketConnectors] leftRound ${leftRound} leftTopPadding:`, leftTopPadding); const rightMatchSpacing = rightRound === 1 ? gap : (Math.pow(2, rightRound - 1) - 1) * actualRightMatchHeight + Math.pow(2, rightRound - 1) * gap; console.log(`[BracketConnectors] rightRound ${rightRound} rightMatchSpacing:`, rightMatchSpacing, "actualRightMatchHeight:", actualRightMatchHeight, "gap:", gap); const rightTopPadding = rightRound === 1 ? 0 : (actualRightMatchHeight + gap) * (Math.pow(2, rightRound - 1) - 1) / 2; console.log(`[BracketConnectors] rightRound ${rightRound} rightTopPadding:`, rightTopPadding); const leftTotalHeight = leftTopPadding + leftRoundMatches.length * actualLeftMatchHeight + (leftRoundMatches.length - 1) * leftMatchSpacing; const rightTotalHeight = rightTopPadding + rightRoundMatches.length * actualRightMatchHeight + (rightRoundMatches.length - 1) * rightMatchSpacing; const totalHeight = Math.max(leftTotalHeight, rightTotalHeight); const connectors = []; rightRoundMatches.forEach((rightMatch, rightIndex) => { let leftMatches = leftRoundMatches.filter( (m) => m.nextMatchId === rightMatch.id ); if (leftRound === 1) { leftMatches = leftMatches.filter((m) => !m.isVirtual); } if (leftMatches.length === 0) return; const rightPos = matchPositions.get(rightMatch.id); if (!rightPos) return; const rightY = rightPos.top + rightPos.height / 2; if (leftMatches.length === 1) { const leftMatch = leftMatches[0]; const leftPos = matchPositions.get(leftMatch.id); if (!leftPos) return; const leftY = leftPos.top + leftPos.height / 2; const meetX = connectorWidth / 2; connectors.push( /* @__PURE__ */ jsx3( "line", { x1: "0", y1: leftY, x2: meetX, y2: leftY, stroke: color, strokeWidth: "2" }, `connector-h-left-${leftMatch.id}` ) ); connectors.push( /* @__PURE__ */ jsx3( "line", { x1: meetX, y1: leftY, x2: meetX, y2: rightY, stroke: color, strokeWidth: "2" }, `connector-v-${leftMatch.id}-${rightMatch.id}` ) ); connectors.push( /* @__PURE__ */ jsx3( "line", { x1: meetX, y1: rightY, x2: connectorWidth, y2: rightY, stroke: color, strokeWidth: "2" }, `connector-h-right-${rightMatch.id}` ) ); } else { const leftYPositions = []; leftMatches.forEach((leftMatch) => { const leftPos = matchPositions.get(leftMatch.id); if (!leftPos) return; const leftY = leftPos.top + leftPos.height / 2; leftYPositions.push(leftY); }); if (leftYPositions.length === 0) return; const meetX = connectorWidth / 2; const minLeftY = Math.min(...leftYPositions); const maxLeftY = Math.max(...leftYPositions); leftMatches.forEach((leftMatch) => { const leftPos = matchPositions.get(leftMatch.id); if (!leftPos) return; const leftY = leftPos.top + leftPos.height / 2; connectors.push( /* @__PURE__ */ jsx3( "line", { x1: "0", y1: leftY, x2: meetX, y2: leftY, stroke: color, strokeWidth: "2" }, `connector-h-${leftMatch.id}` ) ); }); connectors.push( /* @__PURE__ */ jsx3( "line", { x1: meetX, y1: minLeftY, x2: meetX, y2: maxLeftY, stroke: color, strokeWidth: "2" }, `connector-v-${rightMatch.id}` ) ); connectors.push( /* @__PURE__ */ jsx3( "line", { x1: meetX, y1: rightY, x2: connectorWidth, y2: rightY, stroke: color, strokeWidth: "2" }, `connector-r-${rightMatch.id}` ) ); } }); return /* @__PURE__ */ jsxs3("div", { style: { width: `${connectorWidth}px`, display: "flex", flexDirection: "column", position: "relative" }, children: [ showRoundNames && /* @__PURE__ */ jsx3("div", { style: { height: "42px" } }), /* @__PURE__ */ jsx3( "svg", { width: connectorWidth, height: totalHeight, style: { overflow: "visible" }, children: connectors } ) ] }); }; var BracketConnectors_default = BracketConnectors; // src/components/Bracket.tsx import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime"; var DEFAULT_COLORS = { primary: "#1976d2", secondary: "#424242", background: "#f9f9f9", error: "#d32f2f", warning: "#ff9800", winner: "#e8f5e9" }; function Bracket({ matches, className, style, enableDragDrop = false, onBracketChange, enableZoomPan = false, minZoom = 0.5, maxZoom = 3, initialZoom = 1, errors: externalErrors, onErrorClick, showRoundNames = true, roundNames, matchWidth = 220, matchHeight = 100, gap = 20, colors: customColors, onScheduleChange, schedules = {} }) { const [internalMatches, setInternalMatches] = useState3(matches); const [isDragging, setIsDragging] = useState3(false); const [matchPositions, setMatchPositions] = useState3(/* @__PURE__ */ new Map()); const [currentScale, setCurrentScale] = useState3(initialZoom); const dragDataRef = useRef2(null); const [fixedMatchHeight, setFixedMatchHeight] = useState3(matchHeight); const round1MatchRef = useRef2(null); const bracketContentRef = useRef2(null); const [viewportSize, setViewportSize] = useState3({ width: 0, height: 0 }); const [contentSize, setContentSize] = useState3({ width: 0, height: 0 }); const [limitPan, setLimitPan] = useState3(true); const colors = useMemo(() => ({ ...DEFAULT_COLORS, ...customColors }), [customColors]); const allErrors = useMemo(() => { const validationErrors = validateBracket(internalMatches); return externalErrors ? [...validationErrors, ...externalErrors] : validationErrors; }, [internalMatches, externalErrors]); React3.useEffect(() => { setInternalMatches(matches); }, [matches]); const groupedMatches = useMemo(() => { return internalMatches.reduce((acc, match) => { if (!acc[match.round]) { acc[match.round] = []; } acc[match.round].push(match); return acc; }, {}); }, [internalMatches]); const rounds = useMemo( () => Object.keys(groupedMatches).map(Number).sort((a, b) => a - b), [groupedMatches] ); const handleDragStart = useCallback((matchId, participantId, slot) => { setIsDragging(true); dragDataRef.current = { fromMatchId: matchId, participantId, participantSlot: slot }; }, []); const handleDragEnd = useCallback(() => { setIsDragging(false); dragDataRef.current = null; }, []); const handleDragOver = useCallback((e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }, []); const handleDrop = useCallback((toMatchId, toSlot) => { if (!dragDataRef.current || !enableDragDrop) return; const { fromMatchId, participantId, participantSlot } = dragDataRef.current; if (fromMatchId === toMatchId && participantSlot === toSlot) { dragDataRef.current = null; setIsDragging(false); return; } const sourceMatch = internalMatches.find((m) => m.id === fromMatchId); const targetMatch = internalMatches.find((m) => m.id === toMatchId); if (!sourceMatch || !targetMatch) { dragDataRef.current = null; setIsDragging(false); return; } const sourceParticipant = sourceMatch[participantSlot]; const targetSlot = toSlot; const targetParticipant = targetMatch[targetSlot]; const updatedMatches = internalMatches.map((match) => { if (match.id === fromMatchId) { return { ...match, [participantSlot]: targetParticipant || null }; } else if (match.id === toMatchId) { return { ...match, [targetSlot]: sourceParticipant || null }; } else { return match; } }); setInternalMatches(updatedMatches); if (onBracketChange) { const dragDropResult = { fromMatchId, toMatchId, participantId, participantSlot: toSlot }; const event = { type: "drag-drop", matches: updatedMatches, dragDropResult }; onBracketChange(event); } dragDataRef.current = null; setIsDragging(false); }, [internalMatches, enableDragDrop, onBracketChange]); const renderMatch = useCallback((match, round, matchIndex) => { const hasError = hasMatchError(match.id, allErrors); const errorMessage = getMatchErrorMessage(match.id, allErrors); const ref = round === 1 && matchIndex === 0 ? round1MatchRef : void 0; const schedule = schedules && match.id in schedules ? schedules[match.id] : void 0; return /* @__PURE__ */ jsx4( MatchComponent_default, { match, hasError, errorMessage, enableDragDrop, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDragOver: handleDragOver, onDrop: handleDrop, colors, matchWidth, matchHeight: fixedMatchHeight, forwardedRef: ref, onScheduleChange, schedule }, match.id ); }, [allErrors, enableDragDrop, handleDragStart, handleDragEnd, handleDragOver, handleDrop, colors, matchWidth, fixedMatchHeight, schedules]); useEffect2(() => { if (round1MatchRef.current && (!enableZoomPan || currentScale === 1)) { const height = round1MatchRef.current.getBoundingClientRect().height; if (height > 0 && Math.abs(height - fixedMatchHeight) > 1) { setFixedMatchHeight(height); console.log("[Bracket] Measured fixedMatchHeight from round 1 (scale=1):", height); } } }, [internalMatches, enableZoomPan, currentScale]); const getRoundName = useCallback((round) => { if (roundNames && roundNames[round]) { return roundNames[round]; } const totalRounds = rounds.length; const roundsFromEnd = totalRounds - round + 1; if (roundsFromEnd === 1) return "Final"; if (roundsFromEnd === 2) return "Semi-Final"; if (roundsFromEnd === 3) return "Quarter-Final"; return `Round ${round}`; }, [rounds, roundNames]); const handleMatchPositionsChange = useCallback((positions) => { setMatchPositions((prev) => { const newMap = new Map(prev); positions.forEach(({ matchId, top, height }) => { newMap.set(matchId, { top, height }); }); return newMap; }); }, []); const defaultStyles = { border: "1px solid #ddd", borderRadius: "8px", padding: "16px", fontFamily: "Arial, sans-serif", backgroundColor: colors.background, height: "600px", overflow: "hidden" }; useEffect2(() => { if (!enableZoomPan) return; const handleResize = () => { if (bracketContentRef.current) { const contentRect = bracketContentRef.current.getBoundingClientRect(); setContentSize({ width: contentRect.width, height: contentRect.height }); } const parent = bracketContentRef.current?.parentElement; if (parent) { const viewportRect = parent.getBoundingClientRect(); setViewportSize({ width: viewportRect.width, height: viewportRect.height }); } }; handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [enableZoomPan, fixedMatchHeight, internalMatches]); useEffect2(() => { if (!enableZoomPan) return; if (contentSize.width > viewportSize.width || contentSize.height > viewportSize.height) { setLimitPan(false); } else { setLimitPan(true); } }, [contentSize, viewportSize, enableZoomPan]); const bracketContent = /* @__PURE__ */ jsxs4("div", { ref: enableZoomPan ? bracketContentRef : void 0, style: { padding: enableZoomPan ? "20px" : "0", position: "relative" }, children: [ /* @__PURE__ */ jsx4("h3", { style: { margin: "0 0 16px 0", color: colors.secondary }, children: "Tournament Bracket" }), rounds.length === 0 ? /* @__PURE__ */ jsx4("div", { style: { textAlign: "center", color: "#999", fontStyle: "italic", padding: "40px 0" }, children: "No matches available" }) : /* @__PURE__ */ jsx4("div", { style: { display: "flex", alignItems: "flex-start", gap: "0px", minWidth: "min-content" }, children: rounds.map((round, index) => /* @__PURE__ */ jsxs4(React3.Fragment, { children: [ /* @__PURE__ */ jsx4( BracketRound_default, { round, matches: groupedMatches[round], totalRounds: rounds.length, renderMatch: (match, matchIdx) => renderMatch(match, round, matchIdx), matchHeight: fixedMatchHeight, matchWidth, gap, roundName: getRoundName(round), showRoundNames, primaryColor: colors.primary, onMatchPositionsChange: handleMatchPositionsChange, zoomScale: enableZoomPan ? currentScale : 1 } ), index < rounds.length - 1 && /* @__PURE__ */ jsx4( BracketConnectors_default, { leftRoundMatches: groupedMatches[round], rightRoundMatches: groupedMatches[rounds[index + 1]], leftRound: round, rightRound: rounds[index + 1], matchHeight: fixedMatchHeight, gap, color: colors.secondary, showRoundNames, matchPositions, zoomScale: enableZoomPan ? currentScale : 1 } ) ] }, round)) }) ] }); return /* @__PURE__ */ jsxs4( "div", { className, style: { ...defaultStyles, ...style, position: "relative", overflow: "hidden" }, children: [ enableZoomPan && /* @__PURE__ */ jsx4("div", { style: { position: "absolute", bottom: "24px", right: "24px", zIndex: 100, display: "flex", gap: "8px", backgroundColor: "white", padding: "8px", borderRadius: "6px", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" } }), enableZoomPan ? /* @__PURE__ */ jsx4( TransformWrapper, { initialScale: initialZoom, minScale: 0.25, maxScale: 2, wheel: { disabled: true }, doubleClick: { disabled: true }, panning: { disabled: isDragging, excluded: enableDragDrop ? ["draggable-participant", "match-container"] : [], velocityDisabled: false }, limitToBounds: false, centerOnInit: true, onTransformed: (ref, state) => { setCurrentScale(state.scale); }, children: ({ zoomIn, zoomOut, resetTransform, setTransform }) => { const zoomToLevel = (level) => { setTransform(0, 0, level, 300, "easeOut"); }; const zoomLevels = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; const currentLevelIndex = zoomLevels.findIndex((level) => Math.abs(level - currentScale) < 0.01); const canZoomIn = currentLevelIndex < zoomLevels.length - 1; const canZoomOut = currentLevelIndex > 0; const handleZoomIn = () => { if (canZoomIn) { zoomToLevel(zoomLevels[currentLevelIndex + 1]); } }; const handleZoomOut = () => { if (canZoomOut) { zoomToLevel(zoomLevels[currentLevelIndex - 1]); } }; return /* @__PURE__ */ jsxs4(Fragment, { children: [ /* @__PURE__ */ jsxs4("div", { style: { position: "absolute", bottom: "24px", right: "24px", zIndex: 100, display: "flex", gap: "8px", backgroundColor: "white", padding: "8px", borderRadius: "6px", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" }, children: [ /* @__PURE__ */ jsx4( "button", { onClick: handleZoomOut, disabled: !canZoomOut, style: { padding: "6px 12px", border: "1px solid #ddd", borderRadius: "4px", backgroundColor: "white", cursor: canZoomOut ? "pointer" : "not-allowed", fontSize: "16px", opacity: canZoomOut ? 1 : 0.5 }, title: "Zoom Out", children: "\u2212" } ), /* @__PURE__ */ jsxs4("span", { style: { padding: "6px 12px", fontSize: "14px", fontWeight: "bold", display: "flex", alignItems: "center", minWidth: "60px", justifyContent: "center" }, children: [ Math.round(currentScale * 100), "%" ] }), /* @__PURE__ */ jsx4( "button", { onClick: handleZoomIn, disabled: !canZoomIn, style: { padding: "6px 12px", border: "1px solid #ddd", borderRadius: "4px", backgroundColor: "white", cursor: canZoomIn ? "pointer" : "not-allowed", fontSize: "16px", opacity: canZoomIn ? 1 : 0.5 }, title: "Zoom In", children: "+" } ), /* @__PURE__ */ jsx4( "button", { onClick: () => zoomToLevel(1), style: { padding: "6px 12px", border: "1px solid #ddd", borderRadius: "4px", backgroundColor: "white", cursor: "pointer", fontSize: "12px" }, title: "Reset to 100%", children: "\u21BA" } ), /* @__PURE__ */ jsx4( "button", { onClick: () => setTransform(0, 0, currentScale, 300, "easeOut"), style: { padding: "6px 12px", border: "1px solid #1976d2", borderRadius: "4px", backgroundColor: "#1976d2", color: "white", cursor: "pointer", fontSize: "12px", fontWeight: 600 }, title: "Center Bracket", children: "Center" } ) ] }), /* @__PURE__ */ jsx4( TransformComponent, { wrapperStyle: { width: "100%", height: "100%" }, contentStyle: { width: "100%", height: "100%" }, children: bracketContent } ) ] }); } } ) : /* @__PURE__ */ jsx4("div", { style: { width: "100%", height: "100%", overflow: "auto" }, children: bracketContent }) ] } ); } export { Bracket, getMatchErrorMessage, hasMatchError, validateBracket }; //# sourceMappingURL=index.mjs.map