@tuturuuu/ui 0.7.0 → 0.8.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 (67) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/package.json +8 -8
  3. package/src/components/ui/currency-input.test.tsx +43 -0
  4. package/src/components/ui/currency-input.tsx +1 -1
  5. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  6. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  7. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  8. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  9. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  10. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  12. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  13. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  14. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  15. package/src/components/ui/money-input.test.tsx +64 -0
  16. package/src/components/ui/money-input.tsx +63 -0
  17. package/src/components/ui/storefront/cart-summary.tsx +114 -29
  18. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  19. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  20. package/src/components/ui/storefront/image-panel.tsx +6 -0
  21. package/src/components/ui/storefront/index.ts +11 -0
  22. package/src/components/ui/storefront/listing-card.tsx +84 -22
  23. package/src/components/ui/storefront/product-detail.tsx +289 -0
  24. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  25. package/src/components/ui/storefront/storefront-surface.test.tsx +124 -1
  26. package/src/components/ui/storefront/storefront-surface.tsx +333 -133
  27. package/src/components/ui/storefront/types.ts +23 -1
  28. package/src/components/ui/storefront/utils.ts +111 -27
  29. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  30. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  31. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  32. package/src/components/ui/text-editor/content-migration.ts +41 -18
  33. package/src/components/ui/text-editor/extensions.ts +1 -1
  34. package/src/components/ui/text-editor/image-extension.ts +40 -18
  35. package/src/components/ui/text-editor/video-extension.ts +11 -2
  36. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  37. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  38. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  39. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  40. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  41. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  42. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  43. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  44. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  45. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  46. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  47. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  48. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  49. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  50. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  51. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  52. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  53. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  54. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  55. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  56. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  57. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  58. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  59. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  60. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  61. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  62. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  63. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  64. package/src/hooks/useBoardRealtime.ts +6 -3
  65. package/src/hooks/useBoardRealtime.types.ts +11 -0
  66. package/src/hooks/useCursorTracking.ts +91 -27
  67. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveTaskDialogOpeningPresentation } from './task-dialog-presentation';
3
+
4
+ describe('resolveTaskDialogOpeningPresentation', () => {
5
+ it('opens existing document-list tasks fullscreen', () => {
6
+ expect(
7
+ resolveTaskDialogOpeningPresentation({
8
+ defaultPresentation: 'compact',
9
+ mode: 'edit',
10
+ selectedListStatus: 'documents',
11
+ })
12
+ ).toBe('fullscreen');
13
+ });
14
+
15
+ it('keeps create mode compact even in document lists', () => {
16
+ expect(
17
+ resolveTaskDialogOpeningPresentation({
18
+ defaultPresentation: 'fullscreen',
19
+ mode: 'create',
20
+ selectedListStatus: 'documents',
21
+ })
22
+ ).toBe('compact');
23
+ });
24
+
25
+ it('respects the user default for existing non-document tasks', () => {
26
+ expect(
27
+ resolveTaskDialogOpeningPresentation({
28
+ defaultPresentation: 'compact',
29
+ mode: 'edit',
30
+ selectedListStatus: 'active',
31
+ })
32
+ ).toBe('compact');
33
+
34
+ expect(
35
+ resolveTaskDialogOpeningPresentation({
36
+ defaultPresentation: 'fullscreen',
37
+ mode: 'edit',
38
+ selectedListStatus: 'not_started',
39
+ })
40
+ ).toBe('fullscreen');
41
+ });
42
+
43
+ it('keeps drafts fullscreen', () => {
44
+ expect(
45
+ resolveTaskDialogOpeningPresentation({
46
+ defaultPresentation: 'compact',
47
+ draftId: 'draft-1',
48
+ mode: 'create',
49
+ selectedListStatus: 'documents',
50
+ })
51
+ ).toBe('fullscreen');
52
+ });
53
+ });
@@ -2,6 +2,7 @@ export const TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID =
2
2
  'TASK_DIALOG_DEFAULT_PRESENTATION';
