fix(kanban): responsive layout, drag-drop offset, and project view enhancements
Kanban pipeline board: - Fix h-full flex-col layout so board fills viewport height on all screen sizes - KanbanColumn drop zone: flex-1 min-h-0 so it expands to fill full column height - KanbanCard: extract KanbanCardDisplay as named export for DragOverlay rendering above overflow containers - KanbanPage: lift DndContext to page root; add "Add Stage" shortcut to page header - PageTransition: remove slide-in-from-bottom-4 transform — the CSS transform ancestor was creating a containing block for position:fixed DragOverlay, causing drag ghost to render at a viewport offset - Migrate fireCampaignToast to toast.custom() Sonner v2 API with explicit card styling Project & lead views: - LeadProjectPage: enrich project detail with richer info sections aligned with project stage - OwnerProjectDetail: expand construction project detail with additional data panels - OwnerProjectList: project list view improvements - mockStore: extend lead/project data to support richer project detail rendering
This commit is contained in:
@@ -13,7 +13,7 @@ const PageTransition = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
key={location.pathname}
|
||||
className="h-full animate-in fade-in slide-in-from-bottom-4 duration-500 ease-out fill-mode-forwards"
|
||||
className="h-full animate-in fade-in duration-300 ease-out"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -29,33 +29,23 @@ function avatarColor(name) {
|
||||
return AVATAR_COLORS[idx];
|
||||
}
|
||||
|
||||
export default function KanbanCard({ lead, columnColor, onClick, isDragOverlay }) {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: lead.id,
|
||||
data: { lead },
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
opacity: isDragging ? 0.35 : 1,
|
||||
zIndex: isDragging ? 999 : 'auto',
|
||||
};
|
||||
|
||||
// ── Pure visual display — no dnd bindings. Used both inside KanbanCard and
|
||||
// inside DragOverlay (so the dragged clone renders above overflow containers).
|
||||
export function KanbanCardDisplay({ lead, columnColor, isOverlay = false, onClick, forwardRef, dragProps = {} }) {
|
||||
const days = getDaysInStage(lead.activity);
|
||||
const initials = getInitials(lead.name);
|
||||
const avatarCls = avatarColor(lead.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isDragOverlay ? undefined : setNodeRef}
|
||||
style={isDragOverlay ? undefined : style}
|
||||
{...(isDragOverlay ? {} : { ...attributes, ...listeners })}
|
||||
onClick={isDragOverlay ? undefined : onClick}
|
||||
className={`group relative bg-white dark:bg-zinc-900 border rounded-xl cursor-grab active:cursor-grabbing transition-all select-none
|
||||
${isDragOverlay
|
||||
? 'shadow-2xl border-zinc-200 dark:border-white/20 rotate-1 scale-[1.02]'
|
||||
: 'border-zinc-200 dark:border-white/[0.07] hover:shadow-md dark:hover:shadow-black/30 hover:border-zinc-300 dark:hover:border-white/[0.14]'
|
||||
}`}
|
||||
ref={forwardRef}
|
||||
{...dragProps}
|
||||
onClick={onClick}
|
||||
className={`group relative bg-white dark:bg-zinc-900 border rounded-xl select-none transition-[border-color,box-shadow] ${
|
||||
isOverlay
|
||||
? 'border-blue-400 dark:border-blue-500 rotate-1 shadow-[0_20px_40px_rgba(0,0,0,0.35)] cursor-grabbing scale-[1.03]'
|
||||
: 'border-zinc-200 dark:border-white/[0.07] cursor-grab active:cursor-grabbing hover:shadow-md dark:hover:shadow-black/30 hover:border-zinc-300 dark:hover:border-white/[0.14]'
|
||||
}`}
|
||||
>
|
||||
{/* Left color accent bar */}
|
||||
<div
|
||||
@@ -112,3 +102,24 @@ export default function KanbanCard({ lead, columnColor, onClick, isDragOverlay }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Draggable wrapper — ghost stays frozen in place (no transform applied),
|
||||
// DragOverlay handles all visual movement above overflow containers.
|
||||
export default function KanbanCard({ lead, columnColor, onClick }) {
|
||||
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
||||
id: lead.id,
|
||||
data: { lead, columnColor },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{ opacity: isDragging ? 0.3 : 1 }}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={isDragging ? undefined : onClick}
|
||||
>
|
||||
<KanbanCardDisplay lead={lead} columnColor={columnColor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Plus, Settings, Trash2, Pencil } from 'lucide-react';
|
||||
import { Trash2, Pencil } from 'lucide-react';
|
||||
import KanbanCard from './KanbanCard';
|
||||
|
||||
export default function KanbanColumn({
|
||||
@@ -16,9 +16,9 @@ export default function KanbanColumn({
|
||||
const isBucket = column.type === 'bucket';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col shrink-0 w-[272px]">
|
||||
<div className="flex flex-col shrink-0 w-[272px] h-full">
|
||||
{/* Column header */}
|
||||
<div className="mb-3">
|
||||
<div className="shrink-0 mb-3">
|
||||
<div className="flex items-center justify-between gap-2 px-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: column.color }} />
|
||||
@@ -72,16 +72,17 @@ export default function KanbanColumn({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Drop zone */}
|
||||
{/* Drop zone — flex-1 + min-h-0 so it fills remaining column height */}
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`group flex-1 min-h-[120px] rounded-2xl p-2 transition-all duration-150 ${
|
||||
className={`group flex-1 min-h-0 rounded-2xl transition-all duration-150 ${
|
||||
isOver
|
||||
? 'bg-blue-50/80 dark:bg-blue-500/10 ring-2 ring-inset ring-blue-400/40'
|
||||
: 'bg-zinc-100/60 dark:bg-zinc-800/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Inner scroll container — cards scroll within the column */}
|
||||
<div className="flex flex-col gap-2 p-2 h-full overflow-y-auto overflow-x-hidden scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
|
||||
<AnimatePresence initial={false}>
|
||||
{leads.map((lead, i) => (
|
||||
<motion.div
|
||||
@@ -109,15 +110,6 @@ export default function KanbanColumn({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add column button after last stage column */}
|
||||
{isLast && canManage && (
|
||||
<button
|
||||
onClick={onAddColumn}
|
||||
className="mt-3 flex items-center gap-1.5 px-3 py-2 rounded-xl border border-dashed border-zinc-300 dark:border-zinc-700 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:border-zinc-400 dark:hover:border-zinc-500 text-xs font-semibold transition-colors"
|
||||
>
|
||||
<Plus size={12} /> Add Stage
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user