UNPKG

@lumina-study/graph

Version:

Graph library for Lumina Study

513 lines (512 loc) 15.8 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const jsxRuntime = require("react/jsx-runtime"); const react$1 = require("react"); const react = require("@xyflow/react"); const NODE_WIDTH_PX = 240; const NODE_HEIGHT_PX = 80; const MOBILE_NODE_WIDTH_PX = 180; const MOBILE_NODE_HEIGHT_PX = 120; function getNodeDimensions(isMobile) { return { width: isMobile ? MOBILE_NODE_WIDTH_PX : NODE_WIDTH_PX, height: isMobile ? MOBILE_NODE_HEIGHT_PX : NODE_HEIGHT_PX }; } function getHandlePositions(isVertical, dir) { if (isVertical) { return { targetPosition: react.Position.Top, sourcePosition: react.Position.Bottom }; } if (dir === "rtl") { return { targetPosition: react.Position.Right, sourcePosition: react.Position.Left }; } return { targetPosition: react.Position.Left, sourcePosition: react.Position.Right }; } function getTreeNodeClassName(data) { const classes = ["tree-node"]; if (data.isSelected) { classes.push("tree-node--selected"); } if (data.isHighlighted) { classes.push("tree-node--highlighted"); } if (data.disabled) { classes.push("tree-node--disabled"); } return classes.join(" "); } function hasSubModules(data) { return Boolean(data.subModules && data.subModules.length > 0); } function handleClick(event, onClick) { if (!onClick) { return; } const target = event.target; if (!(target instanceof HTMLElement)) { return; } if (target.tagName === "BUTTON" || target.closest("button") || target.tagName === "A" || target.closest("a")) { return; } onClick(); } function handleKeyDown(event, onClick) { if (!onClick) { return; } if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onClick(); } } function highlightText(text, searchTerm) { if (!searchTerm || !searchTerm.trim()) { return text; } const escapedTerm = searchTerm.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&"); const parts = text.split(new RegExp(`(${escapedTerm})`, "gi")); return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map((part, index) => { const isMatch = part.toLowerCase() === searchTerm.toLowerCase(); return isMatch ? /* @__PURE__ */ jsxRuntime.jsx("mark", { className: "bg-yellow-200", children: part }, index) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "", children: part }, index); }) }); } function getBlockIcon(style) { if (!style) { return void 0; } return void 0; } function autoLayout(nodes, edges, options = {}) { const { direction = "vertical", horizontalSpacing = 300, verticalSpacing = 150 } = options; if (nodes.length === 0) { return []; } const childrenMap = /* @__PURE__ */ new Map(); const parentMap = /* @__PURE__ */ new Map(); edges.forEach((edge) => { const children = childrenMap.get(edge.source) || []; children.push(edge.target); childrenMap.set(edge.source, children); parentMap.set(edge.target, edge.source); }); const roots = nodes.filter((node) => !parentMap.has(node.id)); if (roots.length === 0) { roots.push(nodes[0]); } const nodeMap = new Map(nodes.map((n) => [n.id, n])); function buildTree(nodeId) { const node = nodeMap.get(nodeId); const children = (childrenMap.get(nodeId) || []).map(buildTree); const width = children.length === 0 ? 1 : children.reduce((sum, child) => sum + child.width, 0); return { node, children, width }; } const trees = roots.map((root) => buildTree(root.id)); const positioned = /* @__PURE__ */ new Map(); function positionTree(tree, x, y) { if (direction === "vertical") { positioned.set(tree.node.id, { x, y }); if (tree.children.length > 0) { const totalWidth = tree.width; const childY = y + verticalSpacing; let childX = x - (totalWidth - 1) * horizontalSpacing / 2; tree.children.forEach((child) => { const childCenterX = childX + child.width * horizontalSpacing / 2; positionTree(child, childCenterX, childY); childX += child.width * horizontalSpacing; }); } } else { positioned.set(tree.node.id, { x: y, y: x }); if (tree.children.length > 0) { const totalWidth = tree.width; const childX = y + horizontalSpacing; let childY = x - (totalWidth - 1) * verticalSpacing / 2; tree.children.forEach((child) => { const childCenterY = childY + child.width * verticalSpacing / 2; positionTree(child, childCenterY, childX); childY += child.width * verticalSpacing; }); } } } let offsetX = 0; trees.forEach((tree) => { const treeWidth = tree.width * horizontalSpacing; positionTree(tree, offsetX + treeWidth / 2, 0); offsetX += treeWidth + horizontalSpacing; }); return nodes.map((node) => ({ ...node, position: positioned.get(node.id) || { x: 0, y: 0 } })); } function createTreeFromHierarchy(items) { const nodes = items.map((item) => ({ id: item.id, type: "treeNode", position: { x: 0, y: 0 }, // Will be calculated by autoLayout data: item.data })); const edges = items.filter((item) => item.parentId).map((item) => ({ id: `e-${item.parentId}-${item.id}`, source: item.parentId, target: item.id })); return { nodes, edges }; } const TreeNodeHandles = ({ isVertical, dir }) => { const { targetPosition, sourcePosition } = getHandlePositions( isVertical, dir ); return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( react.Handle, { id: "target", type: "target", position: targetPosition, style: { background: "#3b82f6" } } ), /* @__PURE__ */ jsxRuntime.jsx( react.Handle, { id: "source", type: "source", position: sourcePosition, style: { background: "#3b82f6" } } ) ] }); }; const CollapseButton = ({ isCollapsed, setIsCollapsed, onToggleCollapse, nodeId, isMobile }) => { const handleToggle = (e) => { e.stopPropagation(); const newCollapsed = !isCollapsed; setIsCollapsed(newCollapsed); onToggleCollapse(nodeId, newCollapsed); }; const handleKeyDown2 = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleToggle(e); } }; const buttonSize = isMobile ? "w-8 h-8 text-base" : "w-4 h-4 text-xs"; const buttonPadding = isMobile ? "p-1" : "p-0.5"; return /* @__PURE__ */ jsxRuntime.jsx( "button", { id: `tree-node-collapse-button-${nodeId}`, onClick: handleToggle, onKeyDown: handleKeyDown2, className: `bg-transparent border-none cursor-pointer ${buttonPadding} rounded-sm text-gray-500 flex items-center justify-center ${buttonSize}`, title: isCollapsed ? "Expand sub-modules" : "Collapse sub-modules", "aria-label": isCollapsed ? "Expand sub-modules" : "Collapse sub-modules", type: "button", children: isCollapsed ? "▶" : "▼" } ); }; const ZoomBadge = () => /* @__PURE__ */ jsxRuntime.jsxs( "span", { className: "zoom-badge", style: { fontSize: "0.75rem", color: "#2563eb", backgroundColor: "#eff6ff", padding: "0.25rem 0.5rem", borderRadius: "9999px", display: "flex", alignItems: "center", gap: "0.25rem", fontWeight: 400 }, children: [ /* @__PURE__ */ jsxRuntime.jsx( "svg", { className: "zoom-icon", style: { width: "0.75rem", height: "0.75rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx( "path", { className: "zoom-icon-path", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" } ) } ), "Zoom in" ] } ); const TreeNodeHeader = ({ hasSubModules: hasSubModules2, isCollapsed, setIsCollapsed, onToggleCollapse, nodeId, label, searchTerm, iconPath, canZoom, isMobile }) => { return /* @__PURE__ */ jsxRuntime.jsxs( "div", { id: `tree-node-header-${nodeId}`, className: `${isMobile ? "flex flex-col gap-2" : ""}`, style: { display: isMobile ? "flex" : "block" }, children: [ hasSubModules2 ? /* @__PURE__ */ jsxRuntime.jsx( "div", { id: `tree-node-collapse-wrapper-${nodeId}`, className: `${isMobile ? "flex justify-start" : ""}`, style: { display: isMobile ? "flex" : "inline" }, children: /* @__PURE__ */ jsxRuntime.jsx( CollapseButton, { isCollapsed, setIsCollapsed, onToggleCollapse, nodeId, isMobile } ) } ) : null, /* @__PURE__ */ jsxRuntime.jsxs( "div", { id: `tree-node-content-${nodeId}`, className: `relative ${isMobile ? "text-start" : ""}`, style: { lineHeight: "1.25", textAlign: isMobile ? "start" : "initial" }, children: [ iconPath && /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "icon-wrapper", style: { width: "3rem", marginBottom: isMobile ? "0.5rem" : 0, float: isMobile ? "none" : "right" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: iconPath, alt: "", role: "presentation", className: "icon-img" }) } ), /* @__PURE__ */ jsxRuntime.jsxs( "span", { className: "tree-node__label", style: { fontWeight: 600, display: canZoom ? "flex" : "block", fontSize: isMobile ? "1.125rem" : "1.25rem", textAlign: "start" }, children: [ highlightText(label, searchTerm), canZoom && /* @__PURE__ */ jsxRuntime.jsx(ZoomBadge, {}) ] } ) ] } ) ] } ); }; const TreeNodeSubmodules = ({ subModules, searchTerm }) => { if (!subModules || subModules.length === 0) { return null; } return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tree-node__submodules", children: subModules.map((subModule, index) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tree-node__submodule", children: highlightText(subModule, searchTerm) }, index)) }); }; function useMobileDetection() { const [isMobile, setIsMobile] = react$1.useState(false); react$1.useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; checkMobile(); window.addEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile); }, []); return isMobile; } function TreeNodeContainer(props) { const { data } = props; const isMobile = useMobileDetection(); const isVertical = data.direction === "ttb"; const layoutDirection = data.direction || "ttb"; const [isCollapsed, setIsCollapsed] = react$1.useState( data.isCollapsed !== void 0 ? data.isCollapsed : false ); react$1.useEffect(() => { if (data.isCollapsed !== void 0) { setIsCollapsed(data.isCollapsed); } }, [data.isCollapsed]); const hasSubModulesFlag = hasSubModules(data); const iconPath = getBlockIcon(data.style); const textDirection = data.language === "he" ? "rtl" : "ltr"; const textAlign = "start"; const { width: nodeWidth, height: nodeHeight } = getNodeDimensions(isMobile); const handleToggleCollapse = (nodeId, collapsed) => { if (data.onToggleCollapse) { data.onToggleCollapse(nodeId, collapsed); } }; return /* @__PURE__ */ jsxRuntime.jsxs( "div", { id: `tree-node-${props.id}`, onClick: (e) => handleClick(e, data.disabled ? void 0 : data.onClick), onKeyDown: (e) => handleKeyDown(e, data.disabled ? void 0 : data.onClick), className: getTreeNodeClassName(data), style: { direction: textDirection, textAlign, width: nodeWidth, height: nodeHeight }, role: "treeitem", "aria-expanded": hasSubModulesFlag ? !isCollapsed : void 0, "aria-selected": data.isSelected, "aria-disabled": data.disabled, tabIndex: data.isSelected ? 0 : -1, children: [ /* @__PURE__ */ jsxRuntime.jsx(TreeNodeHandles, { isVertical, dir: layoutDirection }), /* @__PURE__ */ jsxRuntime.jsx( TreeNodeHeader, { hasSubModules: hasSubModulesFlag, isCollapsed, setIsCollapsed, onToggleCollapse: handleToggleCollapse, nodeId: props.id, label: data.label, searchTerm: data.searchTerm, iconPath, canZoom: data.canZoom, isMobile } ), !isCollapsed && /* @__PURE__ */ jsxRuntime.jsx( TreeNodeSubmodules, { subModules: data.subModules, searchTerm: data.searchTerm } ) ] } ); } const TreeNode = TreeNodeContainer; function Tree({ nodes, edges = [], width = "100%", height = "600px", showBackground = true, showControls = true, fitView = true, nodeTypes: customNodeTypes, autoLayout: enableAutoLayout = false, layoutOptions, direction = "ttb", ...reactFlowProps }) { const nodeTypes = { treeNode: TreeNode, ...customNodeTypes }; let processedNodes = enableAutoLayout ? autoLayout(nodes, edges, layoutOptions) : nodes; processedNodes = processedNodes.map((node) => ({ ...node, data: { ...node.data, direction: node.data.direction || direction } })); const containerStyle = { width: typeof width === "number" ? `${width}px` : width, height: typeof height === "number" ? `${height}px` : height }; return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs( react.ReactFlow, { nodes: processedNodes, edges, nodeTypes, fitView, ...reactFlowProps, children: [ showBackground && /* @__PURE__ */ jsxRuntime.jsx(react.Background, {}), showControls && /* @__PURE__ */ jsxRuntime.jsx(react.Controls, {}) ] } ) }); } exports.CollapseButton = CollapseButton; exports.MOBILE_NODE_HEIGHT_PX = MOBILE_NODE_HEIGHT_PX; exports.MOBILE_NODE_WIDTH_PX = MOBILE_NODE_WIDTH_PX; exports.NODE_HEIGHT_PX = NODE_HEIGHT_PX; exports.NODE_WIDTH_PX = NODE_WIDTH_PX; exports.Tree = Tree; exports.TreeNode = TreeNode; exports.TreeNodeHandles = TreeNodeHandles; exports.TreeNodeHeader = TreeNodeHeader; exports.TreeNodeSubmodules = TreeNodeSubmodules; exports.ZoomBadge = ZoomBadge; exports.autoLayout = autoLayout; exports.createTreeFromHierarchy = createTreeFromHierarchy; exports.getBlockIcon = getBlockIcon; exports.getHandlePositions = getHandlePositions; exports.getNodeDimensions = getNodeDimensions; exports.getTreeNodeClassName = getTreeNodeClassName; exports.handleClick = handleClick; exports.handleKeyDown = handleKeyDown; exports.hasSubModules = hasSubModules; exports.highlightText = highlightText; //# sourceMappingURL=index.cjs.map