UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

368 lines (359 loc) 18.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _lucideReact = require("lucide-react"); var _ShapeDefinitions = require("../../config/ShapeDefinitions"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Lazy loading hook for shape categories const useLazyShapeLoading = categoryId => { const [shapes, setShapes] = (0, _react.useState)([]); const [isLoading, setIsLoading] = (0, _react.useState)(false); const [isLoaded, setIsLoaded] = (0, _react.useState)(false); (0, _react.useEffect)(() => { if (categoryId && !isLoaded) { setIsLoading(true); // Simulate async loading for better UX const timer = setTimeout(() => { const categoryShapes = (0, _ShapeDefinitions.getShapesByCategory)(categoryId); setShapes(categoryShapes); setIsLoaded(true); setIsLoading(false); }, 100); // Small delay to show loading state return () => clearTimeout(timer); } }, [categoryId, isLoaded]); return { shapes, isLoading, isLoaded }; }; // Loading skeleton component const CategoryLoadingSkeleton = () => /*#__PURE__*/_react.default.createElement("div", { className: "p-4 space-y-3" }, /*#__PURE__*/_react.default.createElement("div", { className: "flex items-center space-x-2" }, /*#__PURE__*/_react.default.createElement("div", { className: "w-4 h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse" }), /*#__PURE__*/_react.default.createElement("div", { className: "w-24 h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse" })), /*#__PURE__*/_react.default.createElement("div", { className: "grid grid-cols-4 gap-2" }, [...Array(8)].map((_, i) => /*#__PURE__*/_react.default.createElement("div", { key: i, className: "h-16 bg-gray-200 dark:bg-gray-600 rounded animate-pulse" })))); const ShapeLibraryPanel = ({ isOpen, onClose, onShapeSelect, className = '' }) => { const [searchTerm, setSearchTerm] = (0, _react.useState)(''); const [selectedCategory, setSelectedCategory] = (0, _react.useState)('all'); const [collapsedCategories, setCollapsedCategories] = (0, _react.useState)(new Set()); const [loadedCategories, setLoadedCategories] = (0, _react.useState)(new Set(['basic'])); // Start with basic category loaded const [favorites, setFavorites] = (0, _react.useState)(() => { // Load favorites from localStorage try { const saved = localStorage.getItem('shapeLibraryFavorites'); return new Set(saved ? JSON.parse(saved) : ['basic-rectangle', 'flowchart-process']); } catch { return new Set(['basic-rectangle', 'flowchart-process']); } }); // Save favorites to localStorage whenever it changes (0, _react.useEffect)(() => { try { localStorage.setItem('shapeLibraryFavorites', JSON.stringify(Array.from(favorites))); } catch (error) { console.warn('Failed to save favorites to localStorage:', error); } }, [favorites]); // Lazy load categories when they become visible const handleCategoryVisibility = (0, _react.useCallback)(categoryId => { if (!loadedCategories.has(categoryId)) { setLoadedCategories(prev => new Set([...prev, categoryId])); } }, [loadedCategories]); // Filter shapes based on search and category const filteredShapes = (0, _react.useMemo)(() => { let shapes; if (searchTerm) { shapes = (0, _ShapeDefinitions.searchShapes)(searchTerm); } else if (selectedCategory !== 'all') { // Only load shapes for selected category if it's loaded if (loadedCategories.has(selectedCategory)) { shapes = (0, _ShapeDefinitions.getShapesByCategory)(selectedCategory); } else { shapes = []; } } else { // For 'all' category, only show shapes from loaded categories shapes = Object.values(_ShapeDefinitions.SHAPE_DEFINITIONS).filter(shape => loadedCategories.has(shape.category)); } return shapes; }, [searchTerm, selectedCategory, loadedCategories]); // Group shapes by category const groupedShapes = (0, _react.useMemo)(() => { const groups = {}; filteredShapes.forEach(shape => { if (!groups[shape.category]) { groups[shape.category] = []; } groups[shape.category].push(shape); }); return groups; }, [filteredShapes]); const handleShapeClick = (0, _react.useCallback)(shapeDefinition => { onShapeSelect({ type: 'universalShape', shapeType: shapeDefinition.id, label: shapeDefinition.name, style: { ...shapeDefinition.style }, defaultSize: { ...shapeDefinition.defaultSize } }); }, [onShapeSelect]); const handleDragStart = (0, _react.useCallback)((event, shapeDefinition) => { event.dataTransfer.setData('application/reactflow', JSON.stringify({ type: 'universalShape', shapeType: shapeDefinition.id, label: shapeDefinition.name, style: { ...shapeDefinition.style }, defaultSize: { ...shapeDefinition.defaultSize } })); event.dataTransfer.effectAllowed = 'move'; }, []); const toggleCategory = (0, _react.useCallback)(categoryId => { setCollapsedCategories(prev => { const newSet = new Set(prev); if (newSet.has(categoryId)) { newSet.delete(categoryId); // Load category when expanded handleCategoryVisibility(categoryId); } else { newSet.add(categoryId); } return newSet; }); }, [handleCategoryVisibility]); const toggleFavorite = (0, _react.useCallback)(shapeId => { setFavorites(prev => { const newSet = new Set(prev); if (newSet.has(shapeId)) { newSet.delete(shapeId); } else { newSet.add(shapeId); } return newSet; }); }, []); const clearSearch = (0, _react.useCallback)(() => { setSearchTerm(''); }, []); if (!isOpen) return null; return /*#__PURE__*/_react.default.createElement("div", { className: `fixed right-0 top-0 h-full w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col ${className}` }, /*#__PURE__*/_react.default.createElement("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between" }, /*#__PURE__*/_react.default.createElement("h2", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100" }, "Shape Library"), /*#__PURE__*/_react.default.createElement("button", { onClick: onClose, className: "p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" }, /*#__PURE__*/_react.default.createElement(_lucideReact.X, { size: 20, className: "text-gray-500 dark:text-gray-400" }))), /*#__PURE__*/_react.default.createElement("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700" }, /*#__PURE__*/_react.default.createElement("div", { className: "relative" }, /*#__PURE__*/_react.default.createElement(_lucideReact.Search, { size: 16, className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" }), /*#__PURE__*/_react.default.createElement("input", { type: "text", placeholder: "Search shapes...", value: searchTerm, onChange: e => setSearchTerm(e.target.value), className: "w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" }), searchTerm && /*#__PURE__*/_react.default.createElement("button", { onClick: clearSearch, className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" }, /*#__PURE__*/_react.default.createElement(_lucideReact.X, { size: 16 })))), /*#__PURE__*/_react.default.createElement("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700" }, /*#__PURE__*/_react.default.createElement("select", { value: selectedCategory, onChange: e => { const category = e.target.value; setSelectedCategory(category); if (category !== 'all') { handleCategoryVisibility(category); } }, className: "w-full p-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" }, /*#__PURE__*/_react.default.createElement("option", { value: "all" }, "All Categories"), Object.entries(_ShapeDefinitions.SHAPE_CATEGORIES).map(([id, category]) => /*#__PURE__*/_react.default.createElement("option", { key: id, value: id }, category.icon, " ", category.name)))), favorites.size > 0 && /*#__PURE__*/_react.default.createElement("div", { className: "border-b border-gray-200 dark:border-gray-700" }, /*#__PURE__*/_react.default.createElement("div", { className: "p-3 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-200 dark:border-yellow-800" }, /*#__PURE__*/_react.default.createElement("div", { className: "flex items-center gap-2 text-sm font-medium text-yellow-800 dark:text-yellow-200" }, /*#__PURE__*/_react.default.createElement(_lucideReact.Star, { size: 16, className: "text-yellow-500 fill-current" }), "Favorites (", favorites.size, ")")), /*#__PURE__*/_react.default.createElement("div", { className: "p-2 grid grid-cols-4 gap-2 max-h-32 overflow-y-auto" }, Array.from(favorites).map(shapeId => { const shape = _ShapeDefinitions.SHAPE_DEFINITIONS[shapeId]; return shape ? /*#__PURE__*/_react.default.createElement(ShapePreview, { key: `fav-${shapeId}`, shape: shape, onClick: () => handleShapeClick(shape), onDragStart: e => handleDragStart(e, shape), isFavorite: true, onToggleFavorite: () => toggleFavorite(shapeId) }) : null; }))), /*#__PURE__*/_react.default.createElement("div", { className: "flex-1 overflow-y-auto" }, Object.keys(groupedShapes).length === 0 ? /*#__PURE__*/_react.default.createElement("div", { className: "p-8 text-center text-gray-500 dark:text-gray-400" }, searchTerm ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_lucideReact.Search, { size: 48, className: "mx-auto mb-4 opacity-50" }), /*#__PURE__*/_react.default.createElement("p", null, "No shapes found"), /*#__PURE__*/_react.default.createElement("p", { className: "text-sm mt-2" }, "Try a different search term")) : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_lucideReact.Loader2, { size: 48, className: "mx-auto mb-4 opacity-50 animate-spin" }), /*#__PURE__*/_react.default.createElement("p", null, "Loading shapes..."))) : Object.entries(groupedShapes).map(([categoryId, shapes]) => { const category = _ShapeDefinitions.SHAPE_CATEGORIES[categoryId]; const isCollapsed = collapsedCategories.has(categoryId); const isLoaded = loadedCategories.has(categoryId); return /*#__PURE__*/_react.default.createElement("div", { key: categoryId, className: "border-b border-gray-200 dark:border-gray-700 last:border-b-0" }, /*#__PURE__*/_react.default.createElement("button", { onClick: () => toggleCategory(categoryId), className: "w-full p-3 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center justify-between transition-colors" }, /*#__PURE__*/_react.default.createElement("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300" }, /*#__PURE__*/_react.default.createElement("span", { className: "text-lg" }, (category === null || category === void 0 ? void 0 : category.icon) || '📦'), /*#__PURE__*/_react.default.createElement("span", null, (category === null || category === void 0 ? void 0 : category.name) || categoryId), /*#__PURE__*/_react.default.createElement("span", { className: "text-xs text-gray-500 bg-gray-200 dark:bg-gray-600 px-2 py-0.5 rounded-full" }, shapes.length), !isLoaded && /*#__PURE__*/_react.default.createElement(_lucideReact.Loader2, { size: 12, className: "animate-spin text-blue-500" })), isCollapsed ? /*#__PURE__*/_react.default.createElement(_lucideReact.ChevronRight, { size: 16, className: "text-gray-500" }) : /*#__PURE__*/_react.default.createElement(_lucideReact.ChevronDown, { size: 16, className: "text-gray-500" })), !isCollapsed && /*#__PURE__*/_react.default.createElement(_react.Suspense, { fallback: /*#__PURE__*/_react.default.createElement(CategoryLoadingSkeleton, null) }, /*#__PURE__*/_react.default.createElement("div", { className: "p-2 grid grid-cols-4 gap-2 bg-gray-25 dark:bg-gray-800/50" }, isLoaded ? shapes.map(shape => /*#__PURE__*/_react.default.createElement(ShapePreview, { key: shape.id, shape: shape, onClick: () => handleShapeClick(shape), onDragStart: e => handleDragStart(e, shape), isFavorite: favorites.has(shape.id), onToggleFavorite: () => toggleFavorite(shape.id) })) : /*#__PURE__*/_react.default.createElement(CategoryLoadingSkeleton, null)))); })), /*#__PURE__*/_react.default.createElement("div", { className: "p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700" }, /*#__PURE__*/_react.default.createElement("p", { className: "text-xs text-gray-500 dark:text-gray-400 text-center" }, "Drag shapes to canvas or click to add at center"))); }; // Shape Preview Component const ShapePreview = ({ shape, onClick, onDragStart, isFavorite, onToggleFavorite }) => { const handleFavoriteClick = (0, _react.useCallback)(e => { e.stopPropagation(); onToggleFavorite(); }, [onToggleFavorite]); const renderShapePreview = () => { var _shape$style8, _shape$style9, _shape$style0, _shape$style1; if (shape.renderType === 'svg' && shape.svgPath) { var _shape$style, _shape$style2, _shape$style3; return /*#__PURE__*/_react.default.createElement("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "w-full h-full" }, /*#__PURE__*/_react.default.createElement("path", { d: shape.svgPath, fill: ((_shape$style = shape.style) === null || _shape$style === void 0 ? void 0 : _shape$style.fill) || '#ffffff', stroke: ((_shape$style2 = shape.style) === null || _shape$style2 === void 0 ? void 0 : _shape$style2.stroke) || '#000000', strokeWidth: ((_shape$style3 = shape.style) === null || _shape$style3 === void 0 ? void 0 : _shape$style3.strokeWidth) || 2 })); } if (shape.renderType === 'icon') { var _shape$style4, _shape$style5, _shape$style6, _shape$style7; return /*#__PURE__*/_react.default.createElement("div", { className: "w-full h-full flex items-center justify-center text-2xl", style: { backgroundColor: ((_shape$style4 = shape.style) === null || _shape$style4 === void 0 ? void 0 : _shape$style4.fill) || '#ffffff', border: `${((_shape$style5 = shape.style) === null || _shape$style5 === void 0 ? void 0 : _shape$style5.strokeWidth) || 2}px solid ${((_shape$style6 = shape.style) === null || _shape$style6 === void 0 ? void 0 : _shape$style6.stroke) || '#000000'}`, borderRadius: ((_shape$style7 = shape.style) === null || _shape$style7 === void 0 ? void 0 : _shape$style7.borderRadius) || '4px' } }, shape.icon); } // Default rendering return /*#__PURE__*/_react.default.createElement("div", { className: "w-full h-full flex items-center justify-center text-2xl", style: { backgroundColor: ((_shape$style8 = shape.style) === null || _shape$style8 === void 0 ? void 0 : _shape$style8.fill) || '#ffffff', border: `${((_shape$style9 = shape.style) === null || _shape$style9 === void 0 ? void 0 : _shape$style9.strokeWidth) || 2}px solid ${((_shape$style0 = shape.style) === null || _shape$style0 === void 0 ? void 0 : _shape$style0.stroke) || '#000000'}`, borderRadius: ((_shape$style1 = shape.style) === null || _shape$style1 === void 0 ? void 0 : _shape$style1.borderRadius) || '4px' } }, shape.icon); }; return /*#__PURE__*/_react.default.createElement("div", { className: "group relative p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:border-blue-400 cursor-pointer bg-white dark:bg-gray-800 transition-all", onClick: onClick, draggable: true, onDragStart: onDragStart, title: `${shape.name}\n${shape.tags.join(', ')}` }, /*#__PURE__*/_react.default.createElement("div", { className: "w-full h-8 mx-auto mb-1 flex items-center justify-center" }, renderShapePreview()), /*#__PURE__*/_react.default.createElement("div", { className: "text-xs text-center text-gray-600 dark:text-gray-400 truncate" }, shape.name), /*#__PURE__*/_react.default.createElement("button", { onClick: handleFavoriteClick, className: "absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded", title: isFavorite ? 'Remove from favorites' : 'Add to favorites' }, /*#__PURE__*/_react.default.createElement(_lucideReact.Star, { size: 12, className: isFavorite ? 'text-yellow-500 fill-current' : 'text-gray-400 hover:text-yellow-500' })), /*#__PURE__*/_react.default.createElement("div", { className: "absolute inset-0 opacity-0 group-hover:opacity-10 bg-blue-500 rounded-lg pointer-events-none transition-opacity" })); }; var _default = exports.default = ShapeLibraryPanel;