@tuturuuu/ui 0.6.2 → 0.8.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 (108) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/biome.json +1 -1
  3. package/package.json +11 -11
  4. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  5. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  6. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  7. package/src/components/ui/calendar.test.tsx +24 -0
  8. package/src/components/ui/calendar.tsx +1 -0
  9. package/src/components/ui/currency-input.test.tsx +43 -0
  10. package/src/components/ui/currency-input.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  12. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  13. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  14. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  15. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  16. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  17. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  18. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  19. package/src/components/ui/date-time-picker.tsx +352 -234
  20. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  21. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  22. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  23. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  24. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  25. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  26. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  27. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  28. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  29. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  30. package/src/components/ui/finance/transactions/form-types.ts +5 -0
  31. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  32. package/src/components/ui/finance/transactions/form.tsx +116 -20
  33. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  34. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  35. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  36. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  37. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  38. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  39. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  40. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  41. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  42. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  43. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  48. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  49. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  50. package/src/components/ui/money-input.test.tsx +64 -0
  51. package/src/components/ui/money-input.tsx +63 -0
  52. package/src/components/ui/optional-time-picker.tsx +95 -0
  53. package/src/components/ui/quick-command-center.test.tsx +90 -0
  54. package/src/components/ui/quick-command-center.tsx +190 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +126 -50
  56. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +23 -20
  58. package/src/components/ui/storefront/image-panel.tsx +6 -0
  59. package/src/components/ui/storefront/index.ts +11 -0
  60. package/src/components/ui/storefront/listing-card.tsx +84 -22
  61. package/src/components/ui/storefront/product-detail.tsx +289 -0
  62. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  63. package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
  64. package/src/components/ui/storefront/storefront-surface.tsx +371 -128
  65. package/src/components/ui/storefront/types.ts +25 -1
  66. package/src/components/ui/storefront/utils.ts +118 -13
  67. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  68. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  69. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  70. package/src/components/ui/text-editor/content-migration.ts +41 -18
  71. package/src/components/ui/text-editor/extensions.ts +1 -1
  72. package/src/components/ui/text-editor/image-extension.ts +40 -18
  73. package/src/components/ui/text-editor/video-extension.ts +11 -2
  74. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  75. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  76. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  79. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  80. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  81. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  84. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  85. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  86. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  87. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  88. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  89. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  90. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  91. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  92. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  93. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  94. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  95. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  100. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  101. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
  102. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  103. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  104. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  105. package/src/hooks/useBoardRealtime.ts +6 -3
  106. package/src/hooks/useBoardRealtime.types.ts +11 -0
  107. package/src/hooks/useCursorTracking.ts +91 -27
  108. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -10,8 +10,10 @@ import { toast } from './use-toast';
10
10
  import {
11
11
  type BoardRealtimePayload,
12
12
  createRealtimeClientId,
13
+ getBoardRealtimeChannelName,
13
14
  isBoardRealtimeEnvelope,
14
15
  LOCAL_BROADCAST_CHANNEL_PREFIX,
16
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG,
15
17
  type RealtimeChannel,
16
18
  SEEN_REALTIME_EVENT_LIMIT,
17
19
  } from './useBoardRealtime.types';
@@ -205,9 +207,10 @@ export function useBoardRealtime(
205
207
  channelRef.current = null;
206
208
  }
207
209
 
208
- const channel = supabase.channel(`board-realtime-${boardId}`, {
209
- config: { broadcast: { self: false } },
210
- });
210
+ const channel = supabase.channel(
211
+ getBoardRealtimeChannelName(boardId),
212
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
213
+ );
211
214
  channelRef.current = channel;
212
215
 
213
216
  channel
@@ -14,8 +14,19 @@ type BoardRealtimeEnvelope = {
14
14
  payload: BoardRealtimePayload;
15
15
  };
16
16
 
17
+ export const BOARD_REALTIME_CHANNEL_PREFIX = 'board-realtime';
17
18
  export const LOCAL_BROADCAST_CHANNEL_PREFIX = 'tuturuuu:board-realtime';
18
19
  export const SEEN_REALTIME_EVENT_LIMIT = 500;
