@tuturuuu/ui 0.4.1 → 0.6.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 (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,358 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertCircle,
5
+ Calendar,
6
+ CheckCircle2,
7
+ Clock,
8
+ Flag,
9
+ FolderKanban,
10
+ ListTodo,
11
+ Loader2,
12
+ Sparkles,
13
+ Tag,
14
+ Timer,
15
+ X,
16
+ } from '@tuturuuu/icons';
17
+ import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
18
+ import { Badge } from '@tuturuuu/ui/badge';
19
+ import { Button } from '@tuturuuu/ui/button';
20
+ import { Checkbox } from '@tuturuuu/ui/checkbox';
21
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
22
+ import { cn } from '@tuturuuu/utils/format';
23
+ import dayjs from 'dayjs';
24
+ import { useTranslations } from 'next-intl';
25
+
26
+ interface SmartTaskSuggestionsButtonProps {
27
+ disabled?: boolean;
28
+ isLoading?: boolean;
29
+ onClick: () => void;
30
+ }
31
+
32
+ interface SmartTaskSuggestionsPanelProps {
33
+ suggestions: WorkspaceTaskSuggestionTask[];
34
+ selectedSuggestionIds: string[];
35
+ createErrors?: Record<string, string>;
36
+ creatingSuggestionIds?: string[];
37
+ errorMessage?: string | null;
38
+ isCreatingSelected?: boolean;
39
+ isLoading?: boolean;
40
+ onApplyFirst: () => void;
41
+ onApplySuggestion: (suggestion: WorkspaceTaskSuggestionTask) => void;
42
+ onClose: () => void;
43
+ onCreateSelected: () => void;
44
+ onRetry: () => void;
45
+ onToggleSuggestion: (suggestionId: string) => void;
46
+ }
47
+
48
+ function formatDuration(minutes: number) {
49
+ if (minutes < 60) {
50
+ return `${minutes}m`;
51
+ }
52
+
53
+ const hours = Math.floor(minutes / 60);
54
+ const remainder = minutes % 60;
55
+
56
+ return remainder ? `${hours}h ${remainder}m` : `${hours}h`;
57
+ }
58
+
59
+ function SuggestionChip({
60
+ children,
61
+ icon,
62
+ }: {
63
+ children: React.ReactNode;
64
+ icon: React.ReactNode;
65
+ }) {
66
+ return (
67
+ <Badge
68
+ variant="secondary"
69
+ className="h-6 gap-1 rounded-md px-1.5 font-normal text-xs"
70
+ >
71
+ {icon}
72
+ <span className="max-w-28 truncate">{children}</span>
73
+ </Badge>
74
+ );
75
+ }
76
+
77
+ function SuggestionCard({
78
+ creating,
79
+ error,
80
+ multi,
81
+ onApply,
82
+ onToggle,
83
+ selected,
84
+ suggestion,
85
+ }: {
86
+ creating?: boolean;
87
+ error?: string;
88
+ multi: boolean;
89
+ onApply: () => void;
90
+ onToggle: () => void;
91
+ selected: boolean;
92
+ suggestion: WorkspaceTaskSuggestionTask;
93
+ }) {
94
+ const t = useTranslations('ws-task-boards.dialog');
95
+
96
+ return (
97
+ <div
98
+ className={cn(
99
+ 'rounded-lg border bg-background p-3 transition-colors',
100
+ selected && 'border-dynamic-blue/60 bg-dynamic-blue/5'
101
+ )}
102
+ >
103
+ <div className="flex items-start gap-2">
104
+ {multi && (
105
+ <Checkbox
106
+ aria-label={t('smart_select_suggestion')}
107
+ checked={selected}
108
+ className="mt-0.5"
109
+ onCheckedChange={onToggle}
110
+ />
111
+ )}
112
+ <div className="min-w-0 flex-1 space-y-2">
113
+ <div className="flex items-start justify-between gap-2">
114
+ <div className="min-w-0">
115
+ <div className="truncate font-medium text-sm">
116
+ {suggestion.title}
117
+ </div>
118
+ {suggestion.description && (
119
+ <div className="line-clamp-2 text-muted-foreground text-xs">
120
+ {suggestion.description}
121
+ </div>
122
+ )}
123
+ </div>
124
+ {!multi && (
125
+ <Button size="sm" type="button" onClick={onApply}>
126
+ <Sparkles className="h-4 w-4" />
127
+ {t('smart_apply_suggestion')}
128
+ </Button>
129
+ )}
130
+ </div>
131
+
132
+ <div className="flex flex-wrap gap-1.5">
133
+ {suggestion.listName && (
134
+ <SuggestionChip icon={<ListTodo className="h-3 w-3" />}>
135
+ {suggestion.listName}
136
+ </SuggestionChip>
137
+ )}
138
+ {suggestion.priority && (
139
+ <SuggestionChip icon={<Flag className="h-3 w-3" />}>
140
+ {t(`priority.${suggestion.priority}`)}
141
+ </SuggestionChip>
142
+ )}
143
+ {suggestion.endDate && (
144
+ <SuggestionChip icon={<Calendar className="h-3 w-3" />}>
145
+ {dayjs(suggestion.endDate).format('MMM D')}
146
+ </SuggestionChip>
147
+ )}
148
+ {suggestion.durationMinutes && (
149
+ <SuggestionChip icon={<Clock className="h-3 w-3" />}>
150
+ {formatDuration(suggestion.durationMinutes)}
151
+ </SuggestionChip>
152
+ )}
153
+ {suggestion.estimationPoints != null && (
154
+ <SuggestionChip icon={<Timer className="h-3 w-3" />}>
155
+ {suggestion.estimationPoints}
156
+ </SuggestionChip>
157
+ )}
158
+ {suggestion.labels.map((label) => (
159
+ <SuggestionChip key={label.id} icon={<Tag className="h-3 w-3" />}>
160
+ {label.name}
161
+ </SuggestionChip>
162
+ ))}
163
+ {suggestion.projects.map((project) => (
164
+ <SuggestionChip
165
+ key={project.id}
166
+ icon={<FolderKanban className="h-3 w-3" />}
167
+ >
168
+ {project.name}
169
+ </SuggestionChip>
170
+ ))}
171
+ </div>
172
+
173
+ {suggestion.reason && (
174
+ <div className="text-muted-foreground text-xs">
175
+ {suggestion.reason}
176
+ </div>
177
+ )}
178
+
179
+ {error && (
180
+ <div className="flex items-center gap-1.5 text-destructive text-xs">
181
+ <AlertCircle className="h-3.5 w-3.5" />
182
+ {error}
183
+ </div>
184
+ )}
185
+
186
+ {creating && (
187
+ <div className="flex items-center gap-1.5 text-muted-foreground text-xs">
188
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
189
+ {t('smart_creating_task')}
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ export function SmartTaskSuggestionsButton({
199
+ disabled,
200
+ isLoading,
201
+ onClick,
202
+ }: SmartTaskSuggestionsButtonProps) {
203
+ const t = useTranslations('ws-task-boards.dialog');
204
+
205
+ return (
206
+ <Tooltip>
207
+ <TooltipTrigger asChild>
208
+ <Button
209
+ type="button"
210
+ variant="ghost"
211
+ size="icon"
212
+ aria-label={t('smart_suggest')}
213
+ disabled={disabled || isLoading}
214
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
215
+ onClick={onClick}
216
+ >
217
+ {isLoading ? (
218
+ <Loader2 className="h-4 w-4 animate-spin" />
219
+ ) : (
220
+ <Sparkles className="h-4 w-4" />
221
+ )}
222
+ </Button>
223
+ </TooltipTrigger>
224
+ <TooltipContent side="bottom">{t('smart_suggest')}</TooltipContent>
225
+ </Tooltip>
226
+ );
227
+ }
228
+
229
+ export function SmartTaskSuggestionsPanel({
230
+ suggestions,
231
+ selectedSuggestionIds,
232
+ createErrors = {},
233
+ creatingSuggestionIds = [],
234
+ errorMessage,
235
+ isCreatingSelected,
236
+ isLoading,
237
+ onApplyFirst,
238
+ onApplySuggestion,
239
+ onClose,
240
+ onCreateSelected,
241
+ onRetry,
242
+ onToggleSuggestion,
243
+ }: SmartTaskSuggestionsPanelProps) {
244
+ const t = useTranslations('ws-task-boards.dialog');
245
+ const selectedCount = selectedSuggestionIds.length;
246
+
247
+ if (isLoading) {
248
+ return (
249
+ <div className="rounded-lg border bg-muted/30 p-3">
250
+ <div className="flex items-center gap-2 text-sm">
251
+ <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
252
+ <span>{t('smart_generating')}</span>
253
+ </div>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ if (errorMessage) {
259
+ return (
260
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 p-3">
261
+ <div className="flex items-start justify-between gap-2">
262
+ <div className="flex min-w-0 gap-2">
263
+ <AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
264
+ <div className="min-w-0 space-y-1">
265
+ <div className="font-medium text-sm">
266
+ {t('smart_suggestions_failed')}
267
+ </div>
268
+ <div className="text-muted-foreground text-xs">
269
+ {errorMessage}
270
+ </div>
271
+ </div>
272
+ </div>
273
+ <Button size="sm" type="button" variant="ghost" onClick={onRetry}>
274
+ {t('retry')}
275
+ </Button>
276
+ </div>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ if (!suggestions.length) {
282
+ return null;
283
+ }
284
+
285
+ const multi = suggestions.length > 1;
286
+
287
+ return (
288
+ <div className="rounded-lg border bg-muted/20 p-3">
289
+ <div className="mb-3 flex items-start justify-between gap-2">
290
+ <div className="min-w-0">
291
+ <div className="flex items-center gap-1.5 font-medium text-sm">
292
+ <Sparkles className="h-4 w-4" />
293
+ {multi
294
+ ? t('smart_multiple_suggestions')
295
+ : t('smart_one_suggestion')}
296
+ </div>
297
+ <div className="text-muted-foreground text-xs">
298
+ {multi
299
+ ? t('smart_multiple_suggestions_description')
300
+ : t('smart_one_suggestion_description')}
301
+ </div>
302
+ </div>
303
+ <Button
304
+ type="button"
305
+ variant="ghost"
306
+ size="icon"
307
+ aria-label={t('smart_dismiss')}
308
+ className="h-7 w-7 shrink-0"
309
+ onClick={onClose}
310
+ >
311
+ <X className="h-3.5 w-3.5" />
312
+ </Button>
313
+ </div>
314
+
315
+ <div className="space-y-2">
316
+ {suggestions.map((suggestion) => (
317
+ <SuggestionCard
318
+ key={suggestion.id}
319
+ creating={creatingSuggestionIds.includes(suggestion.id)}
320
+ error={createErrors[suggestion.id]}
321
+ multi={multi}
322
+ selected={selectedSuggestionIds.includes(suggestion.id)}
323
+ suggestion={suggestion}
324
+ onApply={() => onApplySuggestion(suggestion)}
325
+ onToggle={() => onToggleSuggestion(suggestion.id)}
326
+ />
327
+ ))}
328
+ </div>
329
+
330
+ {multi && (
331
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-2">
332
+ <Button
333
+ type="button"
334
+ size="sm"
335
+ variant="secondary"
336
+ onClick={onApplyFirst}
337
+ >
338
+ <CheckCircle2 className="h-4 w-4" />
339
+ {t('smart_apply_first')}
340
+ </Button>
341
+ <Button
342
+ type="button"
343
+ size="sm"
344
+ disabled={!selectedCount || isCreatingSelected}
345
+ onClick={onCreateSelected}
346
+ >
347
+ {isCreatingSelected ? (
348
+ <Loader2 className="h-4 w-4 animate-spin" />
349
+ ) : (
350
+ <Sparkles className="h-4 w-4" />
351
+ )}
352
+ {t('smart_create_selected', { count: selectedCount })}
353
+ </Button>
354
+ </div>
355
+ )}
356
+ </div>
357
+ );
358
+ }
@@ -36,7 +36,7 @@ export interface TaskDescriptionEditorProps {
36
36
  // Refs
37
37
  editorRef: React.RefObject<HTMLDivElement | null>;
38
38
  richTextEditorRef: React.RefObject<HTMLDivElement | null>;
39
- titleInputRef: React.RefObject<HTMLInputElement | null>;
39
+ titleInputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
40
40
  lastCursorPositionRef: React.RefObject<number | null>;
41
41
  targetEditorCursorRef: React.RefObject<number | null>;
42
42
  flushEditorPendingRef: React.RefObject<
@@ -222,6 +222,8 @@ interface TaskDialogHeaderProps {
222
222
  onOpenShareDialog?: () => void;
223
223
  /** Whether the dialog is in read-only mode */
224
224
  disabled?: boolean;
225
+ /** Whether task-dependent actions should be disabled while the task hydrates. */
226
+ controlsDisabled?: boolean;
225
227
  /** Callback to scroll the editor to a collaborator's cursor position */
226
228
  onScrollToUserCursor?: (userId: string, displayName: string) => void;
227
229
  }
@@ -262,6 +264,7 @@ export function TaskDialogHeader({
262
264
  isPersonalWorkspace = false,
263
265
  onOpenShareDialog,
264
266
  disabled = false,
267
+ controlsDisabled = false,
265
268
  onScrollToUserCursor,
266
269
  }: TaskDialogHeaderProps) {
267
270
  const t = useTranslations();
@@ -448,7 +451,7 @@ export function TaskDialogHeader({
448
451
  )}
449
452
 
450
453
  {/* Quick Settings */}
451
- {!disabled && (
454
+ {!controlsDisabled && (
452
455
  <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
453
456
  )}
454
457
 
@@ -467,10 +470,11 @@ export function TaskDialogHeader({
467
470
  onNavigateBack={onNavigateBack}
468
471
  onOpenShareDialog={onOpenShareDialog}
469
472
  disabled={disabled}
473
+ controlsDisabled={controlsDisabled}
470
474
  />
471
475
 
472
476
  {/* Hide save button in edit mode when realtime is enabled (either cursors or Yjs sync) */}
473
- {!disabled &&
477
+ {!controlsDisabled &&
474
478
  (isCreateMode || (!collaborationMode && !realtimeEnabled)) && (
475
479
  <Tooltip>
476
480
  <TooltipTrigger asChild>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
5
6
  import { cn } from '@tuturuuu/utils/format';
6
7
  import { useTranslations } from 'next-intl';
7
8
  import { useMemo, useState } from 'react';
@@ -19,6 +20,7 @@ interface TaskListSelectorProps {
19
20
  selectedListId: string;
20
21
  availableLists: TaskList[];
21
22
  disabled?: boolean;
23
+ compact?: boolean;
22
24
  onListChange: (listId: string) => void;
23
25
  }
24
26
 
@@ -28,6 +30,7 @@ export function TaskListSelector({
28
30
  selectedListId,
29
31
  availableLists,
30
32
  disabled = false,
33
+ compact = false,
31
34
  onListChange,
32
35
  }: TaskListSelectorProps) {
33
36
  const t = useTranslations();
@@ -65,30 +68,43 @@ export function TaskListSelector({
65
68
 
66
69
  const TriggerIcon = getTaskListTriggerIcon(selectedList);
67
70
  const triggerSurfaceClass = getTaskListTriggerSurfaceClass(selectedList);
71
+ const triggerLabel = selectedList
72
+ ? translateTaskListNameForDisplay(selectedList.name, nameLabels)
73
+ : t('ws-task-boards.dialog.field.list');
74
+ const triggerButton = (
75
+ <button
76
+ type="button"
77
+ disabled={disabled}
78
+ aria-label={compact ? triggerLabel : undefined}
79
+ className={cn(
80
+ 'inline-flex shrink-0 items-center border font-medium text-xs transition-colors',
81
+ compact
82
+ ? 'h-9 w-9 justify-center rounded-md p-0'
83
+ : 'h-8 gap-1.5 rounded-lg px-3',
84
+ selectedList && triggerSurfaceClass
85
+ ? triggerSurfaceClass
86
+ : 'border-border bg-background text-muted-foreground hover:border-primary/30 hover:bg-muted hover:text-foreground',
87
+ disabled && 'cursor-not-allowed opacity-50'
88
+ )}
89
+ >
90
+ <TriggerIcon className="h-3.5 w-3.5 shrink-0" />
91
+ <span className={compact ? 'sr-only' : undefined}>{triggerLabel}</span>
92
+ </button>
93
+ );
68
94
 
69
95
  return (
70
96
  <>
71
97
  <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
72
- <PopoverTrigger asChild>
73
- <button
74
- type="button"
75
- disabled={disabled}
76
- className={cn(
77
- 'inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border px-3 font-medium text-xs transition-colors',
78
- selectedList && triggerSurfaceClass
79
- ? triggerSurfaceClass
80
- : 'border-border bg-background text-muted-foreground hover:border-primary/30 hover:bg-muted hover:text-foreground',
81
- disabled && 'cursor-not-allowed opacity-50'
82
- )}
83
- >
84
- <TriggerIcon className="h-3.5 w-3.5 shrink-0" />
85
- <span>
86
- {selectedList
87
- ? translateTaskListNameForDisplay(selectedList.name, nameLabels)
88
- : t('ws-task-boards.dialog.field.list')}
89
- </span>
90
- </button>
91
- </PopoverTrigger>
98
+ {compact ? (
99
+ <Tooltip>
100
+ <TooltipTrigger asChild>
101
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
102
+ </TooltipTrigger>
103
+ <TooltipContent side="bottom">{triggerLabel}</TooltipContent>
104
+ </Tooltip>
105
+ ) : (
106
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
107
+ )}
92
108
  <PopoverContent align="start" className="w-80 p-0">
93
109
  <TaskListPickerPanel
94
110
  selectedListId={selectedListId}
@@ -13,9 +13,13 @@ vi.mock('next-intl', () => ({
13
13
  function renderTaskNameInput({
14
14
  isCreateMode = true,
15
15
  targetCursor = 12,
16
+ variant = 'fullscreen',
17
+ onSubmit,
16
18
  }: {
17
19
  isCreateMode?: boolean;
18
20
  targetCursor?: number | null;
21
+ variant?: 'fullscreen' | 'compact';
22
+ onSubmit?: () => void;
19
23
  } = {}) {
20
24
  const editorWrapper = document.createElement('div');
21
25
  const editorElement = document.createElement('div');
@@ -23,7 +27,9 @@ function renderTaskNameInput({
23
27
  editorElement.tabIndex = -1;
24
28
  editorWrapper.append(editorElement);
25
29
 
26
- const titleInputRef = { current: null };
30
+ const titleInputRef = {
31
+ current: null as HTMLInputElement | HTMLTextAreaElement | null,
32
+ };
27
33
  const editorRef = { current: editorWrapper };
28
34
  const lastCursorPositionRef = { current: null };
29
35
  const targetEditorCursorRef = { current: targetCursor };
@@ -37,6 +43,8 @@ function renderTaskNameInput({
37
43
  setName: vi.fn(),
38
44
  updateName: vi.fn(),
39
45
  flushNameUpdate: vi.fn(),
46
+ variant,
47
+ onSubmit,
40
48
  };
41
49
 
42
50
  render(<TaskNameInput {...props} />);
@@ -60,6 +68,18 @@ describe('TaskNameInput', () => {
60
68
  vi.restoreAllMocks();
61
69
  });
62
70
 
71
+ it.each([
72
+ 'fullscreen',
73
+ 'compact',
74
+ ] as const)('places the initial %s title caret at the end', (variant) => {
75
+ const { input } = renderTaskNameInput({ variant });
76
+ const expectedCursorPosition = 'APIs for Agent Pi'.length;
77
+
78
+ expect(input).toHaveFocus();
79
+ expect(input).toHaveProperty('selectionStart', expectedCursorPosition);
80
+ expect(input).toHaveProperty('selectionEnd', expectedCursorPosition);
81
+ });
82
+
63
83
  it('defers focusing the description editor after Enter', () => {
64
84
  const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput();
65
85
 
@@ -137,4 +157,24 @@ describe('TaskNameInput', () => {
137
157
  window.removeEventListener('keydown', windowKeyDown);
138
158
  }
139
159
  });
160
+
161
+ it('submits from compact mode without focusing the description editor', () => {
162
+ const onSubmit = vi.fn();
163
+ const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput({
164
+ onSubmit,
165
+ variant: 'compact',
166
+ });
167
+
168
+ const eventAllowed = fireEvent.keyDown(input, { key: 'Enter' });
169
+
170
+ expect(input.tagName).toBe('TEXTAREA');
171
+ expect(eventAllowed).toBe(false);
172
+ expect(onSubmit).toHaveBeenCalledTimes(1);
173
+ expect(targetEditorCursorRef.current).toBe(12);
174
+ expect(input).toHaveProperty('value', 'APIs for Agent Pi');
175
+
176
+ vi.runOnlyPendingTimers();
177
+
178
+ expect(focusEditor).not.toHaveBeenCalled();
179
+ });
140
180
  });