@tuturuuu/ui 0.30.2 → 0.31.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 (91) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/biome.json +1 -1
  3. package/package.json +95 -49
  4. package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
  5. package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
  7. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
  8. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
  9. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
  10. package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
  11. package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
  12. package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
  13. package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
  14. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
  15. package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
  16. package/src/components/ui/custom/nav-link.tsx +7 -7
  17. package/src/components/ui/custom/nav.tsx +15 -63
  18. package/src/components/ui/custom/notification-card.tsx +361 -0
  19. package/src/components/ui/custom/notification-list.tsx +178 -0
  20. package/src/components/ui/custom/notification-popover-client.tsx +12 -780
  21. package/src/components/ui/custom/notification-runtime.tsx +23 -0
  22. package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
  23. package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
  24. package/src/components/ui/custom/satellite-brand.tsx +122 -0
  25. package/src/components/ui/custom/satellite-content.tsx +148 -0
  26. package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
  27. package/src/components/ui/custom/satellite-language-item.tsx +26 -0
  28. package/src/components/ui/custom/satellite-navigation.tsx +86 -0
  29. package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
  30. package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
  31. package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
  32. package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
  33. package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
  34. package/src/components/ui/custom/satellite-shell.tsx +50 -0
  35. package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
  36. package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
  37. package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
  38. package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
  39. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
  40. package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
  41. package/src/components/ui/custom/sidebar-context.tsx +5 -14
  42. package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
  43. package/src/components/ui/custom/structure.tsx +14 -0
  44. package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
  45. package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
  46. package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
  47. package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
  48. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
  49. package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
  50. package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
  51. package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
  52. package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
  53. package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
  54. package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
  56. package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
  57. package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
  58. package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
  59. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
  60. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
  61. package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
  62. package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
  63. package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
  64. package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
  65. package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
  66. package/src/components/ui/pwa-status.test.tsx +53 -0
  67. package/src/components/ui/pwa-status.tsx +113 -0
  68. package/src/components/ui/report-problem-dialog-content.tsx +350 -0
  69. package/src/components/ui/report-problem-dialog.tsx +6 -975
  70. package/src/components/ui/report-problem-model.ts +133 -0
  71. package/src/components/ui/report-problem-mutation.ts +122 -0
  72. package/src/components/ui/save-notification.test.tsx +36 -0
  73. package/src/components/ui/save-notification.tsx +17 -0
  74. package/src/components/ui/use-report-problem.ts +488 -0
  75. package/src/declarations.d.ts +2 -0
  76. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
  77. package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
  78. package/src/hooks/calendar-date-utils.ts +11 -0
  79. package/src/hooks/calendar-event-lookup.test.ts +77 -0
  80. package/src/hooks/calendar-event-lookup.ts +47 -0
  81. package/src/hooks/calendar-range-cache.ts +52 -0
  82. package/src/hooks/pwa-install-prompt.ts +44 -0
  83. package/src/hooks/use-calendar-sync.tsx +23 -100
  84. package/src/hooks/use-calendar.tsx +21 -77
  85. package/src/hooks/use-notifications.ts +9 -24
  86. package/src/hooks/use-open-initial-calendar-event.ts +27 -0
  87. package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
  88. package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
  89. package/src/hooks/use-workspace-presence.ts +0 -18
  90. package/src/lib/tasks-app-url.test.ts +25 -0
  91. package/src/lib/tasks-app-url.ts +8 -0
