@tuturuuu/ui 0.37.0 → 0.37.2

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 (63) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +7 -5
  3. package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +181 -0
  4. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
  5. package/src/components/ui/custom/hidden-workspaces-settings.tsx +49 -0
  6. package/src/components/ui/custom/mobile-header-height.ts +16 -0
  7. package/src/components/ui/custom/structure.tsx +37 -5
  8. package/src/components/ui/custom/user-filters.tsx +2 -1
  9. package/src/components/ui/custom/visible-workspace-filter.tsx +41 -0
  10. package/src/components/ui/custom/workspace-picker-content.tsx +293 -0
  11. package/src/components/ui/custom/workspace-select-helpers.ts +11 -0
  12. package/src/components/ui/custom/workspace-select-invitations.tsx +3 -0
  13. package/src/components/ui/custom/workspace-select.tsx +87 -359
  14. package/src/components/ui/date-time-picker-timezone.test.tsx +258 -0
  15. package/src/components/ui/date-time-picker-values.ts +205 -0
  16. package/src/components/ui/date-time-picker.tsx +70 -167
  17. package/src/components/ui/legacy/calendar/agenda-view.tsx +29 -94
  18. package/src/components/ui/legacy/calendar/all-day-color.test.tsx +48 -0
  19. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +29 -41
  20. package/src/components/ui/legacy/calendar/calendar-cell-color.test.tsx +70 -0
  21. package/src/components/ui/legacy/calendar/calendar-cell.tsx +4 -5
  22. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +82 -3
  23. package/src/components/ui/legacy/calendar/calendar-content.tsx +37 -34
  24. package/src/components/ui/legacy/calendar/calendar-day-bridge.test.tsx +94 -0
  25. package/src/components/ui/legacy/calendar/calendar-day-display.test.tsx +106 -0
  26. package/src/components/ui/legacy/calendar/calendar-header.tsx +14 -5
  27. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +4 -9
  28. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +120 -6
  29. package/src/components/ui/legacy/calendar/day-title.tsx +6 -6
  30. package/src/components/ui/legacy/calendar/dynamic-island.test.tsx +140 -0
  31. package/src/components/ui/legacy/calendar/dynamic-island.tsx +27 -24
  32. package/src/components/ui/legacy/calendar/event-card.test.tsx +206 -7
  33. package/src/components/ui/legacy/calendar/event-card.tsx +27 -49
  34. package/src/components/ui/legacy/calendar/event-modal.tsx +6 -26
  35. package/src/components/ui/legacy/calendar/event-picker-bounds.ts +16 -0
  36. package/src/components/ui/legacy/calendar/event-preview-popover.test.tsx +139 -0
  37. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +13 -17
  38. package/src/components/ui/legacy/calendar/event-preview-time.ts +41 -0
  39. package/src/components/ui/legacy/calendar/location-timeline.tsx +9 -17
  40. package/src/components/ui/legacy/calendar/month-calendar.tsx +20 -10
  41. package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +4 -23
  42. package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +3 -6
  43. package/src/components/ui/legacy/calendar/time-indicator-line.tsx +3 -20
  44. package/src/components/ui/legacy/calendar/time-indicator.tsx +11 -3
  45. package/src/components/ui/legacy/calendar/weekday-bar.tsx +2 -8
  46. package/src/components/ui/legacy/calendar/year-calendar.tsx +27 -39
  47. package/src/components/ui/resizable.tsx +2 -1
  48. package/src/hooks/__tests__/use-calendar-sync.test.tsx +45 -0
  49. package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
  50. package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
  51. package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
  52. package/src/hooks/calendar-event-lookup.test.ts +39 -0
  53. package/src/hooks/calendar-event-lookup.ts +22 -8
  54. package/src/hooks/calendar-sync-range.test.ts +92 -0
  55. package/src/hooks/calendar-sync-range.ts +40 -0
  56. package/src/hooks/use-calendar-clock.ts +49 -0
  57. package/src/hooks/use-calendar-day-zone.tsx +12 -0
  58. package/src/hooks/use-calendar-sync.tsx +42 -80
  59. package/src/hooks/use-calendar.tsx +3 -3
  60. package/src/hooks/use-visible-workspaces.ts +57 -0
  61. package/src/hooks/use-workspace-user.ts +15 -3
  62. package/src/hooks/use-workspace-visibility.ts +234 -0
  63. package/src/lib/calendar-day.ts +67 -0
