@tuturuuu/ui 0.37.2 → 0.39.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 (126) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +10 -7
  3. package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +86 -0
  4. package/src/components/ui/custom/education-todo.test.tsx +131 -0
  5. package/src/components/ui/custom/education-todo.tsx +191 -0
  6. package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
  7. package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
  8. package/src/components/ui/custom/workspace-picker-content.tsx +93 -30
  9. package/src/components/ui/custom/workspace-select.tsx +12 -4
  10. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  11. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  12. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  13. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  14. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  15. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  16. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  17. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  18. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  19. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  20. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  21. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  22. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  23. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  24. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  25. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  26. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  27. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  28. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  29. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  30. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  31. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  32. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  33. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  34. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  35. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  36. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
  37. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  38. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  39. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  40. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  41. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  42. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  43. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  44. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  46. package/src/components/ui/finance/invoices/types.ts +3 -3
  47. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  48. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  49. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  50. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  51. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  52. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  53. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  54. package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  56. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  57. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  58. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  59. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1614
  60. package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
  61. package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
  62. package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
  63. package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
  64. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  65. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  66. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  67. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  68. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  69. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  70. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  71. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  72. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  73. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  74. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  75. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  76. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  77. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  78. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  79. package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
  80. package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
  81. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  82. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  83. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
  84. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
  85. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
  86. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
  87. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  88. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  89. package/src/components/ui/popover.tsx +3 -1
  90. package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
  91. package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
  92. package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
  93. package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
  94. package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
  95. package/src/components/ui/text-editor/editor.tsx +11 -3
  96. package/src/components/ui/text-editor/extensions.ts +8 -2
  97. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  98. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  99. package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
  100. package/src/components/ui/text-editor/markdown.ts +11 -0
  101. package/src/components/ui/text-editor/tool-bar.tsx +45 -102
  102. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  103. package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
  104. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  105. package/src/hooks/__tests__/supabase-provider.test.ts +110 -7
  106. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +39 -2
  107. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  108. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  109. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
  110. package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
  111. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +147 -0
  112. package/src/hooks/calendar-connection-types.ts +49 -0
  113. package/src/hooks/calendar-creation-request.test.ts +62 -0
  114. package/src/hooks/calendar-creation-request.ts +34 -0
  115. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  116. package/src/hooks/calendar-event-write-payload.ts +57 -0
  117. package/src/hooks/collaboration-document-hydration.ts +35 -0
  118. package/src/hooks/notification-subscription.ts +5 -2
  119. package/src/hooks/supabase-provider.ts +76 -112
  120. package/src/hooks/use-calendar-sync.tsx +41 -62
  121. package/src/hooks/use-calendar.tsx +44 -82
  122. package/src/hooks/use-exchange-rates.ts +4 -15
  123. package/src/hooks/use-local-storage.ts +9 -11
  124. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  125. package/src/hooks/use-workspace-presence.ts +9 -9
  126. package/src/hooks/use-yjs-collaboration.ts +68 -27
@@ -33,11 +33,13 @@ export interface YjsCollaborationResult {
33
33
  awareness: Awareness | null;
34
34
  provider: SupabaseProvider | null;
35
35
  synced: boolean;
36
+ hydrated: boolean;
37
+ hydrationFailed: boolean;
36
38
  connected: boolean;
37
39
  }
38
40
 
