@tuturuuu/ui 0.8.0 → 0.9.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -14,11 +14,13 @@ import {
14
14
  CommandSeparator,
15
15
  } from '../command';
16
16
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
17
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
17
18
 
18
19
  export type ComboboxOption = {
19
20
  value: string;
20
21
  label: string;
21
22
  searchValue?: string;
23
+ group?: string;
22
24
  description?: React.ReactNode;
23
25
  badge?: React.ReactNode;
24
26
  icon?: React.ReactNode;
@@ -41,6 +43,8 @@ export type ComboboxCreateResult = string | ComboboxOption | undefined;
41
43
  export type ComboboxOptions = ComboboxOption;
42
44
 
43
45
  type Mode = 'single' | 'multiple';
46
+ type ComboboxContentWidth = 'trigger' | 'sm' | 'md' | 'lg' | 'auto';
47
+ type ComboboxTriggerMode = 'default' | 'compact';
44
48
 
45
49
  interface ComboboxProps {
46
50
  /** Options to display in the combobox */
@@ -67,6 +71,26 @@ interface ComboboxProps {
67
71
  creatingText?: string;
68
72
  /** Override label shown on the trigger button */
69
73
  label?: React.ReactNode;
74
+ /** Accessible label for compact/icon-first triggers */
75
+ ariaLabel?: string;
76
+ /** Whether to render the selected option icon inside the trigger button */
77
+ showSelectedIcon?: boolean;
78
+ /** Icon rendered in the trigger instead of the selected option icon */
79
+ triggerIcon?: React.ReactNode;
80
+ /** Whether to render the chevron inside the trigger button */
81
+ showChevron?: boolean;
82
+ /** Trigger layout mode */
83
+ triggerMode?: ComboboxTriggerMode;
84
+ /** Hide the visible trigger label while preserving the accessible label */
85
+ hideTriggerLabel?: boolean;
86
+ /** Tooltip rendered for compact/icon-only triggers */
87
+ triggerTooltip?: React.ReactNode;
88
+ /** Whether selected option colors should be applied to the trigger icon/text */
89
+ colorizeTriggerIcon?: boolean;
90
+ /** Width preset for the popover content */
91
+ contentWidth?: ComboboxContentWidth;
92
+ /** Additional class name for the popover content */
93
+ contentClassName?: string;
70
94
  /** Additional class name for the container */
71
95
  className?: string;
72
96
  /** Whether the combobox is disabled */
@@ -77,6 +101,8 @@ interface ComboboxProps {
77
101
  onCreate?: (value: string) => unknown | Promise<unknown>;
78
102
  /** Callback when search input changes */
79
103
  onSearchChange?: (value: string) => void;
104
+ /** Callback when the popover opens or closes */
105
+ onOpenChange?: (open: boolean) => void;
80
106
  /** Whether there are more options to load */
81
107
  hasMore?: boolean;
82
108
  /** Callback when the options list scrolls near the end */
@@ -107,11 +133,22 @@ export function Combobox({
107
133
  createText,
108
134
  creatingText,
109
135
  label,
136
+ ariaLabel,
137
+ showSelectedIcon = true,
138
+ triggerIcon,
139
+ showChevron = true,
140
+ triggerMode = 'default',
141
+ hideTriggerLabel = false,
142
+ triggerTooltip,
143
+ colorizeTriggerIcon = true,
144
+ contentWidth = 'trigger',
145
+ contentClassName,
110
146
  className,
111
147
  disabled,
112
148
  useFirstValueAsDefault = false,
113
149
  onCreate,
114
150
  onSearchChange,
151
+ onOpenChange,
115
152
  hasMore = false,
116
153
  onLoadMore,
117
154
  loadingMore = false,
@@ -146,6 +183,27 @@ export function Combobox({
146
183
  );
147
184
  }, [normalizedQuery, options]);
148
185
  const canCreate = Boolean(onCreate && normalizedQuery && !hasExactQueryMatch);
186
+ const groupedOptions = React.useMemo(() => {
187
+ const groups = new Map<string, ComboboxOption[]>();
188
+ const ungrouped: ComboboxOption[] = [];
189
+
190
+ for (const option of options) {
191
+ if (!option.group) {
192
+ ungrouped.push(option);
193
+ continue;
194
+ }
195
+
196
+ groups.set(option.group, [...(groups.get(option.group) ?? []), option]);
197
+ }
198
+
199
+ return {
200
+ ungrouped,
201
+ groups: [...groups.entries()].map(([heading, groupOptions]) => ({
202
+ heading,
203
+ options: groupOptions,
204
+ })),
205
+ };
206
+ }, [options]);
149
207
 
150
208
  React.useEffect(() => {
151
209
  if (!open) {
@@ -153,6 +211,14 @@ export function Combobox({
153
211
  }
154
212
  }, [open]);
155
213
 
214
+ const handleOpenChange = React.useCallback(
215
+ (nextOpen: boolean) => {
216
+ setOpen(nextOpen);
217
+ onOpenChange?.(nextOpen);
218
+ },
219
+ [onOpenChange]
220
+ );
221
+
156
222
  const handleListScroll = React.useCallback(
157
223
  (event: React.UIEvent<HTMLDivElement>) => {
158
224
  if (!hasMore || loadingMore || !onLoadMore) return;
@@ -214,7 +280,7 @@ export function Combobox({
214
280
  disabled={action.disabled}
215
281
  onSelect={() => {
216
282
  action.onSelect();
217
- setOpen(false);
283
+ handleOpenChange(false);
218
284
  setQuery('');
219
285
  }}
220
286
  className="font-medium text-primary [&_svg]:text-primary"
@@ -227,6 +293,96 @@ export function Combobox({
227
293
  );
228
294
  };
229
295
 
296
+ const renderOption = (option: ComboboxOption) => (
297
+ <CommandItem
298
+ key={option.value}
299
+ value={option.searchValue ?? option.label}
300
+ className={cn(option.muted && 'bg-muted/30')}
301
+ onSelect={() => {
302
+ if (onChange) {
303
+ if (mode === 'multiple' && Array.isArray(selected)) {
304
+ onChange(
305
+ selected.includes(option.value)
306
+ ? selected.filter((item) => item !== option.value)
307
+ : [...selected, option.value]
308
+ );
309
+ } else {
310
+ onChange(option.value);
311
+ }
312
+ }
313
+ if (mode === 'single') {
314
+ handleOpenChange(false);
315
+ }
316
+ }}
317
+ >
318
+ <span className="flex min-w-0 flex-1 items-center gap-2">
319
+ {option.icon && (
320
+ <span className="flex shrink-0 items-center justify-center">
321
+ {React.isValidElement(option.icon)
322
+ ? React.cloneElement(
323
+ option.icon as React.ReactElement<{
324
+ style?: React.CSSProperties;
325
+ }>,
326
+ {
327
+ style: option.color
328
+ ? {
329
+ ...((
330
+ option.icon as React.ReactElement<{
331
+ style?: React.CSSProperties;
332
+ }>
333
+ ).props?.style || {}),
334
+ color: option.color,
335
+ }
336
+ : (
337
+ option.icon as React.ReactElement<{
338
+ style?: React.CSSProperties;
339
+ }>
340
+ ).props?.style,
341
+ }
342
+ )
343
+ : option.icon}
344
+ </span>
345
+ )}
346
+ <span className="min-w-0 flex-1">
347
+ <span className="flex min-w-0 items-start justify-between gap-2">
348
+ <span
349
+ className={cn(
350
+ 'min-w-0 flex-1 whitespace-normal break-words',
351
+ option.muted && 'text-muted-foreground'
352
+ )}
353
+ style={option.color ? { color: option.color } : undefined}
354
+ >
355
+ {option.label}
356
+ </span>
357
+ {option.badge}
358
+ </span>
359
+ {option.description ? (
360
+ <span className="mt-0.5 block whitespace-normal break-words text-muted-foreground text-xs leading-snug">
361
+ {option.description}
362
+ </span>
363
+ ) : null}
364
+ </span>
365
+ </span>
366
+ <Check
367
+ className={cn(
368
+ 'ml-auto shrink-0',
369
+ isSelected(option.value) ? 'opacity-100' : 'opacity-0'
370
+ )}
371
+ />
372
+ </CommandItem>
373
+ );
374
+
375
+ const contentWidthClass =
376
+ contentWidth === 'trigger'
377
+ ? 'w-(--radix-popover-trigger-width) min-w-[min(16rem,calc(100vw-2rem))]'
378
+ : contentWidth === 'sm'
379
+ ? 'w-64'
380
+ : contentWidth === 'md'
381
+ ? 'w-80'
382
+ : contentWidth === 'lg'
383
+ ? 'w-96'
384
+ : 'w-auto min-w-[min(16rem,calc(100vw-2rem))]';
385
+
230
386
  const commitCreatedValue = React.useCallback(
231
387
  (result: unknown) => {
232
388
  if (!onChange) return;
@@ -264,7 +420,7 @@ export function Combobox({
264
420
  try {
265
421
  const result = await onCreate(trimmedQuery);
266
422
  commitCreatedValue(result);
267
- setOpen(false);
423
+ handleOpenChange(false);
268
424
  setQuery('');
269
425
  } catch {
270
426
  // Keep the popover open so callers can surface their own error UI.
@@ -272,84 +428,117 @@ export function Combobox({
272
428
  createInFlightRef.current = false;
273
429
  setCreating(false);
274
430
  }
275
- }, [commitCreatedValue, onCreate, trimmedQuery]);
431
+ }, [commitCreatedValue, handleOpenChange, onCreate, trimmedQuery]);
276
432
 
277
- return (
278
- <div className={cn('block min-w-0', className)}>
279
- <Popover open={open} onOpenChange={setOpen} modal={true}>
280
- <PopoverTrigger asChild>
281
- <Button
282
- type="button"
283
- variant="outline"
284
- role="combobox"
285
- aria-expanded={open}
286
- className={cn(
287
- 'w-full min-w-0 justify-between overflow-hidden',
288
- !selectedLabel && 'text-muted-foreground'
289
- )}
290
- disabled={disabled}
291
- >
292
- <span
293
- className={cn(
294
- 'flex min-w-0 items-center gap-2',
295
- !selectedLabel && 'text-muted-foreground'
296
- )}
297
- >
298
- {selectedOption?.icon && (
299
- <span className="flex shrink-0 items-center justify-center">
300
- {React.isValidElement(selectedOption.icon)
301
- ? React.cloneElement(
302
- selectedOption.icon as React.ReactElement<{
303
- style?: React.CSSProperties;
304
- }>,
305
- {
306
- style: selectedOption.color
307
- ? {
308
- ...((
309
- selectedOption.icon as React.ReactElement<{
310
- style?: React.CSSProperties;
311
- }>
312
- ).props?.style || {}),
313
- color: selectedOption.color,
314
- }
315
- : (
316
- selectedOption.icon as React.ReactElement<{
433
+ const triggerOptionIcon = triggerIcon ?? selectedOption?.icon;
434
+
435
+ const triggerButton = (
436
+ <Button
437
+ type="button"
438
+ variant="outline"
439
+ role="combobox"
440
+ aria-label={ariaLabel}
441
+ aria-expanded={open}
442
+ className={cn(
443
+ 'min-w-0 overflow-hidden',
444
+ triggerMode === 'compact'
445
+ ? 'w-auto justify-center gap-1.5'
446
+ : 'w-full justify-between',
447
+ hideTriggerLabel && 'aspect-square px-0',
448
+ !selectedLabel && 'text-muted-foreground'
449
+ )}
450
+ disabled={disabled}
451
+ >
452
+ <span
453
+ className={cn(
454
+ 'flex min-w-0 items-center gap-2',
455
+ !selectedLabel && 'text-muted-foreground'
456
+ )}
457
+ >
458
+ {showSelectedIcon && triggerOptionIcon && (
459
+ <span className="flex shrink-0 items-center justify-center">
460
+ {triggerIcon
461
+ ? triggerIcon
462
+ : React.isValidElement(triggerOptionIcon)
463
+ ? React.cloneElement(
464
+ triggerOptionIcon as React.ReactElement<{
465
+ style?: React.CSSProperties;
466
+ }>,
467
+ {
468
+ style:
469
+ colorizeTriggerIcon && selectedOption?.color
470
+ ? {
471
+ ...((
472
+ triggerOptionIcon as React.ReactElement<{
317
473
  style?: React.CSSProperties;
318
474
  }>
319
- ).props?.style,
320
- }
321
- )
322
- : selectedOption.icon}
475
+ ).props?.style || {}),
476
+ color: selectedOption.color,
477
+ }
478
+ : (
479
+ triggerOptionIcon as React.ReactElement<{
480
+ style?: React.CSSProperties;
481
+ }>
482
+ ).props?.style,
483
+ }
484
+ )
485
+ : triggerOptionIcon}
486
+ </span>
487
+ )}
488
+ {hideTriggerLabel ? (
489
+ <span className="sr-only">{selectedLabel ?? placeholder}</span>
490
+ ) : (
491
+ <span className="min-w-0 flex-1 truncate text-left">
492
+ {label ? (
493
+ label
494
+ ) : (
495
+ <span className="flex min-w-0 items-center gap-2">
496
+ <span
497
+ className={cn(
498
+ 'truncate',
499
+ selectedOption?.muted && 'text-muted-foreground'
500
+ )}
501
+ style={
502
+ colorizeTriggerIcon && selectedOption?.color
503
+ ? { color: selectedOption.color }
504
+ : undefined
505
+ }
506
+ >
507
+ {selectedLabel ?? placeholder}
323
508
  </span>
324
- )}
325
- <span className="min-w-0 flex-1 truncate text-left">
326
- {label ? (
327
- label
328
- ) : (
329
- <span className="flex min-w-0 items-center gap-2">
330
- <span
331
- className={cn(
332
- 'truncate',
333
- selectedOption?.muted && 'text-muted-foreground'
334
- )}
335
- style={
336
- selectedOption?.color
337
- ? { color: selectedOption.color }
338
- : undefined
339
- }
340
- >
341
- {selectedLabel ?? placeholder}
342
- </span>
343
- {selectedOption?.badge}
344
- </span>
345
- )}
509
+ {selectedOption?.badge}
346
510
  </span>
347
- </span>
348
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
349
- </Button>
350
- </PopoverTrigger>
511
+ )}
512
+ </span>
513
+ )}
514
+ </span>
515
+ {showChevron && (
516
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
517
+ )}
518
+ </Button>
519
+ );
520
+
521
+ const trigger = triggerTooltip ? (
522
+ <Tooltip delayDuration={0}>
523
+ <TooltipTrigger asChild>
524
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
525
+ </TooltipTrigger>
526
+ <TooltipContent>{triggerTooltip}</TooltipContent>
527
+ </Tooltip>
528
+ ) : (
529
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
530
+ );
531
+
532
+ return (
533
+ <div className={cn('block min-w-0', className)}>
534
+ <Popover open={open} onOpenChange={handleOpenChange} modal={true}>
535
+ {trigger}
351
536
  <PopoverContent
352
- className="z-9999 w-(--radix-popover-trigger-width) max-w-[calc(100vw-2rem)] p-0"
537
+ className={cn(
538
+ 'z-9999 max-w-[calc(100vw-2rem)] p-0',
539
+ contentWidthClass,
540
+ contentClassName
541
+ )}
353
542
  align="start"
354
543
  sideOffset={4}
355
544
  >
@@ -411,88 +600,16 @@ export function Combobox({
411
600
  <CommandSeparator />
412
601
  </>
413
602
  ) : null}
414
- <CommandGroup>
415
- {options.map((option) => (
416
- <CommandItem
417
- key={option.value}
418
- value={option.searchValue ?? option.label}
419
- className={cn(option.muted && 'bg-muted/30')}
420
- onSelect={() => {
421
- if (onChange) {
422
- if (mode === 'multiple' && Array.isArray(selected)) {
423
- onChange(
424
- selected.includes(option.value)
425
- ? selected.filter((item) => item !== option.value)
426
- : [...selected, option.value]
427
- );
428
- } else {
429
- onChange(option.value);
430
- }
431
- }
432
- if (mode === 'single') {
433
- setOpen(false);
434
- }
435
- }}
436
- >
437
- <span className="flex min-w-0 flex-1 items-center gap-2">
438
- {option.icon && (
439
- <span className="flex shrink-0 items-center justify-center">
440
- {React.isValidElement(option.icon)
441
- ? React.cloneElement(
442
- option.icon as React.ReactElement<{
443
- style?: React.CSSProperties;
444
- }>,
445
- {
446
- style: option.color
447
- ? {
448
- ...((
449
- option.icon as React.ReactElement<{
450
- style?: React.CSSProperties;
451
- }>
452
- ).props?.style || {}),
453
- color: option.color,
454
- }
455
- : (
456
- option.icon as React.ReactElement<{
457
- style?: React.CSSProperties;
458
- }>
459
- ).props?.style,
460
- }
461
- )
462
- : option.icon}
463
- </span>
464
- )}
465
- <span className="min-w-0 flex-1">
466
- <span className="flex min-w-0 items-start justify-between gap-2">
467
- <span
468
- className={cn(
469
- 'truncate',
470
- option.muted && 'text-muted-foreground'
471
- )}
472
- style={
473
- option.color ? { color: option.color } : undefined
474
- }
475
- >
476
- {option.label}
477
- </span>
478
- {option.badge}
479
- </span>
480
- {option.description ? (
481
- <span className="mt-0.5 block truncate text-muted-foreground text-xs">
482
- {option.description}
483
- </span>
484
- ) : null}
485
- </span>
486
- </span>
487
- <Check
488
- className={cn(
489
- 'ml-auto shrink-0',
490
- isSelected(option.value) ? 'opacity-100' : 'opacity-0'
491
- )}
492
- />
493
- </CommandItem>
494
- ))}
495
- </CommandGroup>
603
+ {groupedOptions.ungrouped.length ? (
604
+ <CommandGroup>
605
+ {groupedOptions.ungrouped.map(renderOption)}
606
+ </CommandGroup>
607
+ ) : null}
608
+ {groupedOptions.groups.map((group) => (
609
+ <CommandGroup heading={group.heading} key={group.heading}>
610
+ {group.options.map(renderOption)}
611
+ </CommandGroup>
612
+ ))}
496
613
  {actionsPosition === 'bottom' && actions?.length ? (
497
614
  <>
498
615
  <CommandSeparator />
@@ -3,43 +3,36 @@
3
3
  import { Trash } from '@tuturuuu/icons';
4
4
  import { updateWorkspaceCourseModule } from '@tuturuuu/internal-api/education';
5
5
  import { Button } from '@tuturuuu/ui/button';
6
+ import { toast } from '@tuturuuu/ui/sonner';
6
7
  import { useRouter } from 'next/navigation';
7
8
  import { useState } from 'react';
8
9
 
9
10
  export default function DeleteLinkButton({
10
11
  wsId,
11
12
  moduleId,
12
- courseId,
13
13
  link,
14
14
  links,
15
15
  }: {
16
16
  wsId: string;
17
17
  moduleId: string;
18
- courseId: string;
18
+ courseId?: string;
19
19
  link: string;
20
20
  links: string[];
21
21
  }) {
22
22
  const router = useRouter();
23
23
  const [loading, setLoading] = useState(false);
24
24
 
25
- const updateYoutubeLinks = async (
26
- moduleId: string,
27
- courseId: string,
28
- links: string[]
29
- ) => {
25
+ const updateYoutubeLinks = async (moduleId: string, links: string[]) => {
30
26
  setLoading(true);
31
27
  try {
32
28
  await updateWorkspaceCourseModule(wsId, moduleId, {
33
- course_id: courseId,
34
29
  youtube_links: links,
35
30
  });
36
31
  router.refresh();
37
- setLoading(false);
38
- return null;
39
32
  } catch (error) {
40
- console.error('error', error);
33
+ toast.error(error instanceof Error ? error.message : String(error));
34
+ } finally {
41
35
  setLoading(false);
42
- return null;
43
36
  }
44
37
  };
45
38
 
@@ -48,7 +41,6 @@ export default function DeleteLinkButton({
48
41
  onClick={async () => {
49
42
  await updateYoutubeLinks(
50
43
  moduleId,
51
- courseId,
52
44
  links.filter((l) => l !== link)
53
45
  );
54
46
  }}
@@ -1,5 +1,11 @@
1
1
  'use client';
2
- import { Download, Monitor, Smartphone, Tablet, X } from '@tuturuuu/icons';
2
+ import {
3
+ Download,
4
+ Monitor,
5
+ Smartphone,
6
+ Tablet,
7
+ X,
8
+ } from '@tuturuuu/icons/lucide';
3
9
  import { Button } from '@tuturuuu/ui/button';
4
10
  import { cn } from '@tuturuuu/utils/format';
5
11
  import { useLocale, useTranslations } from 'next-intl';
@@ -11,7 +11,7 @@ import {
11
11
  Smartphone,
12
12
  Sun,
13
13
  Tablet,
14
- } from '@tuturuuu/icons';
14
+ } from '@tuturuuu/icons/lucide';
15
15
  import { Button } from '@tuturuuu/ui/button';
16
16
  import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
17
17
  import { Checkbox } from '@tuturuuu/ui/checkbox';
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Image as ImageIcon, Upload, X } from '@tuturuuu/icons';
3
+ import { Image as ImageIcon, Upload, X } from '@tuturuuu/icons/lucide';
4
4
  import { Button } from '@tuturuuu/ui/button';
5
5
  import { Label } from '@tuturuuu/ui/label';
6
6
  import { cn } from '@tuturuuu/utils/format';
@@ -15,7 +15,7 @@ import {
15
15
  ThumbsUp,
16
16
  Tv,
17
17
  X,
18
- } from '@tuturuuu/icons';
18
+ } from '@tuturuuu/icons/lucide';
19
19
  import { cn } from '@tuturuuu/utils/format';
20
20
  import {
21
21
  forwardRef,
@@ -273,7 +273,8 @@ export function SettingsDialogShell({
273
273
 
274
274
  return (
275
275
  <DialogContent
276
- className="top-0 left-0 flex h-dvh max-h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-col gap-0 overflow-hidden rounded-none border-0 p-0 shadow-none sm:max-w-none"
276
+ presentation="fullscreen"
277
+ className="flex-col"
277
278
  onKeyDown={handleKeyboardNavigation}
278
279
  showCloseButton={false}
279
280
  >
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Moon, Sun } from '@tuturuuu/icons';
3
+ import { Moon, Sun } from '@tuturuuu/icons/lucide-static';
4
4
  import { Button } from '@tuturuuu/ui/button';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import { useTheme } from 'next-themes';
@@ -23,9 +23,9 @@ import {
23
23
  toWorkspaceSlug,
24
24
  } from '@tuturuuu/utils/constants';
25
25
  import { cn } from '@tuturuuu/utils/format';
26
- import { getInitials } from '@tuturuuu/utils/name-helper';
27
26
  import { workspaceHandleSchema } from '@tuturuuu/utils/workspace-handle';
28
27
  import { WORKSPACE_LIMIT_ERROR_CODE } from '@tuturuuu/utils/workspace-limits';
28
+ import Image from 'next/image';
29
29
  import { usePathname, useRouter } from 'next/navigation';
30
30
  import { useTranslations } from 'next-intl';
31
31
  import type { ReactNode } from 'react';
@@ -98,6 +98,7 @@ function WorkspaceIcon({
98
98
  avatarUrl,
99
99
  fallbackLogoUrl
100
100
  );
101
+ const shouldSkipFallbackOptimization = /^https?:\/\//u.test(fallbackLogoUrl);
101
102
 
102
103
  return (
103
104
  <Avatar
@@ -124,11 +125,15 @@ function WorkspaceIcon({
124
125
  resolvedAvatarUrl ? 'rounded-xs' : 'rounded-sm'
125
126
  )}
126
127
  >
127
- <AvatarImage
128
+ <Image
129
+ alt=""
130
+ aria-hidden="true"
128
131
  className="h-full w-full object-cover"
132
+ height={20}
129
133
  src={fallbackLogoUrl}
134
+ unoptimized={shouldSkipFallbackOptimization}
135
+ width={20}
130
136
  />
131
- {name ? getInitials(name) : '?'}
132
137
  </AvatarFallback>
133
138
  </Avatar>
134
139
  );