@@ -0,0 +1,293 @@
1
+ 'use client';
2
+
3
+ import { ArrowLeft, Eye, EyeOff, Plus, Search, Star, X } from '@tuturuuu/icons';
4
+ import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
5
+ import { ROOT_WORKSPACE_ID } from '@tuturuuu/utils/constants';
6
+ import { useTranslations } from 'next-intl';
7
+ import { type ReactNode, useRef, useState } from 'react';
8
+ import type { useWorkspaceVisibility } from '../../../hooks/use-workspace-visibility';
9
+ import { Button } from '../button';
10
+ import {
11
+ DialogClose,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogTitle,
15
+ } from '../dialog';
16
+ import { Input } from '../input';
17
+ import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
18
+ import { WorkspaceIcon } from './workspace-select-icon';
19
+
20
+ type Visibility = ReturnType<typeof useWorkspaceVisibility>;
21
+ export function WorkspacePickerContent({
22
+ workspaces,
23
+ currentId,
24
+ defaultId,
25
+ visibility,
26
+ onSelect,
27
+ onDefault,
28
+ onCreate,
29
+ onJoin,
30
+ invitations,
31
+ restoreOnly = false,
32
+ listLoading = false,
33
+ listError,
34
+ onRetryList,
35
+ }: {
36
+ workspaces: InternalApiWorkspaceSummary[];
37
+ currentId?: string;
38
+ defaultId?: string | null;
39
+ visibility: Visibility;
40
+ onSelect: (workspace: InternalApiWorkspaceSummary) => void;
41
+ onDefault?: (id: string) => void;
42
+ onCreate?: () => void;
43
+ onJoin?: () => void;
44
+ invitations?: ReactNode;
45
+ restoreOnly?: boolean;
46
+ listLoading?: boolean;
47
+ listError?: Error | null;
48
+ onRetryList?: () => void;
49
+ }) {
50
+ const t = useTranslations('common');
51
+ const [restoring, setRestoring] = useState(restoreOnly);
52
+ const [searchVisible, setSearchVisible] = useState(false);
53
+ const [search, setSearch] = useState('');
54
+ const input = useRef<HTMLInputElement>(null);
55
+ const query = search.trim().toLocaleLowerCase();
56
+ const choices = workspaces.filter(
57
+ (workspace) => visibility.hiddenIds.includes(workspace.id) === restoring
58
+ );
59
+ const results = choices
60
+ .filter((workspace) =>
61
+ [workspace.name, workspace.id, workspace.tier].some((value) =>
62
+ value?.toLocaleLowerCase().includes(query)
63
+ )
64
+ )
65
+ .sort((a, b) => {
66
+ const rank = (w: InternalApiWorkspaceSummary) =>
67
+ w.id === ROOT_WORKSPACE_ID ? 0 : w.personal ? 1 : 2;
68
+ return (
69
+ rank(a) - rank(b) ||
70
+ (a.name ?? '').localeCompare(b.name ?? '') ||
71
+ a.id.localeCompare(b.id)
72
+ );
73
+ });
74
+ function openSearch() {
75
+ setSearchVisible(true);
76
+ requestAnimationFrame(() => input.current?.focus());
77
+ }
78
+ return (
79
+ <DialogContent
80
+ presentation="fullscreen"
81
+ showCloseButton={false}
82
+ className="flex-col"
83
+ >
84
+ <div className="flex items-center gap-3 border-b p-4 pt-[max(1rem,env(safe-area-inset-top))]">
85
+ {restoring && !restoreOnly && (
86
+ <Button
87
+ size="icon"
88
+ variant="ghost"
89
+ aria-label={t('back')}
90
+ onClick={() => {
91
+ setRestoring(false);
92
+ setSearch('');
93
+ }}
94
+ >
95
+ <ArrowLeft />
96
+ </Button>
97
+ )}
98
+ <WorkspaceIcon fallbackLogoUrl={TUTURUUU_LOGO_URL} name="Tuturuuu" />
99
+ <DialogTitle className="min-w-0 flex-1">
100
+ {restoring ? t('hidden_workspaces') : t('workspaces')}
101
+ </DialogTitle>
102
+ <DialogClose asChild>
103
+ <Button size="icon" variant="ghost" aria-label={t('close')}>
104
+ <X />
105
+ </Button>
106
+ </DialogClose>
107
+ </div>
108
+ <DialogDescription className={restoring ? 'px-4 pt-3' : 'sr-only'}>
109
+ {restoring ? t('hidden_workspaces_description') : t('select_workspace')}
110
+ </DialogDescription>
111
+ {searchVisible && (
112
+ <div className="flex gap-2 px-4 pt-3">
113
+ <Input
114
+ ref={input}
115
+ value={search}
116
+ onChange={(event) => setSearch(event.target.value)}
117
+ placeholder={t('search_workspace')}
118
+ aria-label={t('search_workspace')}
119
+ />
120
+ {query && (
121
+ <Button
122
+ variant="ghost"
123
+ onClick={() => {
124
+ setSearch('');
125
+ input.current?.focus();
126
+ }}
127
+ >
128
+ {t('clear_search')}
129
+ </Button>
130
+ )}
131
+ </div>
132
+ )}
133
+ {(visibility.isError || visibility.updateError || listError) && (
134
+ <div role="alert" className="flex items-center gap-3 px-4 pt-3">
135
+ <p className="flex-1">
136
+ {visibility.updateError
137
+ ? t('hidden_workspaces_update_error')
138
+ : t('hidden_workspaces_load_error')}
139
+ </p>
140
+ <Button
141
+ variant="outline"
142
+ onClick={() => {
143
+ void visibility.refetch();
144
+ onRetryList?.();
145
+ }}
146
+ >
147
+ {t('retry')}
148
+ </Button>
149
+ </div>
150
+ )}
151
+ <div className="min-h-0 flex-1 overflow-y-auto px-4 pt-4 pb-28">
152
+ {!visibility.known || listLoading || listError ? (
153
+ <p role="status">
154
+ {visibility.isError || listError
155
+ ? t('hidden_workspaces_load_error')
156
+ : t('loading')}
157
+ </p>
158
+ ) : (
159
+ <>
160
+ {!restoring && invitations}
161
+ {results.map((workspace) => (
162
+ <div
163
+ key={workspace.id}
164
+ className="mb-2 flex items-center gap-2 rounded-xl border p-2"
165
+ >
166
+ {restoring ? (
167
+ <div className="flex min-w-0 flex-1 items-center gap-3 p-2">
168
+ <WorkspaceIcon
169
+ name={workspace.name ?? ''}
170
+ avatarUrl={workspace.avatar_url ?? undefined}
171
+ />
172
+ <span className="truncate">{workspace.name}</span>
173
+ </div>
174
+ ) : (
175
+ <Button
176
+ variant="ghost"
177
+ className="h-auto min-w-0 flex-1 justify-start gap-3 whitespace-normal py-3 text-start"
178
+ aria-label={[
179
+ workspace.name ?? workspace.id,
180
+ workspace.tier ?? 'FREE',
181
+ workspace.id === currentId
182
+ ? t('current_workspace')
183
+ : null,
184
+ ]
185
+ .filter(Boolean)
186
+ .join(' ')}
187
+ onClick={() => onSelect(workspace)}
188
+ >
189
+ <WorkspaceIcon
190
+ name={workspace.name ?? ''}
191
+ avatarUrl={workspace.avatar_url ?? undefined}
192
+ />
193
+ <span className="min-w-0 flex-1">
194
+ <span className="block break-words">
195
+ {workspace.name}
196
+ </span>
197
+ <span className="text-muted-foreground text-xs">
198
+ {workspace.tier ?? 'FREE'}
199
+ {workspace.id === currentId
200
+ ? ` · ${t('current_workspace')}`
201
+ : ''}
202
+ {workspace.access_type === 'guest'
203
+ ? ` · ${t('guest_access')}`
204
+ : ''}
205
+ </span>
206
+ </span>
207
+ </Button>
208
+ )}
209
+ {!restoring &&
210
+ workspace.access_type !== 'guest' &&
211
+ onDefault && (
212
+ <Button
213
+ size="icon"
214
+ variant="ghost"
215
+ aria-label={t('default_workspace')}
216
+ disabled={workspace.id === defaultId}
217
+ onClick={() => onDefault(workspace.id)}
218
+ >
219
+ <Star
220
+ className={
221
+ workspace.id === defaultId ? 'fill-current' : ''
222
+ }
223
+ />
224
+ </Button>
225
+ )}
226
+ <Button
227
+ size="icon"
228
+ variant="ghost"
229
+ aria-label={`${t(restoring ? 'restore_workspace' : 'hide_workspace')}: ${workspace.name}`}
230
+ disabled={visibility.pending.has(workspace.id)}
231
+ onClick={() => {
232
+ void visibility
233
+ .setHidden(workspace.id, !restoring)
234
+ .catch(() => {});
235
+ }}
236
+ >
237
+ {restoring ? <Eye /> : <EyeOff />}
238
+ </Button>
239
+ </div>
240
+ ))}
241
+ {!results.length && (
242
+ <p role="status" className="py-8 text-center">
243
+ {query
244
+ ? t('no_workspace_found')
245
+ : restoring
246
+ ? t('hidden_workspaces_empty')
247
+ : workspaces.length
248
+ ? t('all_workspaces_hidden')
249
+ : t('no_workspace_found')}
250
+ </p>
251
+ )}
252
+ </>
253
+ )}
254
+ {!restoring && (
255
+ <Button
256
+ variant="link"
257
+ onClick={() => {
258
+ setRestoring(true);
259
+ setSearch('');
260
+ }}
261
+ >
262
+ {t('hidden_workspaces')}
263
+ </Button>
264
+ )}
265
+ {!restoring && onJoin && (
266
+ <Button variant="link" onClick={onJoin}>
267
+ {t('join_workspace_action')}
268
+ </Button>
269
+ )}
270
+ </div>
271
+ <div className="absolute right-4 bottom-[max(1rem,env(safe-area-inset-bottom))] flex gap-3 rounded-full border bg-background p-2 shadow-lg">
272
+ <Button
273
+ size="icon"
274
+ className="rounded-full"
275
+ aria-label={t('search_workspace')}
276
+ onClick={openSearch}
277
+ >
278
+ <Search />
279
+ </Button>
280
+ {!restoring && onCreate && (
281
+ <Button
282
+ size="icon"
283
+ className="rounded-full"
284
+ aria-label={t('create_workspace_action')}
285
+ onClick={onCreate}
286
+ >
287
+ <Plus />
288
+ </Button>
289
+ )}
290
+ </div>
291
+ </DialogContent>
292
+ );
293
+ }
@@ -93,3 +93,14 @@ export function normalizeWorkspaceSwitchPath(
93
93
 
94
94
  return pathname;
95
95
  }
96
+
97
+ export function resolveGuestWorkspaceLanding(
98
+ workspace: Pick<
99
+ InternalApiWorkspaceSummary,
100
+ 'access_type' | 'guest_landing_path'
101
+ >
102
+ ) {
103
+ return workspace.access_type === 'guest'
104
+ ? workspace.guest_landing_path || '/tasks/boards'
105
+ : null;
106
+ }
@@ -55,6 +55,9 @@ export function useWorkspaceInvitations({
55
55
  await Promise.all([
56
56
  queryClient.invalidateQueries({ queryKey: ['workspace-invitations'] }),
57
57
  queryClient.invalidateQueries({ queryKey: ['workspaces'] }),
58
+ queryClient.invalidateQueries({
59
+ queryKey: ['workspace-ui-list', cacheScope],
60
+ }),
58
61
  queryClient.invalidateQueries({ queryKey: ['user-workspaces'] }),
59
62
  queryClient.invalidateQueries({ queryKey: ['workspace-user'] }),
60
63
  queryClient.invalidateQueries({ queryKey: ['current-user'] }),