aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
351 lines (348 loc) • 15.2 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { forwardRef, useState, useRef, useCallback } from 'react';
import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { useA11yId } from '../../utils/a11y.js';
import { useGlassSound } from '../../utils/soundDesign.js';
const GlassKanbanBoard = /*#__PURE__*/forwardRef(({
columns,
title,
description,
enableDrag = true,
showCardCounts = true,
showColumnLimits = true,
cardSize = "md",
virtualized = false,
columnWidth = "300px",
maxHeight,
renderCard,
renderColumnHeader,
onCardMove,
onCardClick,
onCardDoubleClick,
onColumnAdd,
onColumnUpdate,
onColumnDelete,
onCardAdd,
onCardUpdate,
onCardDelete,
showAddColumn = true,
showAddCard = true,
emptyState,
respectMotionPreference = true,
className,
...props
}, ref) => {
const {
prefersReducedMotion
} = useMotionPreferenceContext();
const {
play,
feedback
} = useGlassSound();
const kanbanId = useA11yId("glass-kanban-board");
const [dragState, setDragState] = useState({
isDragging: false
});
const [scrollPositions, setScrollPositions] = useState({});
const columnRefs = useRef({});
// Card size configurations
const cardSizeConfig = {
sm: {
padding: "glass-p-3",
text: "glass-text-sm",
spacing: "glass-gap-2",
minHeight: "min-h-[80px]"
},
md: {
padding: "glass-p-4",
text: "glass-text-sm",
spacing: "glass-gap-3",
minHeight: "min-h-[100px]"
},
lg: {
padding: "glass-p-6",
text: "glass-text-base",
spacing: "glass-gap-4",
minHeight: "min-h-[120px]"
}
};
const config = cardSizeConfig[cardSize];
// Priority colors
const priorityColors = {
low: "border-green-500/30 bg-green-500/5",
medium: "border-yellow-500/30 bg-yellow-500/5",
high: "border-orange-500/30 bg-orange-500/5",
urgent: "border-red-500/30 bg-red-500/5"
};
// Handle drag start
const handleDragStart = useCallback((card, columnId) => {
if (!enableDrag) return;
setDragState({
isDragging: true,
draggedCard: card,
draggedFrom: columnId
});
feedback("slide");
}, [enableDrag, feedback]);
// Handle drag over
const handleDragOver = useCallback((e, columnId) => {
if (!dragState.isDragging) return;
e.preventDefault();
setDragState(prev => ({
...prev,
dragOverColumn: columnId
}));
}, [dragState.isDragging]);
// Handle drag end
const handleDragEnd = useCallback(() => {
setDragState({
isDragging: false
});
}, []);
// Handle drop
const handleDrop = useCallback((e, columnId, position) => {
e.preventDefault();
if (!dragState.draggedCard || !dragState.draggedFrom) return;
const targetPosition = position ?? 0;
if (dragState.draggedFrom !== columnId) {
onCardMove?.(dragState.draggedCard.id, dragState.draggedFrom, columnId, targetPosition);
feedback("success");
}
setDragState({
isDragging: false
});
}, [dragState, onCardMove, feedback]);
// Get card priority indicator
const getPriorityIndicator = priority => {
if (!priority) return null;
const colors = {
low: "bg-green-500",
medium: "bg-yellow-500",
high: "bg-orange-500",
urgent: "bg-red-500"
};
return jsx("div", {
className: cn("w-1 h-full absolute left-0 top-0 rounded-l-md", colors[priority])
});
};
// Default card renderer
const defaultRenderCard = useCallback((card, columnId) => {
return jsxs(OptimizedGlassCore, {
draggable: enableDrag && !columns.find(c => c.id === columnId)?.readOnly,
onDragStart: e => handleDragStart(card, columnId),
onDragEnd: handleDragEnd,
onClick: () => onCardClick?.(card, columnId),
onDoubleClick: () => onCardDoubleClick?.(card, columnId),
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-kanban-card relative cursor-pointer transition-all duration-200", "glass-backdrop-blur-md border border-border/20 glass-radius-md", config.padding, config.minHeight, "hover:scale-[1.02] hover:shadow-lg", dragState.isDragging && dragState.draggedCard?.id === card.id && "opacity-50 scale-95", card.priority && priorityColors[card.priority], enableDrag && "cursor-grab active:cursor-grabbing"),
role: "article",
"aria-label": `Card: ${card.title}`,
tabIndex: 0,
onKeyDown: e => {
if (e.key === "Enter" || e.key === " ") {
onCardClick?.(card, columnId);
}
},
children: [getPriorityIndicator(card.priority), jsxs("div", {
className: cn("relative", config.spacing),
children: [jsx("h3", {
className: cn("font-semibold text-foreground line-clamp-2", config.text),
children: card.title
}), card.description && jsx("p", {
className: 'glass-text-secondary glass-text-sm line-clamp-3',
children: card.description
}), card.content && jsx("div", {
className: "glass-text-sm",
children: card.content
}), card.tags && card.tags.length > 0 && jsxs("div", {
className: "glass-flex glass-flex-wrap glass-gap-1",
children: [card.tags.slice(0, 3).map((tag, index) => jsx("span", {
className: 'glass-px-2 glass-py-1 glass-text-xs glass-surface-primary/10 text-primary glass-radius-full',
children: tag
}, index)), card.tags.length > 3 && jsxs("span", {
className: "glass-px-2 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary glass-radius-full",
children: ["+", card.tags.length - 3]
})]
}), jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between pt-2',
children: [card.assignee && jsx("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: card.assignee.avatar ? jsx("img", {
src: card.assignee.avatar,
alt: card.assignee.name,
className: 'w-6 h-6 glass-radius-full'
}) : jsx("div", {
className: 'w-6 h-6 glass-surface-primary/20 text-primary glass-radius-full glass-flex glass-items-center glass-justify-center glass-text-xs font-medium',
children: card.assignee.name.charAt(0).toUpperCase()
})
}), card.dueDate && jsx("div", {
className: cn("glass-text-xs glass-px-2 glass-py-1 glass-radius-md", new Date(card.dueDate) < new Date() ? "bg-red-500/10 text-red-600" : new Date(card.dueDate).getTime() - Date.now() < 24 * 60 * 60 * 1000 ? "bg-yellow-500/10 text-yellow-600" : "bg-muted/20 glass-text-secondary"),
children: new Date(card.dueDate).toLocaleDateString()
})]
})]
})]
}, card.id);
}, [enableDrag, columns, handleDragStart, handleDragEnd, onCardClick, onCardDoubleClick, config, dragState, priorityColors]);
// Default column header renderer
const defaultRenderColumnHeader = useCallback(column => {
const cardCount = column.cards.length;
const isOverLimit = column.limit && cardCount > column.limit;
return jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("div", {
className: 'w-3 h-3 glass-radius-full',
style: {
backgroundColor: column.color || "var(--glass-gray-500)"
}
}), jsx("h2", {
className: 'font-semibold text-primary',
children: column.title
}), showCardCounts && jsxs("span", {
className: cn("glass-px-2 glass-py-1 glass-text-xs glass-radius-full", isOverLimit ? "bg-red-500/20 text-red-600" : "bg-muted/20 glass-text-secondary"),
children: [cardCount, showColumnLimits && column.limit && ` / ${column.limit}`]
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: [showAddCard && !column.readOnly && jsx("button", {
onClick: () => onCardAdd?.(column.id),
className: cn("w-6 h-6 glass-radius-full bg-primary/10 hover:bg-primary/20", "text-primary glass-text-sm font-bold transition-colors", "flex items-center justify-center glass-focus glass-touch-target glass-contrast-guard"),
title: "Add card",
"aria-label": `Add card to ${column.title}`,
children: "+"
}), onColumnDelete && jsx("button", {
onClick: () => onColumnDelete(column.id),
className: cn("w-6 h-6 glass-radius-full bg-red-500/10 hover:bg-red-500/20", "text-red-500 glass-text-sm transition-colors", "flex items-center justify-center glass-focus glass-touch-target glass-contrast-guard"),
title: "Delete column",
"aria-label": `Delete ${column.title} column`,
children: "\u00D7"
})]
})]
});
}, [showCardCounts, showColumnLimits, showAddCard, onCardAdd, onColumnDelete]);
// Handle column scroll
const handleColumnScroll = useCallback((columnId, scrollTop) => {
setScrollPositions(prev => ({
...prev,
[columnId]: scrollTop
}));
}, []);
return jsx(OptimizedGlassCore, {
ref: ref,
id: kanbanId,
elevation: "level1",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-kanban-board glass-radius-lg glass-backdrop-blur-md border border-border/20", className),
style: maxHeight ? {
maxHeight
} : undefined,
...props,
children: jsxs(MotionFramer, {
preset: !prefersReducedMotion && respectMotionPreference ? "fadeIn" : "none",
className: "glass-flex glass-flex-col glass-h-full",
children: [(title || description) && jsxs("div", {
className: "glass-p-6 glass-border-b glass-border-glass-border/20",
children: [title && jsx("h1", {
className: 'glass-text-xl font-bold text-primary mb-2',
children: title
}), description && jsx("p", {
className: "glass-text-secondary",
children: description
})]
}), jsx("div", {
className: 'glass-flex-1 overflow-x-auto overflow-y-hidden',
children: columns.length === 0 && emptyState ? jsx("div", {
className: "glass-flex glass-items-center glass-justify-center glass-h-full glass-p-8",
children: emptyState
}) : jsxs("div", {
className: "glass-flex glass-gap-6 glass-p-6 glass-h-full",
style: {
minWidth: "fit-content"
},
children: [columns.map(column => jsx(MotionFramer, {
preset: !prefersReducedMotion && respectMotionPreference ? "slideUp" : "none",
delay: columns.indexOf(column) * 100,
children: jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
className: cn("glass-kanban-column flex flex-col h-full", "glass-backdrop-blur-md border border-border/20 glass-radius-lg", dragState.dragOverColumn === column.id && "ring-2 ring-primary/50"),
style: {
width: columnWidth,
minWidth: columnWidth
},
onDragOver: e => handleDragOver(e, column.id),
onDrop: e => handleDrop(e, column.id),
role: "region",
"aria-label": `Column: ${column.title}`,
children: [jsxs("div", {
className: "glass-p-4 glass-border-b glass-border-glass-border/20",
children: [renderColumnHeader ? renderColumnHeader(column) : defaultRenderColumnHeader(column), column.description && jsx("p", {
className: "glass-text-sm glass-text-secondary glass-mt-2",
children: column.description
})]
}), jsxs("div", {
ref: el => {
if (el) columnRefs.current[column.id] = el;
},
className: 'glass-flex-1 overflow-y-auto glass-p-4 glass-gap-3',
onScroll: e => handleColumnScroll(column.id, e.currentTarget.scrollTop),
children: [column.cards.map((card, index) => jsx(MotionFramer, {
preset: !prefersReducedMotion && respectMotionPreference ? "slideUp" : "none",
delay: index * 50,
children: renderCard ? renderCard(card, column.id) : defaultRenderCard(card, column.id)
}, card.id)), enableDrag && dragState.isDragging && jsx("div", {
className: cn("h-2 glass-radius-md border-2 border-dashed border-primary/30 transition-all", dragState.dragOverColumn === column.id && "bg-primary/10"),
onDragOver: e => e.preventDefault(),
onDrop: e => handleDrop(e, column.id, column.cards.length)
}), column.cards.length === 0 && jsx("div", {
className: 'glass-flex glass-items-center glass-justify-center h-32 glass-text-secondary glass-text-sm glass-border-2 glass-border-dashed glass-border-glass-border/20 glass-radius-lg',
children: column.readOnly ? "No cards" : "Drop cards here or click + to add"
})]
})]
})
}, column.id)), showAddColumn && onColumnAdd && jsx(MotionFramer, {
preset: !prefersReducedMotion && respectMotionPreference ? "slideUp" : "none",
delay: columns.length * 100,
children: jsxs("button", {
onClick: onColumnAdd,
className: cn("flex flex-col items-center justify-center h-full min-w-[280px]", "border-2 border-dashed border-border/30 glass-radius-lg", "hover:border-primary/50 hover:bg-primary/5 transition-all", "glass-text-secondary hover:text-primary glass-focus glass-touch-target glass-contrast-guard"),
"aria-label": "Add new column",
children: [jsx("div", {
className: 'w-12 h-12 glass-radius-full bg-transparent/10 glass-flex glass-items-center glass-justify-center mb-2',
children: jsx("span", {
className: 'glass-text-2xl font-light',
children: "+"
})
}), jsx("span", {
className: 'glass-text-sm font-medium',
children: "Add Column"
})]
})
})]
})
})]
})
});
});
GlassKanbanBoard.displayName = "GlassKanbanBoard";
export { GlassKanbanBoard, GlassKanbanBoard as default };
//# sourceMappingURL=GlassKanbanBoard.js.map