@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.
- package/CHANGELOG.md +40 -0
- package/biome.json +1 -1
- package/package.json +73 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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=
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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';
|
|
@@ -273,7 +273,8 @@ export function SettingsDialogShell({
|
|
|
273
273
|
|
|
274
274
|
return (
|
|
275
275
|
<DialogContent
|
|
276
|
-
|
|
276
|
+
presentation="fullscreen"
|
|
277
|
+
className="flex-col"
|
|
277
278
|
onKeyDown={handleKeyboardNavigation}
|
|
278
279
|
showCloseButton={false}
|
|
279
280
|
>
|
|
@@ -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
|
-
<
|
|
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
|
);
|