@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
@@ -43,6 +43,7 @@ interface VirtualizedTaskListProps {
43
43
  onLoadMore?: () => void;
44
44
  hasMore?: boolean;
45
45
  isLoadingMore?: boolean;
46
+ readOnly?: boolean;
46
47
  }
47
48
 
48
49
  interface TaskListContentProps {
@@ -64,6 +65,7 @@ interface TaskListContentProps {
64
65
  bulkUpdateCustomDueDate?: (date: Date | null) => Promise<void>;
65
66
  startIndex?: number;
66
67
  taskOrder?: Pick<Task, 'id'>[];
68
+ readOnly?: boolean;
67
69
  }
68
70
 
69
71
  export function getTaskDragPreviewSlotIndex({
@@ -132,6 +134,7 @@ function TaskListContent({
132
134
  bulkUpdateCustomDueDate,
133
135
  startIndex = 0,
134
136
  taskOrder = tasks,
137
+ readOnly = false,
135
138
  }: TaskListContentProps) {
136
139
  const slotIndex = getTaskDragPreviewSlotIndex({
137
140
  columnId: column.id,
@@ -170,6 +173,7 @@ function TaskListContent({
170
173
  optimisticUpdateInProgress={optimisticUpdateInProgress}
171
174
  selectedTasks={selectedTasks}
172
175
  bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
176
+ readOnly={readOnly}
173
177
  />
174
178
  {slotIndex === globalIndex + 1 && (
175
179
  <DragPreviewSlot
@@ -238,6 +242,7 @@ function VirtualizedTaskListInner({
238
242
  onLoadMore,
239
243
  hasMore,
240
244
  isLoadingMore,
245
+ readOnly = false,
241
246
  }: VirtualizedTaskListProps) {
242
247
  const t = useTranslations('common');
243
248
  const tTasks = useTranslations('ws-tasks');
@@ -474,6 +479,7 @@ function VirtualizedTaskListInner({
474
479
  bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
475
480
  startIndex={startIndex}
476
481
  taskOrder={tasks}
482
+ readOnly={readOnly}
477
483
  />
478
484
  </div>
479
485
  </div>
@@ -502,6 +508,7 @@ function VirtualizedTaskListInner({
502
508
  optimisticUpdateInProgress={optimisticUpdateInProgress}
503
509
  bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
504
510
  taskOrder={tasks}
511
+ readOnly={readOnly}
505
512
  />
506
513
  {loadMoreSentinel}
507
514
  </SortableContext>
@@ -14,6 +14,15 @@ export const HANDLE_WIDTH = 12;
14
14
  export const DRAG_ACTIVATION_PX = 6;
15
15
  export const COLLAPSED_UNSCHEDULED_PREVIEW_COUNT = 4;
16
16
 
17
+ export function getTimelineDayGridBackground(dayWidth: number, opacity = 0.45) {
18
+ const safeDayWidth = Math.max(1, dayWidth);
19
+ const lineStart = Math.max(0, safeDayWidth - 1);
20
+
21
+ return {
22
+ backgroundImage: `repeating-linear-gradient(to right, transparent 0, transparent ${lineStart}px, hsl(var(--border) / ${opacity}) ${lineStart}px, hsl(var(--border) / ${opacity}) ${safeDayWidth}px)`,
23
+ };
24
+ }
25
+
17
26
  export function getDensityConfig(density: Density) {
18
27
  switch (density) {
19
28
  case 'compact':
@@ -6,7 +6,10 @@ import { cn } from '@tuturuuu/utils/format';
6
6
  import dayjs from 'dayjs';
7
7
  import type { useTranslations } from 'next-intl';
8
8
  import type { PointerEvent as ReactPointerEvent } from 'react';
9
- import { getListStatusBadgeClasses } from './timeline-display';
9
+ import {
10
+ getListStatusBadgeClasses,
11
+ getTimelineDayGridBackground,
12
+ } from './timeline-display';
10
13
  import { TimelineTaskRow } from './timeline-task-row';
11
14
  import type {
12
15
  TimelineInteractionMode,
@@ -197,7 +200,6 @@ export function TimelineGrid({
197
200
  groupId={group.id}
198
201
  localTasks={localTasks}
199
202
  dayWidth={dayWidth}
200
- timelineWidth={timelineWidth}
201
203
  groupHeaderHeight={groupHeaderHeight}
202
204
  isMoveTargetGroup={isMoveTargetGroup}
203
205
  isPreviewGroup={isPreviewGroup}
@@ -257,7 +259,6 @@ export function TimelineGrid({
257
259
  groupId={group.id}
258
260
  localTasks={localTasks}
259
261
  dayWidth={dayWidth}
260
- timelineWidth={timelineWidth}
261
262
  groupHeaderHeight={rowHeight}
262
263
  isMoveTargetGroup={isMoveTargetGroup}
263
264
  isPreviewGroup={isPreviewGroup}
@@ -281,7 +282,6 @@ function TimelineGroupDropTarget({
281
282
  groupId,
282
283
  localTasks,
283
284
  dayWidth,
284
- timelineWidth,
285
285
  groupHeaderHeight,
286
286
  isMoveTargetGroup,
287
287
  isPreviewGroup,
@@ -295,7 +295,6 @@ function TimelineGroupDropTarget({
295
295
  groupId: string;
296
296
  localTasks: Task[];
297
297
  dayWidth: number;
298
- timelineWidth: number;
299
298
  groupHeaderHeight: number;
300
299
  isMoveTargetGroup: boolean;
301
300
  isPreviewGroup: boolean;
@@ -364,17 +363,10 @@ function TimelineGroupDropTarget({
364
363
  </div>
365
364
  );
366
365
  })()}
367
- <div className="pointer-events-none absolute inset-0 flex">
368
- {Array.from({
369
- length: Math.max(1, Math.ceil(timelineWidth / dayWidth)),
370
- }).map((_, index) => (
371
- <div
372
- key={`${groupId}-drop-${index}`}
373
- className="h-full border-border/35 border-r"
374
- style={{ width: dayWidth }}
375
- />
376
- ))}
377
- </div>
366
+ <div
367
+ className="pointer-events-none absolute inset-0"
368
+ style={getTimelineDayGridBackground(dayWidth, 0.35)}
369
+ />
378
370
  </div>
379
371
  );
380
372
  }
@@ -18,7 +18,6 @@ import {
18
18
  DropdownMenuSubTrigger,
19
19
  } from '@tuturuuu/ui/dropdown-menu';
20
20
  import { cn } from '@tuturuuu/utils/format';
21
- import dayjs from 'dayjs';
22
21
  import type { useTranslations } from 'next-intl';
23
22
  import type { PointerEvent as ReactPointerEvent } from 'react';
24
23
  import { useState } from 'react';
@@ -26,6 +25,7 @@ import { TaskRowActionsMenu } from '../../../shared/task-row-actions-menu';
26
25
  import {
27
26
  getStatusToneClasses,
28
27
  getTaskEyebrow,
28
+ getTimelineDayGridBackground,
29
29
  HANDLE_WIDTH,
30
30
  } from './timeline-display';
31
31
  import type {
@@ -263,30 +263,10 @@ export function TimelineTaskRow({
263
263
  style={{ minHeight: rowHeight }}
264
264
  onDoubleClick={() => onOpenEditor(item.task)}
265
265
  >
266
- <div className="pointer-events-none absolute inset-0 flex">
267
- {Array.from({
268
- length: Math.max(1, Math.ceil(timelineWidth / dayWidth)),
269
- }).map((_, index) => {
270
- const isToday = todayVisible && index === todayIndex;
271
- const date = item.start
272
- ? dayjs(item.start).add(index - item.offsetDays, 'day')
273
- : null;
274
- const isWeekend = date ? [0, 6].includes(date.day()) : false;
275
-
276
- return (
277
- <div
278
- key={`${item.task.id}-${index}`}
279
- className={cn(
280
- 'h-full border-border/45 border-r',
281
- index === 0 && 'border-l border-l-border/50',
282
- isWeekend && 'bg-muted/[0.12]',
283
- isToday && 'bg-dynamic-blue/[0.035]'
284
- )}
285
- style={{ width: dayWidth }}
286
- />
287
- );
288
- })}
289
- </div>
266
+ <div
267
+ className="pointer-events-none absolute inset-0 border-border/50 border-l"
268
+ style={getTimelineDayGridBackground(dayWidth)}
269
+ />
290
270
 
291
271
  {todayVisible && (
292
272
  <div
@@ -1,7 +1,11 @@
1
1
  import type { Task } from '@tuturuuu/types/primitives/Task';
2
2
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest';
4
- import { buildTimelineModel, computeTimelineSpans } from './timeline-utils';
4
+ import {
5
+ buildTimelineModel,
6
+ computeTimelineSpans,
7
+ MAX_TIMELINE_DAYS,
8
+ } from './timeline-utils';
5
9
 
6
10
  const lists: TaskList[] = [
7
11
  {
@@ -108,6 +112,37 @@ describe('timeline row model', () => {
108
112
  );
109
113
  });
110
114
 
115
+ it('caps the rendered day window for extreme scheduled task ranges', () => {
116
+ vi.useFakeTimers();
117
+ vi.setSystemTime(new Date('2026-05-07T12:00:00.000Z'));
118
+
119
+ const model = buildTimelineModel(
120
+ [
121
+ task({
122
+ id: 'old-task',
123
+ name: 'Very old task',
124
+ start_date: '2020-01-01T00:00:00.000Z',
125
+ end_date: '2020-01-02T23:59:59.999Z',
126
+ }),
127
+ task({
128
+ id: 'future-task',
129
+ name: 'Very future task',
130
+ start_date: '2035-01-01T00:00:00.000Z',
131
+ end_date: '2035-01-02T23:59:59.999Z',
132
+ }),
133
+ ],
134
+ lists
135
+ );
136
+
137
+ expect(model.days).toHaveLength(MAX_TIMELINE_DAYS);
138
+ expect(model.rangeStart.getTime()).toBeLessThanOrEqual(
139
+ new Date('2026-05-07T00:00:00.000Z').getTime()
140
+ );
141
+ expect(model.rangeEnd.getTime()).toBeGreaterThanOrEqual(
142
+ new Date('2026-05-07T00:00:00.000Z').getTime()
143
+ );
144
+ });
145
+
111
146
  it('keeps span computation stable for existing analytics exports', () => {
112
147
  vi.useFakeTimers();
113
148
  vi.setSystemTime(new Date('2026-05-07T12:00:00.000Z'));
@@ -62,6 +62,7 @@ const DEFAULT_PAST_PADDING_DAYS = 3;
62
62
  const DEFAULT_FUTURE_PADDING_DAYS = 6;
63
63
  const EMPTY_PAST_PADDING_DAYS = 7;
64
64
  const EMPTY_FUTURE_PADDING_DAYS = 14;
65
+ export const MAX_TIMELINE_DAYS = 180;
65
66
  const TIMELINE_LIST_STATUS_ORDER: Record<string, number> = {
66
67
  not_started: 0,
67
68
  active: 1,
@@ -126,6 +127,48 @@ export function buildMonthSegments(days: Date[]) {
126
127
  return segments;
127
128
  }
128
129
 
130
+ function capTimelineRange(
131
+ rangeStart: Date,
132
+ rangeEnd: Date,
133
+ todayMid: dayjs.Dayjs
134
+ ) {
135
+ const start = dayjs(rangeStart).startOf('day');
136
+ const end = dayjs(rangeEnd).startOf('day');
137
+ const totalDays = end.diff(start, 'day') + 1;
138
+
139
+ if (totalDays <= MAX_TIMELINE_DAYS) {
140
+ return {
141
+ rangeStart: start.toDate(),
142
+ rangeEnd: end.toDate(),
143
+ };
144
+ }
145
+
146
+ const latestStart = end.subtract(MAX_TIMELINE_DAYS - 1, 'day');
147
+ let cappedStart = start;
148
+
149
+ if (
150
+ (todayMid.isAfter(start) || todayMid.isSame(start, 'day')) &&
151
+ (todayMid.isBefore(end) || todayMid.isSame(end, 'day'))
152
+ ) {
153
+ cappedStart = todayMid.subtract(Math.floor(MAX_TIMELINE_DAYS / 2), 'day');
154
+ } else if (todayMid.isAfter(end)) {
155
+ cappedStart = latestStart;
156
+ }
157
+
158
+ if (cappedStart.isBefore(start)) {
159
+ cappedStart = start;
160
+ }
161
+
162
+ if (cappedStart.isAfter(latestStart)) {
163
+ cappedStart = latestStart;
164
+ }
165
+
166
+ return {
167
+ rangeStart: cappedStart.toDate(),
168
+ rangeEnd: cappedStart.add(MAX_TIMELINE_DAYS - 1, 'day').toDate(),
169
+ };
170
+ }
171
+
129
172
  function withTaskRows(
130
173
  items: Array<Omit<TimelineItem, 'offsetDays'>>,
131
174
  rangeStart: Date
@@ -280,7 +323,7 @@ export function buildTimelineModel(
280
323
  });
281
324
  }
282
325
 
283
- const rangeStart = scheduled.length
326
+ const rawRangeStart = scheduled.length
284
327
  ? dayjs(
285
328
  scheduled.reduce(
286
329
  (min, item) => (item.start < min ? item.start : min),
@@ -292,7 +335,7 @@ export function buildTimelineModel(
292
335
  .toDate()
293
336
  : todayMid.subtract(EMPTY_PAST_PADDING_DAYS, 'day').toDate();
294
337
 
295
- const rangeEnd = scheduled.length
338
+ const rawRangeEnd = scheduled.length
296
339
  ? dayjs(
297
340
  scheduled.reduce(
298
341
  (max, item) => (item.end > max ? item.end : max),
@@ -304,6 +347,12 @@ export function buildTimelineModel(
304
347
  .toDate()
305
348
  : todayMid.add(EMPTY_FUTURE_PADDING_DAYS, 'day').toDate();
306
349
 
350
+ const { rangeStart, rangeEnd } = capTimelineRange(
351
+ rawRangeStart,
352
+ rawRangeEnd,
353
+ todayMid
354
+ );
355
+
307
356
  const days = enumerateDays(rangeStart, rangeEnd);
308
357
  const monthSegments = buildMonthSegments(days);
309
358
  const todayIndex = Math.round(
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import { ChevronDown, Info } from '@tuturuuu/icons';
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from '@tuturuuu/ui/collapsible';
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipProvider,
13
+ TooltipTrigger,
14
+ } from '@tuturuuu/ui/tooltip';
15
+ import { cn } from '@tuturuuu/utils/format';
16
+ import { useTranslations } from 'next-intl';
17
+ import type { ReactNode } from 'react';
18
+
19
+ function ShareInfoTooltip({
20
+ content,
21
+ label,
22
+ }: {
23
+ content: string;
24
+ label: string;
25
+ }) {
26
+ return (
27
+ <TooltipProvider delayDuration={0} skipDelayDuration={0}>
28
+ <Tooltip>
29
+ <TooltipTrigger asChild>
30
+ <button
31
+ type="button"
32
+ className="text-muted-foreground transition-colors hover:text-foreground"
33
+ aria-label={label}
34
+ >
35
+ <Info className="h-3.5 w-3.5" />
36
+ </button>
37
+ </TooltipTrigger>
38
+ <TooltipContent className="max-w-xs">{content}</TooltipContent>
39
+ </Tooltip>
40
+ </TooltipProvider>
41
+ );
42
+ }
43
+
44
+ interface ShareSectionProps {
45
+ children: ReactNode;
46
+ icon: ReactNode;
47
+ onOpenChange: (open: boolean) => void;
48
+ open: boolean;
49
+ statusBadge: ReactNode;
50
+ title: string;
51
+ tooltip: string;
52
+ }
53
+
54
+ export function ShareSection({
55
+ children,
56
+ icon,
57
+ onOpenChange,
58
+ open,
59
+ statusBadge,
60
+ title,
61
+ tooltip,
62
+ }: ShareSectionProps) {
63
+ const t = useTranslations();
64
+
65
+ return (
66
+ <Collapsible
67
+ open={open}
68
+ onOpenChange={onOpenChange}
69
+ className="rounded-md border"
70
+ >
71
+ <div className="flex min-h-11 items-center gap-2 px-3">
72
+ <CollapsibleTrigger asChild>
73
+ <button
74
+ type="button"
75
+ className="flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:text-foreground"
76
+ >
77
+ {icon}
78
+ <span className="min-w-0 flex-1 truncate font-medium text-sm">
79
+ {title}
80
+ </span>
81
+ {statusBadge}
82
+ <ChevronDown
83
+ className={cn(
84
+ 'h-4 w-4 shrink-0 text-muted-foreground transition-transform',
85
+ open && 'rotate-180'
86
+ )}
87
+ />
88
+ </button>
89
+ </CollapsibleTrigger>
90
+ <ShareInfoTooltip
91
+ label={t('ws-task-boards.share.note')}
92
+ content={tooltip}
93
+ />
94
+ </div>
95
+ <CollapsibleContent className="border-t p-3">
96
+ {children}
97
+ </CollapsibleContent>
98
+ </Collapsible>
99
+ );
100
+ }
@@ -72,6 +72,15 @@ export default function WorkspaceProjectsClientPage({
72
72
  const isPermissionLoading = permissionQuery.isLoading;
73
73
 
74
74
  const resolvedWsId = workspace?.id;
75
+ const workspaceAccess = workspace as
76
+ | { access_type?: string; guest_products?: string[] }
77
+ | null
78
+ | undefined;
79
+ const isWorkspaceGuestTasksAccess =
80
+ workspaceAccess?.access_type === 'guest' &&
81
+ workspaceAccess.guest_products?.includes('tasks');
82
+ const canReadBoards =
83
+ canManageProjects === true || isWorkspaceGuestTasksAccess;
75
84
 
76
85
  const {
77
86
  data: boardsPayload,
@@ -85,9 +94,10 @@ export default function WorkspaceProjectsClientPage({
85
94
  page: Number.parseInt(page, 10),
86
95
  pageSize: Number.parseInt(pageSize, 10),
87
96
  }),
88
- enabled: Boolean(resolvedWsId),
97
+ enabled: Boolean(resolvedWsId && canReadBoards),
89
98
  });
90
- const isGuestBoardAccess = boardsPayload?.access_type === 'guest';
99
+ const isGuestBoardAccess =
100
+ isWorkspaceGuestTasksAccess || boardsPayload?.access_type === 'guest';
91
101
 
92
102
  useEffect(() => {
93
103
  if (
@@ -112,7 +122,7 @@ export default function WorkspaceProjectsClientPage({
112
122
  isWorkspacePending ||
113
123
  isWorkspaceUserLoading ||
114
124
  (isPermissionLoading && !isGuestBoardAccess) ||
115
- isBoardsPending
125
+ (canReadBoards && isBoardsPending)
116
126
  ) {
117
127
  return <BoardsListSkeleton />;
118
128
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useQuery } from '@tanstack/react-query';
4
4
  import {
5
+ convertWorkspaceTaskDraft,
5
6
  listWorkspaceTaskBoards,
6
7
  listWorkspaceTaskLists,
7
8
  } from '@tuturuuu/internal-api';
@@ -46,6 +47,10 @@ export function DraftConvertDialog({
46
47
  const [selectedBoardId, setSelectedBoardId] = useState<string>('');
47
48
  const [selectedListId, setSelectedListId] = useState<string>('');
48
49
  const [isConverting, setIsConverting] = useState(false);
50
+ const internalApiOptions =
51
+ typeof window !== 'undefined'
52
+ ? { baseUrl: window.location.origin }
53
+ : undefined;
49
54
 
50
55
  // Sync selections when draft changes (useState initializer only runs on mount)
51
56
  useEffect(() => {
@@ -107,20 +112,13 @@ export function DraftConvertDialog({
107
112
 
108
113
  setIsConverting(true);
109
114
  try {
110
- const res = await fetch(
111
- `/api/v1/workspaces/${wsId}/task-drafts/${draft.id}/convert`,
112
- {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ listId: selectedListId }),
116
- }
115
+ await convertWorkspaceTaskDraft(
116
+ wsId,
117
+ draft.id,
118
+ { listId: selectedListId },
119
+ internalApiOptions
117
120
  );
118
121
 
119
- if (!res.ok) {
120
- const data = await res.json();
121
- throw new Error(data.error || 'Failed to convert draft');
122
- }
123
-
124
122
  toast.success(t('converted_success'));
125
123
  onConverted();
126
124
  onClose();
@@ -2,6 +2,10 @@
2
2
 
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { FileText } from '@tuturuuu/icons';
5
+ import {
6
+ deleteWorkspaceTaskDraft,
7
+ listWorkspaceTaskDrafts,
8
+ } from '@tuturuuu/internal-api/tasks';
5
9
  import { toast } from '@tuturuuu/ui/sonner';
6
10
  import { useTranslations } from 'next-intl';
7
11
  import { useState } from 'react';
@@ -11,38 +15,51 @@ import { DraftConvertDialog } from './draft-convert-dialog';
11
15
 
12
16
  interface DraftsPageProps {
13
17
  wsId: string;
18
+ boardId?: string;
19
+ includeUnassignedForBoard?: boolean;
14
20
  }
15
21
 
16
- export function DraftsPage({ wsId }: DraftsPageProps) {
22
+ function getBrowserInternalApiOptions() {
23
+ return typeof window !== 'undefined'
24
+ ? { baseUrl: window.location.origin }
25
+ : undefined;
26
+ }
27
+
28
+ export function DraftsPage({
29
+ wsId,
30
+ boardId,
31
+ includeUnassignedForBoard = false,
32
+ }: DraftsPageProps) {
17
33
  const t = useTranslations('task-drafts');
18
34
  const queryClient = useQueryClient();
19
35
  const { editDraft } = useTaskDialogContext();
20
36
  const [convertDraft, setConvertDraft] = useState<TaskDraft | null>(null);
37
+ const draftQueryKey = [
38
+ 'task-drafts',
39
+ wsId,
40
+ boardId ?? 'all',
41
+ includeUnassignedForBoard,
42
+ ] as const;
21
43
 
22
44
  const { data: drafts = [], isLoading } = useQuery<TaskDraft[]>({
23
- queryKey: ['task-drafts', wsId],
24
- queryFn: async () => {
25
- const res = await fetch(`/api/v1/workspaces/${wsId}/task-drafts`);
26
- if (!res.ok) throw new Error('Failed to fetch drafts');
27
- const json = await res.json();
28
- return json.data;
29
- },
45
+ queryKey: draftQueryKey,
46
+ queryFn: () =>
47
+ listWorkspaceTaskDrafts(
48
+ wsId,
49
+ { boardId, includeUnassignedForBoard },
50
+ getBrowserInternalApiOptions()
51
+ ) as Promise<TaskDraft[]>,
30
52
  });
31
53
 
32
54
  const deleteMutation = useMutation({
33
- mutationFn: async (draftId: string) => {
34
- const res = await fetch(
35
- `/api/v1/workspaces/${wsId}/task-drafts/${draftId}`,
36
- { method: 'DELETE' }
37
- );
38
- if (!res.ok) throw new Error('Failed to delete draft');
39
- },
55
+ mutationFn: async (draftId: string) =>
56
+ deleteWorkspaceTaskDraft(wsId, draftId, getBrowserInternalApiOptions()),
40
57
  onSuccess: () => {
41
58
  queryClient.invalidateQueries({ queryKey: ['task-drafts', wsId] });
42
59
  toast.success(t('deleted_success'));
43
60
  },
44
61
  onError: () => {
45
- toast.error('Failed to delete draft');
62
+ toast.error(t('delete_failed'));
46
63
  },
47
64
  });
48
65