@tuturuuu/ui 0.2.0 → 0.3.2

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 (129) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +79 -67
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +396 -2
  12. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
  13. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
  14. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
  16. package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
  17. package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
  18. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
  19. package/src/components/ui/chat/chat-workspace.tsx +31 -1
  20. package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
  21. package/src/components/ui/chat/hooks-messages.ts +1 -1
  22. package/src/components/ui/chat/hooks-realtime.ts +13 -16
  23. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  24. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  25. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  26. package/src/components/ui/custom/common-footer.tsx +16 -1
  27. package/src/components/ui/custom/production-indicator.tsx +1 -1
  28. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  29. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  30. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  31. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  32. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  33. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  34. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  35. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  36. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  37. package/src/components/ui/custom/workspace-select.tsx +33 -12
  38. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  39. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  40. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  41. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  42. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  43. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  44. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  45. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  46. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  47. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  48. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  49. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  50. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  51. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  52. package/src/components/ui/finance/invoices/utils.ts +75 -17
  53. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  54. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  55. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  56. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  57. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  58. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  59. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  60. package/src/components/ui/finance/transactions/form.tsx +60 -0
  61. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  62. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  63. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  64. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  65. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  66. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  67. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  68. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  69. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  70. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  71. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  72. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  73. package/src/components/ui/legacy/meet/page.tsx +87 -39
  74. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  83. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  84. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  85. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  86. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  87. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  88. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  89. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  90. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  91. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  92. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  93. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  94. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  95. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  96. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  104. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  105. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  106. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  107. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  108. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  109. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  110. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  111. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  112. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  113. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  114. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-semantic-task-search.ts +10 -33
  125. package/src/hooks/use-task-actions.ts +43 -117
  126. package/src/hooks/use-user-config.ts +1 -1
  127. package/src/hooks/use-workspace-config.ts +6 -2
  128. package/src/hooks/use-workspace-presence.ts +1 -1
  129. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -88,11 +88,34 @@ describe('SettingsDialogShell keyboard navigation', () => {
88
88
  }));
89
89
  });
90
90
 
