{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "kanban", "title": "Kanban Board", "description": "A block for displaying a kanban board with a rough styling.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json", "https://www.bydefaulthuman.fun/r/avatar.json", "https://www.bydefaulthuman.fun/r/badge.json", "https://www.bydefaulthuman.fun/r/button.json", "https://www.bydefaulthuman.fun/r/card.json", "https://www.bydefaulthuman.fun/r/textarea.json", "https://www.bydefaulthuman.fun/r/tooltip.json", "https://www.bydefaulthuman.fun/r/input.json", "https://www.bydefaulthuman.fun/r/sticky-note.json", "https://www.bydefaulthuman.fun/r/select.json" ], "files": [ { "path": "registry/new-york/blocks/KanbanColumnBlock.tsx", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Avatar } from \"@/components/crumble/ui/avatar\";\nimport { Badge } from \"@/components/crumble/ui/badge\";\nimport { Button } from \"@/components/crumble/ui/button\";\nimport { Card } from \"@/components/crumble/ui/card\";\nimport { Input } from \"@/components/crumble/ui/input\";\nimport { Select } from \"@/components/crumble/ui/select\";\nimport {\n StickyNote,\n type StickyNoteColor,\n} from \"@/components/crumble/ui/sticky-note\";\nimport { Textarea } from \"@/components/crumble/ui/textarea\";\nimport { Tooltip } from \"@/components/crumble/ui/tooltip\";\n\nexport interface KanbanTask {\n assigneeFallback?: string;\n description?: string;\n id: string;\n priority: \"high\" | \"medium\" | \"low\";\n tag?: string;\n title: string;\n}\n\nexport interface KanbanColumn {\n color: StickyNoteColor;\n id: string;\n label: string;\n tasks: KanbanTask[];\n}\n\ninterface NewTaskDraft {\n assigneeFallback: string;\n description: string;\n priority: KanbanTask[\"priority\"];\n tag: string;\n title: string;\n}\n\ninterface EditingTaskState {\n columnId: string;\n taskId: string;\n value: string;\n}\n\nexport interface KanbanColumnBlockProps {\n addTaskLabel?: string;\n emptyStateLabel?: string;\n initialColumns?: KanbanColumn[];\n onBoardChange?: (columns: KanbanColumn[]) => void;\n priorityFilterLabel?: string;\n searchPlaceholder?: string;\n title?: string;\n}\n\nconst DEFAULT_COLUMNS: KanbanColumn[] = [\n {\n id: \"todo\",\n label: \"To Do\",\n color: \"yellow\",\n tasks: [\n {\n id: \"task-1\",\n title: \"Audit empty states\",\n description: \"Check docs pages that still use placeholder copy.\",\n priority: \"high\",\n assigneeFallback: \"GL\",\n tag: \"design\",\n },\n {\n id: \"task-2\",\n title: \"Ship rough drawer docs\",\n description: \"Document the open and close patterns.\",\n priority: \"medium\",\n assigneeFallback: \"AC\",\n tag: \"docs\",\n },\n ],\n },\n {\n id: \"doing\",\n label: \"In Progress\",\n color: \"blue\",\n tasks: [\n {\n id: \"task-3\",\n title: \"Replace direct rough calls\",\n description: \"Move remaining components to useRough helpers.\",\n priority: \"high\",\n assigneeFallback: \"DP\",\n tag: \"eng\",\n },\n ],\n },\n {\n id: \"done\",\n label: \"Done\",\n color: \"green\",\n tasks: [\n {\n id: \"task-4\",\n title: \"Polish preview container\",\n description:\n \"Align install command copy with the current registry path.\",\n priority: \"low\",\n assigneeFallback: \"BM\",\n tag: \"site\",\n },\n ],\n },\n];\n\nfunction createEmptyDraft(): NewTaskDraft {\n return {\n title: \"\",\n description: \"\",\n assigneeFallback: \"\",\n tag: \"\",\n priority: \"medium\",\n };\n}\n\nexport function KanbanColumnBlock({\n addTaskLabel = \"+ Add task\",\n emptyStateLabel = \"No tasks match the current filters.\",\n initialColumns = DEFAULT_COLUMNS,\n onBoardChange,\n priorityFilterLabel = \"Priority\",\n searchPlaceholder = \"Search tasks...\",\n title = \"Sprint board\",\n}: KanbanColumnBlockProps) {\n const [columns, setColumns] = useState(initialColumns);\n const [search, setSearch] = useState(\"\");\n const [priorityFilter, setPriorityFilter] = useState<\n \"all\" | KanbanTask[\"priority\"]\n >(\"all\");\n const [openFormColumnId, setOpenFormColumnId] = useState(null);\n const [drafts, setDrafts] = useState>({});\n const [editingTask, setEditingTask] = useState(null);\n\n useEffect(() => {\n setColumns(initialColumns);\n }, [initialColumns]);\n\n const updateBoard = (\n updater: (current: KanbanColumn[]) => KanbanColumn[],\n ) => {\n setColumns((current) => {\n const next = updater(current);\n onBoardChange?.(next);\n return next;\n });\n };\n\n const getDraft = (columnId: string) => drafts[columnId] ?? createEmptyDraft();\n\n const isVisibleTask = (task: KanbanTask) => {\n const matchesPriority =\n priorityFilter === \"all\" || task.priority === priorityFilter;\n const query = search.trim().toLowerCase();\n if (!matchesPriority) return false;\n if (!query) return true;\n return [task.title, task.description, task.tag, task.assigneeFallback]\n .filter(Boolean)\n .some((value) => String(value).toLowerCase().includes(query));\n };\n\n const moveTask = (\n taskId: string,\n fromColumnId: string,\n toColumnId: string,\n ) => {\n if (fromColumnId === toColumnId) return;\n\n updateBoard((current) => {\n let movedTask: KanbanTask | undefined;\n const removed = current.map((column) => {\n if (column.id !== fromColumnId) return column;\n const nextTasks = column.tasks.filter((task) => {\n if (task.id === taskId) {\n movedTask = task;\n return false;\n }\n return true;\n });\n return { ...column, tasks: nextTasks };\n });\n\n if (!movedTask) return current;\n\n return removed.map((column) =>\n column.id === toColumnId\n ? { ...column, tasks: [...column.tasks, movedTask as KanbanTask] }\n : column,\n );\n });\n };\n\n const submitDraft = (columnId: string) => {\n const draft = getDraft(columnId);\n if (!draft.title.trim()) return;\n\n const task: KanbanTask = {\n id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n title: draft.title.trim(),\n description: draft.description.trim() || undefined,\n assigneeFallback: draft.assigneeFallback.trim() || undefined,\n tag: draft.tag.trim() || undefined,\n priority: draft.priority,\n };\n\n updateBoard((current) =>\n current.map((column) =>\n column.id === columnId\n ? { ...column, tasks: [...column.tasks, task] }\n : column,\n ),\n );\n\n setDrafts((current) => ({ ...current, [columnId]: createEmptyDraft() }));\n setOpenFormColumnId(null);\n };\n\n const commitEdit = () => {\n if (!editingTask) return;\n const value = editingTask.value.trim();\n if (!value) {\n setEditingTask(null);\n return;\n }\n\n updateBoard((current) =>\n current.map((column) =>\n column.id === editingTask.columnId\n ? {\n ...column,\n tasks: column.tasks.map((task) =>\n task.id === editingTask.taskId\n ? { ...task, title: value }\n : task,\n ),\n }\n : column,\n ),\n );\n setEditingTask(null);\n };\n\n return (\n
\n
\n

{title}

\n
\n setSearch(event.target.value)}\n placeholder={searchPlaceholder}\n value={search}\n />\n \n setPriorityFilter(value as \"all\" | KanbanTask[\"priority\"])\n }\n options={[\n { value: \"all\", label: `${priorityFilterLabel}: all` },\n { value: \"high\", label: `${priorityFilterLabel}: high` },\n { value: \"medium\", label: `${priorityFilterLabel}: medium` },\n { value: \"low\", label: `${priorityFilterLabel}: low` },\n ]}\n value={priorityFilter}\n />\n
\n
\n\n
\n {columns.map((column, columnIndex) => {\n const draft = getDraft(column.id);\n const visibleTasks = column.tasks.filter(isVisibleTask);\n\n return (\n \n
\n
\n

{column.label}

\n {visibleTasks.length}\n
\n \n setOpenFormColumnId((current) =>\n current === column.id ? null : column.id,\n )\n }\n size=\"sm\"\n variant=\"ghost\"\n >\n {addTaskLabel}\n \n
\n\n {openFormColumnId === column.id ? (\n
\n \n setDrafts((current) => ({\n ...current,\n [column.id]: { ...draft, title: event.target.value },\n }))\n }\n value={draft.title}\n />\n \n setDrafts((current) => ({\n ...current,\n [column.id]: {\n ...draft,\n description: event.target.value,\n },\n }))\n }\n value={draft.description}\n />\n
\n \n setDrafts((current) => ({\n ...current,\n [column.id]: {\n ...draft,\n assigneeFallback: event.target.value.toUpperCase(),\n },\n }))\n }\n value={draft.assigneeFallback}\n />\n \n setDrafts((current) => ({\n ...current,\n [column.id]: { ...draft, tag: event.target.value },\n }))\n }\n value={draft.tag}\n />\n
\n \n setDrafts((current) => ({\n ...current,\n [column.id]: {\n ...draft,\n priority: value as KanbanTask[\"priority\"],\n },\n }))\n }\n options={[\n { value: \"high\", label: \"High priority\" },\n { value: \"medium\", label: \"Medium priority\" },\n { value: \"low\", label: \"Low priority\" },\n ]}\n value={draft.priority}\n />\n
\n \n setOpenFormColumnId(null)}\n size=\"sm\"\n variant=\"ghost\"\n >\n Cancel\n \n
\n
\n ) : null}\n\n
\n {visibleTasks.map((task, taskIndex) => {\n const isEditing =\n editingTask?.columnId === column.id &&\n editingTask.taskId === task.id;\n\n return (\n \n setEditingTask((current) =>\n current\n ? { ...current, value: event.target.value }\n : current,\n )\n }\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitEdit();\n }\n if (event.key === \"Escape\") {\n setEditingTask(null);\n }\n }}\n value={editingTask?.value ?? \"\"}\n />\n ) : (\n \n setEditingTask({\n columnId: column.id,\n taskId: task.id,\n value: task.title,\n })\n }\n type=\"button\"\n >\n {task.title}\n \n )\n }\n >\n
\n {task.description ? (\n

\n {task.description}\n

\n ) : null}\n\n
\n \n {task.priority}\n \n {task.tag ? (\n {task.tag}\n ) : null}\n {task.assigneeFallback ? (\n \n ) : null}\n
\n\n
\n {columns\n .filter(\n (targetColumn) => targetColumn.id !== column.id,\n )\n .map((targetColumn) => (\n \n \n \n moveTask(\n task.id,\n column.id,\n targetColumn.id,\n )\n }\n size=\"sm\"\n variant=\"ghost\"\n >\n {targetColumn.label}\n \n \n \n ))}\n \n \n \n updateBoard((current) =>\n current.map((currentColumn) =>\n currentColumn.id === column.id\n ? {\n ...currentColumn,\n tasks: currentColumn.tasks.filter(\n (currentTask) =>\n currentTask.id !== task.id,\n ),\n }\n : currentColumn,\n ),\n )\n }\n size=\"sm\"\n variant=\"ghost\"\n >\n Delete\n \n \n \n
\n
\n \n );\n })}\n\n {visibleTasks.length === 0 ? (\n

\n {emptyStateLabel}\n

\n ) : null}\n
\n
\n );\n })}\n
\n
\n );\n}\n", "type": "registry:ui", "target": "components/crumble/blocks/kanban-column.tsx" } ], "type": "registry:block" }