20
+ export const PRIVATE_TASK_REALTIME_CHANNEL_CONFIG = {
21
+ config: {
22
+ broadcast: { self: false },
23
+ private: true,
24
+ },
25
+ } as const;
26
+
27
+ export function getBoardRealtimeChannelName(boardId: string) {
28
+ return `${BOARD_REALTIME_CHANNEL_PREFIX}-${boardId}`;
29
+ }
19
30
 
20
31
  const isRecord = (value: unknown): value is Record<string, unknown> =>
21
32
  typeof value === 'object' && value !== null;
@@ -5,15 +5,79 @@ import { DEV_MODE } from '@tuturuuu/utils/constants';
5
5
  import type { RefObject } from 'react';
6
6
  import { useCallback, useEffect, useRef, useState } from 'react';
7
7
  import { usePageVisibility } from './use-page-visibility';
8
+ import { PRIVATE_TASK_REALTIME_CHANNEL_CONFIG } from './useBoardRealtime.types';
9
+
10
+ type CursorUser = Pick<User, 'avatar_url' | 'display_name' | 'id'> & {
11
+ id: string;
12
+ };
8
13
 
9
14
  export interface CursorPosition {
10
15
  x: number;
11
16
  y: number;
12
- user?: User;
17
+ user?: CursorUser;
13
18
  metadata?: { [key: string]: any };
14
19
  lastUpdatedAt: number;
15
20
  }
16
21
 