91
+ it('renders the fullscreen settings sheet chrome', () => {
92
+ renderShell();
93
+
94
+ const dialog = screen.getByRole('dialog');
95
+
96
+ expect(dialog).toHaveClass('h-dvh');
97
+ expect(dialog).toHaveClass('w-screen');
98
+ expect(dialog).toHaveClass('rounded-none');
99
+ expect(
100
+ screen.getAllByRole('button', { name: 'settings.back_to_app' }).length
101
+ ).toBeGreaterThan(0);
102
+ });
103
+
104
+ it('keeps settings groups expanded by default', () => {
105
+ renderShell();
106
+
107
+ expect(screen.getAllByText('Profile').length).toBeGreaterThan(0);
108
+ expect(screen.getByText('Appearance')).toBeVisible();
109
+ expect(screen.getByText('Forms')).toBeVisible();
110
+ });
111
+
91
112
  it('focuses settings search with slash and modifier search shortcuts', () => {
92
113
  renderShell();
93
114
 
94
115
  const dialog = screen.getByRole('dialog');
95
- const searchInput = screen.getByPlaceholderText('search.search');
116
+ const searchInput = screen.getByPlaceholderText(
117
+ 'settings.search_settings_placeholder'
118
+ );
96
119
 
97
120
  fireEvent.keyDown(dialog, { key: '/' });
98
121
  expect(searchInput).toHaveFocus();
@@ -1,10 +1,19 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { TUTURUUU_LOGO_URL } from '../tuturuuu-logo';
2
+ import {
3
+ TUTURUUU_LOCAL_LOGO_URL,
4
+ TUTURUUU_LOGO_URL,
5
+ TUTURUUU_REMOTE_LOGO_URL,
6
+ } from '../tuturuuu-logo';
3
7
 
4
8
  describe('Tuturuuu logo asset URL', () => {
5
- it('uses the canonical hosted logo instead of a per-app relative asset', () => {
6
- expect(TUTURUUU_LOGO_URL).toBe(
9
+ it('keeps the canonical hosted logo as the shared default', () => {
10
+ expect(TUTURUUU_REMOTE_LOGO_URL).toBe(
7
11
  'https://tuturuuu.com/media/logos/transparent.png'
8
12
  );
13
+ expect(TUTURUUU_LOGO_URL).toBe(TUTURUUU_REMOTE_LOGO_URL);
14
+ });
15
+
16
+ it('exports the same-origin public logo path for apps that ship the asset', () => {
17
+ expect(TUTURUUU_LOCAL_LOGO_URL).toBe('/media/logos/transparent.png');
9
18
  });
10
19
  });
@@ -0,0 +1,39 @@
1
+ import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { mergeWorkspaceSelectWorkspaces } from '../workspace-select-helpers';
4
+
5
+ describe('mergeWorkspaceSelectWorkspaces', () => {
6
+ it('uses the current workspace fallback when the workspace list is unavailable', () => {
7
+ const fallback: InternalApiWorkspaceSummary = {
8
+ access_type: 'guest',
9
+ avatar_url: null,
10
+ guest_board_count: 1,
11
+ guest_highest_permission: 'edit',
12
+ guest_landing_path: '/tasks/boards/board-1',
13
+ guest_products: ['tasks'],
14
+ id: 'guest-workspace',
15
+ logo_url: null,
16
+ name: 'Shared workspace',
17
+ personal: false,
18
+ };
19
+
20
+ expect(mergeWorkspaceSelectWorkspaces(undefined, fallback)).toEqual([
21
+ fallback,
22
+ ]);
23
+ });
24
+
25
+ it('does not duplicate the current workspace when the list already includes it', () => {
26
+ const workspace: InternalApiWorkspaceSummary = {
27
+ access_type: 'member',
28
+ avatar_url: null,
29
+ id: 'workspace-1',
30
+ logo_url: null,
31
+ name: 'Workspace',
32
+ personal: false,
33
+ };
34
+
35
+ expect(mergeWorkspaceSelectWorkspaces([workspace], workspace)).toEqual([
36
+ workspace,
37
+ ]);
38
+ });
39
+ });
@@ -3,7 +3,15 @@ import Link from 'next/link';
3
3
  import { Separator } from '../separator';
4
4
  import { TuturuuLogo } from './tuturuuu-logo';
5
5
 
6
- export function CommonFooter({ t, devMode }: { t: any; devMode: boolean }) {
6
+ export function CommonFooter({
7
+ t,
8
+ devMode,
9
+ logoSrc,
10
+ }: {
11
+ t: any;
12
+ devMode: boolean;
13
+ logoSrc?: string;
14
+ }) {
7
15
  const TUTURUUU_URL = devMode
8
16
  ? getLocalInternalAppUrl('platform', 'http://localhost:7803')
9
17
  : 'https://tuturuuu.com';
@@ -35,6 +43,7 @@ export function CommonFooter({ t, devMode }: { t: any; devMode: boolean }) {
35
43
  width={64}
36
44
  height={64}
37
45
  alt="logo"
46
+ src={logoSrc}
38
47
  className="h-12 w-12"
39
48
  />
40
49
  <div className="font-semibold text-4xl">Tuturuuu</div>
@@ -166,6 +175,12 @@ export function CommonFooter({ t, devMode }: { t: any; devMode: boolean }) {
166
175
  >
167
176
  {t('common.branding')}
168
177
  </Link>
178
+ <Link
179
+ href={`${TUTURUUU_URL}/ui`}
180
+ className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
181
+ >
182
+ {t('common.ui')}
183
+ </Link>
169
184
  </div>
170
185
 
171
186
  <div className="grid gap-1 md:items-start">
@@ -1,4 +1,4 @@
1
- import { DatabaseZap } from '@tuturuuu/icons';
1
+ import { DatabaseZap } from '@tuturuuu/icons/lucide';
2
2
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
3
3
 
4
4
  export const IS_PRODUCTION_DB =
@@ -34,7 +34,7 @@ export default function SidebarSettings({ useSidebar }: SidebarSettingsProps) {
34
34
  value: expandSettingsAccordions,
35
35
  setValue: setExpandSettingsAccordions,
36
36
  isLoading: isLoadingExpandAccordions,
37
- } = useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', false);
37
+ } = useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', true);
38
38
 
39
39
  return (
40
40
  <div className="space-y-8">
@@ -20,6 +20,7 @@ import { toast } from '@tuturuuu/ui/sonner';
20
20
  import { Switch } from '@tuturuuu/ui/switch';
21
21
  import { useTranslations } from 'next-intl';
22
22
  import { useEffect } from 'react';
23
+ import { TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID } from '../../tu-do/shared/task-due-date-visibility';
23
24
 
24
25
  interface TaskSettingsData {
25
26
  task_auto_assign_to_self: boolean;
@@ -67,6 +68,12 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
67
68
  isLoading: openDefaultBoardLoading,
68
69
  isPending: openDefaultBoardPending,
69
70
  } = useUserBooleanConfig('TASKS_OPEN_DEFAULT_BOARD', true);
71
+ const {
72
+ value: showReviewDueDates,
73
+ setValue: setShowReviewDueDates,
74
+ isLoading: showReviewDueDatesLoading,
75
+ isPending: showReviewDueDatesPending,
76
+ } = useUserBooleanConfig(TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID, false);
70
77
 
71
78
  const { data: submitShortcut, isLoading: submitShortcutLoading } =
72
79
  useUserConfig('TASK_SUBMIT_SHORTCUT', 'enter');
@@ -202,6 +209,17 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
202
209
  />
203
210
  </SettingItemTab>
204
211
  <Separator />
212
+ <SettingItemTab
213
+ title={t('show_review_due_dates')}
214
+ description={t('show_review_due_dates_description')}
215
+ >
216
+ <Switch
217
+ checked={showReviewDueDates}
218
+ onCheckedChange={setShowReviewDueDates}
219
+ disabled={showReviewDueDatesLoading || showReviewDueDatesPending}
220
+ />
221
+ </SettingItemTab>
222
+ <Separator />
205
223
  <SettingItemTab
206
224
  title={t('submit_shortcut')}
207
225
  description={t('submit_shortcut_description')}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { ChevronDown, ChevronRight, Search } from '@tuturuuu/icons';
3
+ import { ArrowLeft, ChevronDown, ChevronRight, Search } from '@tuturuuu/icons';
4
4
  import {
5
5
  Breadcrumb,
6
6
  BreadcrumbItem,
@@ -24,6 +24,7 @@ import {
24
24
  CommandList,
25
25
  } from '@tuturuuu/ui/command';
26
26
  import {
27
+ DialogClose,
27
28
  DialogContent,
28
29
  DialogDescription,
29
30
  DialogTitle,
@@ -52,7 +53,6 @@ import {
52
53
  SidebarProvider,
53
54
  } from '@tuturuuu/ui/sidebar';
54
55
  import { cn } from '@tuturuuu/utils/format';
55
- import { usePlatform } from '@tuturuuu/utils/hooks/use-platform';
56
56
  import { removeAccents } from '@tuturuuu/utils/text-helper';
57
57
  import { useTranslations } from 'next-intl';
58
58
  import type { ComponentType, KeyboardEvent, ReactNode } from 'react';
@@ -81,11 +81,11 @@ export interface SettingsDialogShellProps {
81
81
  onActiveTabChange: (tab: string) => void;
82
82
  /**
83
83
  * Group labels that should be expanded by default.
84
- * All other groups will be collapsed.
85
- * If not provided, only the first group expands by default.
84
+ * Used when expandAllAccordions is false.
85
+ * If not provided in that mode, only the first group expands.
86
86
  */
87
87
  primaryGroupLabels?: string[];
88
- /** Override to expand all accordions (user preference) */
88
+ /** Override to expand all accordions (user preference). Defaults to expanded. */
89
89
  expandAllAccordions?: boolean;
90
90
  /** Enable dialog-scoped keyboard shortcuts for search and tab navigation */
91
91
  keyboardNavigation?: boolean;
@@ -111,21 +111,19 @@ function isEditableShortcutTarget(target: EventTarget | null) {
111
111
  *
112
112
  * Each app provides its own `navItems` (ordered by priority) and
113
113
  * renders tab-specific content via `children`. The `primaryGroupLabels`
114
- * prop controls which groups expand by default enabling apps to
115
- * highlight their domain (e.g., tasks-first in Tasks, calendar-first
116
- * in a future calendar app).
114
+ * prop controls which groups expand when `expandAllAccordions` is false,
115
+ * enabling apps to highlight their domain while preserving a compact mode.
117
116
  */
118
117
  export function SettingsDialogShell({
119
118
  navItems,
120
119
  activeTab,
121
120
  onActiveTabChange,
122
121
  primaryGroupLabels,
123
- expandAllAccordions = false,
122
+ expandAllAccordions = true,
124
123
  keyboardNavigation = false,
125
124
  children,
126
125
  }: SettingsDialogShellProps) {
127
126
  const t = useTranslations();
128
- const { isMac, modKey } = usePlatform();
129
127
  const isMobile = useIsMobile();
130
128
  const desktopSearchInputRef = useRef<HTMLInputElement>(null);
131
129
  const mobileSearchInputRef = useRef<HTMLInputElement>(null);
@@ -275,8 +273,9 @@ export function SettingsDialogShell({
275
273
 
276
274
  return (
277
275
  <DialogContent
278
- className="flex h-[90vh] flex-col overflow-hidden p-0 md:max-h-200 md:max-w-225 lg:max-h-250 lg:max-w-250 xl:max-w-300"
276
+ className="top-0 left-0 flex h-dvh max-h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-col gap-0 overflow-hidden rounded-none border-0 p-0 shadow-none sm:max-w-none"
279
277
  onKeyDown={handleKeyboardNavigation}
278
+ showCloseButton={false}
280
279
  >
281
280
  <DialogTitle className="sr-only">{t('common.settings')}</DialogTitle>
282
281
  <DialogDescription className="sr-only">
@@ -285,24 +284,29 @@ export function SettingsDialogShell({
285
284
  <SidebarProvider className="flex h-full min-h-0 items-start">
286
285
  <Sidebar
287
286
  collapsible="none"
288
- className="hidden h-full w-64 flex-col border-r bg-muted/30 md:flex"
287
+ className="hidden h-full w-72 flex-col border-r bg-muted/30 md:flex"
289
288
  >
290
- <SidebarHeader className="z-10 p-4 pb-0">
291
- <div className="relative mb-2">
289
+ <SidebarHeader className="z-10 gap-3 p-4 pb-0">
290
+ <DialogClose asChild>
291
+ <Button
292
+ type="button"
293
+ variant="ghost"
294
+ className="h-9 justify-start px-2 text-muted-foreground"
295
+ >
296
+ <ArrowLeft className="h-4 w-4" />
297
+ {t('settings.back_to_app')}
298
+ </Button>
299
+ </DialogClose>
300
+ <div className="relative">
292
301
  <Search className="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
293
302
  <SidebarInput
294
303
  ref={desktopSearchInputRef}
295
- placeholder={t('search.search')}
304
+ placeholder={t('settings.search_settings_placeholder')}
296
305
  className="bg-background pl-8"
297
306
  value={searchQuery}
298
307
  onChange={(e) => setSearchQuery(e.target.value)}
299
308
  />
300
309
  </div>
301
- <div className="flex items-center justify-between px-1">
302
- <span className="text-[10px] text-muted-foreground uppercase tracking-wider">
303
- Detected OS: {isMac ? 'macOS' : 'Windows/Linux'} ({modKey})
304
- </span>
305
- </div>
306
310
  </SidebarHeader>
307
311
  <SidebarContent className="overflow-y-auto p-4">
308
312
  {filteredNavItems.map((group, index) => (
@@ -358,7 +362,18 @@ export function SettingsDialogShell({
358
362
  </SidebarContent>
359
363
  </Sidebar>
360
364
  <main className="flex h-full flex-1 flex-col overflow-hidden bg-background">
361
- <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4 pr-12 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-6 md:pr-6">
365
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-6">
366
+ <DialogClose asChild>
367
+ <Button
368
+ type="button"
369
+ variant="ghost"
370
+ size="icon"
371
+ className="shrink-0 text-muted-foreground md:hidden"
372
+ >
373
+ <ArrowLeft className="h-4 w-4" />
374
+ <span className="sr-only">{t('settings.back_to_app')}</span>
375
+ </Button>
376
+ </DialogClose>
362
377
  <div className="flex flex-1 items-center gap-2 md:flex-initial">
363
378
  {isMobile && (
364
379
  <Drawer open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
@@ -390,7 +405,7 @@ export function SettingsDialogShell({
390
405
  <Command className="rounded-none border-0">
391
406
  <CommandInput
392
407
  ref={mobileSearchInputRef}
393
- placeholder={t('search.search')}
408
+ placeholder={t('settings.search_settings_placeholder')}
394
409
  />
395
410
  <CommandList className="max-h-[50vh]">
396
411
  <CommandEmpty>
@@ -458,7 +473,7 @@ export function SettingsDialogShell({
458
473
  </div>
459
474
  </header>
460
475
  <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-6">
461
- <div className="mx-auto w-full max-w-3xl space-y-6">
476
+ <div className="mx-auto w-full max-w-4xl space-y-6">
462
477
  <div className="space-y-1">
463
478
  <h2 className="font-semibold text-lg tracking-tight">
464
479
  {activeItem?.label}
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ function readSidebarSource(fileName: string) {
6
+ const candidates = [
7
+ join(process.cwd(), 'src/components/ui/custom', fileName),
8
+ join(process.cwd(), 'packages/ui/src/components/ui/custom', fileName),
9
+ ];
10
+ const sourcePath = candidates.find((candidate) => existsSync(candidate));
11
+
12
+ if (!sourcePath) {
13
+ throw new Error(`Unable to resolve ${fileName} from ${process.cwd()}`);
14
+ }
15
+
16
+ return readFileSync(sourcePath, 'utf8');
17
+ }
18
+
19
+ const sidebarContextSource = readSidebarSource('sidebar-context.tsx');
20
+ const sidebarRemoteBehaviorBridgeSource = readSidebarSource(
21
+ 'sidebar-remote-behavior-bridge.tsx'
22
+ );
23
+
24
+ function staticImportPattern(modulePath: string) {
25
+ const escapedModulePath = modulePath.replace(
26
+ /[.*+?^${}()|[\]\\]/gu,
27
+ String.raw`\$&`
28
+ );
29
+
30
+ return new RegExp(
31
+ String.raw`^\s*import\s+(?!type\b)[\s\S]*?\sfrom\s+['"]${escapedModulePath}['"];`,
32
+ 'mu'
33
+ );
34
+ }
35
+
36
+ function dynamicImportPattern(modulePath: string) {
37
+ const escapedModulePath = modulePath.replace(
38
+ /[.*+?^${}()|[\]\\]/gu,
39
+ String.raw`\$&`
40
+ );
41
+
42
+ return new RegExp(
43
+ String.raw`import\s*\(\s*['"]${escapedModulePath}['"]\s*\)`,
44
+ 'mu'
45
+ );
46
+ }
47
+
48
+ describe('sidebar context compile graph', () => {
49
+ it('loads remote user-config sync after hydration', () => {
50
+ expect(sidebarContextSource).not.toMatch(
51
+ staticImportPattern('@tuturuuu/ui/hooks/use-user-config')
52
+ );
53
+ expect(sidebarContextSource).toMatch(
54
+ dynamicImportPattern('./sidebar-remote-behavior-bridge')
55
+ );
56
+ expect(sidebarRemoteBehaviorBridgeSource).toMatch(
57
+ staticImportPattern('@tuturuuu/ui/hooks/use-user-config')
58
+ );
59
+ });
60
+ });
@@ -1,12 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useLocalStorage } from '@tuturuuu/ui/hooks/use-local-storage';
4
- import {
5
- useUpdateUserConfig,
6
- useUserConfig,
7
- } from '@tuturuuu/ui/hooks/use-user-config';
8
4
  import { setCookie } from 'cookies-next';
9
5
  import {
6
+ type ComponentType,
10
7
  createContext,
11
8
  type Dispatch,
12
9
  type ReactNode,
@@ -14,7 +11,6 @@ import {
14
11
  useCallback,
15
12
  useContext,
16
13
  useEffect,
17
- useRef,
18
14
  useState,
19
15
  } from 'react';
20
16
 
@@ -23,9 +19,6 @@ export const SIDEBAR_BEHAVIOR_CONFIG_KEY = 'SIDEBAR_BEHAVIOR';
23
19
 
24
20
  export type SidebarBehavior = 'expanded' | 'collapsed' | 'hover';
25
21
 
26
- const isValidBehavior = (value: string | undefined): value is SidebarBehavior =>
27
- value === 'expanded' || value === 'collapsed' || value === 'hover';
28
-
29
22
  interface SidebarContextProps {
30
23
  behavior: SidebarBehavior;
31
24
  setBehavior: Dispatch<SetStateAction<SidebarBehavior>>;
@@ -42,6 +35,38 @@ export const SidebarContext = createContext<SidebarContextProps | undefined>(
42
35
  // Persistent cookie options — ensures setting survives browser restarts
43
36
  const COOKIE_OPTIONS = { maxAge: 365 * 24 * 60 * 60, path: '/' } as const;
44
37
 
38
+ type SidebarRemoteBehaviorBridgeComponent = ComponentType<{
39
+ behavior: SidebarBehavior;
40
+ localOverride: boolean;
41
+ localOverrideVersion: number;
42
+ onApplyRemoteBehavior: (newBehavior: SidebarBehavior) => void;
43
+ onRemoteBehaviorAvailable: (remoteBehavior: SidebarBehavior) => void;
44
+ userChangeVersion: number;
45
+ }>;
46
+
47
+ function useSidebarRemoteBehaviorBridge() {
48
+ const [RemoteBehaviorBridge, setRemoteBehaviorBridge] =
49
+ useState<SidebarRemoteBehaviorBridgeComponent | null>(null);
50
+
51
+ useEffect(() => {
52
+ let active = true;
53
+
54
+ // biome-ignore lint/suspicious/noTsIgnore: NodeNext requires .js, but Next/Turbopack resolves workspace TypeScript source here before package emit.
55
+ // @ts-ignore
56
+ void import('./sidebar-remote-behavior-bridge').then((module) => {
57
+ if (active) {
58
+ setRemoteBehaviorBridge(() => module.SidebarRemoteBehaviorBridge);
59
+ }
60
+ });
61
+
62
+ return () => {
63
+ active = false;
64
+ };
65
+ }, []);
66
+
67
+ return RemoteBehaviorBridge;
68
+ }
69
+
45
70
  export const SidebarProvider = ({
46
71
  children,
47
72
  initialBehavior,
@@ -54,74 +79,39 @@ export const SidebarProvider = ({
54
79
  'sidebar-local-override',
55
80
  false
56
81
  );
57
- const hasAppliedRemote = useRef(false);
58
- // Prevents remote sync from overwriting a user-initiated change
59
- const userChangedRef = useRef(false);
60
- // Allows reading current behavior inside the sync effect without
61
- // adding `behavior` to its dependency array
62
- const behaviorRef = useRef(behavior);
63
- behaviorRef.current = behavior;
64
-
65
- // Fetch account-wide preference
66
- const { data: remoteBehavior, isSuccess: remoteLoaded } = useUserConfig(
67
- SIDEBAR_BEHAVIOR_CONFIG_KEY,
68
- 'expanded'
82
+ const [remoteBehavior, setRemoteBehavior] = useState<SidebarBehavior | null>(
83
+ null
69
84
  );
85
+ const [userChangeVersion, setUserChangeVersion] = useState(0);
86
+ const [localOverrideVersion, setLocalOverrideVersion] = useState(0);
87
+ const RemoteBehaviorBridge = useSidebarRemoteBehaviorBridge();
70
88
 
71
- const updateConfig = useUpdateUserConfig();
72
-
73
- // Sync from user_configs when not locally overridden
74
- useEffect(() => {
75
- if (
76
- !remoteLoaded ||
77
- localOverride ||
78
- hasAppliedRemote.current ||
79
- userChangedRef.current
80
- )
81
- return;
82
- hasAppliedRemote.current = true;
83
- if (
84
- isValidBehavior(remoteBehavior) &&
85
- remoteBehavior !== behaviorRef.current
86
- ) {
87
- setBehavior(remoteBehavior);
88
- setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, remoteBehavior, COOKIE_OPTIONS);
89
- }
90
- }, [remoteLoaded, remoteBehavior, localOverride]);
89
+ const applyBehavior = useCallback((newBehavior: SidebarBehavior) => {
90
+ setBehavior(newBehavior);
91
+ setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, newBehavior, COOKIE_OPTIONS);
92
+ }, []);
91
93
 
92
94
  const handleBehaviorChange = useCallback(
93
95
  (newBehavior: SidebarBehavior) => {
94
- userChangedRef.current = true;
95
- setBehavior(newBehavior);
96
- // Always update cookie for SSR
97
- setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, newBehavior, COOKIE_OPTIONS);
98
- // Save to user_configs (account-wide) unless locally overridden
96
+ applyBehavior(newBehavior);
97
+
99
98
  if (!localOverride) {
100
- updateConfig.mutate({
101
- configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
102
- value: newBehavior,
103
- });
99
+ setUserChangeVersion((version) => version + 1);
104
100
  }
105
101
  },
106
- [localOverride, updateConfig]
102
+ [applyBehavior, localOverride]
107
103
  );
108
104
 
109
105
  const setLocalOverride = useCallback(
110
106
  (enabled: boolean) => {
111
107
  setLocalOverrideRaw(enabled);
112
- if (!enabled && isValidBehavior(remoteBehavior)) {
113
- // Turning off local override — sync to account-wide value
114
- setBehavior(remoteBehavior);
115
- setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, remoteBehavior, COOKIE_OPTIONS);
116
- } else if (enabled) {
117
- // Turning on local override — save current behavior to account-wide first
118
- updateConfig.mutate({
119
- configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
120
- value: behavior,
121
- });
108
+ setLocalOverrideVersion((version) => version + 1);
109
+
110
+ if (!enabled && remoteBehavior) {
111
+ applyBehavior(remoteBehavior);
122
112
  }
123
113
  },
124
- [setLocalOverrideRaw, remoteBehavior, behavior, updateConfig]
114
+ [applyBehavior, remoteBehavior, setLocalOverrideRaw]
125
115
  );
126
116
 
127
117
  return (
@@ -134,6 +124,16 @@ export const SidebarProvider = ({
134
124
  setLocalOverride,
135
125
  }}
136
126
  >
127
+ {RemoteBehaviorBridge && (
128
+ <RemoteBehaviorBridge
129
+ behavior={behavior}
130
+ localOverride={localOverride}
131
+ localOverrideVersion={localOverrideVersion}
132
+ onApplyRemoteBehavior={applyBehavior}
133
+ onRemoteBehaviorAvailable={setRemoteBehavior}
134
+ userChangeVersion={userChangeVersion}
135
+ />
136
+ )}
137
137
  {children}
138
138
  </SidebarContext.Provider>
139
139
  );