39
41
  /**
40
- * Hook for managing Yjs collaboration with Supabase Realtime.
42
+ * Hook for managing Yjs collaboration with Cloudflare realtime.
41
43
  *
42
44
  * Uses deferred cleanup so that React StrictMode's double-invoke cycle
43
45
  * (mount → cleanup → remount) reuses the existing SupabaseProvider instead
@@ -62,6 +64,10 @@ export function useYjsCollaboration(
62
64
  onSave,
63
65
  } = config;
64
66
 
67
+ const [hydratedProvider, setHydratedProvider] =
68
+ useState<SupabaseProvider | null>(null);
69
+ const [hydrationErrorProvider, setHydrationErrorProvider] =
70
+ useState<SupabaseProvider | null>(null);
65
71
  const [synced, setSynced] = useState(false);
66
72
  const [connected, setConnected] = useState(false);
67
73
  const [providerState, setProviderState] = useState<SupabaseProvider | null>(
@@ -72,17 +78,27 @@ export function useYjsCollaboration(
72
78
  // Ref that event listeners check — survives StrictMode cleanup/remount
73
79
  const mountedRef = useRef(false);
74
80
 
75
- // Create Yjs document and awareness (stable references)
76
- const doc = useMemo(() => (enabled ? new Y.Doc() : null), [enabled]);
77
-
78
- const awareness = useMemo(
79
- () => (enabled && doc ? new Awareness(doc) : null),
80
- [enabled, doc]
81
+ const hasUser = !!user;
82
+ // Create the document without starting awareness timers during render.
83
+ const documentScope = JSON.stringify([
84
+ channel,
85
+ tableName,
86
+ columnName,
87
+ id,
88
+ user?.id,
89
+ ]);
90
+ const doc = useMemo(
91
+ () => (enabled && hasUser ? new Y.Doc({ guid: documentScope }) : null),
92
+ [enabled, hasUser, documentScope]
81
93
  );
82
94
 
95
+ const awareness =
96
+ providerState && providerState.id === doc?.clientID
97
+ ? providerState.awareness
98
+ : null;
99
+
83
100
  // Stabilize callback and user refs — these should never cause provider
84
101
  // destruction/recreation.
85
- const hasUser = !!user;
86
102
  const userRef = useRef(user);
87
103
  userRef.current = user;
88
104
  const onSyncRef = useRef(onSync);
@@ -94,19 +110,22 @@ export function useYjsCollaboration(
94
110
 
95
111
  // Provider lifecycle: create/destroy based on channel config
96
112
  useEffect(() => {
97
- if (!enabled || !doc || !awareness || !hasUser) return;
113
+ if (!enabled || !doc || !hasUser) return;
98
114
 
99
115
  mountedRef.current = true;
100
116
 
101
117
  // ── StrictMode reuse path ───────────────────────────────────────
102
118
  // If a pending deferred destruction exists, cancel it and reuse
103
- // the existing provider. This prevents the Supabase Realtime
104
- // "leave then immediate re-join" race that causes a 10s timeout.
119
+ // the existing provider only when its document identity still matches.
105
120
  if (destroyTimerRef.current) {
106
121
  clearTimeout(destroyTimerRef.current);
107
122
  destroyTimerRef.current = null;
108
123
 
109
- if (providerRef.current && !providerRef.current.destroyed) {
124
+ if (
125
+ providerRef.current &&
126
+ !providerRef.current.destroyed &&
127
+ providerRef.current.id === doc.clientID
128
+ ) {
110
129
  console.log('♻️ Reusing existing SupabaseProvider (StrictMode)');
111
130
  return () => {
112
131
  mountedRef.current = false;
@@ -133,6 +152,7 @@ export function useYjsCollaboration(
133
152
  }
134
153
 
135
154
  const supabase = createClient();
155
+ const awareness = new Awareness(doc);
136
156
 
137
157
  // Set initial awareness state
138
158
  const currentUser = userRef.current;
@@ -152,6 +172,7 @@ export function useYjsCollaboration(
152
172
  tableName: tableName,
153
173
  columnName: columnName,
154
174
  awareness,
175
+ ownsDocument: true,
155
176
  resyncInterval: 30000,
156
177
  saveDebounceMs: saveDebounceMs ?? 300,
157
178
  loadState: loadDocumentState,
@@ -161,29 +182,38 @@ export function useYjsCollaboration(
161
182
 
162
183
  providerRef.current = provider;
163
184
  setProviderState(provider);
185
+ setSynced(false);
186
+ setConnected(false);
187
+
188
+ provider.on('hydrated', () => {
189
+ if (!mountedRef.current || providerRef.current !== provider) return;
190
+ // Publish a new state value so hydration is observable without transport.
191
+ setHydratedProvider(provider);
192
+ setHydrationErrorProvider(null);
193
+ });
164
194
 
165
195
  // Listen to provider events — use mountedRef so listeners stay valid
166
196
  // across StrictMode cleanup/remount without re-registration.
167
197
  provider.on('status', ([{ status }]) => {
168
- if (!mountedRef.current) return;
198
+ if (!mountedRef.current || providerRef.current !== provider) return;
169
199
  console.log('📡 Provider status:', status);
170
200
  setConnected(status === 'connected');
171
201
  });
172
202
 
173
203
  provider.on('synced', ([syncState]) => {
174
- if (!mountedRef.current) return;
204
+ if (!mountedRef.current || providerRef.current !== provider) return;
175
205
  console.log('🔄 Provider synced:', syncState);
176
206
  setSynced(syncState);
177
207
  onSyncRef.current?.(syncState);
178
208
  });
179
209
 
180
210
  provider.on('sync', ([syncState]) => {
181
- if (!mountedRef.current) return;
211
+ if (!mountedRef.current || providerRef.current !== provider) return;
182
212
  console.log('🔄 Provider sync event:', syncState);
183
213
  });
184
214
 
185
215
  provider.on('save', (version) => {
186
- if (!mountedRef.current) return;
216
+ if (!mountedRef.current || providerRef.current !== provider) return;
187
217
  console.log('💾 Document saved to database, version:', version);
188
218
  onSaveRef.current?.(version);
189
219
  });
@@ -196,30 +226,32 @@ export function useYjsCollaboration(
196
226
  channel: string;
197
227
  status: string;
198
228
  }) => {
199
- if (!mountedRef.current) return;
229
+ if (!mountedRef.current || providerRef.current !== provider) return;
200
230
  console.error('❌ Provider error:', {
201
231
  message: errorInfo.message,
202
232
  channel: errorInfo.channel,
203
233
  status: errorInfo.status,
204
234
  });
235
+ if (errorInfo.status === 'DOCUMENT_ERROR')
236
+ setHydrationErrorProvider(provider);
205
237
  onErrorRef.current?.(new Error(errorInfo.message));
206
238
  }
207
239
  );
208
240
 
209
241
  provider.on('connect', () => {
210
- if (!mountedRef.current) return;
242
+ if (!mountedRef.current || providerRef.current !== provider) return;
211
243
  console.log('✅ Provider connected');
212
244
  });
213
245
 
214
246
  provider.on('disconnect', () => {
215
- if (!mountedRef.current) return;
247
+ if (!mountedRef.current || providerRef.current !== provider) return;
216
248
  console.log('🔌 Provider disconnected');
217
249
  setConnected(false);
218
250
  setSynced(false);
219
251
  });
220
252
 
221
253
  provider.on('dom-error', (error: DOMException) => {
222
- if (!mountedRef.current) return;
254
+ if (!mountedRef.current || providerRef.current !== provider) return;
223
255
  console.warn(
224
256
  '⚠️ DOM reconciliation error handled gracefully:',
225
257
  error.message
@@ -227,7 +259,7 @@ export function useYjsCollaboration(
227
259
  });
228
260
 
229
261
  provider.on('reconnect-failed', () => {
230
- if (!mountedRef.current) return;
262
+ if (!mountedRef.current || providerRef.current !== provider) return;
231
263
  // If page is visible (user is active but network dropped), restart
232
264
  // the backoff cycle after a short delay to avoid hammering
233
265
  if (
@@ -240,8 +272,8 @@ export function useYjsCollaboration(
240
272
  setTimeout(() => {
241
273
  if (
242
274
  mountedRef.current &&
243
- providerRef.current &&
244
- !providerRef.current.destroyed
275
+ providerRef.current === provider &&
276
+ !provider.destroyed
245
277
  ) {
246
278
  providerRef.current.resetAndReconnect();
247
279
  }
@@ -269,7 +301,6 @@ export function useYjsCollaboration(
269
301
  columnName,
270
302
  hasUser,
271
303
  doc,
272
- awareness,
273
304
  enabled,
274
305
  broadcastDebounceMs,
275
306
  saveDebounceMs,
@@ -290,8 +321,18 @@ export function useYjsCollaboration(
290
321
  return {
291
322
  doc,
292
323
  awareness,
293
- provider: providerState,
294
- synced,
295
- connected,
324
+ provider:
325
+ providerState && providerState.id === doc?.clientID
326
+ ? providerState
327
+ : null,
328
+ hydrated:
329
+ !!hydratedProvider &&
330
+ hydratedProvider.id === doc?.clientID &&
331
+ hydratedProvider.hydrated,
332
+ hydrationFailed:
333
+ !!hydrationErrorProvider && hydrationErrorProvider.id === doc?.clientID,
334
+ synced: !!providerState && providerState.id === doc?.clientID && synced,
335
+ connected:
336
+ !!providerState && providerState.id === doc?.clientID && connected,
296
337
  };
297
338
  }