UNPKG

@redocly/theme

Version:

Shared UI components lib

312 lines (309 loc) 13.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SvgViewer = SvgViewer; const react_1 = __importStar(require("react")); const styled_components_1 = __importStar(require("styled-components")); const hooks_1 = require("../../core/hooks"); const Button_1 = require("../../components/Button/Button"); const Tooltip_1 = require("../../components/Tooltip/Tooltip"); const AddIcon_1 = require("../../icons/AddIcon/AddIcon"); const SubtractIcon_1 = require("../../icons/SubtractIcon/SubtractIcon"); const CloseIcon_1 = require("../../icons/CloseIcon/CloseIcon"); const FitToViewIcon_1 = require("../../icons/FitToViewIcon/FitToViewIcon"); const MIN_SCALE_FACTOR = 0.1; const MAX_SCALE_FACTOR = 5; const ZOOM_STEP = 0.1; const WHEEL_SENSITIVITY = 0.002; const VIEWPORT_PADDING = 60; const FIT_SCALE_FACTOR = 0.9; function SvgViewer({ isOpen, onClose, children, labels = {}, }) { const [scale, setScale] = (0, react_1.useState)(1); const [baseScale, setBaseScale] = (0, react_1.useState)(1); const [position, setPosition] = (0, react_1.useState)({ x: 0, y: 0 }); const [isDragging, setIsDragging] = (0, react_1.useState)(false); const [dragStart, setDragStart] = (0, react_1.useState)({ x: 0, y: 0 }); const [pinchState, setPinchState] = (0, react_1.useState)(null); const [isWheelZooming, setIsWheelZooming] = (0, react_1.useState)(false); const wheelTimeoutRef = (0, react_1.useRef)(null); const overlayRef = (0, react_1.useRef)(null); const viewportRef = (0, react_1.useRef)(null); const contentRef = (0, react_1.useRef)(null); const renderedScaleRef = (0, react_1.useRef)(scale); (0, hooks_1.useModalScrollLock)(isOpen); // Keep track of the actually rendered scale for accurate measurements (0, react_1.useLayoutEffect)(() => { renderedScaleRef.current = scale; }, [scale]); const minScale = baseScale * MIN_SCALE_FACTOR; const maxScale = baseScale * MAX_SCALE_FACTOR; const clampScale = (0, react_1.useCallback)((value) => Math.min(maxScale, Math.max(minScale, value)), [minScale, maxScale]); const calculateFitScale = (0, react_1.useCallback)(() => { if (!viewportRef.current || !contentRef.current) return 1; const viewport = viewportRef.current.getBoundingClientRect(); const svg = contentRef.current.querySelector('svg'); if (!svg) return 1; const svgRect = svg.getBoundingClientRect(); if (!svgRect.width || !svgRect.height) return 1; // getBoundingClientRect returns transformed size, so compensate for current scale const currentScale = renderedScaleRef.current || 1; const naturalWidth = svgRect.width / currentScale; const naturalHeight = svgRect.height / currentScale; const availableWidth = viewport.width - VIEWPORT_PADDING * 2; const availableHeight = viewport.height - VIEWPORT_PADDING * 2; return (Math.min(availableWidth / naturalWidth, availableHeight / naturalHeight) * FIT_SCALE_FACTOR); }, []); const resetView = (0, react_1.useCallback)(() => { setScale(baseScale); setPosition({ x: 0, y: 0 }); }, [baseScale]); const zoomIn = (0, react_1.useCallback)(() => { setScale((s) => clampScale(s + baseScale * ZOOM_STEP)); }, [baseScale, clampScale]); const zoomOut = (0, react_1.useCallback)(() => { setScale((s) => clampScale(s - baseScale * ZOOM_STEP)); }, [baseScale, clampScale]); const handleKeyDown = (0, react_1.useCallback)((e) => { switch (e.key) { case 'Escape': onClose(); break; case '+': case '=': zoomIn(); break; case '-': zoomOut(); break; case '0': resetView(); break; } }, [onClose, zoomIn, zoomOut, resetView]); const handleWheel = (0, react_1.useCallback)((e) => { e.preventDefault(); e.stopPropagation(); setIsWheelZooming(true); if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current); wheelTimeoutRef.current = setTimeout(() => setIsWheelZooming(false), 150); const delta = -e.deltaY * WHEEL_SENSITIVITY; setScale((s) => clampScale(s + s * delta)); }, [clampScale]); const handleMouseDown = (0, react_1.useCallback)((e) => { if (e.button !== 0) return; e.preventDefault(); setIsDragging(true); setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y }); }, [position]); const handleMouseMove = (0, react_1.useCallback)((e) => { if (!isDragging) return; setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }, [isDragging, dragStart]); const handleMouseUp = (0, react_1.useCallback)(() => setIsDragging(false), []); const getTouchDistance = (touches) => { if (touches.length !== 2) return 0; const dx = touches[0].clientX - touches[1].clientX; const dy = touches[0].clientY - touches[1].clientY; return Math.hypot(dx, dy); }; const handleTouchStart = (0, react_1.useCallback)((e) => { if (e.touches.length === 2) { setPinchState({ distance: getTouchDistance(e.touches), scale }); } else if (e.touches.length === 1) { setIsDragging(true); setDragStart({ x: e.touches[0].clientX - position.x, y: e.touches[0].clientY - position.y, }); } }, [position, scale]); const handleTouchMove = (0, react_1.useCallback)((e) => { e.preventDefault(); if (e.touches.length === 2 && pinchState) { const distance = getTouchDistance(e.touches); setScale(clampScale(pinchState.scale * (distance / pinchState.distance))); } else if (e.touches.length === 1 && isDragging) { setPosition({ x: e.touches[0].clientX - dragStart.x, y: e.touches[0].clientY - dragStart.y, }); } }, [pinchState, isDragging, dragStart, clampScale]); const handleTouchEnd = (0, react_1.useCallback)(() => { setIsDragging(false); setPinchState(null); }, []); (0, react_1.useEffect)(() => { var _a; if (!isOpen) return; setPosition({ x: 0, y: 0 }); (_a = overlayRef.current) === null || _a === void 0 ? void 0 : _a.focus(); // Wait for DOM to be ready before measuring requestAnimationFrame(() => { const fitScale = calculateFitScale(); setBaseScale(fitScale); setScale(fitScale); }); }, [isOpen, calculateFitScale]); if (!isOpen) return null; const zoomPercentage = baseScale > 0 ? Math.round((scale / baseScale) * 100) : 100; const isAnimating = !isDragging && !isWheelZooming && !pinchState; return (react_1.default.createElement(Overlay, { ref: overlayRef, onClick: onClose, onKeyDown: handleKeyDown, tabIndex: 0, "aria-modal": "true", role: "dialog", "aria-label": labels.dialogLabel || 'SVG viewer' }, react_1.default.createElement(Viewport, { ref: viewportRef, onClick: (e) => e.stopPropagation(), onWheel: handleWheel, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, "$isDragging": isDragging }, react_1.default.createElement(Content, { ref: contentRef, "$isAnimating": isAnimating, style: { transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`, } }, children), react_1.default.createElement(Controls, null, react_1.default.createElement(ControlGroup, null, react_1.default.createElement(Tooltip_1.Tooltip, { tip: labels.zoomOut || 'Zoom out', placement: "top" }, react_1.default.createElement(ControlButton, { variant: "text", size: "small", icon: react_1.default.createElement(SubtractIcon_1.SubtractIcon, null), onClick: zoomOut, disabled: scale <= minScale })), react_1.default.createElement(ZoomLabel, null, zoomPercentage, "%"), react_1.default.createElement(Tooltip_1.Tooltip, { tip: labels.zoomIn || 'Zoom in', placement: "top" }, react_1.default.createElement(ControlButton, { variant: "text", size: "small", icon: react_1.default.createElement(AddIcon_1.AddIcon, null), onClick: zoomIn, disabled: scale >= maxScale })), react_1.default.createElement(Divider, null), react_1.default.createElement(Tooltip_1.Tooltip, { tip: labels.fitToView || 'Fit to view', placement: "top" }, react_1.default.createElement(ControlButton, { variant: "text", size: "small", icon: react_1.default.createElement(FitToViewIcon_1.FitToViewIcon, null), onClick: resetView })), react_1.default.createElement(Tooltip_1.Tooltip, { tip: labels.close || 'Close', placement: "top" }, react_1.default.createElement(ControlButton, { variant: "text", size: "small", icon: react_1.default.createElement(CloseIcon_1.CloseIcon, null), onClick: onClose }))))))); } const scaleIn = (0, styled_components_1.keyframes) ` from { transform: scale(0.9); } to { transform: scale(1); } `; const slideUp = (0, styled_components_1.keyframes) ` from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } `; const Overlay = styled_components_1.default.div ` position: fixed; inset: 0; background-color: var(--svg-viewer-overlay-bg-color); backdrop-filter: blur(var(--spacing-unit)); z-index: var(--z-index-overlay, 1000); display: flex; align-items: center; justify-content: center; padding: var(--spacing-xxl); &:focus { outline: none; } @media (max-width: 768px) { padding: var(--spacing-md); } `; const Viewport = styled_components_1.default.div ` position: relative; width: 100%; height: 100%; background-color: var(--svg-viewer-bg-color); border-radius: var(--svg-viewer-border-radius); overflow: hidden; cursor: ${({ $isDragging }) => ($isDragging ? 'grabbing' : 'grab')}; touch-action: none; box-shadow: var(--svg-viewer-box-shadow); animation: ${scaleIn} 0.25s ease-in-out forwards; `; const Content = styled_components_1.default.div ` position: absolute; top: 50%; left: 50%; transform-origin: center center; user-select: none; pointer-events: none; transition: ${({ $isAnimating }) => ($isAnimating ? 'transform 0.25s ease-in-out' : 'none')}; svg { display: block; max-width: none !important; } `; const Controls = styled_components_1.default.div ` position: absolute; bottom: var(--spacing-sm); left: 50%; transform: translateX(-50%); z-index: 10; animation: ${slideUp} 0.3s ease-out 0.1s backwards; `; const ControlGroup = styled_components_1.default.div ` display: flex; align-items: center; gap: 2px; padding: var(--spacing-xxs); background: var(--bg-color-raised); border: 1px solid var(--border-color-primary); border-radius: var(--border-radius-lg); box-shadow: var(--bg-raised-shadow); `; const ControlButton = (0, styled_components_1.default)(Button_1.Button) ` --button-icon-size: 16px; `; const ZoomLabel = styled_components_1.default.span ` min-width: 40px; font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); color: var(--text-color-secondary); text-align: center; font-variant-numeric: tabular-nums; `; const Divider = styled_components_1.default.div ` width: 1px; height: var(--spacing-base); background: var(--border-color-primary); margin: 0 var(--spacing-xxs); `; //# sourceMappingURL=SvgViewer.js.map