@tuturuuu/ui 0.1.0 → 0.3.1

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 (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  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-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  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-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -0,0 +1,110 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import type { ChatConversation } from '@tuturuuu/internal-api';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { ChatSidebarPanel } from './chat-sidebar-panel';
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ chatSidebarProps: null as Record<string, unknown> | null,
8
+ fetchNextPage: vi.fn(),
9
+ useChatMessageSearch: vi.fn(),
10
+ useInfiniteChatConversations: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('next-intl', () => ({
14
+ useTranslations: () => (key: string) => key,
15
+ }));
16
+
17
+ vi.mock('next/navigation', () => ({
18
+ usePathname: () => '/personal',
19
+ useSearchParams: () => new URLSearchParams('scope=personal'),
20
+ }));
21
+
22
+ vi.mock('./chat-sidebar', () => ({
23
+ ChatSidebar: (props: Record<string, unknown>) => {
24
+ mocks.chatSidebarProps = props;
25
+ return (
26
+ <button
27
+ onClick={() =>
28
+ (props.onLoadMoreConversations as (() => void) | undefined)?.()
29
+ }
30
+ type="button"
31
+ >
32
+ load more
33
+ </button>
34
+ );
35
+ },
36
+ }));
37
+
38
+ vi.mock('./create-conversation-dialog', () => ({
39
+ CreateConversationDialog: () => null,
40
+ }));
41
+
42
+ vi.mock('./hooks', () => ({
43
+ flattenChatConversationPages: (
44
+ data?: { pages?: { conversations?: ChatConversation[] }[] } | null
45
+ ) => data?.pages?.flatMap((page) => page.conversations ?? []) ?? [],
46
+ useChatMessageSearch: (...args: unknown[]) =>
47
+ mocks.useChatMessageSearch(...args),
48
+ useInfiniteChatConversations: (...args: unknown[]) =>
49
+ mocks.useInfiniteChatConversations(...args),
50
+ }));
51
+
52
+ const conversation: ChatConversation = {
53
+ aiEnabled: false,
54
+ archivedAt: null,
55
+ createdAt: '2026-06-02T00:00:00.000Z',
56
+ createdBy: 'user-1',
57
+ description: null,
58
+ id: 'conversation-1',
59
+ latestMessage: null,
60
+ memberCount: 2,
61
+ members: [],
62
+ metadata: {},
63
+ title: 'Planning',
64
+ type: 'group',
65
+ unreadCount: 0,
66
+ updatedAt: '2026-06-02T00:00:00.000Z',
67
+ wsId: 'personal',
68
+ };
69
+
70
+ describe('ChatSidebarPanel', () => {
71
+ beforeEach(() => {
72
+ vi.clearAllMocks();
73
+ mocks.chatSidebarProps = null;
74
+ mocks.useChatMessageSearch.mockReturnValue({ data: [] });
75
+ mocks.useInfiniteChatConversations.mockReturnValue({
76
+ data: {
77
+ pages: [{ conversations: [conversation], nextOffset: 40 }],
78
+ },
79
+ fetchNextPage: mocks.fetchNextPage,
80
+ hasNextPage: true,
81
+ isFetchingNextPage: false,
82
+ isLoading: false,
83
+ });
84
+ });
85
+
86
+ it('uses the infinite conversations query and forwards load-more controls', () => {
87
+ render(
88
+ <ChatSidebarPanel
89
+ currentUserId="user-1"
90
+ isCollapsed={false}
91
+ wsId="personal"
92
+ />
93
+ );
94
+
95
+ expect(mocks.useInfiniteChatConversations).toHaveBeenCalledWith({
96
+ archived: 'active',
97
+ wsId: 'personal',
98
+ });
99
+ expect(mocks.chatSidebarProps).toMatchObject({
100
+ conversations: [conversation],
101
+ hasMoreConversations: true,
102
+ isFetchingMoreConversations: false,
103
+ isLoading: false,
104
+ });
105
+
106
+ fireEvent.click(screen.getByRole('button', { name: 'load more' }));
107
+
108
+ expect(mocks.fetchNextPage).toHaveBeenCalledTimes(1);
109
+ });
110
+ });
@@ -3,7 +3,11 @@ import { usePathname, useSearchParams } from 'next/navigation';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { ChatSidebar } from './chat-sidebar';
5
5
  import { CreateConversationDialog } from './create-conversation-dialog';
6
- import { useChatConversations, useChatMessageSearch } from './hooks';
6
+ import {
7
+ flattenChatConversationPages,
8
+ useChatMessageSearch,
9
+ useInfiniteChatConversations,
10
+ } from './hooks';
7
11
  import {
8
12
  CHAT_CONVERSATION_TYPE_FILTERS,
9
13
  type ChatConversationArchiveFilter,
@@ -62,7 +66,10 @@ export function ChatSidebarPanel({
62
66
  const setCreateOpen = onCreateOpenChange ?? setInternalCreateOpen;
63
67
  const archiveFilter = controlledArchiveFilter ?? internalArchiveFilter;
64
68
  const selectedTypes = controlledSelectedTypes ?? internalSelectedTypes;
65
- const conversationsQuery = useChatConversations(wsId, archiveFilter);
69
+ const conversationsQuery = useInfiniteChatConversations({
70
+ archived: archiveFilter,
71
+ wsId,
72
+ });
66
73
  const searchQuery = useChatMessageSearch({
67
74
  query: searchValue,
68
75
  wsId,
@@ -70,7 +77,7 @@ export function ChatSidebarPanel({
70
77
  const conversationScope = normalizeChatConversationScope(
71
78
  searchParams.get('scope') ?? defaultConversationScope
72
79
  );
73
- const conversations = conversationsQuery.data ?? [];
80
+ const conversations = flattenChatConversationPages(conversationsQuery.data);
74
81
  const scopeConversations = filterChatConversationsByScope(
75
82
  conversations,
76
83
  conversationScope
@@ -158,7 +165,10 @@ export function ChatSidebarPanel({
158
165
  conversations={scopedConversations}
159
166
  currentUserId={currentUserId}
160
167
  embedded
168
+ hasMoreConversations={conversationsQuery.hasNextPage}
169
+ isFetchingMoreConversations={conversationsQuery.isFetchingNextPage}
161
170
  isLoading={conversationsQuery.isLoading}
171
+ onLoadMoreConversations={() => conversationsQuery.fetchNextPage()}
162
172
  onSearchChange={setSearchValue}
163
173
  onSelectConversation={selectConversation}
164
174
  searchResults={scopedSearchResults}
@@ -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
+ });