@@ -0,0 +1,350 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertCircle,
5
+ AlertTriangle,
6
+ CheckCircle,
7
+ MessageSquareWarning,
8
+ Upload,
9
+ X,
10
+ } from '@tuturuuu/icons';
11
+ import { Button } from '@tuturuuu/ui/button';
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ DialogTrigger,
19
+ } from '@tuturuuu/ui/dialog';
20
+ import { Input } from '@tuturuuu/ui/input';
21
+ import { Label } from '@tuturuuu/ui/label';
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@tuturuuu/ui/select';
29
+ import { Textarea } from '@tuturuuu/ui/textarea';
30
+ import { cn, isValidBlobUrl } from '@tuturuuu/utils/format';
31
+ import {
32
+ ALLOWED_VIDEO_TYPES,
33
+ DEFAULT_PRODUCTS,
34
+ DEFAULT_SUPPORT_TYPES,
35
+ MAX_FILES,
36
+ type ReportProblemDialogProps,
37
+ } from './report-problem-model';
38
+ import { useReportProblem } from './use-report-problem';
39
+ export function ReportProblemDialogContent({
40
+ ImageComponent,
41
+ products = DEFAULT_PRODUCTS,
42
+ className,
43
+ trigger,
44
+ showTrigger = true,
45
+ ...props
46
+ }: ReportProblemDialogProps) {
47
+ const { t } = props;
48
+ const PreviewImage = ImageComponent ?? 'img';
49
+ const {
50
+ dialogOpen,
51
+ handleOpenChange,
52
+ handleSubmit,
53
+ formData,
54
+ validationErrors,
55
+ isSubmitting,
56
+ isCompressing,
57
+ suggestionId,
58
+ mediaUploadId,
59
+ isDragOver,
60
+ fileInputRef,
61
+ handleDragOver,
62
+ handleDragLeave,
63
+ handleDrop,
64
+ handleProductChange,
65
+ handleTypeChange,
66
+ handleSuggestionChange,
67
+ handleMediaUpload,
68
+ mediaPreviews,
69
+ removeMedia,
70
+ } = useReportProblem(props);
71
+ return (
72
+ <Dialog open={dialogOpen} onOpenChange={handleOpenChange}>
73
+ {showTrigger && (
74
+ <DialogTrigger asChild>
75
+ {trigger || (
76
+ <Button variant="secondary" size="sm" className={cn(className)}>
77
+ <MessageSquareWarning className="mr-2 h-4 w-4" />
78
+ {t('report-problem')}
79
+ </Button>
80
+ )}
81
+ </DialogTrigger>
82
+ )}
83
+ <DialogContent className="mx-auto flex max-h-[90vh] w-[calc(100vw-1.5rem)] max-w-7xl flex-col overflow-hidden">
84
+ <DialogHeader className="border-b pb-4">
85
+ <DialogTitle className="flex items-center gap-2 text-lg sm:text-xl">
86
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900">
87
+ <AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
88
+ </div>
89
+ {t('report-problem')}
90
+ </DialogTitle>
91
+ <DialogDescription className="mt-2 text-muted-foreground text-sm">
92
+ {t('report-problem-description')}
93
+ </DialogDescription>
94
+ </DialogHeader>
95
+
96
+ <div className="flex-1 overflow-y-auto">
97
+ <form onSubmit={handleSubmit} className="space-y-6">
98
+ <div className="space-y-3">
99
+ <Label
100
+ htmlFor="product"
101
+ className="flex items-center gap-1 font-medium text-sm"
102
+ >
103
+ {t('affected-product-required')}
104
+ </Label>
105
+ <Select
106
+ value={formData.product}
107
+ onValueChange={handleProductChange}
108
+ >
109
+ <SelectTrigger
110
+ className={cn(
111
+ 'h-11',
112
+ validationErrors.product &&
113
+ 'border-red-500 focus:border-red-500'
114
+ )}
115
+ >
116
+ <SelectValue placeholder={t('select-product-placeholder')} />
117
+ </SelectTrigger>
118
+ <SelectContent>
119
+ {products.map((product) => (
120
+ <SelectItem key={product.value} value={product.value}>
121
+ {product.label}
122
+ </SelectItem>
123
+ ))}
124
+ </SelectContent>
125
+ </Select>
126
+ {validationErrors.product && (
127
+ <div className="flex items-center gap-1 text-red-600 text-sm">
128
+ <AlertCircle className="h-3 w-3" />
129
+ {validationErrors.product}
130
+ </div>
131
+ )}
132
+ </div>
133
+
134
+ <div className="space-y-3">
135
+ <Label
136
+ htmlFor="type"
137
+ className="flex items-center gap-1 font-medium text-sm"
138
+ >
139
+ Support Type *
140
+ </Label>
141
+ <Select value={formData.type} onValueChange={handleTypeChange}>
142
+ <SelectTrigger
143
+ className={cn(
144
+ 'h-11',
145
+ validationErrors.type &&
146
+ 'border-red-500 focus:border-red-500'
147
+ )}
148
+ >
149
+ <SelectValue placeholder="Select support type..." />
150
+ </SelectTrigger>
151
+ <SelectContent>
152
+ {DEFAULT_SUPPORT_TYPES.map((type) => (
153
+ <SelectItem key={type.value} value={type.value}>
154
+ {type.label}
155
+ </SelectItem>
156
+ ))}
157
+ </SelectContent>
158
+ </Select>
159
+ {validationErrors.type && (
160
+ <div className="flex items-center gap-1 text-red-600 text-sm">
161
+ <AlertCircle className="h-3 w-3" />
162
+ {validationErrors.type}
163
+ </div>
164
+ )}
165
+ </div>
166
+
167
+ <div className="space-y-3">
168
+ <Label
169
+ htmlFor="suggestion"
170
+ className="flex items-center gap-1 font-medium text-sm"
171
+ >
172
+ {t('suggestion-improve')}
173
+ </Label>
174
+ <Textarea
175
+ id={suggestionId}
176
+ placeholder={t('suggestion-placeholder')}
177
+ value={formData.suggestion}
178
+ onChange={handleSuggestionChange}
179
+ className={cn(
180
+ 'max-h-62.5 min-h-30 resize-y',
181
+ validationErrors.suggestion &&
182
+ 'border-red-500 focus:border-red-500'
183
+ )}
184
+ />
185
+ {validationErrors.suggestion && (
186
+ <div className="flex items-center gap-1 text-red-600 text-sm">
187
+ <AlertCircle className="h-3 w-3" />
188
+ {validationErrors.suggestion}
189
+ </div>
190
+ )}
191
+ <div className="text-muted-foreground text-xs">
192
+ {formData.suggestion.length}/1000 characters
193
+ </div>
194
+ </div>
195
+
196
+ <div className="space-y-4">
197
+ <Label className="font-medium text-sm">
198
+ {t('media-optional')} ({formData.media.length}/{MAX_FILES})
199
+ </Label>
200
+
201
+ {formData.media.length < MAX_FILES && (
202
+ <button
203
+ type="button"
204
+ className={cn(
205
+ 'relative mt-2 w-full rounded-lg border-2 border-dashed transition-all duration-200',
206
+ isDragOver
207
+ ? 'border-orange-400 bg-orange-50 dark:bg-orange-950/20'
208
+ : 'border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500',
209
+ isCompressing && 'pointer-events-none opacity-50'
210
+ )}
211
+ onDragOver={handleDragOver}
212
+ onDragLeave={handleDragLeave}
213
+ onDrop={handleDrop}
214
+ aria-label="Click to upload or drag and drop media files"
215
+ disabled={isCompressing}
216
+ >
217
+ <Input
218
+ ref={fileInputRef}
219
+ type="file"
220
+ accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm,video/quicktime"
221
+ multiple
222
+ onChange={handleMediaUpload}
223
+ disabled={isSubmitting || isCompressing}
224
+ className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
225
+ id={mediaUploadId}
226
+ />
227
+ <div className="flex flex-col items-center justify-center px-4 py-8 text-center">
228
+ <div
229
+ className={cn(
230
+ 'mb-3 flex h-12 w-12 items-center justify-center rounded-full transition-colors',
231
+ isDragOver
232
+ ? 'bg-orange-100 dark:bg-orange-900'
233
+ : 'bg-gray-100 dark:bg-gray-800'
234
+ )}
235
+ >
236
+ <Upload
237
+ className={cn(
238
+ 'h-5 w-5',
239
+ isDragOver ? 'text-orange-600' : 'text-gray-400'
240
+ )}
241
+ />
242
+ </div>
243
+ <p className="mb-1 font-medium text-sm">
244
+ {isCompressing
245
+ ? 'Compressing...'
246
+ : isDragOver
247
+ ? 'Drop files here'
248
+ : 'Click to upload or drag and drop'}
249
+ </p>
250
+ <p className="text-muted-foreground text-xs">
251
+ Images (PNG, JPG, GIF, WebP) or Videos (MP4, WebM, MOV) up
252
+ to 5MB each
253
+ </p>
254
+ </div>
255
+ </button>
256
+ )}
257
+
258
+ {validationErrors.media && (
259
+ <div className="text-red-600 text-sm">
260
+ {validationErrors.media}
261
+ </div>
262
+ )}
263
+
264
+ {mediaPreviews.length > 0 && (
265
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
266
+ {mediaPreviews.map((preview, index) => {
267
+ const file = formData.media[index];
268
+ const isVideo =
269
+ file && ALLOWED_VIDEO_TYPES.includes(file.type);
270
+
271
+ return (
272
+ <div key={preview} className="group relative">
273
+ <div className="aspect-square overflow-hidden rounded-lg border-2 border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
274
+ {isVideo ? (
275
+ <video
276
+ src={isValidBlobUrl(preview) ? preview : ''}
277
+ className="h-full w-full object-cover"
278
+ controls={false}
279
+ muted
280
+ playsInline
281
+ />
282
+ ) : (
283
+ <PreviewImage
284
+ src={
285
+ isValidBlobUrl(preview)
286
+ ? preview
287
+ : '/placeholder.svg'
288
+ }
289
+ alt={t('media-alt', { number: index + 1 })}
290
+ className="h-full w-full object-cover"
291
+ width={100}
292
+ height={100}
293
+ />
294
+ )}
295
+ </div>
296
+ <Button
297
+ type="button"
298
+ variant="destructive"
299
+ size="icon"
300
+ className="absolute -top-2 -right-2 h-6 w-6 rounded-full opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
301
+ onClick={() => removeMedia(index)}
302
+ >
303
+ <X className="h-3 w-3" />
304
+ </Button>
305
+ </div>
306
+ );
307
+ })}
308
+ </div>
309
+ )}
310
+ </div>
311
+
312
+ <div className="flex w-full flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
313
+ <Button
314
+ type="button"
315
+ variant="outline"
316
+ onClick={() => handleOpenChange(false)}
317
+ disabled={isSubmitting}
318
+ className="w-full sm:w-auto"
319
+ >
320
+ {t('cancel')}
321
+ </Button>
322
+ <Button
323
+ type="submit"
324
+ disabled={
325
+ !formData.product ||
326
+ !formData.type ||
327
+ !formData.suggestion.trim() ||
328
+ isSubmitting
329
+ }
330
+ className="w-full bg-orange-600 hover:bg-orange-700 disabled:opacity-50 sm:w-auto"
331
+ >
332
+ {isSubmitting ? (
333
+ <div className="flex items-center gap-2">
334
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
335
+ {t('submitting')}
336
+ </div>
337
+ ) : (
338
+ <div className="flex items-center gap-2">
339
+ <CheckCircle className="h-4 w-4" />
340
+ {t('submit-report')}
341
+ </div>
342
+ )}
343
+ </Button>
344
+ </div>
345
+ </form>
346
+ </div>
347
+ </DialogContent>
348
+ </Dialog>
349
+ );
350
+ }