@btst/stack 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +1 -1
  2. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +1 -1
  3. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +77 -0
  4. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +79 -3
  5. package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.cjs +140 -0
  6. package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.mjs +119 -0
  7. package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.cjs +80 -0
  8. package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.mjs +64 -0
  9. package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.cjs +18 -0
  10. package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.mjs +16 -0
  11. package/dist/packages/better-stack/src/plugins/kanban/api/plugin.cjs +846 -0
  12. package/dist/packages/better-stack/src/plugins/kanban/api/plugin.mjs +844 -0
  13. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.cjs +85 -0
  14. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.mjs +83 -0
  15. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.cjs +72 -0
  16. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.mjs +70 -0
  17. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.cjs +200 -0
  18. package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.mjs +198 -0
  19. package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.cjs +47 -0
  20. package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.mjs +45 -0
  21. package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.cjs +30 -0
  22. package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.mjs +28 -0
  23. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.cjs +27 -0
  24. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.mjs +25 -0
  25. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.cjs +31 -0
  26. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +458 -0
  27. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +456 -0
  28. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.mjs +29 -0
  29. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.cjs +30 -0
  30. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.cjs +72 -0
  31. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.mjs +70 -0
  32. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.mjs +28 -0
  33. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.cjs +30 -0
  34. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.cjs +51 -0
  35. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.mjs +49 -0
  36. package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.mjs +28 -0
  37. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.cjs +76 -0
  38. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.mjs +74 -0
  39. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.cjs +27 -0
  40. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.mjs +25 -0
  41. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.cjs +32 -0
  42. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.mjs +30 -0
  43. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.cjs +78 -0
  44. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.mjs +76 -0
  45. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.cjs +15 -0
  46. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.mjs +13 -0
  47. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.cjs +68 -0
  48. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.mjs +66 -0
  49. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.cjs +32 -0
  50. package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.mjs +30 -0
  51. package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +391 -0
  52. package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +381 -0
  53. package/dist/packages/better-stack/src/plugins/kanban/client/plugin.cjs +290 -0
  54. package/dist/packages/better-stack/src/plugins/kanban/client/plugin.mjs +288 -0
  55. package/dist/packages/better-stack/src/plugins/kanban/db.cjs +125 -0
  56. package/dist/packages/better-stack/src/plugins/kanban/db.mjs +123 -0
  57. package/dist/packages/better-stack/src/plugins/kanban/schemas.cjs +117 -0
  58. package/dist/packages/better-stack/src/plugins/kanban/schemas.mjs +102 -0
  59. package/dist/packages/better-stack/src/plugins/kanban/utils.cjs +49 -0
  60. package/dist/packages/better-stack/src/plugins/kanban/utils.mjs +45 -0
  61. package/dist/packages/ui/src/components/avatar.cjs +58 -0
  62. package/dist/packages/ui/src/components/avatar.mjs +54 -0
  63. package/dist/packages/ui/src/components/command.cjs +3 -3
  64. package/dist/packages/ui/src/components/command.mjs +3 -3
  65. package/dist/packages/ui/src/components/form-builder/index.mjs +2 -2
  66. package/dist/packages/ui/src/components/kanban.cjs +835 -0
  67. package/dist/packages/ui/src/components/kanban.mjs +805 -0
  68. package/dist/packages/ui/src/components/popover.cjs +8 -3
  69. package/dist/packages/ui/src/components/popover.mjs +9 -4
  70. package/dist/packages/ui/src/components/search-select.cjs +75 -0
  71. package/dist/packages/ui/src/components/search-select.mjs +73 -0
  72. package/dist/packages/ui/src/lib/compose-refs.cjs +56 -0
  73. package/dist/packages/ui/src/lib/compose-refs.mjs +39 -0
  74. package/dist/plugins/blog/api/index.d.cts +1 -1
  75. package/dist/plugins/blog/api/index.d.mts +1 -1
  76. package/dist/plugins/blog/api/index.d.ts +1 -1
  77. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  78. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  79. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  80. package/dist/plugins/blog/client/index.d.cts +1 -1
  81. package/dist/plugins/blog/client/index.d.mts +1 -1
  82. package/dist/plugins/blog/client/index.d.ts +1 -1
  83. package/dist/plugins/blog/query-keys.d.cts +2 -2
  84. package/dist/plugins/blog/query-keys.d.mts +2 -2
  85. package/dist/plugins/blog/query-keys.d.ts +2 -2
  86. package/dist/plugins/kanban/api/index.cjs +7 -0
  87. package/dist/plugins/kanban/api/index.d.cts +403 -0
  88. package/dist/plugins/kanban/api/index.d.mts +403 -0
  89. package/dist/plugins/kanban/api/index.d.ts +403 -0
  90. package/dist/plugins/kanban/api/index.mjs +1 -0
  91. package/dist/plugins/kanban/client/components/index.cjs +35 -0
  92. package/dist/plugins/kanban/client/components/index.d.cts +102 -0
  93. package/dist/plugins/kanban/client/components/index.d.mts +102 -0
  94. package/dist/plugins/kanban/client/components/index.d.ts +102 -0
  95. package/dist/plugins/kanban/client/components/index.mjs +15 -0
  96. package/dist/plugins/kanban/client/hooks/index.cjs +15 -0
  97. package/dist/plugins/kanban/client/hooks/index.d.cts +143 -0
  98. package/dist/plugins/kanban/client/hooks/index.d.mts +143 -0
  99. package/dist/plugins/kanban/client/hooks/index.d.ts +143 -0
  100. package/dist/plugins/kanban/client/hooks/index.mjs +1 -0
  101. package/dist/plugins/kanban/client/index.cjs +7 -0
  102. package/dist/plugins/kanban/client/index.d.cts +196 -0
  103. package/dist/plugins/kanban/client/index.d.mts +196 -0
  104. package/dist/plugins/kanban/client/index.d.ts +196 -0
  105. package/dist/plugins/kanban/client/index.mjs +1 -0
  106. package/dist/plugins/kanban/client.css +68 -0
  107. package/dist/plugins/kanban/query-keys.cjs +105 -0
  108. package/dist/plugins/kanban/query-keys.d.cts +59 -0
  109. package/dist/plugins/kanban/query-keys.d.mts +59 -0
  110. package/dist/plugins/kanban/query-keys.d.ts +59 -0
  111. package/dist/plugins/kanban/query-keys.mjs +103 -0
  112. package/dist/plugins/kanban/style.css +7 -0
  113. package/dist/plugins/ui-builder/style.css +6 -0
  114. package/dist/shared/stack.DKDMI-QO.d.cts +70 -0
  115. package/dist/shared/stack.DKDMI-QO.d.mts +70 -0
  116. package/dist/shared/stack.DKDMI-QO.d.ts +70 -0
  117. package/dist/shared/stack.FeaWkglm.d.cts +190 -0
  118. package/dist/shared/stack.FeaWkglm.d.mts +190 -0
  119. package/dist/shared/stack.FeaWkglm.d.ts +190 -0
  120. package/package.json +56 -2
  121. package/src/plugins/kanban/api/index.ts +6 -0
  122. package/src/plugins/kanban/api/plugin.ts +1245 -0
  123. package/src/plugins/kanban/client/components/forms/board-form.tsx +108 -0
  124. package/src/plugins/kanban/client/components/forms/column-form.tsx +97 -0
  125. package/src/plugins/kanban/client/components/forms/task-form.tsx +274 -0
  126. package/src/plugins/kanban/client/components/index.tsx +21 -0
  127. package/src/plugins/kanban/client/components/loading/board-skeleton.tsx +49 -0
  128. package/src/plugins/kanban/client/components/loading/boards-list-skeleton.tsx +34 -0
  129. package/src/plugins/kanban/client/components/loading/index.tsx +2 -0
  130. package/src/plugins/kanban/client/components/pages/404-page.tsx +28 -0
  131. package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +575 -0
  132. package/src/plugins/kanban/client/components/pages/board-page.tsx +31 -0
  133. package/src/plugins/kanban/client/components/pages/boards-list-page.internal.tsx +101 -0
  134. package/src/plugins/kanban/client/components/pages/boards-list-page.tsx +26 -0
  135. package/src/plugins/kanban/client/components/pages/new-board-page.internal.tsx +65 -0
  136. package/src/plugins/kanban/client/components/pages/new-board-page.tsx +26 -0
  137. package/src/plugins/kanban/client/components/shared/column-content.tsx +108 -0
  138. package/src/plugins/kanban/client/components/shared/default-error.tsx +32 -0
  139. package/src/plugins/kanban/client/components/shared/empty-state.tsx +37 -0
  140. package/src/plugins/kanban/client/components/shared/kanban-board.tsx +87 -0
  141. package/src/plugins/kanban/client/components/shared/page-wrapper.tsx +20 -0
  142. package/src/plugins/kanban/client/components/shared/task-card.tsx +79 -0
  143. package/src/plugins/kanban/client/components/shared/user-avatar.tsx +63 -0
  144. package/src/plugins/kanban/client/hooks/index.tsx +11 -0
  145. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +560 -0
  146. package/src/plugins/kanban/client/index.ts +8 -0
  147. package/src/plugins/kanban/client/localization/index.ts +28 -0
  148. package/src/plugins/kanban/client/localization/kanban-common.ts +69 -0
  149. package/src/plugins/kanban/client/localization/kanban-forms.ts +70 -0
  150. package/src/plugins/kanban/client/localization/kanban-list.ts +36 -0
  151. package/src/plugins/kanban/client/overrides.ts +145 -0
  152. package/src/plugins/kanban/client/plugin.tsx +463 -0
  153. package/src/plugins/kanban/client.css +68 -0
  154. package/src/plugins/kanban/db.ts +125 -0
  155. package/src/plugins/kanban/query-keys.ts +154 -0
  156. package/src/plugins/kanban/schemas.ts +143 -0
  157. package/src/plugins/kanban/style.css +7 -0
  158. package/src/plugins/kanban/types.ts +106 -0
  159. package/src/plugins/kanban/utils.ts +107 -0
  160. package/src/plugins/ui-builder/style.css +6 -0
  161. package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.cts} +1 -1
  162. package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
  163. package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { ComposedRoute } from "@btst/stack/client/components";
