@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
368 lines (359 loc) • 18.4 kB
JavaScript
"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;