@tuturuuu/ui 0.7.0 → 0.9.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -11,6 +11,14 @@ import type { ListStatusFilter } from './board-header';
11
11
  import CursorOverlay from './cursor-overlay';
12
12
  import type { BoardFiltersMetadata, TaskFilters } from './task-filter.types';
13
13
 
14
+ type CursorScopeMetadata =
15
+ | { type: 'board'; boardId: string }
16
+ | { type: 'task-description'; taskId: string };
17
+
18
+ type CursorOverlayMetadata = Partial<BoardFiltersMetadata> & {
19
+ cursorScope: CursorScopeMetadata;
20
+ };
21
+
14
22
  /**
15
23
  * Efficiently compares two sorted arrays of primitive values
16
24
  */
@@ -26,10 +34,16 @@ function arraysEqual<T extends string | number>(arr1: T[], arr2: T[]): boolean {
26
34
  * Checks if two cursor metadata objects represent the same view configuration
27
35
  */
28
36
  function isMatchingFilters(
29
- metadata1?: BoardFiltersMetadata,
30
- metadata2?: BoardFiltersMetadata
37
+ metadata1?: CursorOverlayMetadata,
38
+ metadata2?: CursorOverlayMetadata
31
39
  ): boolean {
32
40
  if (!metadata1 || !metadata2) return true; // Show all cursors if no metadata
41
+ if (!isMatchingCursorScope(metadata1.cursorScope, metadata2.cursorScope)) {
42
+ return false;
43
+ }
44
+
45
+ if (!metadata1.filters || !metadata2.filters) return true;
46
+ if (!metadata1.listStatusFilter || !metadata2.listStatusFilter) return true;
33
47
 
34
48
  // Check list status filter
35
49
  if (metadata1.listStatusFilter !== metadata2.listStatusFilter) return false;
@@ -81,14 +95,33 @@ function isMatchingFilters(
81
95
  return true;
82
96
  }
83
97
 
98
+ function isMatchingCursorScope(
99
+ scope1: CursorScopeMetadata,
100
+ scope2: CursorScopeMetadata
101
+ ) {
102
+ if (scope1.type !== scope2.type) return false;
103
+ if (scope1.type === 'board' && scope2.type === 'board') {
104
+ return scope1.boardId === scope2.boardId;
105
+ }
106
+ if (
107
+ scope1.type === 'task-description' &&
108
+ scope2.type === 'task-description'
109
+ ) {
110
+ return scope1.taskId === scope2.taskId;
111
+ }
112
+ return false;
113
+ }
114
+
84
115
  export default function CursorOverlayMultiWrapper({
85
116
  channelName,
86
117
  containerRef,
118
+ cursorScope,
87
119
  listStatusFilter,
88
120
  filters,
89
121
  }: {
90
122
  channelName: string;
91
123
  containerRef: React.RefObject<HTMLDivElement | null>;
124
+ cursorScope: CursorScopeMetadata;
92
125
  listStatusFilter?: ListStatusFilter;
93
126
  filters?: TaskFilters;
94
127
  }) {
@@ -97,6 +130,9 @@ export default function CursorOverlayMultiWrapper({
97
130
  height: number;
98
131
  }>({ width: 0, height: 0 });
99
132
  const [currentUser, setCurrentUser] = useState<User | null>(null);
133
+ const cursorScopeType = cursorScope.type;
134
+ const cursorScopeId =
135
+ cursorScope.type === 'board' ? cursorScope.boardId : cursorScope.taskId;
100
136
 
101
137
  // Fetch current user
102
138
  useEffect(() => {
@@ -105,10 +141,9 @@ export default function CursorOverlayMultiWrapper({
105
141
  const userData = await getCurrentUserProfile();
106
142
  if (!userData?.id) return;
107
143
  setCurrentUser({
108
- id: userData.id,
109
- display_name: userData.display_name,
110
- email: userData.email,
111
144
  avatar_url: userData.avatar_url,
145
+ display_name: userData.display_name,
146
+ id: userData.id,
112
147
  });
113
148
  } catch (err) {
114
149
  console.warn('Error fetching user:', err);
@@ -118,14 +153,22 @@ export default function CursorOverlayMultiWrapper({
118
153
  fetchUser();
119
154
  }, []);
120
155
 
121
- // Create metadata object from view options (only if both are provided)
122
- const metadata: BoardFiltersMetadata | undefined = useMemo(() => {
123
- if (!listStatusFilter || !filters) return undefined;
156
+ const metadata: CursorOverlayMetadata = useMemo(() => {
157
+ const resolvedCursorScope =
158
+ cursorScopeType === 'board'
159
+ ? ({ boardId: cursorScopeId, type: 'board' } as const)
160
+ : ({ taskId: cursorScopeId, type: 'task-description' } as const);
161
+
162
+ if (!listStatusFilter || !filters) {
163
+ return { cursorScope: resolvedCursorScope };
164
+ }
165
+
124
166
  return {
167
+ cursorScope: resolvedCursorScope,
125
168
  listStatusFilter,
126
169
  filters,
127
170
  };
128
- }, [listStatusFilter, filters]);
171
+ }, [cursorScopeId, cursorScopeType, listStatusFilter, filters]);
129
172
 
130
173
  const { cursors, error } = useCursorTracking(
131
174
  channelName,
@@ -139,15 +182,13 @@ export default function CursorOverlayMultiWrapper({
139
182
  const filtered = new Map<string, CursorPosition>();
140
183
 
141
184
  for (const [userId, cursor] of cursors.entries()) {
142
- // Always show cursors without metadata (backward compatibility)
143
185
  if (!cursor.metadata) {
144
- filtered.set(userId, cursor);
145
186
  continue;
146
187
  }
147
188
 
148
189
  // Filter based on view matching
149
190
  if (
150
- isMatchingFilters(metadata, cursor.metadata as BoardFiltersMetadata)
191
+ isMatchingFilters(metadata, cursor.metadata as CursorOverlayMetadata)
151
192
  ) {
152
193
  filtered.set(userId, cursor);
153
194
  }
@@ -88,6 +88,7 @@ interface Props {
88
88
  preserveTaskOrder?: boolean;
89
89
  searchQuery?: string;
90
90
  weekStartsOn?: 0 | 1 | 6;
91
+ readOnly?: boolean;
91
92
  }
92
93
 
93
94
  interface ColumnVisibility {
@@ -105,7 +106,232 @@ type TaskMenuState = {
105
106
  point?: { x: number; y: number } | null;
106
107
  };
107
108
 
108
- export function ListView({
109
+ export function ListView(props: Props) {
110
+ if (props.readOnly) {
111
+ return <ReadOnlyListView {...props} />;
112
+ }
113
+
114
+ return <InteractiveListView {...props} />;
115
+ }
116
+
117
+ function ReadOnlyListView({
118
+ tasks,
119
+ lists,
120
+ isPersonalWorkspace = false,
121
+ preserveTaskOrder = false,
122
+ searchQuery,
123
+ }: Props) {
124
+ const t = useTranslations();
125
+ const tc = useTranslations('common');
126
+ const locale = useLocale();
127
+ const dateLocale = locale === 'vi' ? vi : enUS;
128
+ const [sortField, setSortField] = useState<ListViewSortField>('created_at');
129
+ const [sortOrder, setSortOrder] = useState<ListViewSortOrder>('desc');
130
+ const listsById = useMemo(
131
+ () => new Map(lists.map((list) => [list.id, list])),
132
+ [lists]
133
+ );
134
+ const sortedTasks = useMemo(
135
+ () =>
136
+ sortListViewTasks(tasks, {
137
+ preserveTaskOrder,
138
+ searchQuery,
139
+ sortField,
140
+ sortOrder,
141
+ }),
142
+ [preserveTaskOrder, searchQuery, sortField, sortOrder, tasks]
143
+ );
144
+
145
+ function handleSort(field: ListViewSortField) {
146
+ if (field === sortField) {
147
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
148
+ } else {
149
+ setSortField(field);
150
+ setSortOrder('asc');
151
+ }
152
+ }
153
+
154
+ function getSortIcon(field: ListViewSortField) {
155
+ if (sortField !== field) {
156
+ return <ArrowDownUp className="ml-2 h-3 w-3 text-muted-foreground" />;
157
+ }
158
+ return sortOrder === 'asc' ? (
159
+ <ArrowUp className="ml-2 h-3 w-3 text-foreground" />
160
+ ) : (
161
+ <ArrowDown className="ml-2 h-3 w-3 text-foreground" />
162
+ );
163
+ }
164
+
165
+ function formatDate(date: string) {
166
+ return format(new Date(date), 'MMM dd', { locale: dateLocale });
167
+ }
168
+
169
+ return (
170
+ <div className="flex h-full flex-col">
171
+ {sortedTasks.length === 0 ? (
172
+ <div className="flex flex-1 items-center justify-center">
173
+ <p className="text-muted-foreground text-sm">{tc('no_tasks')}</p>
174
+ </div>
175
+ ) : (
176
+ <div className="relative flex-1 overflow-auto">
177
+ <Table>
178
+ <TableHeader className="sticky top-0 z-10 border-b bg-background">
179
+ <TableRow className="hover:bg-transparent">
180
+ <TableHead className="h-9 min-w-62.5 px-3">
181
+ <Button
182
+ variant="ghost"
183
+ className={cn(
184
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
185
+ sortField === 'name'
186
+ ? 'text-foreground'
187
+ : 'text-muted-foreground'
188
+ )}
189
+ onClick={() => handleSort('name')}
190
+ >
191
+ {tc('task_header')}
192
+ {getSortIcon('name')}
193
+ </Button>
194
+ </TableHead>
195
+ <TableHead className="h-9 w-32 px-2">
196
+ <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
197
+ {tc('status')}
198
+ </span>
199
+ </TableHead>
200
+ <TableHead className="h-9 w-24 px-2">
201
+ <Button
202
+ variant="ghost"
203
+ className={cn(
204
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
205
+ sortField === 'priority'
206
+ ? 'text-foreground'
207
+ : 'text-muted-foreground'
208
+ )}
209
+ onClick={() => handleSort('priority')}
210
+ >
211
+ {tc('priority')}
212
+ {getSortIcon('priority')}
213
+ </Button>
214
+ </TableHead>
215
+ <TableHead className="h-9 w-28 px-2">
216
+ <Button
217
+ variant="ghost"
218
+ className={cn(
219
+ '-ml-2 h-6 justify-start gap-1 px-2 font-medium text-[10px] uppercase tracking-wider transition-colors hover:bg-muted/50',
220
+ sortField === 'end_date'
221
+ ? 'text-foreground'
222
+ : 'text-muted-foreground'
223
+ )}
224
+ onClick={() => handleSort('end_date')}
225
+ >
226
+ {tc('due')}
227
+ {getSortIcon('end_date')}
228
+ </Button>
229
+ </TableHead>
230
+ {!isPersonalWorkspace && (
231
+ <TableHead className="h-9 w-32 px-2">
232
+ <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
233
+ {tc('assignee')}
234
+ </span>
235
+ </TableHead>
236
+ )}
237
+ </TableRow>
238
+ </TableHeader>
239
+ <TableBody>
240
+ {sortedTasks.map((task) => {
241
+ const list = listsById.get(task.list_id);
242
+ return (
243
+ <TableRow key={task.id} className="h-12 border-b">
244
+ <TableCell className="px-3 py-2">
245
+ <div className="min-w-0 space-y-1">
246
+ <div
247
+ className={cn(
248
+ 'truncate font-medium text-sm',
249
+ (task.completed_at || task.closed_at) &&
250
+ 'text-muted-foreground line-through'
251
+ )}
252
+ >
253
+ {task.name}
254
+ </div>
255
+ {(task.labels?.length || task.projects?.length) && (
256
+ <div className="flex flex-wrap items-center gap-1">
257
+ {task.labels?.slice(0, 3).map((label) => (
258
+ <Badge
259
+ key={label.id}
260
+ variant="outline"
261
+ className="h-4 gap-1 px-1.5 font-normal text-[10px]"
262
+ >
263
+ <span
264
+ aria-hidden="true"
265
+ className="h-2 w-2 rounded-full"
266
+ style={{ backgroundColor: label.color }}
267
+ />
268
+ {label.name}
269
+ </Badge>
270
+ ))}
271
+ {task.projects?.slice(0, 2).map((project) => (
272
+ <Badge
273
+ key={project.id}
274
+ variant="secondary"
275
+ className="h-4 px-1.5 font-normal text-[10px]"
276
+ >
277
+ {project.name}
278
+ </Badge>
279
+ ))}
280
+ </div>
281
+ )}
282
+ </div>
283
+ </TableCell>
284
+ <TableCell className="px-2 py-2">
285
+ <Badge variant="outline" className="font-normal">
286
+ {list?.name ?? tc('untitled')}
287
+ </Badge>
288
+ </TableCell>
289
+ <TableCell className="px-2 py-2">
290
+ {task.priority && (
291
+ <Badge variant="secondary" className="font-normal">
292
+ {t(`tasks.priority_${task.priority}`)}
293
+ </Badge>
294
+ )}
295
+ </TableCell>
296
+ <TableCell className="px-2 py-2">
297
+ {task.end_date && (
298
+ <span className="text-muted-foreground text-xs">
299
+ {formatDate(task.end_date)}
300
+ </span>
301
+ )}
302
+ </TableCell>
303
+ {!isPersonalWorkspace && (
304
+ <TableCell className="px-2 py-2">
305
+ {task.assignees && task.assignees.length > 0 && (
306
+ <div className="flex flex-wrap gap-1">
307
+ {task.assignees.slice(0, 2).map((assignee) => (
308
+ <Badge
309
+ key={assignee.id}
310
+ variant="secondary"
311
+ className="font-normal"
312
+ >
313
+ {assignee.display_name ||
314
+ assignee.email ||
315
+ assignee.handle ||
316
+ tc('assignee')}
317
+ </Badge>
318
+ ))}
319
+ </div>
320
+ )}
321
+ </TableCell>
322
+ )}
323
+ </TableRow>
324
+ );
325
+ })}
326
+ </TableBody>
327
+ </Table>
328
+ </div>
329
+ )}
330
+ </div>
331
+ );
332
+ }
333
+
334
+ function InteractiveListView({
109
335
  workspaceId,
110
336
  boardId,
111
337
  tasks,
@@ -73,14 +73,24 @@ interface RecycleBinPanelProps {
73
73
  };
74
74
  }
75
75
 
76
- export function RecycleBinPanel({
77
- open,
78
- onOpenChange,
76
+ interface RecycleBinContentProps
77
+ extends Omit<RecycleBinPanelProps, 'open' | 'onOpenChange'> {
78
+ active?: boolean;
79
+ className?: string;
80
+ onParentOpenChange?: (open: boolean) => void;
81
+ showHeader?: boolean;
82
+ }
83
+
84
+ export function RecycleBinContent({
85
+ active = true,
86
+ className,
87
+ onParentOpenChange,
88
+ showHeader = true,
79
89
  wsId,
80
90
  boardId,
81
91
  lists,
82
92
  translations,
83
- }: RecycleBinPanelProps) {
93
+ }: RecycleBinContentProps) {
84
94
  // Use provided translations or fall back to English defaults
85
95
  const t = useMemo(
86
96
  () => ({
@@ -143,7 +153,7 @@ export function RecycleBinPanel({
143
153
  boardId,
144
154
  wsId,
145
155
  {
146
- enabled: open,
156
+ enabled: active,
147
157
  staleTime: 60000,
148
158
  }
149
159
  );
@@ -193,20 +203,20 @@ export function RecycleBinPanel({
193
203
  (isOpen: boolean) => {
194
204
  setRestoreDialogOpen(isOpen);
195
205
  if (isOpen) {
196
- onOpenChange(false); // Close Sheet when dialog opens
206
+ onParentOpenChange?.(false); // Close Sheet when dialog opens
197
207
  }
198
208
  },
199
- [onOpenChange]
209
+ [onParentOpenChange]
200
210
  );
201
211
 
202
212
  const handleDeleteDialogOpenChange = useCallback(
203
213
  (isOpen: boolean) => {
204
214
  setDeleteDialogOpen(isOpen);
205
215
  if (isOpen) {
206
- onOpenChange(false); // Close Sheet when dialog opens
216
+ onParentOpenChange?.(false); // Close Sheet when dialog opens
207
217
  }
208
218
  },
209
- [onOpenChange]
219
+ [onParentOpenChange]
210
220
  );
211
221
 
212
222
  const handleRestore = useCallback(async () => {
@@ -254,97 +264,96 @@ export function RecycleBinPanel({
254
264
 
255
265
  return (
256
266
  <>
257
- <Sheet open={open} onOpenChange={onOpenChange}>
258
- <SheetContent side="right" className="flex w-full flex-col sm:max-w-md">
259
- <SheetHeader>
260
- <SheetTitle className="flex items-center gap-2">
267
+ <div className={cn('flex min-h-0 flex-1 flex-col', className)}>
268
+ {showHeader && (
269
+ <div className="border-b p-4">
270
+ <h2 className="flex items-center gap-2 font-semibold text-lg">
261
271
  <Trash2 className="h-5 w-5" />
262
272
  {t.recycleBin}
263
- </SheetTitle>
264
- <SheetDescription>{t.recycleBinDescription}</SheetDescription>
265
- </SheetHeader>
266
-
267
- <div className="flex flex-1 flex-col overflow-hidden">
268
- {/* Header with select all */}
269
- {deletedTasks.length > 0 && (
270
- <div className="flex items-center gap-3 border-b p-3">
271
- <Checkbox
272
- checked={allSelected}
273
- onCheckedChange={handleSelectAll}
274
- aria-label={t.selectAllTasks}
273
+ </h2>
274
+ <p className="mt-1 text-muted-foreground text-sm">
275
+ {t.recycleBinDescription}
276
+ </p>
277
+ </div>
278
+ )}
279
+ {/* Header with select all */}
280
+ {deletedTasks.length > 0 && (
281
+ <div className="flex items-center gap-3 border-b p-3">
282
+ <Checkbox
283
+ checked={allSelected}
284
+ onCheckedChange={handleSelectAll}
285
+ aria-label={t.selectAllTasks}
286
+ />
287
+ <span className="text-foreground text-sm">
288
+ {someSelected
289
+ ? t.selectedOfTotal
290
+ .replace('{selected}', String(selectedTasks.size))
291
+ .replace('{total}', String(deletedTasks.length))
292
+ : t.deletedTasksCount.replace(
293
+ '{count}',
294
+ String(deletedTasks.length)
295
+ )}
296
+ </span>
297
+ </div>
298
+ )}
299
+
300
+ {/* Task list */}
301
+ <div className="flex-1 overflow-y-auto p-3">
302
+ {isLoading ? (
303
+ <div className="flex items-center justify-center py-12">
304
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
305
+ </div>
306
+ ) : deletedTasks.length === 0 ? (
307
+ <div className="flex flex-col items-center justify-center py-12 text-center">
308
+ <Trash2 className="mb-3 h-12 w-12 text-muted-foreground/50" />
309
+ <p className="text-muted-foreground">{t.noDeletedTasks}</p>
310
+ <p className="mt-1 text-muted-foreground/70 text-xs">
311
+ {t.deletedTasksWillAppearHere}
312
+ </p>
313
+ </div>
314
+ ) : (
315
+ <div className="space-y-2">
316
+ {deletedTasks.map((task: Task) => (
317
+ <RecycleBinTaskRow
318
+ key={task.id}
319
+ task={task}
320
+ listName={listMap.get(task.list_id)}
321
+ isSelected={selectedTasks.has(task.id)}
322
+ onSelect={(checked) => handleSelectTask(task.id, checked)}
323
+ disabled={isWorking}
324
+ translations={t}
275
325
  />
276
- <span className="text-foreground text-sm">
277
- {someSelected
278
- ? t.selectedOfTotal
279
- .replace('{selected}', String(selectedTasks.size))
280
- .replace('{total}', String(deletedTasks.length))
281
- : t.deletedTasksCount.replace(
282
- '{count}',
283
- String(deletedTasks.length)
284
- )}
285
- </span>
286
- </div>
287
- )}
288
-
289
- {/* Task list */}
290
- <div className="flex-1 overflow-y-auto p-3">
291
- {isLoading ? (
292
- <div className="flex items-center justify-center py-12">
293
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
294
- </div>
295
- ) : deletedTasks.length === 0 ? (
296
- <div className="flex flex-col items-center justify-center py-12 text-center">
297
- <Trash2 className="mb-3 h-12 w-12 text-muted-foreground/50" />
298
- <p className="text-muted-foreground">{t.noDeletedTasks}</p>
299
- <p className="mt-1 text-muted-foreground/70 text-xs">
300
- {t.deletedTasksWillAppearHere}
301
- </p>
302
- </div>
303
- ) : (
304
- <div className="space-y-2">
305
- {deletedTasks.map((task: Task) => (
306
- <RecycleBinTaskRow
307
- key={task.id}
308
- task={task}
309
- listName={listMap.get(task.list_id)}
310
- isSelected={selectedTasks.has(task.id)}
311
- onSelect={(checked) => handleSelectTask(task.id, checked)}
312
- disabled={isWorking}
313
- translations={t}
314
- />
315
- ))}
316
- </div>
317
- )}
326
+ ))}
318
327
  </div>
328
+ )}
329
+ </div>
319
330
 
320
- {/* Action bar */}
321
- {someSelected && (
322
- <div className="flex items-center gap-2 border-t px-3 py-4">
323
- <Button
324
- variant="outline"
325
- size="sm"
326
- className="flex-1"
327
- onClick={() => setRestoreDialogOpen(true)}
328
- disabled={isWorking}
329
- >
330
- <RotateCcw className="mr-2 h-4 w-4" />
331
- {t.restore} ({selectedTasks.size})
332
- </Button>
333
- <Button
334
- variant="destructive"
335
- size="sm"
336
- className="flex-1"
337
- onClick={() => setDeleteDialogOpen(true)}
338
- disabled={isWorking}
339
- >
340
- <Trash2 className="mr-2 h-4 w-4" />
341
- {t.delete} ({selectedTasks.size})
342
- </Button>
343
- </div>
344
- )}
331
+ {/* Action bar */}
332
+ {someSelected && (
333
+ <div className="flex items-center gap-2 border-t px-3 py-4">
334
+ <Button
335
+ variant="outline"
336
+ size="sm"
337
+ className="flex-1"
338
+ onClick={() => handleRestoreDialogOpenChange(true)}
339
+ disabled={isWorking}
340
+ >
341
+ <RotateCcw className="mr-2 h-4 w-4" />
342
+ {t.restore} ({selectedTasks.size})
343
+ </Button>
344
+ <Button
345
+ variant="destructive"
346
+ size="sm"
347
+ className="flex-1"
348
+ onClick={() => handleDeleteDialogOpenChange(true)}
349
+ disabled={isWorking}
350
+ >
351
+ <Trash2 className="mr-2 h-4 w-4" />
352
+ {t.delete} ({selectedTasks.size})
353
+ </Button>
345
354
  </div>
346
- </SheetContent>
347
- </Sheet>
355
+ )}
356
+ </div>
348
357
 
349
358
  {/* Restore Confirmation Dialog */}
350
359
  <AlertDialog
@@ -433,6 +442,45 @@ export function RecycleBinPanel({
433
442
  );
434
443
  }
435
444
 
445
+ export function RecycleBinPanel({
446
+ open,
447
+ onOpenChange,
448
+ wsId,
449
+ boardId,
450
+ lists,
451
+ translations,
452
+ }: RecycleBinPanelProps) {
453
+ const t = {
454
+ recycleBin: translations?.recycleBin ?? 'Recycle Bin',
455
+ recycleBinDescription:
456
+ translations?.recycleBinDescription ??
457
+ 'Restore or permanently delete tasks that were previously removed.',
458
+ };
459
+
460
+ return (
461
+ <Sheet open={open} onOpenChange={onOpenChange}>
462
+ <SheetContent side="right" className="flex w-full flex-col sm:max-w-md">
463
+ <SheetHeader>
464
+ <SheetTitle className="flex items-center gap-2">
465
+ <Trash2 className="h-5 w-5" />
466
+ {t.recycleBin}
467
+ </SheetTitle>
468
+ <SheetDescription>{t.recycleBinDescription}</SheetDescription>
469
+ </SheetHeader>
470
+ <RecycleBinContent
471
+ active={open}
472
+ boardId={boardId}
473
+ lists={lists}
474
+ onParentOpenChange={onOpenChange}
475
+ showHeader={false}
476
+ translations={translations}
477
+ wsId={wsId}
478
+ />
479
+ </SheetContent>
480
+ </Sheet>
481
+ );
482
+ }
483
+
436
484
  interface RecycleBinTaskRowProps {
437
485
  task: Task;
438
486
  listName?: string;