@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
@@ -0,0 +1,284 @@
1
+ import type { Editor } from '@tiptap/react';
2
+ import { Highlighter, Paintbrush, TextIcon, X } from '@tuturuuu/icons';
3
+ import { Button } from '@tuturuuu/ui/button';
4
+ import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
6
+ import { cn } from '@tuturuuu/utils/format';
7
+
8
+ interface EditorColorSwatch {
9
+ name: string;
10
+ foreground: string;
11
+ background: string;
12
+ }
13
+
14
+ const EDITOR_COLOR_SWATCHES: EditorColorSwatch[] = [
15
+ {
16
+ name: 'Yellow',
17
+ foreground: 'var(--yellow)',
18
+ background: 'var(--calendar-bg-yellow)',
19
+ },
20
+ {
21
+ name: 'Orange',
22
+ foreground: 'var(--orange)',
23
+ background: 'var(--calendar-bg-orange)',
24
+ },
25
+ {
26
+ name: 'Red',
27
+ foreground: 'var(--red)',
28
+ background: 'var(--calendar-bg-red)',
29
+ },
30
+ {
31
+ name: 'Pink',
32
+ foreground: 'var(--pink)',
33
+ background: 'var(--calendar-bg-pink)',
34
+ },
35
+ {
36
+ name: 'Purple',
37
+ foreground: 'var(--purple)',
38
+ background: 'var(--calendar-bg-purple)',
39
+ },
40
+ {
41
+ name: 'Blue',
42
+ foreground: 'var(--blue)',
43
+ background: 'var(--calendar-bg-blue)',
44
+ },
45
+ {
46
+ name: 'Green',
47
+ foreground: 'var(--green)',
48
+ background: 'var(--calendar-bg-green)',
49
+ },
50
+ {
51
+ name: 'Gray',
52
+ foreground: 'var(--gray)',
53
+ background: 'var(--calendar-bg-gray)',
54
+ },
55
+ ];
56
+
57
+ type HighlightAttributes = {
58
+ color: string;
59
+ textColor?: string;
60
+ };
61
+
62
+ type TextStyleAttributes = {
63
+ color?: string | null;
64
+ backgroundColor?: string | null;
65
+ };
66
+
67
+ interface TextEditorColorControlsProps {
68
+ editor: Editor | null;
69
+ }
70
+
71
+ export function TextEditorColorControls({
72
+ editor,
73
+ }: TextEditorColorControlsProps) {
74
+ if (!editor) return null;
75
+
76
+ const getAttributes = editor.getAttributes?.bind(editor);
77
+ const highlightAttributes = (getAttributes?.('highlight') ?? {}) as {
78
+ color?: string | null;
79
+ textColor?: string | null;
80
+ };
81
+ const textStyleAttributes = (getAttributes?.('textStyle') ??
82
+ {}) as TextStyleAttributes;
83
+
84
+ const textColor = textStyleAttributes.color ?? null;
85
+ const backgroundColor = textStyleAttributes.backgroundColor ?? null;
86
+ const highlightColor = highlightAttributes.color ?? null;
87
+ const highlightTextColor = highlightAttributes.textColor ?? null;
88
+
89
+ return (
90
+ <>
91
+ <ColorControlPopover
92
+ active={editor.isActive('highlight')}
93
+ clearLabel="Clear highlight"
94
+ indicatorBackground={highlightColor}
95
+ indicatorColor={highlightTextColor}
96
+ label="Highlight color"
97
+ icon={<Highlighter className="size-4" />}
98
+ onClear={() => editor.chain().focus().unsetHighlight().run()}
99
+ swatchKind="highlight"
100
+ onSelect={(swatch) => {
101
+ const attributes: HighlightAttributes = {
102
+ color: swatch.background,
103
+ textColor: swatch.foreground,
104
+ };
105
+
106
+ editor.chain().focus().setHighlight(attributes).run();
107
+ }}
108
+ isSwatchActive={(swatch) =>
109
+ editor.isActive('highlight', {
110
+ color: swatch.background,
111
+ textColor: swatch.foreground,
112
+ })
113
+ }
114
+ />
115
+ <ColorControlPopover
116
+ active={Boolean(textColor)}
117
+ clearLabel="Clear text color"
118
+ indicatorColor={textColor}
119
+ label="Text color"
120
+ icon={<TextIcon className="size-4" />}
121
+ onClear={() => editor.chain().focus().unsetColor().run()}
122
+ swatchKind="text"
123
+ onSelect={(swatch) =>
124
+ editor.chain().focus().setColor(swatch.foreground).run()
125
+ }
126
+ isSwatchActive={(swatch) =>
127
+ editor.isActive('textStyle', { color: swatch.foreground })
128
+ }
129
+ />
130
+ <ColorControlPopover
131
+ active={Boolean(backgroundColor)}
132
+ clearLabel="Clear background color"
133
+ indicatorBackground={backgroundColor}
134
+ label="Background color"
135
+ icon={<Paintbrush className="size-4" />}
136
+ onClear={() => editor.chain().focus().unsetBackgroundColor().run()}
137
+ swatchKind="background"
138
+ onSelect={(swatch) =>
139
+ editor.chain().focus().setBackgroundColor(swatch.background).run()
140
+ }
141
+ isSwatchActive={(swatch) =>
142
+ editor.isActive('textStyle', {
143
+ backgroundColor: swatch.background,
144
+ })
145
+ }
146
+ />
147
+ </>
148
+ );
149
+ }
150
+
151
+ interface ColorControlPopoverProps {
152
+ active: boolean;
153
+ clearLabel: string;
154
+ icon: React.ReactNode;
155
+ indicatorBackground?: string | null;
156
+ indicatorColor?: string | null;
157
+ isSwatchActive: (swatch: EditorColorSwatch) => boolean;
158
+ label: string;
159
+ onClear: () => void;
160
+ onSelect: (swatch: EditorColorSwatch) => void;
161
+ swatchKind: 'highlight' | 'text' | 'background';
162
+ }
163
+
164
+ function ColorControlPopover({
165
+ active,
166
+ clearLabel,
167
+ icon,
168
+ indicatorBackground,
169
+ indicatorColor,
170
+ isSwatchActive,
171
+ label,
172
+ onClear,
173
+ onSelect,
174
+ swatchKind,
175
+ }: ColorControlPopoverProps) {
176
+ return (
177
+ <Popover>
178
+ <Tooltip>
179
+ <TooltipTrigger asChild>
180
+ <PopoverTrigger asChild>
181
+ <Button
182
+ aria-label={label}
183
+ aria-pressed={active}
184
+ className={cn(
185
+ 'relative h-8 w-8 rounded-md border border-transparent transition-colors hover:bg-dynamic-surface/80',
186
+ active &&
187
+ 'border-foreground/10 bg-dynamic-surface/80 text-foreground'
188
+ )}
189
+ onMouseDown={(event) => event.preventDefault()}
190
+ size="icon"
191
+ type="button"
192
+ variant="ghost"
193
+ >
194
+ {icon}
195
+ <span
196
+ className="pointer-events-none absolute right-1 bottom-1 h-1.5 w-4 rounded-full border border-background"
197
+ style={{
198
+ backgroundColor:
199
+ indicatorBackground ?? indicatorColor ?? 'transparent',
200
+ }}
201
+ />
202
+ </Button>
203
+ </PopoverTrigger>
204
+ </TooltipTrigger>
205
+ <TooltipContent side="bottom">{label}</TooltipContent>
206
+ </Tooltip>
207
+ <PopoverContent align="start" className="w-56 p-2">
208
+ <div className="space-y-2">
209
+ <div className="font-medium text-muted-foreground text-xs">
210
+ {label}
211
+ </div>
212
+ <div className="grid grid-cols-4 gap-1.5">
213
+ {EDITOR_COLOR_SWATCHES.map((swatch) => (
214
+ <SwatchButton
215
+ active={isSwatchActive(swatch)}
216
+ key={`${swatchKind}-${swatch.name}`}
217
+ swatch={swatch}
218
+ swatchKind={swatchKind}
219
+ onSelect={() => onSelect(swatch)}
220
+ />
221
+ ))}
222
+ </div>
223
+ <Button
224
+ className="h-8 w-full justify-start px-2 text-xs"
225
+ onClick={onClear}
226
+ onMouseDown={(event) => event.preventDefault()}
227
+ type="button"
228
+ variant="ghost"
229
+ >
230
+ <X className="mr-1.5 size-3.5" />
231
+ {clearLabel}
232
+ </Button>
233
+ </div>
234
+ </PopoverContent>
235
+ </Popover>
236
+ );
237
+ }
238
+
239
+ interface SwatchButtonProps {
240
+ active: boolean;
241
+ onSelect: () => void;
242
+ swatch: EditorColorSwatch;
243
+ swatchKind: 'highlight' | 'text' | 'background';
244
+ }
245
+
246
+ function SwatchButton({
247
+ active,
248
+ onSelect,
249
+ swatch,
250
+ swatchKind,
251
+ }: SwatchButtonProps) {
252
+ const backgroundColor =
253
+ swatchKind === 'text' ? 'var(--background)' : swatch.background;
254
+ const color =
255
+ swatchKind === 'background' ? 'var(--foreground)' : swatch.foreground;
256
+
257
+ return (
258
+ <Button
259
+ aria-label={`${swatch.name} ${swatchKind}`}
260
+ className={cn(
261
+ 'h-8 w-8 rounded-md border border-dynamic-border p-0 transition-all hover:scale-105',
262
+ active &&
263
+ 'ring-2 ring-foreground/30 ring-offset-1 ring-offset-background'
264
+ )}
265
+ onClick={onSelect}
266
+ onMouseDown={(event) => event.preventDefault()}
267
+ size="icon"
268
+ style={{ backgroundColor, color }}
269
+ type="button"
270
+ variant="ghost"
271
+ >
272
+ {swatchKind === 'text' ? (
273
+ <TextIcon className="size-4" />
274
+ ) : (
275
+ <span
276
+ className="size-4 rounded-sm border border-foreground/10"
277
+ style={{
278
+ backgroundColor: swatch.background,
279
+ }}
280
+ />
281
+ )}
282
+ </Button>
283
+ );
284
+ }
@@ -42,6 +42,26 @@ const hasContent = (node: JSONContent): boolean => {
42
42
  return false;
43
43
  };