22
+ type ParsedCursorPosition = CursorPosition & { user: CursorUser };
23
+
24
+ function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === 'object' && value !== null;
26
+ }
27
+
28
+ function sanitizeCursorUser(user: User | undefined): CursorUser | null {
29
+ if (!user?.id) return null;
30
+
31
+ return {
32
+ avatar_url:
33
+ typeof user.avatar_url === 'string' && user.avatar_url.length > 0
34
+ ? user.avatar_url
35
+ : null,
36
+ display_name:
37
+ typeof user.display_name === 'string' && user.display_name.length > 0
38
+ ? user.display_name
39
+ : null,
40
+ id: user.id,
41
+ };
42
+ }
43
+
44
+ function sanitizeCursorMetadata(metadata: { [key: string]: any } | undefined) {
45
+ if (metadata == null || !isRecord(metadata) || Array.isArray(metadata)) {
46
+ return undefined;
47
+ }
48
+
49
+ return metadata;
50
+ }
51
+
52
+ function parseCursorMovePayload(payload: unknown): ParsedCursorPosition | null {
53
+ if (!isRecord(payload)) return null;
54
+
55
+ const { metadata, user, x, y } = payload;
56
+ if (typeof x !== 'number' || !Number.isFinite(x)) return null;
57
+ if (typeof y !== 'number' || !Number.isFinite(y)) return null;
58
+ if (!isRecord(user) || typeof user.id !== 'string' || user.id.length === 0) {
59
+ return null;
60
+ }
61
+
62
+ return {
63
+ lastUpdatedAt: Date.now(),
64
+ metadata: sanitizeCursorMetadata(metadata as { [key: string]: any }),
65
+ user: {
66
+ avatar_url:
67
+ typeof user.avatar_url === 'string' && user.avatar_url.length > 0
68
+ ? user.avatar_url
69
+ : null,
70
+ display_name:
71
+ typeof user.display_name === 'string' && user.display_name.length > 0
72
+ ? user.display_name
73
+ : null,
74
+ id: user.id,
75
+ },
76
+ x,
77
+ y,
78
+ };
79
+ }
80
+
17
81
  export function useCursorTracking(
18
82
  channelName: string,
19
83
  containerRef?: RefObject<HTMLElement | null>,
@@ -59,6 +123,8 @@ export function useCursorTracking(
59
123
  metadata?: { [key: string]: any }
60
124
  ) => {
61
125
  if (!channelRef.current) return;
126
+ const cursorUser = sanitizeCursorUser(user);
127
+ if (cursorUser == null) return;
62
128
  // Check error count instead of state to avoid dependency
63
129
  if (errorCountRef.current >= MAX_ERROR_COUNT) return;
64
130
 
@@ -73,7 +139,12 @@ export function useCursorTracking(
73
139
  await channelRef.current?.send({
74
140
  type: 'broadcast',
75
141
  event: 'cursor-move',
76
- payload: { x, y, user, metadata },
142
+ payload: {
143
+ metadata: sanitizeCursorMetadata(metadata),
144
+ user: cursorUser,
145
+ x,
146
+ y,
147
+ },
77
148
  });
78
149
 
79
150
  lastBroadcastTimeRef.current = Date.now();
@@ -90,7 +161,12 @@ export function useCursorTracking(
90
161
  await channelRef.current.send({
91
162
  type: 'broadcast',
92
163
  event: 'cursor-move',
93
- payload: { x, y, user, metadata },
164
+ payload: {
165
+ metadata: sanitizeCursorMetadata(metadata),
166
+ user: cursorUser,
167
+ x,
168
+ y,
169
+ },
94
170
  });
95
171
 
96
172
  lastBroadcastTimeRef.current = now;
@@ -207,38 +283,25 @@ export function useCursorTracking(
207
283
  channelRef.current = null;
208
284
  }
209
285
 
210
- const channel = supabase.channel(channelName, {
211
- config: {
212
- broadcast: {
213
- self: false, // Don't receive own broadcasts
214
- },
215
- },
216
- });
286
+ const channel = supabase.channel(
287
+ channelName,
288
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
289
+ );
217
290
 
218
291
  channelRef.current = channel;
219
292
  // Listen for cursor movements from other users
220
293
  channel
221
294
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
222
295
  try {
223
- const {
224
- x,
225
- y,
226
- user: broadcastUser,
227
- metadata: broadcastMetadata,
228
- } = payload.payload;
296
+ const cursor = parseCursorMovePayload(payload.payload);
297
+ if (cursor == null) return;
229
298
 
230
299
  // Ignore own broadcasts (extra safety)
231
- if (broadcastUser.id === user?.id) return;
300
+ if (cursor.user?.id === user?.id) return;
232
301
 
233
302
  setCursors((prev) => {
234
303
  const updated = new Map(prev);
235
- updated.set(broadcastUser.id || '', {
236
- x,
237
- y,
238
- user: broadcastUser,
239
- metadata: broadcastMetadata,
240
- lastUpdatedAt: Date.now(),
241
- });
304
+ updated.set(cursor.user.id, cursor);
242
305
  return updated;
243
306
  });
244
307
  } catch (err) {
@@ -289,16 +352,17 @@ export function useCursorTracking(
289
352
 
290
353
  // Broadcast cursor removal before unsubscribing (best effort)
291
354
  // This helps other users see the cursor disappear immediately
292
- if (channelRef.current && user?.id) {
355
+ const cursorUser = sanitizeCursorUser(user);
356
+ if (channelRef.current && cursorUser != null) {
293
357
  try {
294
358
  channelRef.current.send({
295
359
  type: 'broadcast',
296
360
  event: 'cursor-move',
297
361
  payload: {
362
+ metadata: sanitizeCursorMetadata(metadata),
363
+ user: cursorUser,
298
364
  x: -1000,
299
365
  y: -1000,
300
- user,
301
- metadata,
302
366
  },
303
367
  });
304
368
  } catch (err) {
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useRef } from 'react';
8
8
  import {
9
9
  type BoardRealtimePayload,
10
10
  createRealtimeClientId,
11
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG,
11
12
  type RealtimeChannel,
12
13
  SEEN_REALTIME_EVENT_LIMIT,
13
14
  } from './useBoardRealtime.types';
@@ -255,9 +256,10 @@ export function useTaskUserRealtime(userId: string | null | undefined) {
255
256
  return;
256
257
  }
257
258
 
258
- const channel = supabase.channel(getTaskUserRealtimeChannelName(userId), {
259
- config: { broadcast: { self: false } },
260
- });
259
+ const channel = supabase.channel(
260
+ getTaskUserRealtimeChannelName(userId),
261
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
262
+ );
261
263
  channelRef.current = channel;
262
264
 
263
265
  channel