@btst/stack 1.10.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 (237) 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/minimal-tiptap/utils.cjs +15 -11
  69. package/dist/packages/ui/src/components/minimal-tiptap/utils.mjs +15 -11
  70. package/dist/packages/ui/src/components/popover.cjs +8 -3
  71. package/dist/packages/ui/src/components/popover.mjs +9 -4
  72. package/dist/packages/ui/src/components/search-select.cjs +75 -0
  73. package/dist/packages/ui/src/components/search-select.mjs +73 -0
  74. package/dist/packages/ui/src/components/ui-builder/index.cjs +9 -7
  75. package/dist/packages/ui/src/components/ui-builder/index.mjs +9 -7
  76. package/dist/packages/ui/src/components/ui-builder/internal/canvas/auto-frame.cjs +6 -3
  77. package/dist/packages/ui/src/components/ui-builder/internal/canvas/auto-frame.mjs +6 -3
  78. package/dist/packages/ui/src/components/ui-builder/internal/components/add-component-popover.cjs +228 -48
  79. package/dist/packages/ui/src/components/ui-builder/internal/components/add-component-popover.mjs +228 -48
  80. package/dist/packages/ui/src/components/ui-builder/internal/components/element-selector.cjs +1 -1
  81. package/dist/packages/ui/src/components/ui-builder/internal/components/element-selector.mjs +1 -1
  82. package/dist/packages/ui/src/components/ui-builder/internal/components/error-fallback.cjs +4 -2
  83. package/dist/packages/ui/src/components/ui-builder/internal/components/error-fallback.mjs +4 -2
  84. package/dist/packages/ui/src/components/ui-builder/internal/components/multi-select.cjs +6 -3
  85. package/dist/packages/ui/src/components/ui-builder/internal/components/multi-select.mjs +6 -3
  86. package/dist/packages/ui/src/components/ui-builder/internal/dnd/draggable-new-component.cjs +67 -0
  87. package/dist/packages/ui/src/components/ui-builder/internal/dnd/draggable-new-component.mjs +62 -0
  88. package/dist/packages/ui/src/components/ui-builder/internal/dnd/drop-zone.cjs +181 -37
  89. package/dist/packages/ui/src/components/ui-builder/internal/dnd/drop-zone.mjs +181 -38
  90. package/dist/packages/ui/src/components/ui-builder/internal/editor-panel.cjs +1 -1
  91. package/dist/packages/ui/src/components/ui-builder/internal/editor-panel.mjs +1 -1
  92. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-group-control.cjs +1 -1
  93. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-group-control.mjs +1 -1
  94. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-item-control.cjs +9 -2
  95. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/classname-control/classname-item-control.mjs +9 -2
  96. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/iconname-field.cjs +3 -2
  97. package/dist/packages/ui/src/components/ui-builder/internal/form-fields/iconname-field.mjs +3 -2
  98. package/dist/packages/ui/src/components/ui-builder/internal/layers-panel.cjs +1 -1
  99. package/dist/packages/ui/src/components/ui-builder/internal/layers-panel.mjs +1 -1
  100. package/dist/packages/ui/src/components/ui-builder/internal/props-panel.cjs +17 -5
  101. package/dist/packages/ui/src/components/ui-builder/internal/props-panel.mjs +17 -5
  102. package/dist/packages/ui/src/components/ui-builder/internal/utils/render-utils.cjs +70 -16
  103. package/dist/packages/ui/src/components/ui-builder/internal/utils/render-utils.mjs +73 -20
  104. package/dist/packages/ui/src/lib/compose-refs.cjs +56 -0
  105. package/dist/packages/ui/src/lib/compose-refs.mjs +39 -0
  106. package/dist/packages/ui/src/lib/ui-builder/context/dnd-context-colission-utils.cjs +14 -9
  107. package/dist/packages/ui/src/lib/ui-builder/context/dnd-context-colission-utils.mjs +14 -9
  108. package/dist/packages/ui/src/lib/ui-builder/context/dnd-context.cjs +38 -10
  109. package/dist/packages/ui/src/lib/ui-builder/context/dnd-context.mjs +35 -11
  110. package/dist/packages/ui/src/lib/ui-builder/context/dnd-contexts.cjs +1 -0
  111. package/dist/packages/ui/src/lib/ui-builder/context/dnd-contexts.mjs +1 -0
  112. package/dist/packages/ui/src/lib/ui-builder/context/drag-overlay.cjs +7 -4
  113. package/dist/packages/ui/src/lib/ui-builder/context/drag-overlay.mjs +7 -4
  114. package/dist/packages/ui/src/lib/ui-builder/hooks/use-auto-scroll.cjs +4 -4
  115. package/dist/packages/ui/src/lib/ui-builder/hooks/use-auto-scroll.mjs +4 -4
  116. package/dist/packages/ui/src/lib/ui-builder/hooks/use-dnd-event-handlers.cjs +53 -16
  117. package/dist/packages/ui/src/lib/ui-builder/hooks/use-dnd-event-handlers.mjs +53 -16
  118. package/dist/packages/ui/src/lib/ui-builder/hooks/use-drop-validation.cjs +23 -7
  119. package/dist/packages/ui/src/lib/ui-builder/hooks/use-drop-validation.mjs +23 -7
  120. package/dist/packages/ui/src/lib/ui-builder/registry/form-field-overrides.cjs +110 -11
  121. package/dist/packages/ui/src/lib/ui-builder/registry/form-field-overrides.mjs +111 -13
  122. package/dist/packages/ui/src/lib/ui-builder/store/editor-store.cjs +3 -2
  123. package/dist/packages/ui/src/lib/ui-builder/store/editor-store.mjs +3 -2
  124. package/dist/packages/ui/src/lib/ui-builder/store/layer-store.cjs +53 -7
  125. package/dist/packages/ui/src/lib/ui-builder/store/layer-store.mjs +54 -8
  126. package/dist/packages/ui/src/lib/ui-builder/store/layer-utils.cjs +4 -3
  127. package/dist/packages/ui/src/lib/ui-builder/store/layer-utils.mjs +4 -3
  128. package/dist/packages/ui/src/lib/ui-builder/utils/variable-resolver.cjs +12 -0
  129. package/dist/packages/ui/src/lib/ui-builder/utils/variable-resolver.mjs +12 -1
  130. package/dist/plugins/blog/api/index.d.cts +1 -1
  131. package/dist/plugins/blog/api/index.d.mts +1 -1
  132. package/dist/plugins/blog/api/index.d.ts +1 -1
  133. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  134. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  135. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  136. package/dist/plugins/blog/client/index.d.cts +1 -1
  137. package/dist/plugins/blog/client/index.d.mts +1 -1
  138. package/dist/plugins/blog/client/index.d.ts +1 -1
  139. package/dist/plugins/blog/query-keys.d.cts +2 -2
  140. package/dist/plugins/blog/query-keys.d.mts +2 -2
  141. package/dist/plugins/blog/query-keys.d.ts +2 -2
  142. package/dist/plugins/kanban/api/index.cjs +7 -0
  143. package/dist/plugins/kanban/api/index.d.cts +403 -0
  144. package/dist/plugins/kanban/api/index.d.mts +403 -0
  145. package/dist/plugins/kanban/api/index.d.ts +403 -0
  146. package/dist/plugins/kanban/api/index.mjs +1 -0
  147. package/dist/plugins/kanban/client/components/index.cjs +35 -0
  148. package/dist/plugins/kanban/client/components/index.d.cts +102 -0
  149. package/dist/plugins/kanban/client/components/index.d.mts +102 -0
  150. package/dist/plugins/kanban/client/components/index.d.ts +102 -0
  151. package/dist/plugins/kanban/client/components/index.mjs +15 -0
  152. package/dist/plugins/kanban/client/hooks/index.cjs +15 -0
  153. package/dist/plugins/kanban/client/hooks/index.d.cts +143 -0
  154. package/dist/plugins/kanban/client/hooks/index.d.mts +143 -0
  155. package/dist/plugins/kanban/client/hooks/index.d.ts +143 -0
  156. package/dist/plugins/kanban/client/hooks/index.mjs +1 -0
  157. package/dist/plugins/kanban/client/index.cjs +7 -0
  158. package/dist/plugins/kanban/client/index.d.cts +196 -0
  159. package/dist/plugins/kanban/client/index.d.mts +196 -0
  160. package/dist/plugins/kanban/client/index.d.ts +196 -0
  161. package/dist/plugins/kanban/client/index.mjs +1 -0
  162. package/dist/plugins/kanban/client.css +68 -0
  163. package/dist/plugins/kanban/query-keys.cjs +105 -0
  164. package/dist/plugins/kanban/query-keys.d.cts +59 -0
  165. package/dist/plugins/kanban/query-keys.d.mts +59 -0
  166. package/dist/plugins/kanban/query-keys.d.ts +59 -0
  167. package/dist/plugins/kanban/query-keys.mjs +103 -0
  168. package/dist/plugins/kanban/style.css +7 -0
  169. package/dist/plugins/ui-builder/client/components/index.d.cts +1 -1
  170. package/dist/plugins/ui-builder/client/components/index.d.mts +1 -1
  171. package/dist/plugins/ui-builder/client/components/index.d.ts +1 -1
  172. package/dist/plugins/ui-builder/client/hooks/index.d.cts +2 -2
  173. package/dist/plugins/ui-builder/client/hooks/index.d.mts +2 -2
  174. package/dist/plugins/ui-builder/client/hooks/index.d.ts +2 -2
  175. package/dist/plugins/ui-builder/client/index.d.cts +17 -7
  176. package/dist/plugins/ui-builder/client/index.d.mts +17 -7
  177. package/dist/plugins/ui-builder/client/index.d.ts +17 -7
  178. package/dist/plugins/ui-builder/index.d.cts +2 -2
  179. package/dist/plugins/ui-builder/index.d.mts +2 -2
  180. package/dist/plugins/ui-builder/index.d.ts +2 -2
  181. package/dist/plugins/ui-builder/style.css +6 -0
  182. package/dist/shared/{stack.BSM2cgoq.d.cts → stack.BYysGdHl.d.cts} +1 -1
  183. package/dist/shared/{stack.CqfZWfjJ.d.cts → stack.BdJFrdyt.d.cts} +8 -2
  184. package/dist/shared/{stack.e1FN86dE.d.mts → stack.ChVuHi5e.d.mts} +8 -2
  185. package/dist/shared/stack.DKDMI-QO.d.cts +70 -0
  186. package/dist/shared/stack.DKDMI-QO.d.mts +70 -0
  187. package/dist/shared/stack.DKDMI-QO.d.ts +70 -0
  188. package/dist/shared/{stack.CLtOoAqF.d.mts → stack.DYCFcnkL.d.mts} +1 -1
  189. package/dist/shared/{stack.MMntCVZZ.d.ts → stack.EhM4pmtN.d.ts} +8 -2
  190. package/dist/shared/stack.FeaWkglm.d.cts +190 -0
  191. package/dist/shared/stack.FeaWkglm.d.mts +190 -0
  192. package/dist/shared/stack.FeaWkglm.d.ts +190 -0
  193. package/dist/shared/{stack.BD1m-4yB.d.ts → stack.kFbDspnF.d.ts} +1 -1
  194. package/package.json +56 -2
  195. package/src/plugins/kanban/api/index.ts +6 -0
  196. package/src/plugins/kanban/api/plugin.ts +1245 -0
  197. package/src/plugins/kanban/client/components/forms/board-form.tsx +108 -0
  198. package/src/plugins/kanban/client/components/forms/column-form.tsx +97 -0
  199. package/src/plugins/kanban/client/components/forms/task-form.tsx +274 -0
  200. package/src/plugins/kanban/client/components/index.tsx +21 -0
  201. package/src/plugins/kanban/client/components/loading/board-skeleton.tsx +49 -0
  202. package/src/plugins/kanban/client/components/loading/boards-list-skeleton.tsx +34 -0
  203. package/src/plugins/kanban/client/components/loading/index.tsx +2 -0
  204. package/src/plugins/kanban/client/components/pages/404-page.tsx +28 -0
  205. package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +575 -0
  206. package/src/plugins/kanban/client/components/pages/board-page.tsx +31 -0
  207. package/src/plugins/kanban/client/components/pages/boards-list-page.internal.tsx +101 -0
  208. package/src/plugins/kanban/client/components/pages/boards-list-page.tsx +26 -0
  209. package/src/plugins/kanban/client/components/pages/new-board-page.internal.tsx +65 -0
  210. package/src/plugins/kanban/client/components/pages/new-board-page.tsx +26 -0
  211. package/src/plugins/kanban/client/components/shared/column-content.tsx +108 -0
  212. package/src/plugins/kanban/client/components/shared/default-error.tsx +32 -0
  213. package/src/plugins/kanban/client/components/shared/empty-state.tsx +37 -0
  214. package/src/plugins/kanban/client/components/shared/kanban-board.tsx +87 -0
  215. package/src/plugins/kanban/client/components/shared/page-wrapper.tsx +20 -0
  216. package/src/plugins/kanban/client/components/shared/task-card.tsx +79 -0
  217. package/src/plugins/kanban/client/components/shared/user-avatar.tsx +63 -0
  218. package/src/plugins/kanban/client/hooks/index.tsx +11 -0
  219. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +560 -0
  220. package/src/plugins/kanban/client/index.ts +8 -0
  221. package/src/plugins/kanban/client/localization/index.ts +28 -0
  222. package/src/plugins/kanban/client/localization/kanban-common.ts +69 -0
  223. package/src/plugins/kanban/client/localization/kanban-forms.ts +70 -0
  224. package/src/plugins/kanban/client/localization/kanban-list.ts +36 -0
  225. package/src/plugins/kanban/client/overrides.ts +145 -0
  226. package/src/plugins/kanban/client/plugin.tsx +463 -0
  227. package/src/plugins/kanban/client.css +68 -0
  228. package/src/plugins/kanban/db.ts +125 -0
  229. package/src/plugins/kanban/query-keys.ts +154 -0
  230. package/src/plugins/kanban/schemas.ts +143 -0
  231. package/src/plugins/kanban/style.css +7 -0
  232. package/src/plugins/kanban/types.ts +106 -0
  233. package/src/plugins/kanban/utils.ts +107 -0
  234. package/src/plugins/ui-builder/style.css +6 -0
  235. package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
  236. package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.mts} +1 -1
  237. package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.ts} +1 -1