44
44
 
45
+ function syncEditorContent(editor: Editor, nextContent: JSONContent | null) {
46
+ const migratedContent = migrateInlineImagesToBlock(nextContent);
47
+ const currentContent = editor.getJSON();
48
+ const contentChanged =
49
+ JSON.stringify(currentContent) !== JSON.stringify(migratedContent);
50
+
51
+ if (contentChanged) {
52
+ editor.commands.setContent(
53
+ migratedContent || { type: 'doc', content: [] },
54
+ {
55
+ emitUpdate: false,
56
+ }
57
+ );
58
+ }
59
+ }
60
+
61
+ function serializeEditorContent(content: JSONContent | null) {
62
+ return JSON.stringify(content ?? { type: 'doc', content: [] });
63
+ }
64
+
45
65
  interface RichTextEditorProps {
46
66
  content: JSONContent | null;
47
67
  onChange?: (content: JSONContent | null) => void;
@@ -127,6 +147,9 @@ export function RichTextEditor({
127
147
  const debouncedOnChangeRef = useRef<ReturnType<typeof debounce> | null>(null);
128
148
  // Track when we're in a programmatic update to skip content sync
129
149
  const isProgrammaticUpdateRef = useRef(false);
150
+ const hasDeferredExternalContentRef = useRef(false);
151
+ const deferredExternalContentRef = useRef<JSONContent | null>(null);
152
+ const deferredEditorSnapshotRef = useRef<string | null>(null);
130
153
  const getDelegatedImageUpload = useCallback(
131
154
  () => onImageUploadRef.current,
132
155
  []
@@ -566,26 +589,58 @@ export function RichTextEditor({
566
589
  // This prevents the effect from reverting editor content before state update propagates
567
590
  if (isProgrammaticUpdateRef.current) {
568
591
  isProgrammaticUpdateRef.current = false;
592
+ hasDeferredExternalContentRef.current = false;
593
+ deferredExternalContentRef.current = null;
594
+ deferredEditorSnapshotRef.current = null;
569
595
  return;
570
596
  }
571
597
 
572
- // Migrate inline images to block-level for backward compatibility
573
- const migratedContent = migrateInlineImagesToBlock(content);
574
- const currentContent = editor.getJSON();
575
- const contentChanged =
576
- JSON.stringify(currentContent) !== JSON.stringify(migratedContent);
577
-
578
- if (contentChanged) {
579
- // Update editor content without triggering onChange
580
- editor.commands.setContent(
581
- migratedContent || { type: 'doc', content: [] },
582
- {
583
- emitUpdate: false,
584
- }
585
- );
598
+ if (editor.isFocused) {
599
+ if (!hasDeferredExternalContentRef.current) {
600
+ deferredEditorSnapshotRef.current = serializeEditorContent(
601
+ editor.getJSON()
602
+ );
603
+ }
604
+ hasDeferredExternalContentRef.current = true;
605
+ deferredExternalContentRef.current = content;
606
+ return;
586
607
  }
608
+
609
+ hasDeferredExternalContentRef.current = false;
610
+ deferredExternalContentRef.current = null;
611
+ deferredEditorSnapshotRef.current = null;
612
+ syncEditorContent(editor, content);
587
613
  }, [editor, content, allowCollaboration]);
588
614
 
615
+ useEffect(() => {
616
+ if (!editor || allowCollaboration) return;
617
+
618
+ const syncDeferredContent = () => {
619
+ if (!hasDeferredExternalContentRef.current) return;
620
+
621
+ hasDeferredExternalContentRef.current = false;
622
+ const deferredContent = deferredExternalContentRef.current;
623
+ const deferredEditorSnapshot = deferredEditorSnapshotRef.current;
624
+ deferredExternalContentRef.current = null;
625
+ deferredEditorSnapshotRef.current = null;
626
+
627
+ if (
628
+ deferredEditorSnapshot !== null &&
629
+ serializeEditorContent(editor.getJSON()) !== deferredEditorSnapshot
630
+ ) {
631
+ return;
632
+ }
633
+
634
+ syncEditorContent(editor, deferredContent);
635
+ };
636
+
637
+ editor.on('blur', syncDeferredContent);
638
+
639
+ return () => {
640
+ editor.off('blur', syncDeferredContent);
641
+ };
642
+ }, [editor, allowCollaboration]);
643
+
589
644
  // Handle initial cursor positioning when focusing from title
590
645
  useEffect(() => {
591
646
  if (
@@ -1,7 +1,7 @@
1
1
  import { InputRule } from '@tiptap/core';
2
2
  import Collaboration from '@tiptap/extension-collaboration';
3
3
  import CollaborationCaret from '@tiptap/extension-collaboration-caret';
4
- import Highlight from '@tiptap/extension-highlight';
4
+ import { Color } from '@tiptap/extension-color';
5
5
  import HorizontalRule from '@tiptap/extension-horizontal-rule';
6
6
  import Link from '@tiptap/extension-link';
7
7
  import { TaskList } from '@tiptap/extension-list';
@@ -14,11 +14,14 @@ import TableCell from '@tiptap/extension-table-cell';
14
14
  import TableHeader from '@tiptap/extension-table-header';
15
15
  import TableRow from '@tiptap/extension-table-row';
16
16
  import TextAlign from '@tiptap/extension-text-align';
17
+ import { TextStyle } from '@tiptap/extension-text-style';
17
18
  import Youtube from '@tiptap/extension-youtube';
18
19
  import type { Extensions } from '@tiptap/react';
19
20
  import StarterKit from '@tiptap/starter-kit';
20
21
  import type SupabaseProvider from '@tuturuuu/ui/hooks/supabase-provider';
21
22
  import type * as Y from 'yjs';
23
+ import { BackgroundColor } from './background-color-extension';
24
+ import { ThemeAwareHighlight } from './highlight-extension';
22
25
  import { CustomImage } from './image-extension';
23
26
  import { ListConverter } from './list-converter-extension';
24
27
  import { ListItemBase } from './list-item-extension';
@@ -148,7 +151,10 @@ export function getEditorExtensions({
148
151
  }),
149
152
  TextShortcuts,
150
153
  MarkdownPaste,
151
- Highlight,
154
+ TextStyle,
155
+ Color,
156
+ BackgroundColor,
157
+ ThemeAwareHighlight,
152
158
  Link.configure({
153
159
  openOnClick: true,
154
160
  autolink: true,
@@ -0,0 +1,22 @@
1
+ import Highlight from '@tiptap/extension-highlight';
2
+
3
+ export const ThemeAwareHighlight = Highlight.extend({
4
+ addAttributes() {
5
+ return {
6
+ ...(this.parent?.() ?? {}),
7
+ textColor: {
8
+ default: null,
9
+ parseHTML: (element) => element.style.getPropertyValue('color') || null,
10
+ renderHTML: (attributes) => {
11
+ if (!attributes.textColor) return {};
12
+
13
+ return {
14
+ style: `color: ${attributes.textColor}`,
15
+ };
16
+ },
17
+ },
18
+ };
19
+ },
20
+ }).configure({
21
+ multicolor: true,
22
+ });
@@ -13,7 +13,6 @@ import {
13
13
  Heading1,
14
14
  Heading2,
15
15
  Heading3,
16
- Highlighter,
17
16
  ImageIcon,
18
17
  Italic,
19
18
  Link,
@@ -37,6 +36,7 @@ import { Toggle } from '@tuturuuu/ui/toggle';
37
36
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
38
37
  import { cn } from '@tuturuuu/utils/format';
39
38
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
39
+ import { TextEditorColorControls } from './color-controls';
40
40
  import {
41
41
  MAX_IMAGE_SIZE,
42
42
  MAX_VIDEO_SIZE,
@@ -65,7 +65,6 @@ const HOTKEYS = {
65
65
  'ordered-list': 'Mod+Shift+7',
66
66
  'task-list': 'Mod+Shift+9',
67
67
  table: '',
68
- highlight: 'Mod+Shift+H',
69
68
  link: 'Mod+K',
70
69
  image: '',
71
70
  video: '',
@@ -90,7 +89,6 @@ const LABELS: Record<string, string> = {
90
89
  'ordered-list': 'Ordered List',
91
90
  'task-list': 'Task List',
92
91
  table: 'Insert Table',
93
- highlight: 'Highlight',
94
92
  link: 'Link',
95
93
  image: 'Upload Image',
96
94
  video: 'Upload Video',
@@ -104,7 +102,7 @@ const TOOLBAR_GROUPS = [
104
102
  ['bold', 'italic', 'strike', 'subscript', 'superscript'],
105
103
  ['align-left', 'align-center', 'align-right'],
106
104
  ['bullet-list', 'ordered-list', 'task-list'],
107
- ['table', 'highlight', 'link'],
105
+ ['table', 'link'],
108
106
  ] as const;
109
107
 
110
108
  /** Format a hotkey combo string for display (platform-aware). */
@@ -371,12 +369,6 @@ export function ToolBar({
371
369
  .run(),
372
370
  pressed: editor?.isActive('table'),
373
371
  },
374
- {
375
- key: 'highlight',
376
- icon: <Highlighter className="size-4" />,
377
- onClick: () => editor?.chain().focus().toggleHighlight().run(),
378
- pressed: editor?.isActive('highlight'),
379
- },
380
372
  ] as const,
381
373
  [editor]
382
374
  );
@@ -601,6 +593,10 @@ export function ToolBar({
601
593
  />
602
594
  ))}
603
595
 
596
+ {/* Color controls */}
597
+ <ToolbarSeparator />
598
+ <TextEditorColorControls editor={editor} />
599
+
604
600
  {/* Link */}
605
601
  <ToolbarSeparator />
606
602
  <ToolbarButton
@@ -1098,12 +1094,6 @@ export function FixedToolbar({
1098
1094
  .run(),
1099
1095
  pressed: editor.isActive('table'),
1100
1096
  },
1101
- {
1102
- key: 'highlight',
1103
- icon: <Highlighter className="size-4" />,
1104
- onClick: () => editor.chain().focus().toggleHighlight().run(),
1105
- pressed: editor.isActive('highlight'),
1106
- },
1107
1097
  {
1108
1098
  key: 'link',
1109
1099
  icon: <Link className="size-4" />,
@@ -1244,6 +1234,9 @@ export function FixedToolbar({
1244
1234
  </div>
1245
1235
  ))}
1246
1236
 
1237
+ <ToolbarSeparator />
1238
+ <TextEditorColorControls editor={editor} />
1239
+
1247
1240
  {/* Media group */}
1248
1241
  {workspaceId && onImageUpload && (
1249
1242
  <>
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as ToastPrimitives from '@radix-ui/react-toast';
4
- import { X } from '@tuturuuu/icons';
4
+ import { X } from '@tuturuuu/icons/lucide-static';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import { cva, type VariantProps } from 'class-variance-authority';
7
7
  import * as React from 'react';