5
+ import { DefaultError } from "../shared/default-error";
6
+ import { BoardsListSkeleton } from "../loading/boards-list-skeleton";
7
+ import { NotFoundPage } from "./404-page";
8
+
9
+ const BoardsListPage = lazy(() =>
10
+ import("./boards-list-page.internal").then((m) => ({
11
+ default: m.BoardsListPage,
12
+ })),
13
+ );
14
+
15
+ export function BoardsListPageComponent() {
16
+ return (
17
+ <ComposedRoute
18
+ path="/kanban"
19
+ PageComponent={BoardsListPage}
20
+ ErrorComponent={DefaultError}
21
+ LoadingComponent={BoardsListSkeleton}
22
+ NotFoundComponent={NotFoundPage}
23
+ onError={(error) => console.error("BoardsListPage error:", error)}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { ArrowLeft } from "lucide-react";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@workspace/ui/components/card";
11
+ import { usePluginOverrides } from "@btst/stack/context";
12
+ import type { KanbanPluginOverrides } from "../../overrides";
13
+ import { BoardForm } from "../forms/board-form";
14
+ import { PageWrapper } from "../shared/page-wrapper";
15
+
16
+ export function NewBoardPage() {
17
+ const { Link: OverrideLink, navigate: overrideNavigate } =
18
+ usePluginOverrides<KanbanPluginOverrides>("kanban");
19
+ const navigate =
20
+ overrideNavigate ||
21
+ ((path: string) => {
22
+ window.location.href = path;
23
+ });
24
+ const Link = OverrideLink || "a";
25
+
26
+ const handleSuccess = (boardId: string) => {
27
+ navigate(`/pages/kanban/${boardId}`);
28
+ };
29
+
30
+ return (
31
+ <PageWrapper data-testid="new-board-page">
32
+ <div className="flex items-center gap-4 mb-8">
33
+ <Link
34
+ href="/pages/kanban"
35
+ className="text-muted-foreground hover:text-foreground"
36
+ >
37
+ <ArrowLeft className="h-5 w-5" />
38
+ </Link>
39
+ <div>
40
+ <h1 className="text-3xl font-bold" data-testid="page-header">
41
+ Create New Board
42
+ </h1>
43
+ <p className="text-muted-foreground mt-1">
44
+ Set up a new kanban board for your project
45
+ </p>
46
+ </div>
47
+ </div>
48
+
49
+ <Card className="max-w-2xl">
50
+ <CardHeader>
51
+ <CardTitle>Board Details</CardTitle>
52
+ <CardDescription>
53
+ Enter the details for your new kanban board.
54
+ </CardDescription>
55
+ </CardHeader>
56
+ <CardContent>
57
+ <BoardForm
58
+ onClose={() => navigate("/pages/kanban")}
59
+ onSuccess={handleSuccess}
60
+ />
61
+ </CardContent>
62
+ </Card>
63
+ </PageWrapper>
64
+ );
65
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { ComposedRoute } from "@btst/stack/client/components";
5
+ import { DefaultError } from "../shared/default-error";
6
+ import { BoardsListSkeleton } from "../loading/boards-list-skeleton";
7
+ import { NotFoundPage } from "./404-page";
8
+
9
+ const NewBoardPage = lazy(() =>
10
+ import("./new-board-page.internal").then((m) => ({
11
+ default: m.NewBoardPage,
12
+ })),
13
+ );
14
+
15
+ export function NewBoardPageComponent() {
16
+ return (
17
+ <ComposedRoute
18
+ path="/kanban/new"
19
+ PageComponent={NewBoardPage}
20
+ ErrorComponent={DefaultError}
21
+ LoadingComponent={BoardsListSkeleton}
22
+ NotFoundComponent={NotFoundPage}
23
+ onError={(error) => console.error("NewBoardPage error:", error)}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import { GripVertical, MoreVertical, Pencil, Plus, Trash2 } from "lucide-react";
5
+ import { Button } from "@workspace/ui/components/button";
6
+ import { Badge } from "@workspace/ui/components/badge";
7
+ import * as Kanban from "@workspace/ui/components/kanban";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from "@workspace/ui/components/dropdown-menu";
15
+ import { TaskCard } from "./task-card";
16
+ import type { SerializedColumn, SerializedTask } from "../../../types";
17
+
18
+ interface ColumnContentProps {
19
+ column: SerializedColumn & { tasks: SerializedTask[] };
20
+ onAddTask: () => void;
21
+ onEditTask: (taskId: string) => void;
22
+ onEditColumn: () => void;
23
+ onDeleteColumn: () => void;
24
+ }
25
+
26
+ function ColumnContentComponent({
27
+ column,
28
+ onAddTask,
29
+ onEditTask,
30
+ onEditColumn,
31
+ onDeleteColumn,
32
+ }: ColumnContentProps) {
33
+ const hasTasks = column.tasks && column.tasks.length > 0;
34
+
35
+ return (
36
+ <Kanban.Column key={column.id} value={column.id}>
37
+ <div className="flex items-center">
38
+ <Kanban.ColumnHandle asChild>
39
+ <Button variant="ghost" size="icon">
40
+ <GripVertical className="h-4 w-4" />
41
+ </Button>
42
+ </Kanban.ColumnHandle>
43
+ <div className="flex items-center gap-2 flex-1">
44
+ <span className="font-bold text-lg line-clamp-1 flex-1 text-left">
45
+ {column.title}
46
+ </span>
47
+ <Badge variant="outline" className="pointer-events-none rounded-sm">
48
+ {column.tasks?.length || 0}
49
+ </Badge>
50
+ </div>
51
+ <DropdownMenu>
52
+ <DropdownMenuTrigger asChild>
53
+ <Button variant="ghost" size="icon">
54
+ <MoreVertical className="h-4 w-4" />
55
+ </Button>
56
+ </DropdownMenuTrigger>
57
+ <DropdownMenuContent align="end">
58
+ <DropdownMenuItem onClick={onEditColumn}>
59
+ <Pencil className="mr-2 h-4 w-4" />
60
+ Edit Column
61
+ </DropdownMenuItem>
62
+ <DropdownMenuItem onClick={onAddTask}>
63
+ <Plus className="mr-2 h-4 w-4" />
64
+ Add Task
65
+ </DropdownMenuItem>
66
+ <DropdownMenuSeparator />
67
+ <DropdownMenuItem
68
+ onClick={onDeleteColumn}
69
+ className="text-red-600 focus:text-red-600"
70
+ >
71
+ <Trash2 className="mr-2 h-4 w-4" />
72
+ Delete Column
73
+ </DropdownMenuItem>
74
+ </DropdownMenuContent>
75
+ </DropdownMenu>
76
+ </div>
77
+ <div className="p-0.5 space-y-2">
78
+ {hasTasks ? (
79
+ column.tasks.map((task) => (
80
+ <TaskCard
81
+ key={task.id}
82
+ task={task}
83
+ onClick={() => onEditTask(task.id)}
84
+ />
85
+ ))
86
+ ) : (
87
+ <div className="flex flex-col items-center justify-center py-1 md:py-8 text-center">
88
+ <div className="rounded-full bg-muted p-4 mb-3 hidden md:block">
89
+ <Plus className="h-5 w-5 text-muted-foreground" />
90
+ </div>
91
+ <div className="space-y-1 mb-2 md:space-y-2 md:mb-4">
92
+ <p className="text-sm text-muted-foreground">No tasks yet</p>
93
+ <p className="text-xs text-muted-foreground">
94
+ Add a task to get started
95
+ </p>
96
+ </div>
97
+ <Button onClick={onAddTask} size="sm">
98
+ <Plus className="mr-2 h-4 w-4" />
99
+ Add Task
100
+ </Button>
101
+ </div>
102
+ )}
103
+ </div>
104
+ </Kanban.Column>
105
+ );
106
+ }
107
+
108
+ export const ColumnContent = memo(ColumnContentComponent);
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { AlertCircle, RefreshCw } from "lucide-react";
4
+ import { Button } from "@workspace/ui/components/button";
5
+
6
+ interface DefaultErrorProps {
7
+ error?: Error;
8
+ reset?: () => void;
9
+ }
10
+
11
+ export function DefaultError({ error, reset }: DefaultErrorProps) {
12
+ return (
13
+ <div
14
+ className="flex flex-col items-center justify-center py-12 text-center"
15
+ data-testid="error-placeholder"
16
+ >
17
+ <div className="rounded-full bg-destructive/10 p-6 mb-4">
18
+ <AlertCircle className="h-8 w-8 text-destructive" />
19
+ </div>
20
+ <h3 className="text-lg font-semibold mb-2">Something went wrong</h3>
21
+ <p className="text-muted-foreground max-w-md mb-4">
22
+ {error?.message || "An unexpected error occurred. Please try again."}
23
+ </p>
24
+ {reset && (
25
+ <Button onClick={reset} variant="outline">
26
+ <RefreshCw className="mr-2 h-4 w-4" />
27
+ Try Again
28
+ </Button>
29
+ )}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { cn } from "@workspace/ui/lib/utils";
5
+
6
+ interface EmptyStateProps {
7
+ title: string;
8
+ description?: string;
9
+ action?: ReactNode;
10
+ icon?: ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ export function EmptyState({
15
+ title,
16
+ description,
17
+ action,
18
+ icon,
19
+ className,
20
+ }: EmptyStateProps) {
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "flex flex-col items-center justify-center py-12 text-center",
25
+ className,
26
+ )}
27
+ data-testid="empty-state"
28
+ >
29
+ {icon && <div className="rounded-full bg-muted p-6 mb-4">{icon}</div>}
30
+ <h3 className="text-lg font-semibold mb-2">{title}</h3>
31
+ {description && (
32
+ <p className="text-muted-foreground max-w-md mb-4">{description}</p>
33
+ )}
34
+ {action && <div>{action}</div>}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { memo, useMemo } from "react";
4
+ import * as Kanban from "@workspace/ui/components/kanban";
5
+ import { cn } from "@workspace/ui/lib/utils";
6
+ import { ColumnContent } from "./column-content";
7
+ import type { SerializedColumn, SerializedTask } from "../../../types";
8
+
9
+ interface KanbanBoardProps {
10
+ columns: (SerializedColumn & { tasks: SerializedTask[] })[];
11
+ kanbanState: Record<string, SerializedTask[]>;
12
+ onKanbanChange: (newData: Record<string, SerializedTask[]>) => void;
13
+ onAddTask: (columnId: string) => void;
14
+ onEditTask: (columnId: string, taskId: string) => void;
15
+ onEditColumn: (columnId: string) => void;
16
+ onDeleteColumn: (columnId: string) => void;
17
+ }
18
+
19
+ function KanbanBoardComponent({
20
+ columns,
21
+ kanbanState,
22
+ onKanbanChange,
23
+ onAddTask,
24
+ onEditTask,
25
+ onEditColumn,
26
+ onDeleteColumn,
27
+ }: KanbanBoardProps) {
28
+ const orderedColumns = useMemo(() => {
29
+ const columnMap = new Map(columns.map((c) => [c.id, c]));
30
+ return Object.keys(kanbanState)
31
+ .map((columnId) => {
32
+ const column = columnMap.get(columnId);
33
+ if (!column) return null;
34
+ return {
35
+ ...column,
36
+ tasks: kanbanState[columnId] || [],
37
+ };
38
+ })
39
+ .filter(
40
+ (c): c is SerializedColumn & { tasks: SerializedTask[] } => c !== null,
41
+ );
42
+ }, [columns, kanbanState]);
43
+
44
+ const mdClass = useMemo(() => {
45
+ const gridClassMap: Record<number, string> = {
46
+ 1: "md:grid-cols-1",
47
+ 2: "md:grid-cols-2",
48
+ 3: "md:grid-cols-3",
49
+ 4: "md:grid-cols-4",
50
+ 5: "md:grid-cols-5",
51
+ 6: "md:grid-cols-6",
52
+ };
53
+ return gridClassMap[orderedColumns.length] || "md:grid-cols-6";
54
+ }, [orderedColumns.length]);
55
+
56
+ return (
57
+ <Kanban.Root
58
+ orientation="horizontal"
59
+ value={kanbanState}
60
+ onValueChange={onKanbanChange}
61
+ getItemValue={(item: SerializedTask) => item.id}
62
+ >
63
+ <Kanban.Board
64
+ className={cn(
65
+ "flex flex-col gap-4 md:auto-rows-fr md:grid-cols-1 md:grid min-h-[400px]",
66
+ mdClass,
67
+ )}
68
+ >
69
+ {orderedColumns.map((column) => (
70
+ <ColumnContent
71
+ key={column.id}
72
+ column={column}
73
+ onAddTask={() => onAddTask(column.id)}
74
+ onEditTask={(taskId) => onEditTask(column.id, taskId)}
75
+ onEditColumn={() => onEditColumn(column.id)}
76
+ onDeleteColumn={() => onDeleteColumn(column.id)}
77
+ />
78
+ ))}
79
+ </Kanban.Board>
80
+ <Kanban.Overlay>
81
+ <div className="size-full rounded-md bg-primary/10" />
82
+ </Kanban.Overlay>
83
+ </Kanban.Root>
84
+ );
85
+ }
86
+
87
+ export const KanbanBoard = memo(KanbanBoardComponent);
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, HTMLAttributes } from "react";
4
+ import { cn } from "@workspace/ui/lib/utils";
5
+
6
+ interface PageWrapperProps extends HTMLAttributes<HTMLDivElement> {
7
+ children: ReactNode;
8
+ }
9
+
10
+ export function PageWrapper({
11
+ children,
12
+ className,
13
+ ...props
14
+ }: PageWrapperProps) {
15
+ return (
16
+ <div className={cn("container mx-auto py-8 px-4", className)} {...props}>
17
+ {children}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import { GripVertical } from "lucide-react";
5
+ import { Button } from "@workspace/ui/components/button";
6
+ import { Badge } from "@workspace/ui/components/badge";
7
+ import * as Kanban from "@workspace/ui/components/kanban";
8
+ import { format } from "date-fns";
9
+ import type { SerializedTask } from "../../../types";
10
+ import { getPriorityConfig } from "../../../utils";
11
+ import { useResolveUser } from "../../hooks/kanban-hooks";
12
+ import { UserAvatar } from "./user-avatar";
13
+
14
+ interface TaskCardProps {
15
+ task: SerializedTask;
16
+ onClick: () => void;
17
+ }
18
+
19
+ function TaskCardComponent({ task, onClick }: TaskCardProps) {
20
+ const priorityConfig = getPriorityConfig(task.priority);
21
+ const { data: assignee } = useResolveUser(task.assigneeId);
22
+
23
+ return (
24
+ <Kanban.Item value={task.id} asChild>
25
+ <div
26
+ className="rounded-md border bg-card p-3 shadow-xs cursor-pointer hover:shadow-md transition-shadow"
27
+ onClick={onClick}
28
+ >
29
+ <div className="flex flex-col gap-2">
30
+ <div className="flex items-center gap-2">
31
+ <Kanban.ItemHandle asChild>
32
+ <Button
33
+ variant="ghost"
34
+ size="icon"
35
+ className="h-6 w-6"
36
+ onClick={(e: React.MouseEvent) => e.stopPropagation()}
37
+ >
38
+ <GripVertical className="h-3 w-3" />
39
+ </Button>
40
+ </Kanban.ItemHandle>
41
+ <span
42
+ className="line-clamp-1 font-medium text-base flex-1 text-left cursor-pointer hover:text-primary"
43
+ title={task.title}
44
+ >
45
+ {task.title}
46
+ </span>
47
+ <Badge
48
+ variant={priorityConfig.variant}
49
+ className={`pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize ${priorityConfig.className}`}
50
+ >
51
+ {priorityConfig.label}
52
+ </Badge>
53
+ </div>
54
+
55
+ <div className="flex items-center justify-between text-muted-foreground text-xs">
56
+ {task.assigneeId ? (
57
+ <div className="flex items-center gap-1.5">
58
+ <UserAvatar user={assignee ?? null} size="sm" />
59
+ <span className="line-clamp-1">
60
+ {assignee?.name || "Assigned"}
61
+ </span>
62
+ </div>
63
+ ) : (
64
+ <div className="flex items-center gap-1.5">
65
+ <UserAvatar user={null} size="sm" />
66
+ <span className="line-clamp-1">Unassigned</span>
67
+ </div>
68
+ )}
69
+ <time className="tabular-nums">
70
+ {format(new Date(task.createdAt), "MMM d")}
71
+ </time>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </Kanban.Item>
76
+ );
77
+ }
78
+
79
+ export const TaskCard = memo(TaskCardComponent);
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import { User } from "lucide-react";
5
+ import {
6
+ Avatar,
7
+ AvatarImage,
8
+ AvatarFallback,
9
+ } from "@workspace/ui/components/avatar";
10
+ import type { KanbanUser } from "../../overrides";
11
+
12
+ interface UserAvatarProps {
13
+ user: KanbanUser | null;
14
+ size?: "sm" | "default" | "lg";
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * Get initials from a user's name
20
+ */
21
+ function getInitials(name: string): string {
22
+ const parts = name.trim().split(/\s+/);
23
+ if (parts.length === 1) {
24
+ return parts[0]?.charAt(0).toUpperCase() || "";
25
+ }
26
+ return (
27
+ (parts[0]?.charAt(0) || "") + (parts[parts.length - 1]?.charAt(0) || "")
28
+ ).toUpperCase();
29
+ }
30
+
31
+ /**
32
+ * UserAvatar component displays a user's avatar or initials fallback
33
+ * Uses the shadcn Avatar component with Radix primitives
34
+ */
35
+ function UserAvatarComponent({
36
+ user,
37
+ size = "sm",
38
+ className,
39
+ }: UserAvatarProps) {
40
+ // No user - show placeholder icon
41
+ if (!user) {
42
+ return (
43
+ <Avatar size={size} className={className} title="Unassigned">
44
+ <AvatarFallback>
45
+ <User className="size-3 group-data-[size=default]/avatar:size-4 group-data-[size=lg]/avatar:size-5" />
46
+ </AvatarFallback>
47
+ </Avatar>
48
+ );
49
+ }
50
+
51
+ const initials = getInitials(user.name);
52
+
53
+ return (
54
+ <Avatar size={size} className={className} title={user.name}>
55
+ {user.avatarUrl && <AvatarImage src={user.avatarUrl} alt={user.name} />}
56
+ <AvatarFallback className="bg-primary/10 text-primary font-medium">
57
+ {initials}
58
+ </AvatarFallback>
59
+ </Avatar>
60
+ );
61
+ }
62
+
63
+ export const UserAvatar = memo(UserAvatarComponent);
@@ -0,0 +1,11 @@
1
+ export {
2
+ useBoards,
3
+ useSuspenseBoards,
4
+ useBoard,
5
+ useSuspenseBoard,
6
+ useBoardMutations,
7
+ useColumnMutations,
8
+ useTaskMutations,
9
+ useResolveUser,
10
+ useSearchUsers,
11
+ } from "./kanban-hooks";