UNPKG

@zamansheikh/percentage-bar

Version:

A customizable, interactive percentage bar React component

250 lines (249 loc) 15.6 kB
"use strict"; "use client"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = PercentageBar; var jsx_runtime_1 = require("react/jsx-runtime"); var react_1 = require("react"); var utils_1 = require("./utils"); var lucide_react_1 = require("lucide-react"); // Predefined colors for new items var COLORS = [ "bg-red-500", "bg-blue-500", "bg-green-500", "bg-yellow-500", "bg-purple-500", "bg-pink-500", "bg-indigo-500", "bg-orange-500", "bg-teal-500", "bg-cyan-500", "bg-lime-500", "bg-emerald-500", "bg-sky-500", "bg-violet-500", "bg-fuchsia-500", "bg-rose-500", "bg-amber-500", ]; function PercentageBar() { var _a = (0, react_1.useState)([ { id: 1, name: "Item 1", percentage: 20, color: "bg-red-500" }, { id: 2, name: "Item 2", percentage: 20, color: "bg-blue-500" }, { id: 3, name: "Item 3", percentage: 20, color: "bg-green-500" }, { id: 4, name: "Item 4", percentage: 20, color: "bg-yellow-500" }, { id: 5, name: "Item 5", percentage: 20, color: "bg-purple-500" }, ]), items = _a[0], setItems = _a[1]; var _b = (0, react_1.useState)(null), draggingIndex = _b[0], setDraggingIndex = _b[1]; var _c = (0, react_1.useState)(0), startX = _c[0], setStartX = _c[1]; var _d = (0, react_1.useState)([]), startPercentages = _d[0], setStartPercentages = _d[1]; var _e = (0, react_1.useState)(null), hoveredIndex = _e[0], setHoveredIndex = _e[1]; var _f = (0, react_1.useState)(null), editingPercentage = _f[0], setEditingPercentage = _f[1]; var barRef = (0, react_1.useRef)(null); var nextId = (0, react_1.useRef)(6); // Start from 6 since we already have 5 items // Handle mouse down to start dragging var handleMouseDown = function (index, e) { e.preventDefault(); setDraggingIndex(index); setStartX(e.clientX); setStartPercentages(items.map(function (item) { return item.percentage; })); }; // Handle mouse move to update percentages var handleMouseMove = function (e) { if (draggingIndex === null || !barRef.current) return; var barWidth = barRef.current.offsetWidth; var deltaX = e.clientX - startX; var deltaPercentage = (deltaX / barWidth) * 100; // Calculate new percentages var newPercentages = __spreadArray([], startPercentages, true); // Adjust current and next item if (draggingIndex < items.length - 1) { // Don't allow items to go below 2% or above 98% var maxIncrease = Math.min(startPercentages[draggingIndex + 1] - 2, 98 - startPercentages[draggingIndex]); var maxDecrease = Math.min(startPercentages[draggingIndex] - 2, 98 - startPercentages[draggingIndex + 1]); var clampedDelta = Math.max(Math.min(deltaPercentage, maxIncrease), -maxDecrease); newPercentages[draggingIndex] = startPercentages[draggingIndex] + clampedDelta; newPercentages[draggingIndex + 1] = startPercentages[draggingIndex + 1] - clampedDelta; } // Update items with new percentages setItems(items.map(function (item, i) { return (__assign(__assign({}, item), { percentage: Math.round(newPercentages[i] * 10) / 10 })); })); }; // Handle mouse up to stop dragging var handleMouseUp = function () { setDraggingIndex(null); }; // Add and remove event listeners (0, react_1.useEffect)(function () { if (draggingIndex !== null) { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } return function () { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [draggingIndex, startX, startPercentages]); // Update item name var updateItemName = function (id, name) { setItems(items.map(function (item) { return (item.id === id ? __assign(__assign({}, item), { name: name }) : item); })); }; // Update item color var updateItemColor = function (id, color) { setItems(items.map(function (item) { return (item.id === id ? __assign(__assign({}, item), { color: "bg-".concat(color, "-500") }) : item); })); }; // Handle percentage input change (while typing) var handlePercentageChange = function (index, value) { setEditingPercentage({ index: index, value: value }); }; // Apply percentage change when input is complete var applyPercentageChange = function (index) { if (!editingPercentage || editingPercentage.index !== index) return; // Convert to number and validate var newPercentage = Number.parseFloat(editingPercentage.value); // If not a valid number, revert to the current percentage if (isNaN(newPercentage)) { setEditingPercentage(null); return; } // Create a copy of the current items var newItems = __spreadArray([], items, true); var oldPercentage = newItems[index].percentage; var difference = newPercentage - oldPercentage; // If there's no change, return early if (difference === 0) { setEditingPercentage(null); return; } // Determine which item to adjust var adjustIndex; if (index === items.length - 1) { // If it's the last item, adjust the second-to-last item adjustIndex = items.length - 2; } else { // Otherwise, adjust the next item adjustIndex = index + 1; } // Calculate the new percentage for the adjusted item var adjustedItemNewPercentage = newItems[adjustIndex].percentage - difference; // Validate the changes if (newPercentage < 2 || adjustedItemNewPercentage < 2) { alert("Percentage cannot be less than 2%"); setEditingPercentage(null); return; } // Apply the changes newItems[index].percentage = Math.round(newPercentage * 10) / 10; newItems[adjustIndex].percentage = Math.round(adjustedItemNewPercentage * 10) / 10; // Ensure the total is exactly 100% var total = newItems.reduce(function (sum, item) { return sum + item.percentage; }, 0); if (Math.abs(total - 100) > 0.1) { // Adjust the last item to make the total exactly 100% var lastIndex = newItems.length - 1; newItems[lastIndex].percentage += 100 - total; newItems[lastIndex].percentage = Math.round(newItems[lastIndex].percentage * 10) / 10; } setItems(newItems); setEditingPercentage(null); }; // Handle key press in percentage input var handlePercentageKeyDown = function (index, e) { if (e.key === "Enter") { applyPercentageChange(index); } }; // Reset to equal distribution var resetDistribution = function () { setItems(items.map(function (item) { return (__assign(__assign({}, item), { percentage: Math.round((100 / items.length) * 10) / 10 })); })); }; // Add new item var addItem = function () { var newItemId = nextId.current; nextId.current += 1; // Calculate new percentage (equal distribution) var newPercentage = 100 / (items.length + 1); // Choose a color that's not already in use if possible var usedColors = items.map(function (item) { return item.color; }); var newColor = COLORS[0]; for (var _i = 0, COLORS_1 = COLORS; _i < COLORS_1.length; _i++) { var color = COLORS_1[_i]; if (!usedColors.includes(color)) { newColor = color; break; } } // Create new item var newItem = { id: newItemId, name: "Item ".concat(newItemId), percentage: newPercentage, color: newColor, }; // Update all items with new percentages var updatedItems = items.map(function (item) { return (__assign(__assign({}, item), { percentage: Math.round(newPercentage * 10) / 10 })); }); setItems(__spreadArray(__spreadArray([], updatedItems, true), [newItem], false)); }; // Delete item var deleteItem = function (id) { // Don't allow deleting if only 2 items remain if (items.length <= 2) { alert("You must have at least 2 items"); return; } // Get the percentage of the item to be deleted var itemToDelete = items.find(function (item) { return item.id === id; }); if (!itemToDelete) return; var percentageToRedistribute = itemToDelete.percentage; var remainingItems = items.filter(function (item) { return item.id !== id; }); // Redistribute the percentage equally among remaining items var addPerItem = percentageToRedistribute / remainingItems.length; setItems(remainingItems.map(function (item) { return (__assign(__assign({}, item), { percentage: Math.round((item.percentage + addPerItem) * 10) / 10 })); })); }; // Color options for the dropdown var colorOptions = [ "red", "blue", "green", "yellow", "purple", "pink", "indigo", "orange", "teal", "cyan", "lime", "emerald", "sky", "violet", "fuchsia", "rose", "amber", ]; return ((0, jsx_runtime_1.jsxs)("div", { className: "w-full max-w-4xl mx-auto p-4 md:p-6 space-y-6", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-xl md:text-2xl font-bold text-center", children: "Interactive Percentage Bar" }), (0, jsx_runtime_1.jsx)("div", { ref: barRef, className: "w-full h-12 md:h-16 flex rounded-md overflow-hidden border border-gray-300 relative", children: items.map(function (item, index) { return ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)(item.color, "h-full flex items-center justify-center relative transition-all duration-75", index < items.length - 1 ? "cursor-ew-resize" : ""), style: { width: "".concat(item.percentage, "%") }, onMouseEnter: function () { return setHoveredIndex(index); }, onMouseLeave: function () { return setHoveredIndex(null); }, children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-white font-medium text-xs md:text-sm", children: [item.percentage.toFixed(1), "%"] }), hoveredIndex === index && ((0, jsx_runtime_1.jsx)("div", { className: "absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-full bg-black text-white px-2 py-1 rounded text-xs whitespace-nowrap z-20", children: item.name })), index < items.length - 1 && ((0, jsx_runtime_1.jsx)("div", { className: "absolute right-0 top-0 w-1 md:w-2 h-full bg-black bg-opacity-20 cursor-ew-resize z-10 hover:bg-opacity-30", onMouseDown: function (e) { return handleMouseDown(index, e); } }))] }, item.id)); }) }), (0, jsx_runtime_1.jsx)("div", { className: "space-y-3 max-h-[50vh] overflow-y-auto pr-1", children: items.map(function (item, index) { return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap md:flex-nowrap items-center gap-2 md:gap-3 py-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center", children: (0, jsx_runtime_1.jsx)("select", { value: item.color.replace("bg-", "").replace("-500", ""), onChange: function (e) { return updateItemColor(item.id, e.target.value); }, className: "border border-gray-300 rounded h-8 text-sm px-1", style: { backgroundColor: "var(--".concat(item.color.replace("bg-", "").replace("-500", ""), "-500)"), color: "black", }, children: colorOptions.map(function (color) { return ((0, jsx_runtime_1.jsx)("option", { value: color, children: color }, color)); }) }) }), (0, jsx_runtime_1.jsx)("input", { type: "text", value: item.name, onChange: function (e) { return updateItemName(item.id, e.target.value); }, className: "border border-gray-300 rounded px-3 py-1.5 flex-1 min-w-[120px]" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", value: (editingPercentage === null || editingPercentage === void 0 ? void 0 : editingPercentage.index) === index ? editingPercentage.value : item.percentage.toFixed(1), onChange: function (e) { return handlePercentageChange(index, e.target.value); }, onBlur: function () { return applyPercentageChange(index); }, onKeyDown: function (e) { return handlePercentageKeyDown(index, e); }, className: "border border-gray-300 rounded px-3 py-1.5 w-16 md:w-20 text-right" }), (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium ml-1", children: "%" })] }), (0, jsx_runtime_1.jsx)("button", { onClick: function () { return deleteItem(item.id); }, className: "p-1.5 text-red-500 hover:text-red-700 transition-colors", title: "Delete item", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 18 }) })] }, item.id)); }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap gap-3", children: [(0, jsx_runtime_1.jsx)("button", { onClick: resetDistribution, className: "px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors", children: "Reset to Equal Distribution" }), (0, jsx_runtime_1.jsxs)("button", { onClick: addItem, className: "px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors flex items-center gap-1", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.PlusCircle, { size: 18 }), (0, jsx_runtime_1.jsx)("span", { children: "Add Item" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-sm text-gray-500 space-y-1", children: [(0, jsx_runtime_1.jsx)("p", { children: "Drag the dividers between sections to adjust percentages or input values directly." }), (0, jsx_runtime_1.jsx)("p", { children: "When you change a percentage, it affects the item to the right (or the second-to-last item if editing the last one)." }), (0, jsx_runtime_1.jsx)("p", { children: "Minimum value for any section is 2%." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-8 pt-4 border-t border-gray-200 text-center text-sm text-gray-500 flex flex-wrap items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { children: "Developed by Zaman Sheikh" }), (0, jsx_runtime_1.jsxs)("a", { href: "https://github.com/zamansheikh", target: "_blank", rel: "noopener noreferrer", className: "flex items-center text-gray-700 hover:text-black transition-colors", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Github, { size: 16, className: "mr-1" }), "github.com/zamansheikh"] })] })] })); }