3
3
 
4
4
  export type TaskDialogPresentation = 'compact' | 'fullscreen';
5
+ export type TaskDialogMode = 'edit' | 'create';
5
6
 
6
7
  export function normalizeTaskDialogPresentation(
7
8
  value: unknown,
@@ -9,3 +10,21 @@ export function normalizeTaskDialogPresentation(
9
10
  ): TaskDialogPresentation {
10
11
  return value === 'fullscreen' || value === 'compact' ? value : fallback;
11
12
  }
13
+
14
+ export function resolveTaskDialogOpeningPresentation({
15
+ defaultPresentation,
16
+ draftId,
17
+ mode = 'edit',
18
+ selectedListStatus,
19
+ }: {
20
+ defaultPresentation?: unknown;
21
+ draftId?: string;
22
+ mode?: TaskDialogMode;
23
+ selectedListStatus?: string | null;
24
+ }): TaskDialogPresentation {
25
+ if (draftId) return 'fullscreen';
26
+ if (mode === 'create') return 'compact';
27
+ if (selectedListStatus === 'documents') return 'fullscreen';
28
+
29
+ return normalizeTaskDialogPresentation(defaultPresentation);
30
+ }
@@ -178,6 +178,63 @@ describe('CompactTaskCreatePopover', () => {
178
178
  ).not.toBeInTheDocument();
179
179
  });
180
180
 