@@ -0,0 +1,575 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useMemo, useEffect } from "react";
4
+ import { ArrowLeft, Plus, Settings, Trash2, Pencil } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import { Button } from "@workspace/ui/components/button";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "@workspace/ui/components/dropdown-menu";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "@workspace/ui/components/dialog";
21
+ import {
22
+ AlertDialog,
23
+ AlertDialogAction,
24
+ AlertDialogCancel,
25
+ AlertDialogContent,
26
+ AlertDialogDescription,
27
+ AlertDialogFooter,
28
+ AlertDialogHeader,
29
+ AlertDialogTitle,
30
+ } from "@workspace/ui/components/alert-dialog";
31
+ import {
32
+ useSuspenseBoard,
33
+ useBoardMutations,
34
+ useColumnMutations,
35
+ useTaskMutations,
36
+ } from "../../hooks/kanban-hooks";
37
+ import { usePluginOverrides } from "@btst/stack/context";
38
+ import type { KanbanPluginOverrides } from "../../overrides";
39
+ import { KanbanBoard } from "../shared/kanban-board";
40
+ import { ColumnForm } from "../forms/column-form";
41
+ import { BoardForm } from "../forms/board-form";
42
+ import { TaskForm } from "../forms/task-form";
43
+ import { PageWrapper } from "../shared/page-wrapper";
44
+ import { EmptyState } from "../shared/empty-state";
45
+ import type { SerializedTask, SerializedColumn } from "../../../types";
46
+
47
+ interface BoardPageProps {
48
+ boardId: string;
49
+ }
50
+
51
+ type ModalState =
52
+ | { type: "none" }
53
+ | { type: "addColumn" }
54
+ | { type: "editColumn"; columnId: string }
55
+ | { type: "deleteColumn"; columnId: string }
56
+ | { type: "editBoard" }
57
+ | { type: "deleteBoard" }
58
+ | { type: "addTask"; columnId: string }
59
+ | { type: "editTask"; columnId: string; taskId: string };
60
+
61
+ export function BoardPage({ boardId }: BoardPageProps) {
62
+ const { data: board, error, refetch, isFetching } = useSuspenseBoard(boardId);
63
+
64
+ // Suspense hooks only throw on initial fetch, not refetch failures
65
+ if (error && !isFetching) {
66
+ throw error;
67
+ }
68
+
69
+ const { Link: OverrideLink, navigate: overrideNavigate } =
70
+ usePluginOverrides<KanbanPluginOverrides>("kanban");
71
+ const navigate =
72
+ overrideNavigate ||
73
+ ((path: string) => {
74
+ window.location.href = path;
75
+ });
76
+ const Link = OverrideLink || "a";
77
+
78
+ const { deleteBoard, isDeleting } = useBoardMutations();
79
+ const { deleteColumn, reorderColumns } = useColumnMutations();
80
+ const { deleteTask, moveTask, reorderTasks } = useTaskMutations();
81
+
82
+ const [modalState, setModalState] = useState<ModalState>({ type: "none" });
83
+
84
+ // Helper function to convert board columns to kanban state format
85
+ const computeKanbanData = useCallback(
86
+ (
87
+ columns: SerializedColumn[] | undefined,
88
+ ): Record<string, SerializedTask[]> => {
89
+ if (!columns) return {};
90
+ return columns.reduce(
91
+ (acc, column) => {
92
+ acc[column.id] = column.tasks || [];
93
+ return acc;
94
+ },
95
+ {} as Record<string, SerializedTask[]>,
96
+ );
97
+ },
98
+ [],
99
+ );
100
+
101
+ // Initialize kanbanState with data from board to avoid flash of empty state
102
+ // Using lazy initializer ensures we have the correct state on first render
103
+ const [kanbanState, setKanbanState] = useState<
104
+ Record<string, SerializedTask[]>
105
+ >(() => computeKanbanData(board?.columns));
106
+
107
+ // Keep kanbanState in sync when server data changes (e.g., after refetch)
108
+ const serverKanbanData = useMemo(
109
+ () => computeKanbanData(board?.columns),
110
+ [board?.columns, computeKanbanData],
111
+ );
112
+
113
+ useEffect(() => {
114
+ setKanbanState(serverKanbanData);
115
+ }, [serverKanbanData]);
116
+
117
+ const closeModal = useCallback(() => {
118
+ setModalState({ type: "none" });
119
+ }, []);
120
+
121
+ const handleDeleteBoard = useCallback(async () => {
122
+ try {
123
+ await deleteBoard(boardId);
124
+ closeModal();
125
+ // Use both navigate and a fallback to ensure navigation works
126
+ // Some frameworks may have issues with router.push after mutations
127
+ navigate("/pages/kanban");
128
+ // Fallback: if navigate doesn't work, use window.location
129
+ if (typeof window !== "undefined") {
130
+ setTimeout(() => {
131
+ // Only redirect if we're still on the same page after 100ms
132
+ if (window.location.pathname.includes(boardId)) {
133
+ window.location.href = "/pages/kanban";
134
+ }
135
+ }, 100);
136
+ }
137
+ } catch (error) {
138
+ const message =
139
+ error instanceof Error ? error.message : "Failed to delete board";
140
+ toast.error(message);
141
+ }
142
+ }, [deleteBoard, boardId, navigate, closeModal]);
143
+
144
+ const handleKanbanChange = useCallback(
145
+ async (newData: Record<string, SerializedTask[]>) => {
146
+ if (!board) return;
147
+
148
+ // Capture current state for change detection
149
+ // Note: We use a functional update to get the actual current state,
150
+ // avoiding stale closure issues with rapid successive operations
151
+ let previousState: Record<string, SerializedTask[]> = {};
152
+ setKanbanState((current) => {
153
+ previousState = current;
154
+ return newData;
155
+ });
156
+
157
+ try {
158
+ // Detect column reorder
159
+ const oldKeys = Object.keys(previousState);
160
+ const newKeys = Object.keys(newData);
161
+ const isColumnMove =
162
+ oldKeys.length === newKeys.length &&
163
+ oldKeys.join("") !== newKeys.join("");
164
+
165
+ if (isColumnMove) {
166
+ // Column reorder - use atomic batch endpoint with transaction support
167
+ await reorderColumns(board.id, newKeys);
168
+ } else {
169
+ // Task changes - detect cross-column moves and within-column reorders
170
+ const crossColumnMoves: Array<{
171
+ taskId: string;
172
+ targetColumnId: string;
173
+ targetOrder: number;
174
+ }> = [];
175
+ const columnsToReorder: Map<string, string[]> = new Map();
176
+ const targetColumnsOfCrossMove = new Set<string>();
177
+
178
+ for (const [columnId, tasks] of Object.entries(newData)) {
179
+ const oldTasks = previousState[columnId] || [];
180
+ let hasOrderChanges = false;
181
+
182
+ for (let i = 0; i < tasks.length; i++) {
183
+ const task = tasks[i];
184
+ if (!task) continue;
185
+
186
+ if (task.columnId !== columnId) {
187
+ // Task moved from another column - needs cross-column move
188
+ crossColumnMoves.push({
189
+ taskId: task.id,
190
+ targetColumnId: columnId,
191
+ targetOrder: i,
192
+ });
193
+ targetColumnsOfCrossMove.add(columnId);
194
+ } else if (task.order !== i) {
195
+ // Task order changed within same column
196
+ hasOrderChanges = true;
197
+ }
198
+ }
199
+
200
+ // Check if tasks were removed from this column (moved elsewhere)
201
+ const newTaskIds = new Set(tasks.map((t) => t.id));
202
+ const tasksRemoved = oldTasks.some((t) => !newTaskIds.has(t.id));
203
+
204
+ // If order changes within column (not a target of cross-column move),
205
+ // use atomic reorder
206
+ if (
207
+ hasOrderChanges &&
208
+ !targetColumnsOfCrossMove.has(columnId) &&
209
+ !tasksRemoved
210
+ ) {
211
+ columnsToReorder.set(
212
+ columnId,
213
+ tasks.map((t) => t.id),
214
+ );
215
+ }
216
+ }
217
+
218
+ // Handle cross-column moves first (these need individual moveTask calls)
219
+ for (const move of crossColumnMoves) {
220
+ await moveTask(move.taskId, move.targetColumnId, move.targetOrder);
221
+ }
222
+
223
+ // Then handle within-column reorders atomically
224
+ for (const [columnId, taskIds] of columnsToReorder) {
225
+ await reorderTasks(columnId, taskIds);
226
+ }
227
+
228
+ // Reorder target columns of cross-column moves to fix order collisions
229
+ // The moveTask only sets the moved task's order, so other tasks need reordering
230
+ for (const targetColumnId of targetColumnsOfCrossMove) {
231
+ const tasks = newData[targetColumnId];
232
+ if (tasks) {
233
+ await reorderTasks(
234
+ targetColumnId,
235
+ tasks.map((t) => t.id),
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ // Sync with server after successful mutations
242
+ refetch();
243
+ } catch (error) {
244
+ // On error, refetch from server to get the authoritative state.
245
+ // We avoid manual rollback to previousState because with rapid successive
246
+ // operations, the captured previousState may be stale - a later operation
247
+ // may have already updated the state, and reverting would incorrectly
248
+ // undo that operation too. The server is the source of truth.
249
+ refetch();
250
+ // Re-throw so error boundaries or toast handlers can catch it
251
+ throw error;
252
+ }
253
+ },
254
+ [board, reorderColumns, moveTask, reorderTasks, refetch],
255
+ );
256
+
257
+ const orderedColumns = useMemo(() => {
258
+ if (!board?.columns) return [];
259
+ const columnMap = new Map(board.columns.map((c) => [c.id, c]));
260
+ return Object.keys(kanbanState)
261
+ .map((columnId) => {
262
+ const column = columnMap.get(columnId);
263
+ if (!column) return null;
264
+ return {
265
+ ...column,
266
+ tasks: kanbanState[columnId] || [],
267
+ };
268
+ })
269
+ .filter(
270
+ (c): c is SerializedColumn & { tasks: SerializedTask[] } => c !== null,
271
+ );
272
+ }, [board?.columns, kanbanState]);
273
+
274
+ // Board not found - only shown after data has loaded (not during loading)
275
+ if (!board) {
276
+ return (
277
+ <EmptyState
278
+ title="Board not found"
279
+ description="The board you're looking for doesn't exist or you don't have access to it."
280
+ action={
281
+ <Button onClick={() => navigate("/pages/kanban")}>
282
+ <ArrowLeft className="mr-2 h-4 w-4" />
283
+ Back to Boards
284
+ </Button>
285
+ }
286
+ />
287
+ );
288
+ }
289
+
290
+ return (
291
+ <PageWrapper
292
+ data-testid="board-page"
293
+ className="flex flex-col items-center"
294
+ >
295
+ <div className="w-full flex items-center justify-between mb-8">
296
+ <div className="flex items-center gap-4">
297
+ <Link
298
+ href="/pages/kanban"
299
+ className="text-muted-foreground hover:text-foreground"
300
+ >
301
+ <ArrowLeft className="h-5 w-5" />
302
+ </Link>
303
+ <div>
304
+ <h1 className="text-3xl font-bold" data-testid="page-header">
305
+ {board.name}
306
+ </h1>
307
+ {board.description && (
308
+ <p className="text-muted-foreground mt-1">{board.description}</p>
309
+ )}
310
+ </div>
311
+ </div>
312
+ <DropdownMenu>
313
+ <DropdownMenuTrigger asChild>
314
+ <Button variant="outline">
315
+ <Settings className="mr-2 h-4 w-4" />
316
+ Actions
317
+ </Button>
318
+ </DropdownMenuTrigger>
319
+ <DropdownMenuContent align="end">
320
+ <DropdownMenuItem
321
+ onClick={() => setModalState({ type: "addColumn" })}
322
+ >
323
+ <Plus className="mr-2 h-4 w-4" />
324
+ Add Column
325
+ </DropdownMenuItem>
326
+ <DropdownMenuItem
327
+ onClick={() => setModalState({ type: "editBoard" })}
328
+ >
329
+ <Pencil className="mr-2 h-4 w-4" />
330
+ Edit Board
331
+ </DropdownMenuItem>
332
+ <DropdownMenuSeparator />
333
+ <DropdownMenuItem
334
+ onClick={() => setModalState({ type: "deleteBoard" })}
335
+ className="text-red-600 focus:text-red-600"
336
+ >
337
+ <Trash2 className="mr-2 h-4 w-4" />
338
+ Delete Board
339
+ </DropdownMenuItem>
340
+ </DropdownMenuContent>
341
+ </DropdownMenu>
342
+ </div>
343
+
344
+ {orderedColumns.length > 0 ? (
345
+ <KanbanBoard
346
+ columns={orderedColumns}
347
+ kanbanState={kanbanState}
348
+ onKanbanChange={handleKanbanChange}
349
+ onAddTask={(columnId) => setModalState({ type: "addTask", columnId })}
350
+ onEditTask={(columnId, taskId) =>
351
+ setModalState({ type: "editTask", columnId, taskId })
352
+ }
353
+ onEditColumn={(columnId) =>
354
+ setModalState({ type: "editColumn", columnId })
355
+ }
356
+ onDeleteColumn={(columnId) =>
357
+ setModalState({ type: "deleteColumn", columnId })
358
+ }
359
+ />
360
+ ) : (
361
+ <EmptyState
362
+ title="No columns yet"
363
+ description="Create your first column to start organizing tasks."
364
+ action={
365
+ <Button onClick={() => setModalState({ type: "addColumn" })}>
366
+ <Plus className="mr-2 h-4 w-4" />
367
+ Add Column
368
+ </Button>
369
+ }
370
+ />
371
+ )}
372
+
373
+ {/* Add Column Modal */}
374
+ <Dialog
375
+ open={modalState.type === "addColumn"}
376
+ onOpenChange={(open: boolean) => !open && closeModal()}
377
+ >
378
+ <DialogContent>
379
+ <DialogHeader>
380
+ <DialogTitle>Add Column</DialogTitle>
381
+ <DialogDescription>
382
+ Add a new column to this board.
383
+ </DialogDescription>
384
+ </DialogHeader>
385
+ <ColumnForm
386
+ boardId={boardId}
387
+ onClose={closeModal}
388
+ onSuccess={() => {
389
+ closeModal();
390
+ refetch();
391
+ }}
392
+ />
393
+ </DialogContent>
394
+ </Dialog>
395
+
396
+ {/* Edit Column Modal */}
397
+ <Dialog
398
+ open={modalState.type === "editColumn"}
399
+ onOpenChange={(open: boolean) => !open && closeModal()}
400
+ >
401
+ <DialogContent>
402
+ <DialogHeader>
403
+ <DialogTitle>Edit Column</DialogTitle>
404
+ <DialogDescription>Update the column details.</DialogDescription>
405
+ </DialogHeader>
406
+ {modalState.type === "editColumn" && (
407
+ <ColumnForm
408
+ boardId={boardId}
409
+ columnId={modalState.columnId}
410
+ column={board.columns?.find((c) => c.id === modalState.columnId)}
411
+ onClose={closeModal}
412
+ onSuccess={() => {
413
+ closeModal();
414
+ refetch();
415
+ }}
416
+ />
417
+ )}
418
+ </DialogContent>
419
+ </Dialog>
420
+
421
+ {/* Delete Column Modal */}
422
+ <AlertDialog
423
+ open={modalState.type === "deleteColumn"}
424
+ onOpenChange={(open: boolean) => !open && closeModal()}
425
+ >
426
+ <AlertDialogContent>
427
+ <AlertDialogHeader>
428
+ <AlertDialogTitle>Delete Column</AlertDialogTitle>
429
+ <AlertDialogDescription>
430
+ Are you sure you want to delete this column? All tasks in this
431
+ column will be permanently removed.
432
+ </AlertDialogDescription>
433
+ </AlertDialogHeader>
434
+ <AlertDialogFooter>
435
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
436
+ <AlertDialogAction
437
+ onClick={async () => {
438
+ if (modalState.type === "deleteColumn") {
439
+ try {
440
+ await deleteColumn(modalState.columnId);
441
+ closeModal();
442
+ refetch();
443
+ } catch (error) {
444
+ const message =
445
+ error instanceof Error
446
+ ? error.message
447
+ : "Failed to delete column";
448
+ toast.error(message);
449
+ }
450
+ }
451
+ }}
452
+ className="bg-red-600 hover:bg-red-700"
453
+ >
454
+ Delete
455
+ </AlertDialogAction>
456
+ </AlertDialogFooter>
457
+ </AlertDialogContent>
458
+ </AlertDialog>
459
+
460
+ {/* Edit Board Modal */}
461
+ <Dialog
462
+ open={modalState.type === "editBoard"}
463
+ onOpenChange={(open: boolean) => !open && closeModal()}
464
+ >
465
+ <DialogContent>
466
+ <DialogHeader>
467
+ <DialogTitle>Edit Board</DialogTitle>
468
+ <DialogDescription>Update board details.</DialogDescription>
469
+ </DialogHeader>
470
+ <BoardForm
471
+ board={board}
472
+ onClose={closeModal}
473
+ onSuccess={() => {
474
+ closeModal();
475
+ refetch();
476
+ }}
477
+ />
478
+ </DialogContent>
479
+ </Dialog>
480
+
481
+ {/* Delete Board Modal */}
482
+ <AlertDialog
483
+ open={modalState.type === "deleteBoard"}
484
+ onOpenChange={(open: boolean) => !open && closeModal()}
485
+ >
486
+ <AlertDialogContent>
487
+ <AlertDialogHeader>
488
+ <AlertDialogTitle>Delete Board</AlertDialogTitle>
489
+ <AlertDialogDescription>
490
+ Are you sure you want to delete this board? This action cannot be
491
+ undone. All columns and tasks will be permanently removed.
492
+ </AlertDialogDescription>
493
+ </AlertDialogHeader>
494
+ <AlertDialogFooter>
495
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
496
+ <Button
497
+ onClick={handleDeleteBoard}
498
+ disabled={isDeleting}
499
+ className="bg-red-600 hover:bg-red-700"
500
+ >
501
+ {isDeleting ? "Deleting..." : "Delete"}
502
+ </Button>
503
+ </AlertDialogFooter>
504
+ </AlertDialogContent>
505
+ </AlertDialog>
506
+
507
+ {/* Add Task Modal */}
508
+ <Dialog
509
+ open={modalState.type === "addTask"}
510
+ onOpenChange={(open: boolean) => !open && closeModal()}
511
+ >
512
+ <DialogContent className="max-w-3xl! max-h-screen overflow-y-auto">
513
+ <DialogHeader>
514
+ <DialogTitle>Add Task</DialogTitle>
515
+ <DialogDescription>Create a new task.</DialogDescription>
516
+ </DialogHeader>
517
+ {modalState.type === "addTask" && (
518
+ <TaskForm
519
+ columnId={modalState.columnId}
520
+ boardId={boardId}
521
+ columns={board.columns || []}
522
+ onClose={closeModal}
523
+ onSuccess={() => {
524
+ closeModal();
525
+ refetch();
526
+ }}
527
+ />
528
+ )}
529
+ </DialogContent>
530
+ </Dialog>
531
+
532
+ {/* Edit Task Modal */}
533
+ <Dialog
534
+ open={modalState.type === "editTask"}
535
+ onOpenChange={(open: boolean) => !open && closeModal()}
536
+ >
537
+ <DialogContent className="max-w-3xl! max-h-screen overflow-y-auto">
538
+ <DialogHeader>
539
+ <DialogTitle>Edit Task</DialogTitle>
540
+ <DialogDescription>Update task details.</DialogDescription>
541
+ </DialogHeader>
542
+ {modalState.type === "editTask" && (
543
+ <TaskForm
544
+ columnId={modalState.columnId}
545
+ boardId={boardId}
546
+ taskId={modalState.taskId}
547
+ task={board.columns
548
+ ?.find((c) => c.id === modalState.columnId)
549
+ ?.tasks?.find((t) => t.id === modalState.taskId)}
550
+ columns={board.columns || []}
551
+ onClose={closeModal}
552
+ onSuccess={() => {
553
+ closeModal();
554
+ refetch();
555
+ }}
556
+ onDelete={async () => {
557
+ try {
558
+ await deleteTask(modalState.taskId);
559
+ closeModal();
560
+ refetch();
561
+ } catch (error) {
562
+ const message =
563
+ error instanceof Error
564
+ ? error.message
565
+ : "Failed to delete task";
566
+ toast.error(message);
567
+ }
568
+ }}
569
+ />
570
+ )}
571
+ </DialogContent>
572
+ </Dialog>
573
+ </PageWrapper>
574
+ );
575
+ }
@@ -0,0 +1,31 @@
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 { BoardSkeleton } from "../loading/board-skeleton";
7
+ import { NotFoundPage } from "./404-page";
8
+
9
+ const BoardPage = lazy(() =>
10
+ import("./board-page.internal").then((m) => ({
11
+ default: m.BoardPage,
12
+ })),
13
+ );
14
+
15
+ interface BoardPageComponentProps {
16
+ boardId: string;
17
+ }
18
+
19
+ export function BoardPageComponent({ boardId }: BoardPageComponentProps) {
20
+ return (
21
+ <ComposedRoute
22
+ path={`/kanban/${boardId}`}
23
+ PageComponent={BoardPage}
24
+ ErrorComponent={DefaultError}
25
+ LoadingComponent={BoardSkeleton}
26
+ NotFoundComponent={NotFoundPage}
27
+ props={{ boardId }}
28
+ onError={(error) => console.error("BoardPage error:", error)}
29
+ />
30
+ );
31
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { Plus } from "lucide-react";
4
+ import { Button } from "@workspace/ui/components/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@workspace/ui/components/card";
12
+ import { useSuspenseBoards } from "../../hooks/kanban-hooks";
13
+ import { usePluginOverrides } from "@btst/stack/context";
14
+ import type { KanbanPluginOverrides } from "../../overrides";
15
+ import { EmptyState } from "../shared/empty-state";
16
+ import { PageWrapper } from "../shared/page-wrapper";
17
+ import { format } from "date-fns";
18
+
19
+ export function BoardsListPage() {
20
+ const { data: boards, error, isFetching } = useSuspenseBoards();
21
+
22
+ // Suspense hooks only throw on initial fetch, not refetch failures
23
+ if (error && !isFetching) {
24
+ throw error;
25
+ }
26
+ const { Link: OverrideLink, navigate: overrideNavigate } =
27
+ usePluginOverrides<KanbanPluginOverrides>("kanban");
28
+ const Link = OverrideLink || "a";
29
+ const navigate =
30
+ overrideNavigate ||
31
+ ((path: string) => {
32
+ window.location.href = path;
33
+ });
34
+
35
+ const handleNewBoard = () => {
36
+ navigate("/pages/kanban/new");
37
+ };
38
+
39
+ return (
40
+ <PageWrapper data-testid="boards-list-page">
41
+ <div className="w-full flex items-center justify-between mb-8">
42
+ <div>
43
+ <h1 className="text-3xl font-bold" data-testid="page-header">
44
+ Kanban Boards
45
+ </h1>
46
+ <p className="text-muted-foreground mt-1">
47
+ Manage your projects and tasks
48
+ </p>
49
+ </div>
50
+ <Button onClick={handleNewBoard}>
51
+ <Plus className="mr-2 h-4 w-4" />
52
+ New Board
53
+ </Button>
54
+ </div>
55
+
56
+ {boards.length > 0 ? (
57
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
58
+ {boards.map((board) => (
59
+ <Link
60
+ key={board.id}
61
+ href={`/pages/kanban/${board.id}`}
62
+ className="block group"
63
+ >
64
+ <Card className="h-full transition-shadow hover:shadow-md cursor-pointer">
65
+ <CardHeader>
66
+ <CardTitle className="group-hover:text-primary transition-colors">
67
+ {board.name}
68
+ </CardTitle>
69
+ {board.description && (
70
+ <CardDescription className="line-clamp-2">
71
+ {board.description}
72
+ </CardDescription>
73
+ )}
74
+ </CardHeader>
75
+ <CardContent>
76
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
77
+ <span>{board.columns?.length || 0} columns</span>
78
+ <span>
79
+ {format(new Date(board.createdAt), "MMM d, yyyy")}
80
+ </span>
81
+ </div>
82
+ </CardContent>
83
+ </Card>
84
+ </Link>
85
+ ))}
86
+ </div>
87
+ ) : (
88
+ <EmptyState
89
+ title="No boards yet"
90
+ description="Create your first kanban board to start organizing your tasks."
91
+ action={
92
+ <Button onClick={handleNewBoard}>
93
+ <Plus className="mr-2 h-4 w-4" />
94
+ Create Board
95
+ </Button>
96
+ }
97
+ />
98
+ )}
99
+ </PageWrapper>
100
+ );
101
+ }