@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,268 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { fireEvent, render, screen } from '@testing-library/react';
6
+ import { Dialog } from '@tuturuuu/ui/dialog';
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import { CompactTaskCreatePopover } from './compact-task-create-popover';
9
+
10
+ vi.mock('next-intl', () => ({
11
+ useTranslations: () => (key: string) => key,
12
+ }));
13
+
14
+ vi.mock('./quick-settings-popover', () => ({
15
+ QuickSettingsPopover: () => (
16
+ <button type="button" aria-label="Quick Settings">
17
+ Quick Settings
18
+ </button>
19
+ ),
20
+ }));
21
+
22
+ function renderCompactTaskCreatePopover({
23
+ canSave = true,
24
+ createMultiple = false,
25
+ saveAsDraft = false,
26
+ }: {
27
+ canSave?: boolean;
28
+ createMultiple?: boolean;
29
+ saveAsDraft?: boolean;
30
+ } = {}) {
31
+ const props = {
32
+ title: 'Create task',
33
+ description: 'New task',
34
+ titleInput: (
35
+ <input aria-label="Task title" defaultValue="Draft compact title" />
36
+ ),
37
+ propertyControls: (
38
+ <button type="button" aria-label="Priority: High">
39
+ Priority
40
+ </button>
41
+ ),
42
+ saveAsDraft,
43
+ createMultiple,
44
+ canSave,
45
+ isLoading: false,
46
+ isPersonalWorkspace: false,
47
+ onSaveAsDraftChange: vi.fn(),
48
+ onCreateMultipleChange: vi.fn(),
49
+ onClose: vi.fn(),
50
+ onFullscreen: vi.fn(),
51
+ onSave: vi.fn(),
52
+ };
53
+
54
+ render(
55
+ <Dialog open={true}>
56
+ <CompactTaskCreatePopover {...props} />
57
+ </Dialog>
58
+ );
59
+
60
+ return props;
61
+ }
62
+
63
+ describe('CompactTaskCreatePopover', () => {
64
+ it('renders compact create content with accessible icon actions', () => {
65
+ renderCompactTaskCreatePopover();
66
+
67
+ expect(screen.getByTestId('compact-task-dialog-panel')).toBeTruthy();
68
+ expect(screen.getByText('Create task')).toBeTruthy();
69
+ expect(screen.getByLabelText('Task title')).toHaveProperty(
70
+ 'value',
71
+ 'Draft compact title'
72
+ );
73
+ expect(screen.getByLabelText('Priority: High')).toBeTruthy();
74
+ expect(
75
+ screen.getByLabelText('ws-task-boards.dialog.open_fullscreen')
76
+ ).toBeTruthy();
77
+ expect(screen.getByLabelText('common.close')).toBeTruthy();
78
+ expect(screen.getByLabelText('task-drafts.save_as_draft')).toBeTruthy();
79
+ expect(
80
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
81
+ ).toBeTruthy();
82
+ expect(screen.getByLabelText('Quick Settings')).toBeTruthy();
83
+ });
84
+
85
+ it('routes compact actions to the caller while keeping current form nodes mounted', () => {
86
+ const props = renderCompactTaskCreatePopover();
87
+
88
+ fireEvent.click(
89
+ screen.getByLabelText('ws-task-boards.dialog.open_fullscreen')
90
+ );
91
+ fireEvent.click(screen.getByLabelText('task-drafts.save_as_draft'));
92
+ fireEvent.click(
93
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
94
+ );
95
+ fireEvent.click(
96
+ screen.getByRole('button', {
97
+ name: 'ws-task-boards.dialog.create_task',
98
+ })
99
+ );
100
+
101
+ expect(props.onFullscreen).toHaveBeenCalledTimes(1);
102
+ expect(props.onSaveAsDraftChange).toHaveBeenCalledWith(true);
103
+ expect(props.onCreateMultipleChange).toHaveBeenCalledWith(true);
104
+ expect(props.onSave).toHaveBeenCalledTimes(1);
105
+ expect(screen.getByLabelText('Task title')).toHaveProperty(
106
+ 'value',
107
+ 'Draft compact title'
108
+ );
109
+ expect(screen.getByLabelText('Priority: High')).toBeTruthy();
110
+ });
111
+
112
+ it('disables create when the dialog cannot save', () => {
113
+ const props = renderCompactTaskCreatePopover({ canSave: false });
114
+ const saveButton = screen.getByRole('button', {
115
+ name: 'ws-task-boards.dialog.create_task',
116
+ });
117
+
118
+ expect(saveButton).toHaveProperty('disabled', true);
119
+ fireEvent.click(saveButton);
120
+
121
+ expect(props.onSave).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('reflects draft and create-multiple toggled states', () => {
125
+ renderCompactTaskCreatePopover({
126
+ createMultiple: true,
127
+ saveAsDraft: true,
128
+ });
129
+
130
+ expect(screen.getByLabelText('task-drafts.save_as_draft')).toHaveAttribute(
131
+ 'aria-pressed',
132
+ 'true'
133
+ );
134
+ expect(
135
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
136
+ ).toHaveAttribute('aria-pressed', 'true');
137
+ expect(
138
+ screen.getAllByRole('button', { name: 'task-drafts.save_as_draft' })
139
+ ).toHaveLength(2);
140
+ });
141
+
142
+ it('renders compact edit content without create-only footer actions', () => {
143
+ render(
144
+ <Dialog open={true}>
145
+ <CompactTaskCreatePopover
146
+ title="Edit task"
147
+ titleInput={<input aria-label="Task title" defaultValue="Existing" />}
148
+ propertyControls={
149
+ <button type="button" aria-label="List: Inbox">
150
+ List
151
+ </button>
152
+ }
153
+ smartAction={
154
+ <button type="button" aria-label="Smart action">
155
+ Smart
156
+ </button>
157
+ }
158
+ onClose={vi.fn()}
159
+ onFullscreen={vi.fn()}
160
+ />
161
+ </Dialog>
162
+ );
163
+
164
+ expect(screen.getByText('Edit task')).toBeTruthy();
165
+ expect(screen.getByLabelText('Task title')).toHaveProperty(
166
+ 'value',
167
+ 'Existing'
168
+ );
169
+ expect(screen.getByLabelText('List: Inbox')).toBeTruthy();
170
+ expect(screen.getByLabelText('Smart action')).toBeTruthy();
171
+ expect(
172
+ screen.queryByLabelText('task-drafts.save_as_draft')
173
+ ).not.toBeInTheDocument();
174
+ expect(
175
+ screen.queryByRole('button', {
176
+ name: 'ws-task-boards.dialog.create_task',
177
+ })
178
+ ).not.toBeInTheDocument();
179
+ });
180
+
181
+ it('renders compact edit actions when provided', () => {
182
+ const onDelete = vi.fn();
183
+ const onDone = vi.fn();
184
+ const onClosed = vi.fn();
185
+
186
+ render(
187
+ <Dialog open={true}>
188
+ <CompactTaskCreatePopover
189
+ title="Edit task"
190
+ titleInput={<input aria-label="Task title" defaultValue="Existing" />}
191
+ propertyControls={
192
+ <button type="button" aria-label="List: Inbox">
193
+ List
194
+ </button>
195
+ }
196
+ editActions={
197
+ <>
198
+ <button
199
+ type="button"
200
+ aria-label="common.mark_as_done"
201
+ onClick={onDone}
202
+ >
203
+ Done
204
+ </button>
205
+ <button
206
+ type="button"
207
+ aria-label="common.archive"
208
+ onClick={onClosed}
209
+ >
210
+ Archive
211
+ </button>
212
+ <button
213
+ type="button"
214
+ aria-label="common.delete_task"
215
+ onClick={onDelete}
216
+ >
217
+ Delete
218
+ </button>
219
+ </>
220
+ }
221
+ onClose={vi.fn()}
222
+ onFullscreen={vi.fn()}
223
+ />
224
+ </Dialog>
225
+ );
226
+
227
+ fireEvent.click(screen.getByLabelText('common.mark_as_done'));
228
+ fireEvent.click(screen.getByLabelText('common.archive'));
229
+ fireEvent.click(screen.getByLabelText('common.delete_task'));
230
+
231
+ expect(
232
+ screen.getByLabelText('common.delete_task').closest('.border-b')
233
+ ).toBeTruthy();
234
+ expect(
235
+ screen.getByLabelText('common.delete_task').closest('.border-t')
236
+ ).toBeNull();
237
+ expect(onDone).toHaveBeenCalledTimes(1);
238
+ expect(onClosed).toHaveBeenCalledTimes(1);
239
+ expect(onDelete).toHaveBeenCalledTimes(1);
240
+ });
241
+
242
+ it('keeps the compact edit title accessible but not visibly rendered', () => {
243
+ render(
244
+ <Dialog open={true}>
245
+ <CompactTaskCreatePopover
246
+ title="Edit task"
247
+ showHeaderTitle={false}
248
+ titleInput={<input aria-label="Task title" defaultValue="Existing" />}
249
+ propertyControls={
250
+ <button type="button" aria-label="List: Inbox">
251
+ List
252
+ </button>
253
+ }
254
+ editActions={
255
+ <button type="button" aria-label="common.delete_task">
256
+ Delete
257
+ </button>
258
+ }
259
+ onClose={vi.fn()}
260
+ onFullscreen={vi.fn()}
261
+ />
262
+ </Dialog>
263
+ );
264
+
265
+ expect(screen.getByText('Edit task')).toHaveClass('sr-only');
266
+ expect(screen.getByLabelText('common.delete_task')).toBeTruthy();
267
+ });
268
+ });
@@ -0,0 +1,243 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Check,
5
+ Copy,
6
+ FileEdit,
7
+ ListTodo,
8
+ Loader2,
9
+ Maximize2,
10
+ X,
11
+ } from '@tuturuuu/icons';
12
+ import { Button } from '@tuturuuu/ui/button';
13
+ import { DialogDescription, DialogTitle } from '@tuturuuu/ui/dialog';
14
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
15
+ import { cn } from '@tuturuuu/utils/format';
16
+ import { useTranslations } from 'next-intl';
17
+ import type { ReactNode } from 'react';
18
+ import { QuickSettingsPopover } from './quick-settings-popover';
19
+
20
+ interface CompactTaskDialogPanelProps {
21
+ title: string;
22
+ description?: ReactNode;
23
+ icon?: ReactNode;
24
+ iconBgClass?: string;
25
+ iconRingClass?: string;
26
+ titleInput: ReactNode;
27
+ showHeaderTitle?: boolean;
28
+ taskStatus?: ReactNode;
29
+ propertyControls: ReactNode;
30
+ editActions?: ReactNode;
31
+ smartAction?: ReactNode;
32
+ smartPanel?: ReactNode;
33
+ saveAsDraft?: boolean;
34
+ createMultiple?: boolean;
35
+ canSave?: boolean;
36
+ isLoading?: boolean;
37
+ isPersonalWorkspace?: boolean;
38
+ onSaveAsDraftChange?: (value: boolean) => void;
39
+ onCreateMultipleChange?: (value: boolean) => void;
40
+ onClose: () => void;
41
+ onFullscreen: () => void;
42
+ onSave?: () => void;
43
+ }
44
+
45
+ function CompactIconButton({
46
+ active = false,
47
+ children,
48
+ label,
49
+ onClick,
50
+ }: {
51
+ active?: boolean;
52
+ children: ReactNode;
53
+ label: ReactNode;
54
+ onClick: () => void;
55
+ }) {
56
+ return (
57
+ <Tooltip>
58
+ <TooltipTrigger asChild>
59
+ <Button
60
+ type="button"
61
+ variant={active ? 'secondary' : 'ghost'}
62
+ size="icon"
63
+ aria-label={typeof label === 'string' ? label : undefined}
64
+ aria-pressed={active}
65
+ className={cn(
66
+ 'h-8 w-8 text-muted-foreground hover:text-foreground',
67
+ active && 'text-foreground'
68
+ )}
69
+ onClick={onClick}
70
+ >
71
+ {children}
72
+ </Button>
73
+ </TooltipTrigger>
74
+ <TooltipContent side="bottom">{label}</TooltipContent>
75
+ </Tooltip>
76
+ );
77
+ }
78
+
79
+ export function CompactTaskDialogPanel({
80
+ title,
81
+ description,
82
+ icon,
83
+ iconBgClass = 'bg-dynamic-orange/10',
84
+ iconRingClass = 'ring-dynamic-orange/20',
85
+ titleInput,
86
+ showHeaderTitle = true,
87
+ taskStatus,
88
+ propertyControls,
89
+ editActions,
90
+ smartAction,
91
+ smartPanel,
92
+ saveAsDraft,
93
+ createMultiple,
94
+ canSave,
95
+ isLoading = false,
96
+ isPersonalWorkspace,
97
+ onSaveAsDraftChange,
98
+ onCreateMultipleChange,
99
+ onClose,
100
+ onFullscreen,
101
+ onSave,
102
+ }: CompactTaskDialogPanelProps) {
103
+ const t = useTranslations();
104
+ const hasCreateActions =
105
+ typeof saveAsDraft === 'boolean' &&
106
+ typeof createMultiple === 'boolean' &&
107
+ typeof canSave === 'boolean' &&
108
+ !!onSave &&
109
+ !!onSaveAsDraftChange &&
110
+ !!onCreateMultipleChange;
111
+ const saveLabel = saveAsDraft
112
+ ? t('task-drafts.save_as_draft')
113
+ : t('ws-task-boards.dialog.create_task');
114
+ const hasHeaderTitle = showHeaderTitle;
115
+
116
+ return (
117
+ <div
118
+ data-testid="compact-task-dialog-panel"
119
+ className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
120
+ >
121
+ <div
122
+ className={cn(
123
+ 'flex items-start gap-3 border-b px-4 py-3',
124
+ hasHeaderTitle ? 'justify-between' : 'justify-end'
125
+ )}
126
+ >
127
+ {hasHeaderTitle ? (
128
+ <div className="flex min-w-0 items-start gap-2.5">
129
+ <div
130
+ className={cn(
131
+ 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
132
+ iconBgClass,
133
+ iconRingClass
134
+ )}
135
+ >
136
+ {icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
137
+ </div>
138
+ <div className="min-w-0 space-y-0.5">
139
+ <DialogTitle className="truncate font-semibold text-base">
140
+ {title}
141
+ </DialogTitle>
142
+ {description && (
143
+ <DialogDescription className="truncate text-muted-foreground text-xs">
144
+ {description}
145
+ </DialogDescription>
146
+ )}
147
+ </div>
148
+ </div>
149
+ ) : (
150
+ <DialogTitle className="sr-only">{title}</DialogTitle>
151
+ )}
152
+ <div className="flex shrink-0 items-center gap-1">
153
+ {smartAction}
154
+ {editActions}
155
+ <Tooltip>
156
+ <TooltipTrigger asChild>
157
+ <Button
158
+ type="button"
159
+ variant="ghost"
160
+ size="icon"
161
+ aria-label={t('ws-task-boards.dialog.open_fullscreen')}
162
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
163
+ onClick={onFullscreen}
164
+ >
165
+ <Maximize2 className="h-4 w-4" />
166
+ </Button>
167
+ </TooltipTrigger>
168
+ <TooltipContent side="bottom">
169
+ {t('ws-task-boards.dialog.open_fullscreen')}
170
+ </TooltipContent>
171
+ </Tooltip>
172
+ <Tooltip>
173
+ <TooltipTrigger asChild>
174
+ <Button
175
+ type="button"
176
+ variant="ghost"
177
+ size="icon"
178
+ aria-label={t('common.close')}
179
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
180
+ onClick={onClose}
181
+ >
182
+ <X className="h-4 w-4" />
183
+ </Button>
184
+ </TooltipTrigger>
185
+ <TooltipContent side="bottom">{t('common.close')}</TooltipContent>
186
+ </Tooltip>
187
+ </div>
188
+ </div>
189
+
190
+ <div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
191
+ {titleInput}
192
+ {taskStatus}
193
+ <div className="flex flex-wrap items-center gap-1.5">
194
+ {propertyControls}
195
+ </div>
196
+ {smartPanel}
197
+ </div>
198
+
199
+ {hasCreateActions && (
200
+ <div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
201
+ <div className="flex items-center gap-1">
202
+ <CompactIconButton
203
+ active={!!saveAsDraft}
204
+ label={t('task-drafts.save_as_draft')}
205
+ onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
206
+ >
207
+ <FileEdit className="h-4 w-4" />
208
+ </CompactIconButton>
209
+ <CompactIconButton
210
+ active={!!createMultiple}
211
+ label={t('ws-task-boards.dialog.create_multiple')}
212
+ onClick={() => onCreateMultipleChange?.(!createMultiple)}
213
+ >
214
+ <Copy className="h-4 w-4" />
215
+ </CompactIconButton>
216
+ <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
217
+ </div>
218
+ <Button
219
+ type="button"
220
+ size="sm"
221
+ disabled={!canSave}
222
+ onClick={() => onSave?.()}
223
+ className="min-w-28"
224
+ >
225
+ {isLoading ? (
226
+ <>
227
+ <Loader2 className="h-4 w-4 animate-spin" />
228
+ {t('ws-task-boards.dialog.saving')}
229
+ </>
230
+ ) : (
231
+ <>
232
+ <Check className="h-4 w-4" />
233
+ {saveLabel}
234
+ </>
235
+ )}
236
+ </Button>
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ export const CompactTaskCreatePopover = CompactTaskDialogPanel;
@@ -3,11 +3,13 @@
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { Settings } from '@tuturuuu/icons';
5
5
  import { Button } from '@tuturuuu/ui/button';
6
+ import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
6
7
  import { Label } from '@tuturuuu/ui/label';
7
8
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
8
9
  import { Switch } from '@tuturuuu/ui/switch';
9
10
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
10
11
  import { useTranslations } from 'next-intl';
12
+ import { TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID } from '../../task-sound-effects';
11
13
 
12
14
  interface TaskSettingsData {
13
15
  task_auto_assign_to_self: boolean;
@@ -38,6 +40,12 @@ export function QuickSettingsPopover({
38
40
  const t = useTranslations('settings.tasks');
39
41
  const tCommon = useTranslations('common');
40
42
  const queryClient = useQueryClient();
43
+ const {
44
+ value: soundEffectsEnabled,
45
+ setValue: setSoundEffectsEnabled,
46
+ isLoading: soundEffectsEnabledLoading,
47
+ isPending: soundEffectsEnabledPending,
48
+ } = useUserBooleanConfig(TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID, true);
41
49
 
42
50
  const { data: settings, isLoading } = useQuery({
43
51
  queryKey: ['user-task-settings'],
@@ -156,6 +164,24 @@ export function QuickSettingsPopover({
156
164
  disabled={isLoading || updateSettings.isPending}
157
165
  />
158
166
  </div>
167
+ <div className="flex items-center justify-between gap-2">
168
+ <div className="space-y-0.5">
169
+ <Label htmlFor="task-sound-effects" className="text-sm">
170
+ {t('sound_effects')}
171
+ </Label>
172
+ <p className="text-muted-foreground text-xs">
173
+ {t('sound_effects_description')}
174
+ </p>
175
+ </div>
176
+ <Switch
177
+ id="task-sound-effects"
178
+ checked={soundEffectsEnabled}
179
+ onCheckedChange={setSoundEffectsEnabled}
180
+ disabled={
181
+ soundEffectsEnabledLoading || soundEffectsEnabledPending
182
+ }
183
+ />
184
+ </div>
159
185
  </div>
160
186
  </div>
161
187
  </PopoverContent>
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import '@testing-library/jest-dom/vitest';
6
+ import { fireEvent, render, screen } from '@testing-library/react';
7
+ import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import {
10
+ SmartTaskSuggestionsButton,
11
+ SmartTaskSuggestionsPanel,
12
+ } from './smart-task-suggestions-panel';
13
+
14
+ vi.mock('next-intl', () => ({
15
+ useTranslations: () => (key: string, values?: Record<string, unknown>) =>
16
+ values ? `${key}:${JSON.stringify(values)}` : key,
17
+ }));
18
+
19
+ const baseSuggestion: WorkspaceTaskSuggestionTask = {
20
+ id: 'suggestion-1',
21
+ title: 'Prepare launch checklist',
22
+ description: 'Confirm owners and deadlines.',
23
+ priority: 'high',
24
+ listId: 'list-1',
25
+ listName: 'Inbox',
26
+ labelIds: ['label-1'],
27
+ labels: [
28
+ {
29
+ id: 'label-1',
30
+ name: 'Launch',
31
+ color: 'blue',
32
+ created_at: '2026-06-11T00:00:00.000Z',
33
+ },
34
+ ],
35
+ projectIds: ['project-1'],
36
+ projects: [{ id: 'project-1', name: 'Website', status: 'active' }],
37
+ endDate: '2026-06-12T16:59:59.000Z',
38
+ estimationPoints: 3,
39
+ durationMinutes: 90,
40
+ isSplittable: true,
41
+ minSplitDurationMinutes: 30,
42
+ maxSplitDurationMinutes: 60,
43
+ calendarHours: 'work_hours',
44
+ autoSchedule: true,
45
+ reason: 'Launch work with a deadline.',
46
+ };
47
+
48
+ describe('SmartTaskSuggestionsPanel', () => {
49
+ it('renders loading state from the sparkle button and panel', () => {
50
+ render(
51
+ <>
52
+ <SmartTaskSuggestionsButton isLoading={true} onClick={vi.fn()} />
53
+ <SmartTaskSuggestionsPanel
54
+ suggestions={[]}
55
+ selectedSuggestionIds={[]}
56
+ isLoading={true}
57
+ onApplyFirst={vi.fn()}
58
+ onApplySuggestion={vi.fn()}
59
+ onClose={vi.fn()}
60
+ onCreateSelected={vi.fn()}
61
+ onRetry={vi.fn()}
62
+ onToggleSuggestion={vi.fn()}
63
+ />
64
+ </>
65
+ );
66
+
67
+ expect(screen.getByLabelText('smart_suggest')).toBeDisabled();
68
+ expect(screen.getByText('smart_generating')).toBeInTheDocument();
69
+ });
70
+
71
+ it('applies a single suggestion without creating it immediately', () => {
72
+ const onApplySuggestion = vi.fn();
73
+
74
+ render(
75
+ <SmartTaskSuggestionsPanel
76
+ suggestions={[baseSuggestion]}
77
+ selectedSuggestionIds={['suggestion-1']}
78
+ onApplyFirst={vi.fn()}
79
+ onApplySuggestion={onApplySuggestion}
80
+ onClose={vi.fn()}
81
+ onCreateSelected={vi.fn()}
82
+ onRetry={vi.fn()}
83
+ onToggleSuggestion={vi.fn()}
84
+ />
85
+ );
86
+
87
+ fireEvent.click(
88
+ screen.getByRole('button', { name: 'smart_apply_suggestion' })
89
+ );
90
+
91
+ expect(onApplySuggestion).toHaveBeenCalledWith(baseSuggestion);
92
+ expect(
93
+ screen.queryByRole('button', { name: /smart_create_selected/ })
94
+ ).not.toBeInTheDocument();
95
+ });
96
+
97
+ it('creates selected tasks from multiple suggestions', () => {
98
+ const onCreateSelected = vi.fn();
99
+ const onToggleSuggestion = vi.fn();
100
+ const secondSuggestion = {
101
+ ...baseSuggestion,
102
+ id: 'suggestion-2',
103
+ title: 'Draft launch note',
104
+ };
105
+
106
+ render(
107
+ <SmartTaskSuggestionsPanel
108
+ suggestions={[baseSuggestion, secondSuggestion]}
109
+ selectedSuggestionIds={['suggestion-1']}
110
+ onApplyFirst={vi.fn()}
111
+ onApplySuggestion={vi.fn()}
112
+ onClose={vi.fn()}
113
+ onCreateSelected={onCreateSelected}
114
+ onRetry={vi.fn()}
115
+ onToggleSuggestion={onToggleSuggestion}
116
+ />
117
+ );
118
+
119
+ fireEvent.click(screen.getAllByLabelText('smart_select_suggestion')[1]!);
120
+ fireEvent.click(
121
+ screen.getByRole('button', {
122
+ name: 'smart_create_selected:{"count":1}',
123
+ })
124
+ );
125
+
126
+ expect(onToggleSuggestion).toHaveBeenCalledWith('suggestion-2');
127
+ expect(onCreateSelected).toHaveBeenCalledTimes(1);
128
+ });
129
+ });