181
+ it('renders compact description preview without affecting panel layout', () => {
182
+ const onDescriptionPreviewClick = vi.fn();
183
+
184
+ render(
185
+ <Dialog open={true}>
186
+ <CompactTaskCreatePopover
187
+ title="Edit task"
188
+ titleInput={<input aria-label="Task title" defaultValue="Existing" />}
189
+ propertyControls={
190
+ <button type="button" aria-label="List: Inbox">
191
+ List
192
+ </button>
193
+ }
194
+ descriptionPreview="Confirm the plan and publish the final notes."
195
+ descriptionPreviewLabel="Open full task"
196
+ onDescriptionPreviewClick={onDescriptionPreviewClick}
197
+ onClose={vi.fn()}
198
+ onFullscreen={vi.fn()}
199
+ />
200
+ </Dialog>
201
+ );
202
+
203
+ const preview = screen.getByTestId('compact-task-description-preview');
204
+
205
+ expect(preview).toHaveTextContent(
206
+ 'Confirm the plan and publish the final notes.'
207
+ );
208
+ expect(preview).toHaveAttribute('aria-label', 'Open full task');
209
+ expect(preview).toHaveClass('absolute', 'top-full');
210
+
211
+ fireEvent.click(preview);
212
+
213
+ expect(onDescriptionPreviewClick).toHaveBeenCalledTimes(1);
214
+ });
215
+
216
+ it('omits compact description preview when the caller does not provide one', () => {
217
+ render(
218
+ <Dialog open={true}>
219
+ <CompactTaskCreatePopover
220
+ title="Create task"
221
+ titleInput={<input aria-label="Task title" defaultValue="New" />}
222
+ propertyControls={
223
+ <button type="button" aria-label="List: Inbox">
224
+ List
225
+ </button>
226
+ }
227
+ onClose={vi.fn()}
228
+ onFullscreen={vi.fn()}
229
+ />
230
+ </Dialog>
231
+ );
232
+
233
+ expect(
234
+ screen.queryByTestId('compact-task-description-preview')
235
+ ).not.toBeInTheDocument();
236
+ });
237
+
181
238
  it('renders compact edit actions when provided', () => {
182
239
  const onDelete = vi.fn();
183
240
  const onDone = vi.fn();
@@ -25,6 +25,8 @@ interface CompactTaskDialogPanelProps {
25
25
  iconRingClass?: string;
26
26
  titleInput: ReactNode;
27
27
  showHeaderTitle?: boolean;
28
+ descriptionPreview?: string | null;
29
+ descriptionPreviewLabel?: string;
28
30
  taskStatus?: ReactNode;
29
31
  propertyControls: ReactNode;
30
32
  editActions?: ReactNode;
@@ -39,6 +41,7 @@ interface CompactTaskDialogPanelProps {
39
41
  onCreateMultipleChange?: (value: boolean) => void;
40
42
  onClose: () => void;
41
43
  onFullscreen: () => void;
44
+ onDescriptionPreviewClick?: () => void;
42
45
  onSave?: () => void;
43
46
  }
44
47
 
@@ -84,6 +87,8 @@ export function CompactTaskDialogPanel({
84
87
  iconRingClass = 'ring-dynamic-orange/20',
85
88
  titleInput,
86
89
  showHeaderTitle = true,
90
+ descriptionPreview,
91
+ descriptionPreviewLabel,
87
92
  taskStatus,
88
93
  propertyControls,
89
94
  editActions,
@@ -98,6 +103,7 @@ export function CompactTaskDialogPanel({
98
103
  onCreateMultipleChange,
99
104
  onClose,
100
105
  onFullscreen,
106
+ onDescriptionPreviewClick,
101
107
  onSave,
102
108
  }: CompactTaskDialogPanelProps) {
103
109
  const t = useTranslations();
@@ -114,127 +120,146 @@ export function CompactTaskDialogPanel({
114
120
  const hasHeaderTitle = showHeaderTitle;
115
121
 
116
122
  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
- >
123
+ <div className="relative">
121
124
  <div
122
- className={cn(
123
- 'flex items-start gap-3 border-b px-4 py-3',
124
- hasHeaderTitle ? 'justify-between' : 'justify-end'
125
- )}
125
+ data-testid="compact-task-dialog-panel"
126
+ className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
126
127
  >
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
- )}
128
+ <div
129
+ className={cn(
130
+ 'flex items-start gap-3 border-b px-4 py-3',
131
+ hasHeaderTitle ? 'justify-between' : 'justify-end'
132
+ )}
133
+ >
134
+ {hasHeaderTitle ? (
135
+ <div className="flex min-w-0 items-start gap-2.5">
136
+ <div
137
+ className={cn(
138
+ 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
139
+ iconBgClass,
140
+ iconRingClass
141
+ )}
142
+ >
143
+ {icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
144
+ </div>
145
+ <div className="min-w-0 space-y-0.5">
146
+ <DialogTitle className="truncate font-semibold text-base">
147
+ {title}
148
+ </DialogTitle>
149
+ {description && (
150
+ <DialogDescription className="truncate text-muted-foreground text-xs">
151
+ {description}
152
+ </DialogDescription>
153
+ )}
154
+ </div>
147
155
  </div>
156
+ ) : (
157
+ <DialogTitle className="sr-only">{title}</DialogTitle>
158
+ )}
159
+ <div className="flex shrink-0 items-center gap-1">
160
+ {smartAction}
161
+ {editActions}
162
+ <Tooltip>
163
+ <TooltipTrigger asChild>
164
+ <Button
165
+ type="button"
166
+ variant="ghost"
167
+ size="icon"
168
+ aria-label={t('ws-task-boards.dialog.open_fullscreen')}
169
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
170
+ onClick={onFullscreen}
171
+ >
172
+ <Maximize2 className="h-4 w-4" />
173
+ </Button>
174
+ </TooltipTrigger>
175
+ <TooltipContent side="bottom">
176
+ {t('ws-task-boards.dialog.open_fullscreen')}
177
+ </TooltipContent>
178
+ </Tooltip>
179
+ <Tooltip>
180
+ <TooltipTrigger asChild>
181
+ <Button
182
+ type="button"
183
+ variant="ghost"
184
+ size="icon"
185
+ aria-label={t('common.close')}
186
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
187
+ onClick={onClose}
188
+ >
189
+ <X className="h-4 w-4" />
190
+ </Button>
191
+ </TooltipTrigger>
192
+ <TooltipContent side="bottom">{t('common.close')}</TooltipContent>
193
+ </Tooltip>
148
194
  </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
195
  </div>
188
- </div>
189
196
 
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}
197
+ <div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
198
+ {titleInput}
199
+ {taskStatus}
200
+ <div className="flex flex-wrap items-center gap-1.5">
201
+ {propertyControls}
202
+ </div>
203
+ {smartPanel}
195
204
  </div>
196
- {smartPanel}
197
- </div>
198
205
 
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)}
206
+ {hasCreateActions && (
207
+ <div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
208
+ <div className="flex items-center gap-1">
209
+ <CompactIconButton
210
+ active={!!saveAsDraft}
211
+ label={t('task-drafts.save_as_draft')}
212
+ onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
213
+ >
214
+ <FileEdit className="h-4 w-4" />
215
+ </CompactIconButton>
216
+ <CompactIconButton
217
+ active={!!createMultiple}
218
+ label={t('ws-task-boards.dialog.create_multiple')}
219
+ onClick={() => onCreateMultipleChange?.(!createMultiple)}
220
+ >
221
+ <Copy className="h-4 w-4" />
222
+ </CompactIconButton>
223
+ <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
224
+ </div>
225
+ <Button
226
+ type="button"
227
+ size="sm"
228
+ disabled={!canSave}
229
+ onClick={() => onSave?.()}
230
+ className="min-w-28"
213
231
  >
214
- <Copy className="h-4 w-4" />
215
- </CompactIconButton>
216
- <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
232
+ {isLoading ? (
233
+ <>
234
+ <Loader2 className="h-4 w-4 animate-spin" />
235
+ {t('ws-task-boards.dialog.saving')}
236
+ </>
237
+ ) : (
238
+ <>
239
+ <Check className="h-4 w-4" />
240
+ {saveLabel}
241
+ </>
242
+ )}
243
+ </Button>
217
244
  </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>
245
+ )}
246
+ </div>
247
+
248
+ {descriptionPreview && onDescriptionPreviewClick && (
249
+ <button
250
+ type="button"
251
+ data-testid="compact-task-description-preview"
252
+ aria-label={
253
+ descriptionPreviewLabel ??
254
+ t('ws-task-boards.dialog.open_fullscreen')
255
+ }
256
+ className="absolute top-full left-1/2 mt-2 w-full max-w-[30rem] -translate-x-1/2 rounded-lg border bg-background/95 px-4 py-3 text-left opacity-70 shadow-xl ring-1 ring-border/60 backdrop-blur transition hover:bg-muted/70 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
257
+ onClick={onDescriptionPreviewClick}
258
+ >
259
+ <span className="line-clamp-3 whitespace-pre-line text-muted-foreground text-sm leading-relaxed">
260
+ {descriptionPreview}
261
+ </span>
262
+ </button>
238
263
  )}
239
264
  </div>
240
265
  );
@@ -4,6 +4,7 @@ import type { QueryClient } from '@tanstack/react-query';
4
4
  import type { Editor, JSONContent } from '@tiptap/react';
5
5
  import { Loader2 } from '@tuturuuu/icons';
6
6
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
7
+ import { getBoardRealtimeChannelName } from '@tuturuuu/ui/hooks/useBoardRealtime.types';
7
8
  import { toast } from '@tuturuuu/ui/sonner';
8
9
  import { cn } from '@tuturuuu/utils/format';
9
10
  import { useTranslations } from 'next-intl';
@@ -352,8 +353,9 @@ export function TaskDescriptionEditor({
352
353
 
353
354
  {showCollaborationCursors && taskId && (
354
355
  <CursorOverlayMultiWrapper
355
- channelName={`task-cursor-${taskId}`}
356
+ channelName={getBoardRealtimeChannelName(boardId)}
356
357
  containerRef={richTextEditorRef}
358
+ cursorScope={{ taskId, type: 'task-description' }}
357
359
  />
358
360
  )}
359
361
 
@@ -47,6 +47,7 @@ import { createInitialSuggestionState } from './mention-system/types';
47
47
  import { SyncWarningDialog } from './sync-warning-dialog';
48
48
  import {
49
49
  normalizeTaskDialogPresentation,
50
+ resolveTaskDialogOpeningPresentation,
50
51
  type TaskDialogPresentation,
51
52
  } from './task-dialog-presentation';
52
53
  import { CompactTaskDialogPanel } from './task-edit-dialog/components/compact-task-create-popover';
@@ -107,6 +108,7 @@ import {
107
108
  getDescriptionContent,
108
109
  getDraftStorageKey,
109
110
  getTaskDescriptionPercentLeft,
111
+ getTaskDescriptionPreviewText,
110
112
  getTaskDescriptionStorageLength,
111
113
  saveAndVerifyYjsDescriptionToDatabase,
112
114
  saveYjsDescriptionToDatabase,
@@ -494,6 +496,23 @@ export function TaskEditDialog({
494
496
  taskSearchQuery,
495
497
  sharedContext,
496
498
  });
499
+ const currentList = availableLists?.find(
500
+ (list) => list.id === formState.selectedListId
501
+ );
502
+ const normalizedDefaultPresentation = useMemo(
503
+ () => normalizeTaskDialogPresentation(defaultPresentation),
504
+ [defaultPresentation]
505
+ );
506
+ const openingPresentation = useMemo(
507
+ () =>
508
+ resolveTaskDialogOpeningPresentation({
509
+ defaultPresentation: normalizedDefaultPresentation,
510
+ draftId,
511
+ mode,
512
+ selectedListStatus: currentList?.status,
513
+ }),
514
+ [currentList?.status, draftId, mode, normalizedDefaultPresentation]
515
+ );
497
516
 
498
517
  // Update browser tab title
499
518
  useEffect(() => {
@@ -552,13 +571,8 @@ export function TaskEditDialog({
552
571
  useState(false);
553
572
  const [showShareDialog, setShowShareDialog] = useState(false);
554
573
  const [saveAsDraft, setSaveAsDraft] = useState(draftModeEnabled);
555
- const normalizedDefaultPresentation = useMemo(
556
- () => normalizeTaskDialogPresentation(defaultPresentation),
557
- [defaultPresentation]
558
- );
559
- const [presentation, setPresentation] = useState<TaskDialogPresentation>(
560
- normalizedDefaultPresentation
561
- );
574
+ const [presentation, setPresentation] =
575
+ useState<TaskDialogPresentation>(openingPresentation);
562
576
  const [smartSuggestions, setSmartSuggestions] = useState<
563
577
  WorkspaceTaskSuggestionTask[]
564
578
  >([]);
@@ -849,9 +863,6 @@ export function TaskEditDialog({
849
863
  onUpdate,
850
864
  });
851
865
 
852
- const currentList = availableLists?.find(
853
- (list) => list.id === formState.selectedListId
854
- );
855
866
  const doneList = availableLists?.find(
856
867
  (list) => list.status === 'done' && !list.deleted
857
868
  );
@@ -1946,7 +1957,7 @@ export function TaskEditDialog({
1946
1957
  previousOpenRef.current = isOpen;
1947
1958
 
1948
1959
  if (!isOpen) {
1949
- setPresentation(normalizedDefaultPresentation);
1960
+ setPresentation(openingPresentation);
1950
1961
  setSmartSuggestions([]);
1951
1962
  setSelectedSmartSuggestionIds([]);
1952
1963
  setSmartSuggestionError(null);
@@ -1957,9 +1968,14 @@ export function TaskEditDialog({
1957
1968
  }
1958
1969
 
1959
1970
  if (justOpened) {
1960
- setPresentation(draftId ? 'fullscreen' : normalizedDefaultPresentation);
1971
+ setPresentation(openingPresentation);
1972
+ return;
1973
+ }
1974
+
1975
+ if (!isCreateMode && currentList?.status === 'documents') {
1976
+ setPresentation('fullscreen');
1961
1977
  }
1962
- }, [isOpen, draftId, normalizedDefaultPresentation]);
1978
+ }, [currentList?.status, isCreateMode, isOpen, openingPresentation]);
1963
1979
 
1964
1980
  // Track whether the title input is scrolled out of view
1965
1981
  useEffect(() => {
@@ -2023,6 +2039,15 @@ export function TaskEditDialog({
2023
2039
  ]);
2024
2040
 
2025
2041
  const showCompactDialog = presentation === 'compact' && !draftId;
2042
+ const compactDescriptionPreview = useMemo(() => {
2043
+ if (isCreateMode) return null;
2044
+
2045
+ const previewText = getTaskDescriptionPreviewText(
2046
+ formState.description
2047
+ ).trim();
2048
+
2049
+ return previewText || null;
2050
+ }, [formState.description, isCreateMode]);
2026
2051
  const taskHydrationNotice = taskLoadError ? (
2027
2052
  <div
2028
2053
  className="mx-4 mb-2 flex items-center justify-between gap-3 rounded-md border border-dynamic-red/30 bg-dynamic-red/10 px-3 py-2 text-dynamic-red text-sm md:mx-8"
@@ -2235,7 +2260,7 @@ export function TaskEditDialog({
2235
2260
  showCloseButton={false}
2236
2261
  className={
2237
2262
  showCompactDialog
2238
- ? 'w-[min(calc(100vw-2rem),30rem)] max-w-[30rem] gap-0 overflow-hidden rounded-lg border p-0 shadow-xl'
2263
+ ? 'w-[min(calc(100vw-2rem),30rem)] max-w-[30rem] gap-0 overflow-visible rounded-lg border p-0 shadow-xl'
2239
2264
  : 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-bottom-2 data-[state=open]:slide-in-from-bottom-2 inset-0! top-0! left-0! flex h-screen max-h-screen w-screen max-w-none! translate-x-0! translate-y-0! gap-0 rounded-none! border-0 p-0'
2240
2265
  }
2241
2266
  onContextMenu={(e) => {
@@ -2271,6 +2296,8 @@ export function TaskEditDialog({
2271
2296
  iconBgClass={compactHeaderInfo.iconBgClass}
2272
2297
  iconRingClass={compactHeaderInfo.iconRingClass}
2273
2298
  showHeaderTitle={isCreateMode}
2299
+ descriptionPreview={compactDescriptionPreview}
2300
+ descriptionPreviewLabel={dialogT('open_fullscreen')}
2274
2301
  titleInput={
2275
2302
  <TaskNameInput
2276
2303
  name={formState.name}
@@ -2311,6 +2338,7 @@ export function TaskEditDialog({
2311
2338
  }
2312
2339
  onClose={handleAttemptClose}
2313
2340
  onFullscreen={() => setPresentation('fullscreen')}
2341
+ onDescriptionPreviewClick={() => setPresentation('fullscreen')}
2314
2342
  onSave={
2315
2343
  isCreateMode && !taskControlsDisabled ? handleSave : undefined
2316
2344
  }
@@ -248,7 +248,7 @@ describe('useBoardRealtime', () => {
248
248
  expect(mockChannel.subscribe).toHaveBeenCalledTimes(1);
249
249
  });
250
250
 
251
- it('should create channel with self: false config', () => {
251
+ it('should create a private channel with self: false config', () => {
252
252
  renderHook(() => useBoardRealtime('board-1', { enabled: true }), {
253
253
  wrapper,
254
254
  });
@@ -258,7 +258,7 @@ describe('useBoardRealtime', () => {
258
258
  )();
259
259
  expect(supabaseInstance.channel).toHaveBeenCalledWith(
260
260
  'board-realtime-board-1',
261
- { config: { broadcast: { self: false } } }
261
+ { config: { broadcast: { self: false }, private: true } }
262
262
  );
263
263
  });
264
264