@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
@@ -7,8 +7,42 @@ import { describe, expect, it, vi } from 'vitest';
7
7
  import { SmartCalendar } from './smart-calendar';
8
8
 
9
9
  vi.mock('@tuturuuu/ui/hooks/use-calendar', () => ({
10
- CalendarProvider: ({ children }: { children: ReactNode }) => (
11
- <div data-testid="calendar-provider">{children}</div>
10
+ CalendarProvider: ({
11
+ children,
12
+ eventAdapter,
13
+ }: {
14
+ children: ReactNode;
15
+ eventAdapter?: {
16
+ disableBuiltInEventUi?: boolean;
17
+ preservePastEventOpacity?: boolean;
18
+ };
19
+ }) => (
20
+ <div
21
+ data-adapter={eventAdapter ? 'true' : 'false'}
22
+ data-preserve-past-opacity={
23
+ eventAdapter?.preservePastEventOpacity ? 'true' : 'false'
24
+ }
25
+ data-testid="calendar-provider"
26
+ >
27
+ {children}
28
+ </div>
29
+ ),
30
+ }));
31
+
32
+ vi.mock('@tuturuuu/ui/hooks/use-calendar-sync', () => ({
33
+ CalendarSyncProvider: ({
34
+ children,
35
+ externalEvents,
36
+ }: {
37
+ children: ReactNode;
38
+ externalEvents?: unknown[];
39
+ }) => (
40
+ <div
41
+ data-external-events={externalEvents?.length ?? 0}
42
+ data-testid="calendar-sync-provider"
43
+ >
44
+ {children}
45
+ </div>
12
46
  ),
13
47
  }));
14
48
 
@@ -19,8 +53,17 @@ vi.mock('./settings/settings-context', () => ({
19
53
  }));
20
54
 
21
55
  vi.mock('./calendar-content', () => ({
22
- CalendarContent: ({ extras }: { extras?: ReactNode }) => (
23
- <div data-testid="calendar-content">
56
+ CalendarContent: ({
57
+ disableBuiltInEventUi,
58
+ extras,
59
+ }: {
60
+ disableBuiltInEventUi?: boolean;
61
+ extras?: ReactNode;
62
+ }) => (
63
+ <div
64
+ data-disable-built-in-ui={disableBuiltInEventUi ? 'true' : 'false'}
65
+ data-testid="calendar-content"
66
+ >
24
67
  <div data-testid="header-extras">{extras}</div>
25
68
  </div>
26
69
  ),
@@ -73,4 +116,43 @@ describe('SmartCalendar', () => {
73
116
  expect(screen.queryByTestId('connections-manager')).toBeNull();
74
117
  expect(screen.getByTestId('custom-extra').textContent).toBe('extra');
75
118
  });
119
+
120
+ it('passes custom event adapters and external events through the calendar shell', () => {
121
+ render(
122
+ <SmartCalendar
123
+ {...baseProps}
124
+ workspace={{ id: 'workspace-1' } as Workspace}
125
+ showConnectionsManager={false}
126
+ eventAdapter={{
127
+ disableBuiltInEventUi: true,
128
+ preservePastEventOpacity: true,
129
+ }}
130
+ externalEvents={[
131
+ {
132
+ id: 'session-1',
133
+ title: 'Class',
134
+ start_at: '2026-06-19T12:00:00.000Z',
135
+ end_at: '2026-06-19T13:00:00.000Z',
136
+ },
137
+ ]}
138
+ />
139
+ );
140
+
141
+ expect(screen.getByTestId('calendar-sync-provider')).toHaveAttribute(
142
+ 'data-external-events',
143
+ '1'
144
+ );
145
+ expect(screen.getByTestId('calendar-provider')).toHaveAttribute(
146
+ 'data-adapter',
147
+ 'true'
148
+ );
149
+ expect(screen.getByTestId('calendar-provider')).toHaveAttribute(
150
+ 'data-preserve-past-opacity',
151
+ 'true'
152
+ );
153
+ expect(screen.getByTestId('calendar-content')).toHaveAttribute(
154
+ 'data-disable-built-in-ui',
155
+ 'true'
156
+ );
157
+ });
76
158
  });
@@ -4,7 +4,12 @@ import type {
4
4
  Workspace,
5
5
  WorkspaceCalendarGoogleTokenClient,
6
6
  } from '@tuturuuu/types';
7
- import { CalendarProvider } from '@tuturuuu/ui/hooks/use-calendar';
7
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
8
+ import {
9
+ type CalendarEventAdapter,
10
+ CalendarProvider,
11
+ } from '@tuturuuu/ui/hooks/use-calendar';
12
+ import { CalendarSyncProvider } from '@tuturuuu/ui/hooks/use-calendar-sync';
8
13
  import type { CalendarView } from '../../../../hooks/use-view-transition';
9
14
  import CalendarConnectionsUnified from '../../calendar-app/components/calendar-connections-unified';
10
15
  import { CalendarContent } from './calendar-content';
@@ -28,6 +33,10 @@ export const SmartCalendar = ({
28
33
  initialSettings,
29
34
  onSaveSettings,
30
35
  showConnectionsManager = true,
36
+ eventAdapter,
37
+ externalEvents,
38
+ externalEventsLoading,
39
+ externalEventsRefresh,
31
40
  }: {
32
41
  t: any;
33
42
  locale: string;
@@ -49,6 +58,10 @@ export const SmartCalendar = ({
49
58
  initialSettings?: Partial<CalendarSettings>;
50
59
  onSaveSettings?: (settings: CalendarSettings) => Promise<void>;
51
60
  showConnectionsManager?: boolean;
61
+ eventAdapter?: CalendarEventAdapter;
62
+ externalEvents?: CalendarEvent[];
63
+ externalEventsLoading?: boolean;
64
+ externalEventsRefresh?: () => void;
52
65
  }) => {
53
66
  const handleSaveSettings = async (newSettings: CalendarSettings) => {
54
67
  if (onSaveSettings) {
@@ -65,12 +78,13 @@ export const SmartCalendar = ({
65
78
  extras
66
79
  );
67
80
 
68
- return (
81
+ const calendar = (
69
82
  <CalendarProvider
70
83
  ws={workspace}
71
84
  useQuery={useQuery}
72
85
  useQueryClient={useQueryClient}
73
86
  experimentalGoogleToken={experimentalGoogleToken}
87
+ eventAdapter={eventAdapter}
74
88
  readOnly={disabled}
75
89
  >
76
90
  <CalendarSettingsProvider
@@ -87,8 +101,24 @@ export const SmartCalendar = ({
87
101
  externalState={externalState}
88
102
  extras={headerExtras}
89
103
  overlay={overlay}
104
+ disableBuiltInEventUi={eventAdapter?.disableBuiltInEventUi}
90
105
  />
91
106
  </CalendarSettingsProvider>
92
107
  </CalendarProvider>
93
108
  );
109
+
110
+ if (externalEvents) {
111
+ return (
112
+ <CalendarSyncProvider
113
+ wsId={workspace?.id ?? 'external'}
114
+ externalEvents={externalEvents}
115
+ externalEventsLoading={externalEventsLoading}
116
+ externalRefresh={externalEventsRefresh}
117
+ >
118
+ {calendar}
119
+ </CalendarSyncProvider>
120
+ );
121
+ }
122
+
123
+ return calendar;
94
124
  };
@@ -32,14 +32,19 @@ import { toast } from '@tuturuuu/ui/hooks/use-toast';
32
32
  import { Input } from '@tuturuuu/ui/input';
33
33
  import { zodResolver } from '@tuturuuu/ui/resolvers';
34
34
  import { Separator } from '@tuturuuu/ui/separator';
35
- import { RichTextEditor } from '@tuturuuu/ui/text-editor/editor';
36
35
  import { cn } from '@tuturuuu/utils/format';
37
36
  import dayjs from 'dayjs';
38
37
  import { useRouter } from 'next/navigation';
39
38
  import { useTranslations } from 'next-intl';
40
- import { useState } from 'react';
39
+ import { lazy, Suspense, useState } from 'react';
41
40
  import * as z from 'zod';
42
41
 
42
+ const RichTextEditor = lazy(async () => {
43
+ const module = await import('@tuturuuu/ui/text-editor/editor');
44
+
45
+ return { default: module.RichTextEditor };
46
+ });
47
+
43
48
  interface Props {
44
49
  user: Partial<User> | null;
45
50
  plan: {
@@ -369,14 +374,18 @@ export default function CreatePlanDialog({ plan }: Props) {
369
374
  <FormItem>
370
375
  <FormControl>
371
376
  <div className="rounded-lg border border-dynamic-green/30 bg-dynamic-green/5 p-3">
372
- <RichTextEditor
373
- content={field.value || null}
374
- onChange={field.onChange}
375
- readOnly={false}
376
- titlePlaceholder={t('agenda_title_placeholder')}
377
- writePlaceholder={t('agenda_content_placeholder')}
378
- className="h-64 border-0 bg-transparent"
379
- />
377
+ <Suspense fallback={<div className="h-64" />}>
378
+ <RichTextEditor
379
+ content={field.value || null}
380
+ onChange={field.onChange}
381
+ readOnly={false}
382
+ titlePlaceholder={t('agenda_title_placeholder')}
383
+ writePlaceholder={t(
384
+ 'agenda_content_placeholder'
385
+ )}
386
+ className="h-64 border-0 bg-transparent"
387
+ />
388
+ </Suspense>
380
389
  </div>
381
390
  </FormControl>
382
391
  <FormMessage />
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
4
- import { ChevronDownIcon } from '@tuturuuu/icons';
4
+ import { ChevronDownIcon } from '@tuturuuu/icons/lucide-static';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import { cva } from 'class-variance-authority';
7
7
  import type * as React from 'react';
@@ -2,7 +2,7 @@ import {
2
2
  ChevronLeftIcon,
3
3
  ChevronRightIcon,
4
4
  MoreHorizontalIcon,
5
- } from '@tuturuuu/icons';
5
+ } from '@tuturuuu/icons/lucide-static';
6
6
  import { cn } from '@tuturuuu/utils/format';
7
7
  import type * as React from 'react';
8
8
  import { type Button, buttonVariants } from './button';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
4
- import { CircleIcon } from '@tuturuuu/icons';
4
+ import { CircleIcon } from '@tuturuuu/icons/lucide-static';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import type * as React from 'react';
7
7
 
@@ -1,7 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import * as SelectPrimitive from '@radix-ui/react-select';
4
- import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@tuturuuu/icons';
4
+ import {
5
+ CheckIcon,
6
+ ChevronDownIcon,
7
+ ChevronUpIcon,
8
+ } from '@tuturuuu/icons/lucide-static';
5
9
  import { cn } from '@tuturuuu/utils/format';
6
10
  import type * as React from 'react';
7
11
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as SheetPrimitive from '@radix-ui/react-dialog';
4
- import { XIcon } from '@tuturuuu/icons';
4
+ import { XIcon } from '@tuturuuu/icons/lucide-static';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import type * as React from 'react';
7
7
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Slot } from '@radix-ui/react-slot';
4
- import { PanelLeftIcon } from '@tuturuuu/icons';
4
+ import { PanelLeftIcon } 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';
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { ShoppingCart } from '@tuturuuu/icons';
4
+ import { cn } from '@tuturuuu/utils/format';
5
+ import type { CSSProperties, ReactNode } from 'react';
6
+ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
7
+ import type { StorefrontSurfaceLabels } from './types';
8
+
9
+ export function StorefrontCartPopover({
10
+ cartQuantity,
11
+ children,
12
+ labels,
13
+ onOpenChange,
14
+ open,
15
+ radius,
16
+ }: {
17
+ cartQuantity: number;
18
+ children: ReactNode;
19
+ labels: StorefrontSurfaceLabels;
20
+ onOpenChange: (open: boolean) => void;
21
+ open: boolean;
22
+ radius: string;
23
+ }) {
24
+ const cartControlStyle: CSSProperties | undefined =
25
+ cartQuantity > 0
26
+ ? {
27
+ borderColor: 'var(--storefront-accent-border, var(--border))',
28
+ color: 'var(--storefront-accent-text, var(--primary))',
29
+ }
30
+ : undefined;
31
+
32
+ return (
33
+ <Popover onOpenChange={onOpenChange} open={open}>
34
+ <PopoverTrigger asChild>
35
+ <button
36
+ aria-label={`${labels.cart}: ${cartQuantity}`}
37
+ className={cn(
38
+ 'inline-flex h-11 min-w-14 shrink-0 items-center justify-center gap-2 border bg-card px-3 font-semibold text-sm tabular-nums transition hover:bg-muted/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
39
+ radius
40
+ )}
41
+ style={cartControlStyle}
42
+ type="button"
43
+ >
44
+ <ShoppingCart aria-hidden className="size-5 shrink-0" />
45
+ <span className="sr-only">{labels.cart}: </span>
46
+ <span className="min-w-4 text-center">{cartQuantity}</span>
47
+ </button>
48
+ </PopoverTrigger>
49
+ <PopoverContent
50
+ align="end"
51
+ className={cn(
52
+ 'w-[min(calc(100vw-2rem),24rem)] border-border/70 p-4 shadow-xl',
53
+ radius
54
+ )}
55
+ sideOffset={10}
56
+ >
57
+ {children}
58
+ </PopoverContent>
59
+ </Popover>
60
+ );
61
+ }
@@ -0,0 +1,290 @@
1
+ 'use client';
2
+
3
+ import { ArrowRight, Tag, TriangleAlert, Zap } from '@tuturuuu/icons';
4
+ import { ChevronDownIcon } from '@tuturuuu/icons/lucide-static';
5
+ import { cn } from '@tuturuuu/utils/format';
6
+ import type { ReactNode } from 'react';
7
+ import { Button } from '../button';
8
+ import {
9
+ Collapsible,
10
+ CollapsibleContent,
11
+ CollapsibleTrigger,
12
+ } from '../collapsible';
13
+ import { AccentButton } from './accent-button';
14
+ import type {
15
+ StorefrontBuyerDefaults,
16
+ StorefrontCartEntry,
17
+ StorefrontSurfaceLabels,
18
+ } from './types';
19
+ import {
20
+ formatStorefrontPrice,
21
+ getStorefrontLinePrice,
22
+ getStorefrontVariantLabel,
23
+ storefrontCartLineKey,
24
+ } from './utils';
25
+
26
+ export function CartContents({
27
+ cartEntries,
28
+ currency,
29
+ hasCart,
30
+ isCheckoutDisabled,
31
+ labels,
32
+ total,
33
+ }: {
34
+ cartEntries: StorefrontCartEntry[];
35
+ currency: string;
36
+ hasCart: boolean;
37
+ isCheckoutDisabled: boolean;
38
+ labels: StorefrontSurfaceLabels;
39
+ total: number;
40
+ }) {
41
+ return (
42
+ <div className="grid gap-4">
43
+ {hasCart ? (
44
+ <CartLines cartEntries={cartEntries} currency={currency} />
45
+ ) : null}
46
+ <CartTotal currency={currency} labels={labels} total={total} />
47
+ {hasCart && !isCheckoutDisabled ? (
48
+ <p className="flex items-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-3 py-2 text-muted-foreground text-xs leading-5">
49
+ <Tag className="h-3.5 w-3.5 shrink-0" />
50
+ {labels.couponNote}
51
+ </p>
52
+ ) : null}
53
+ {!hasCart ? (
54
+ <p className="flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-muted-foreground text-sm">
55
+ <TriangleAlert className="h-4 w-4 shrink-0" />
56
+ {labels.emptyCart}
57
+ </p>
58
+ ) : null}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function CartActions({
64
+ canOpenCheckout,
65
+ checkoutHref,
66
+ isCheckoutDisabled,
67
+ isPreview,
68
+ isSubmitting,
69
+ labels,
70
+ onCheckoutOpen,
71
+ onInstantCheckout,
72
+ radius,
73
+ }: {
74
+ canOpenCheckout: boolean;
75
+ checkoutHref?: string;
76
+ isCheckoutDisabled: boolean;
77
+ isPreview: boolean;
78
+ isSubmitting: boolean;
79
+ labels: StorefrontSurfaceLabels;
80
+ onCheckoutOpen?: () => void;
81
+ onInstantCheckout?: () => void;
82
+ radius: string;
83
+ }) {
84
+ if (isPreview || isCheckoutDisabled) {
85
+ return (
86
+ <Button className={cn('w-full', radius)} disabled type="button">
87
+ {labels.checkoutDisabled}
88
+ </Button>
89
+ );
90
+ }
91
+
92
+ if (!canOpenCheckout) {
93
+ return (
94
+ <Button className={cn('w-full', radius)} disabled type="button">
95
+ {labels.checkout}
96
+ <ArrowRight className="size-4 shrink-0" />
97
+ </Button>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div className="grid gap-2">
103
+ {onInstantCheckout ? (
104
+ <AccentButton
105
+ disabled={isSubmitting}
106
+ onClick={onInstantCheckout}
107
+ radius={radius}
108
+ >
109
+ <Zap className="size-4 shrink-0" />
110
+ {isSubmitting ? labels.reserving : labels.instantCheckout}
111
+ </AccentButton>
112
+ ) : null}
113
+ {onCheckoutOpen ? (
114
+ <AccentButton
115
+ disabled={isSubmitting}
116
+ onClick={onCheckoutOpen}
117
+ radius={radius}
118
+ >
119
+ {labels.checkout}
120
+ <ArrowRight className="size-4 shrink-0" />
121
+ </AccentButton>
122
+ ) : (
123
+ <Button asChild className={cn('w-full', radius)} variant="outline">
124
+ <a href={checkoutHref}>
125
+ {labels.checkout}
126
+ <ArrowRight className="size-4 shrink-0" />
127
+ </a>
128
+ </Button>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export function CheckoutSection({
135
+ children,
136
+ defaultOpen,
137
+ meta,
138
+ title,
139
+ }: {
140
+ children: ReactNode;
141
+ defaultOpen?: boolean;
142
+ meta?: string;
143
+ title: string;
144
+ }) {
145
+ return (
146
+ <Collapsible defaultOpen={defaultOpen}>
147
+ <div className="rounded-lg bg-muted/25 px-3 py-2">
148
+ <CollapsibleTrigger
149
+ className="group flex w-full items-center gap-3 text-left"
150
+ type="button"
151
+ >
152
+ <span className="min-w-0 flex-1 font-semibold text-sm">{title}</span>
153
+ {meta ? (
154
+ <span className="max-w-[9rem] overflow-hidden text-ellipsis whitespace-nowrap text-right font-semibold text-sm tabular-nums">
155
+ {meta}
156
+ </span>
157
+ ) : null}
158
+ <ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
159
+ </CollapsibleTrigger>
160
+ <CollapsibleContent className="pt-4">{children}</CollapsibleContent>
161
+ </div>
162
+ </Collapsible>
163
+ );
164
+ }
165
+
166
+ export function CheckoutContactFields({
167
+ buyerDefaults,
168
+ labels,
169
+ }: {
170
+ buyerDefaults?: StorefrontBuyerDefaults;
171
+ labels: StorefrontSurfaceLabels;
172
+ }) {
173
+ const buyerEmail = buyerDefaults?.email?.trim() || undefined;
174
+ const buyerName = buyerDefaults?.name?.trim() || undefined;
175
+ const inputClassName =
176
+ 'h-11 rounded-md border border-input bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40';
177
+ const labelClassName = 'grid gap-1.5 text-sm';
178
+
179
+ return (
180
+ <div className="grid gap-3">
181
+ <label className={labelClassName}>
182
+ <span className="font-medium text-xs">{labels.form.name}</span>
183
+ <input
184
+ autoComplete="name"
185
+ className={inputClassName}
186
+ defaultValue={buyerName}
187
+ name="customerName"
188
+ placeholder={labels.form.name}
189
+ required
190
+ />
191
+ </label>
192
+ <label className={labelClassName}>
193
+ <span className="font-medium text-xs">{labels.form.email}</span>
194
+ <input
195
+ autoComplete="email"
196
+ className={inputClassName}
197
+ defaultValue={buyerEmail}
198
+ name="customerEmail"
199
+ placeholder={labels.form.email}
200
+ required
201
+ type="email"
202
+ />
203
+ </label>
204
+ <label className={labelClassName}>
205
+ <span className="font-medium text-xs">{labels.form.phone}</span>
206
+ <input
207
+ autoComplete="tel"
208
+ className={inputClassName}
209
+ name="customerPhone"
210
+ placeholder={labels.form.phone}
211
+ type="tel"
212
+ />
213
+ </label>
214
+ <textarea
215
+ className="min-h-24 rounded-md border border-input bg-background px-3 py-2.5 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40"
216
+ name="note"
217
+ placeholder={labels.form.note}
218
+ />
219
+ </div>
220
+ );
221
+ }
222
+
223
+ function CartLines({
224
+ cartEntries,
225
+ currency,
226
+ }: {
227
+ cartEntries: StorefrontCartEntry[];
228
+ currency: string;
229
+ }) {
230
+ return (
231
+ <div className="-mr-1 grid max-h-72 gap-3 overflow-y-auto pr-1">
232
+ {cartEntries.map(({ line, listing, variant }) => {
233
+ const unitPrice = getStorefrontLinePrice(listing, variant);
234
+ const variantLabel = variant
235
+ ? getStorefrontVariantLabel(variant)
236
+ : null;
237
+ const lineTotal = formatStorefrontPrice(
238
+ unitPrice * line.quantity,
239
+ currency
240
+ );
241
+
242
+ return (
243
+ <div
244
+ className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1 text-sm"
245
+ key={storefrontCartLineKey(line.listingId, line.variantId)}
246
+ >
247
+ <div className="min-w-0">
248
+ <p className="line-clamp-2 break-words font-medium leading-5">
249
+ {listing.title}
250
+ </p>
251
+ {variantLabel ? (
252
+ <p className="line-clamp-1 break-words text-muted-foreground text-xs">
253
+ {variantLabel}
254
+ </p>
255
+ ) : null}
256
+ <p className="text-muted-foreground text-xs tabular-nums">
257
+ {line.quantity} × {formatStorefrontPrice(unitPrice, currency)}
258
+ </p>
259
+ </div>
260
+ <span
261
+ className="max-w-[9rem] justify-self-end overflow-hidden text-ellipsis whitespace-nowrap text-right font-medium tabular-nums leading-5 sm:max-w-[11rem]"
262
+ title={lineTotal}
263
+ >
264
+ {lineTotal}
265
+ </span>
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ );
271
+ }
272
+
273
+ function CartTotal({
274
+ currency,
275
+ labels,
276
+ total,
277
+ }: {
278
+ currency: string;
279
+ labels: StorefrontSurfaceLabels;
280
+ total: number;
281
+ }) {
282
+ return (
283
+ <div className="flex items-center justify-between gap-3 border-border border-t pt-4">
284
+ <span className="text-muted-foreground text-sm">{labels.total}</span>
285
+ <span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-right font-semibold tabular-nums">
286
+ {formatStorefrontPrice(total, currency)}
287
+ </span>
288
+ </div>
289
+ );
290
+ }