@tuturuuu/ui 0.30.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/biome.json +1 -1
- package/package.json +95 -49
- package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
- package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
- package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
- package/src/components/ui/chat/ai-message-markdown.tsx +17 -2
- package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
- package/src/components/ui/custom/nav-link.tsx +7 -7
- package/src/components/ui/custom/nav.tsx +15 -63
- package/src/components/ui/custom/notification-card.tsx +361 -0
- package/src/components/ui/custom/notification-list.tsx +178 -0
- package/src/components/ui/custom/notification-popover-client.tsx +12 -780
- package/src/components/ui/custom/notification-runtime.tsx +23 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
- package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
- package/src/components/ui/custom/satellite-brand.tsx +122 -0
- package/src/components/ui/custom/satellite-content.tsx +148 -0
- package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
- package/src/components/ui/custom/satellite-language-item.tsx +26 -0
- package/src/components/ui/custom/satellite-navigation.tsx +86 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
- package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
- package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
- package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
- package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
- package/src/components/ui/custom/satellite-shell.tsx +50 -0
- package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
- package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
- package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
- package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
- package/src/components/ui/custom/sidebar-context.tsx +5 -14
- package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
- package/src/components/ui/custom/structure.tsx +14 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
- package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
- package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
- package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
- package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
- package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
- package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
- package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
- package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
- package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
- package/src/components/ui/pwa-status.test.tsx +53 -0
- package/src/components/ui/pwa-status.tsx +113 -0
- package/src/components/ui/report-problem-dialog-content.tsx +350 -0
- package/src/components/ui/report-problem-dialog.tsx +6 -975
- package/src/components/ui/report-problem-model.ts +133 -0
- package/src/components/ui/report-problem-mutation.ts +122 -0
- package/src/components/ui/save-notification.test.tsx +36 -0
- package/src/components/ui/save-notification.tsx +17 -0
- package/src/components/ui/use-report-problem.ts +488 -0
- package/src/declarations.d.ts +2 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
- package/src/hooks/calendar-date-utils.ts +11 -0
- package/src/hooks/calendar-event-lookup.test.ts +77 -0
- package/src/hooks/calendar-event-lookup.ts +47 -0
- package/src/hooks/calendar-range-cache.ts +52 -0
- package/src/hooks/pwa-install-prompt.ts +44 -0
- package/src/hooks/use-calendar-sync.tsx +23 -100
- package/src/hooks/use-calendar.tsx +21 -77
- package/src/hooks/use-notifications.ts +9 -24
- package/src/hooks/use-open-initial-calendar-event.ts +27 -0
- package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
- package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
- package/src/hooks/use-workspace-presence.ts +0 -18
- package/src/lib/tasks-app-url.test.ts +25 -0
- package/src/lib/tasks-app-url.ts +8 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeft,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Compass,
|
|
8
|
+
Search,
|
|
9
|
+
X,
|
|
10
|
+
} from '@tuturuuu/icons';
|
|
11
|
+
import {
|
|
12
|
+
Breadcrumb,
|
|
13
|
+
BreadcrumbItem,
|
|
14
|
+
BreadcrumbLink,
|
|
15
|
+
BreadcrumbList,
|
|
16
|
+
BreadcrumbPage,
|
|
17
|
+
BreadcrumbSeparator,
|
|
18
|
+
} from '@tuturuuu/ui/breadcrumb';
|
|
19
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
20
|
+
import {
|
|
21
|
+
Collapsible,
|
|
22
|
+
CollapsibleContent,
|
|
23
|
+
CollapsibleTrigger,
|
|
24
|
+
} from '@tuturuuu/ui/collapsible';
|
|
25
|
+
import {
|
|
26
|
+
Command,
|
|
27
|
+
CommandEmpty,
|
|
28
|
+
CommandGroup,
|
|
29
|
+
CommandInput,
|
|
30
|
+
CommandItem,
|
|
31
|
+
CommandList,
|
|
32
|
+
} from '@tuturuuu/ui/command';
|
|
33
|
+
import {
|
|
34
|
+
DialogClose,
|
|
35
|
+
DialogContent,
|
|
36
|
+
DialogDescription,
|
|
37
|
+
DialogTitle,
|
|
38
|
+
} from '@tuturuuu/ui/dialog';
|
|
39
|
+
import {
|
|
40
|
+
Drawer,
|
|
41
|
+
DrawerContent,
|
|
42
|
+
DrawerDescription,
|
|
43
|
+
DrawerHeader,
|
|
44
|
+
DrawerTitle,
|
|
45
|
+
DrawerTrigger,
|
|
46
|
+
} from '@tuturuuu/ui/drawer';
|
|
47
|
+
import { useIsMobile } from '@tuturuuu/ui/hooks/use-mobile';
|
|
48
|
+
import { Separator } from '@tuturuuu/ui/separator';
|
|
49
|
+
import {
|
|
50
|
+
Sidebar,
|
|
51
|
+
SidebarContent,
|
|
52
|
+
SidebarGroup,
|
|
53
|
+
SidebarGroupContent,
|
|
54
|
+
SidebarGroupLabel,
|
|
55
|
+
SidebarHeader,
|
|
56
|
+
SidebarInput,
|
|
57
|
+
SidebarMenu,
|
|
58
|
+
SidebarMenuButton,
|
|
59
|
+
SidebarMenuItem,
|
|
60
|
+
SidebarProvider,
|
|
61
|
+
} from '@tuturuuu/ui/sidebar';
|
|
62
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
63
|
+
import type { ComponentType, KeyboardEvent, ReactNode } from 'react';
|
|
64
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
65
|
+
import type { createSettingsSearchEngine } from './settings-dialog-search';
|
|
66
|
+
import { loadSettingsSearchEngine } from './settings-dialog-search-loader';
|
|
67
|
+
|
|
68
|
+
export interface SettingsNavItem {
|
|
69
|
+
name: string;
|
|
70
|
+
label: string;
|
|
71
|
+
icon: ComponentType<{ className?: string }>;
|
|
72
|
+
description?: string;
|
|
73
|
+
hideContentHeader?: boolean;
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
keywords?: string[];
|
|
76
|
+
aliases?: string[];
|
|
77
|
+
searchLabels?: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SettingsNavGroup {
|
|
81
|
+
label: string;
|
|
82
|
+
items: SettingsNavItem[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SettingsDialogShellProps {
|
|
86
|
+
t: (
|
|
87
|
+
key:
|
|
88
|
+
| 'common.close'
|
|
89
|
+
| 'common.no_results_found'
|
|
90
|
+
| 'common.settings'
|
|
91
|
+
| 'onboarding_guide.description'
|
|
92
|
+
| 'onboarding_guide.replay_app'
|
|
93
|
+
| 'onboarding_guide.restart_journey'
|
|
94
|
+
| 'onboarding_guide.title'
|
|
95
|
+
| 'search.search'
|
|
96
|
+
| 'settings.back_to_app'
|
|
97
|
+
| 'settings.manage_settings'
|
|
98
|
+
| 'settings.search_settings_placeholder',
|
|
99
|
+
values?: Record<string, string | number>
|
|
100
|
+
) => string;
|
|
101
|
+
persistedTab: string | null;
|
|
102
|
+
setPersistedTab: (value: string) => void;
|
|
103
|
+
onboardingPanel: ReactNode;
|
|
104
|
+
/** Ordered list of navigation groups */
|
|
105
|
+
navItems: SettingsNavGroup[];
|
|
106
|
+
/** Currently active tab name */
|
|
107
|
+
activeTab: string;
|
|
108
|
+
/** Callback when active tab changes */
|
|
109
|
+
onActiveTabChange: (tab: string) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Group labels that should be expanded by default.
|
|
112
|
+
* Used when expandAllAccordions is false.
|
|
113
|
+
* If not provided in that mode, only the first group expands.
|
|
114
|
+
*/
|
|
115
|
+
primaryGroupLabels?: string[];
|
|
116
|
+
/** Override to expand all accordions (user preference). Defaults to expanded. */
|
|
117
|
+
expandAllAccordions?: boolean;
|
|
118
|
+
/** Enable dialog-scoped keyboard shortcuts for search and tab navigation */
|
|
119
|
+
keyboardNavigation?: boolean;
|
|
120
|
+
/** Optional context control that replaces the active group breadcrumb */
|
|
121
|
+
activeGroupBreadcrumb?: ReactNode;
|
|
122
|
+
/** Content to render in the main area */
|
|
123
|
+
children: ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type SettingsSearchEngine = ReturnType<typeof createSettingsSearchEngine>;
|
|
127
|
+
type SettingsSearchEngineFactory = typeof createSettingsSearchEngine;
|
|
128
|
+
|
|
129
|
+
function isEditableShortcutTarget(target: EventTarget | null) {
|
|
130
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
131
|
+
|
|
132
|
+
const tagName = target.tagName.toLowerCase();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
target.isContentEditable ||
|
|
136
|
+
tagName === 'input' ||
|
|
137
|
+
tagName === 'textarea' ||
|
|
138
|
+
tagName === 'select'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Shared settings dialog shell providing the layout framework.
|
|
144
|
+
*
|
|
145
|
+
* Each app provides its own `navItems` (ordered by priority) and
|
|
146
|
+
* renders tab-specific content via `children`. The `primaryGroupLabels`
|
|
147
|
+
* prop controls which groups expand when `expandAllAccordions` is false,
|
|
148
|
+
* enabling apps to highlight their domain while preserving a compact mode.
|
|
149
|
+
*/
|
|
150
|
+
export function SatelliteSettingsDialogShell({
|
|
151
|
+
t,
|
|
152
|
+
persistedTab,
|
|
153
|
+
setPersistedTab,
|
|
154
|
+
onboardingPanel,
|
|
155
|
+
navItems,
|
|
156
|
+
activeTab,
|
|
157
|
+
onActiveTabChange,
|
|
158
|
+
primaryGroupLabels,
|
|
159
|
+
expandAllAccordions = true,
|
|
160
|
+
keyboardNavigation = false,
|
|
161
|
+
activeGroupBreadcrumb,
|
|
162
|
+
children,
|
|
163
|
+
}: SettingsDialogShellProps) {
|
|
164
|
+
const isMobile = useIsMobile();
|
|
165
|
+
const desktopSearchInputRef = useRef<HTMLInputElement>(null);
|
|
166
|
+
const mobileSearchInputRef = useRef<HTMLInputElement>(null);
|
|
167
|
+
const searchEngineLoadRef = useRef<Promise<void> | null>(null);
|
|
168
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
169
|
+
const [searchEngineFactory, setSearchEngineFactory] =
|
|
170
|
+
useState<SettingsSearchEngineFactory | null>(null);
|
|
171
|
+
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
|
172
|
+
const extendedNavItems = useMemo<SettingsNavGroup[]>(
|
|
173
|
+
() => [
|
|
174
|
+
...navItems,
|
|
175
|
+
{
|
|
176
|
+
label: t('onboarding_guide.title'),
|
|
177
|
+
items: [
|
|
178
|
+
{
|
|
179
|
+
name: 'onboarding_guide',
|
|
180
|
+
label: t('onboarding_guide.title'),
|
|
181
|
+
description: t('onboarding_guide.description'),
|
|
182
|
+
icon: Compass,
|
|
183
|
+
keywords: [
|
|
184
|
+
t('onboarding_guide.replay_app'),
|
|
185
|
+
t('onboarding_guide.restart_journey'),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
[navItems, t]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const searchEngine = useMemo<SettingsSearchEngine | null>(
|
|
195
|
+
() => searchEngineFactory?.(extendedNavItems) ?? null,
|
|
196
|
+
[extendedNavItems, searchEngineFactory]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const allNavItems = useMemo(
|
|
200
|
+
() =>
|
|
201
|
+
searchEngine?.allItems ??
|
|
202
|
+
extendedNavItems.flatMap((group) => group.items),
|
|
203
|
+
[extendedNavItems, searchEngine]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const activeGroup = extendedNavItems.find((group) =>
|
|
207
|
+
group.items.some((item) => item.name === activeTab)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const activeItem =
|
|
211
|
+
allNavItems.find((item) => item.name === activeTab) ||
|
|
212
|
+
allNavItems.find((item) => !item.disabled) ||
|
|
213
|
+
allNavItems[0];
|
|
214
|
+
const showContentHeader = !activeItem?.hideContentHeader;
|
|
215
|
+
const persistedItem = allNavItems.find(
|
|
216
|
+
(item) => !item.disabled && item.name === persistedTab
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (persistedItem && persistedItem.name !== activeTab) {
|
|
221
|
+
onActiveTabChange(persistedItem.name);
|
|
222
|
+
}
|
|
223
|
+
}, [activeTab, onActiveTabChange, persistedItem]);
|
|
224
|
+
|
|
225
|
+
const selectActiveTab = useCallback(
|
|
226
|
+
(tab: string) => {
|
|
227
|
+
onActiveTabChange(tab);
|
|
228
|
+
void setPersistedTab(tab);
|
|
229
|
+
},
|
|
230
|
+
[onActiveTabChange, setPersistedTab]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const ensureSearchEngine = useCallback(() => {
|
|
234
|
+
if (searchEngineFactory || searchEngineLoadRef.current) return;
|
|
235
|
+
|
|
236
|
+
searchEngineLoadRef.current = loadSettingsSearchEngine()
|
|
237
|
+
.then((module) => {
|
|
238
|
+
setSearchEngineFactory(() => module.createSettingsSearchEngine);
|
|
239
|
+
})
|
|
240
|
+
.finally(() => {
|
|
241
|
+
searchEngineLoadRef.current = null;
|
|
242
|
+
});
|
|
243
|
+
}, [searchEngineFactory]);
|
|
244
|
+
|
|
245
|
+
const filteredNavItems = useMemo(
|
|
246
|
+
() =>
|
|
247
|
+
searchQuery
|
|
248
|
+
? (searchEngine?.search(searchQuery) ?? extendedNavItems)
|
|
249
|
+
: extendedNavItems,
|
|
250
|
+
[extendedNavItems, searchEngine, searchQuery]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const filteredEnabledItems = useMemo(
|
|
254
|
+
() =>
|
|
255
|
+
searchEngine?.getEnabledItems(searchQuery) ??
|
|
256
|
+
filteredNavItems.flatMap((group) =>
|
|
257
|
+
group.items.filter((item) => !item.disabled)
|
|
258
|
+
),
|
|
259
|
+
[filteredNavItems, searchEngine, searchQuery]
|
|
260
|
+
);
|
|
261
|
+
const keyboardEnabledItems = useMemo(
|
|
262
|
+
() =>
|
|
263
|
+
filteredEnabledItems.filter((item) => item.name !== 'onboarding_guide'),
|
|
264
|
+
[filteredEnabledItems]
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const updateSearchQuery = useCallback(
|
|
268
|
+
(value: string) => {
|
|
269
|
+
if (value) ensureSearchEngine();
|
|
270
|
+
setSearchQuery(value);
|
|
271
|
+
},
|
|
272
|
+
[ensureSearchEngine]
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const isGroupExpandedByDefault = (groupLabel: string, index: number) => {
|
|
276
|
+
if (expandAllAccordions || searchQuery) return true;
|
|
277
|
+
if (primaryGroupLabels) return primaryGroupLabels.includes(groupLabel);
|
|
278
|
+
return index === 0;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const focusSearch = useCallback(() => {
|
|
282
|
+
if (isMobile) {
|
|
283
|
+
setMobileNavOpen(true);
|
|
284
|
+
ensureSearchEngine();
|
|
285
|
+
requestAnimationFrame(() => {
|
|
286
|
+
mobileSearchInputRef.current?.focus();
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
ensureSearchEngine();
|
|
292
|
+
desktopSearchInputRef.current?.focus();
|
|
293
|
+
}, [ensureSearchEngine, isMobile]);
|
|
294
|
+
|
|
295
|
+
const changeActiveItem = useCallback(
|
|
296
|
+
(targetIndex: number) => {
|
|
297
|
+
const targetItem = keyboardEnabledItems[targetIndex];
|
|
298
|
+
if (targetItem) selectActiveTab(targetItem.name);
|
|
299
|
+
},
|
|
300
|
+
[keyboardEnabledItems, selectActiveTab]
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const moveActiveItem = useCallback(
|
|
304
|
+
(direction: 1 | -1) => {
|
|
305
|
+
if (keyboardEnabledItems.length === 0) return;
|
|
306
|
+
|
|
307
|
+
const currentIndex = keyboardEnabledItems.findIndex(
|
|
308
|
+
(item) => item.name === activeTab
|
|
309
|
+
);
|
|
310
|
+
const nextIndex =
|
|
311
|
+
currentIndex === -1
|
|
312
|
+
? direction === 1
|
|
313
|
+
? 0
|
|
314
|
+
: keyboardEnabledItems.length - 1
|
|
315
|
+
: (currentIndex + direction + keyboardEnabledItems.length) %
|
|
316
|
+
keyboardEnabledItems.length;
|
|
317
|
+
|
|
318
|
+
changeActiveItem(nextIndex);
|
|
319
|
+
},
|
|
320
|
+
[activeTab, changeActiveItem, keyboardEnabledItems]
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const handleKeyboardNavigation = useCallback(
|
|
324
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
325
|
+
if (!keyboardNavigation || event.defaultPrevented) return;
|
|
326
|
+
|
|
327
|
+
const key = event.key.toLowerCase();
|
|
328
|
+
const isModifierSearch =
|
|
329
|
+
(event.metaKey || event.ctrlKey) && !event.altKey && key === 'f';
|
|
330
|
+
const isSlashSearch =
|
|
331
|
+
!event.metaKey && !event.ctrlKey && !event.altKey && event.key === '/';
|
|
332
|
+
|
|
333
|
+
if (isModifierSearch || isSlashSearch) {
|
|
334
|
+
if (isSlashSearch && isEditableShortcutTarget(event.target)) return;
|
|
335
|
+
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
focusSearch();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
!event.altKey ||
|
|
343
|
+
event.metaKey ||
|
|
344
|
+
event.ctrlKey ||
|
|
345
|
+
isEditableShortcutTarget(event.target)
|
|
346
|
+
) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (event.key === 'ArrowDown') {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
moveActiveItem(1);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (event.key === 'ArrowUp') {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
moveActiveItem(-1);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event.key === 'Home') {
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
changeActiveItem(0);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (event.key === 'End') {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
changeActiveItem(keyboardEnabledItems.length - 1);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
[
|
|
374
|
+
changeActiveItem,
|
|
375
|
+
keyboardEnabledItems.length,
|
|
376
|
+
focusSearch,
|
|
377
|
+
keyboardNavigation,
|
|
378
|
+
moveActiveItem,
|
|
379
|
+
]
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<DialogContent
|
|
384
|
+
presentation="fullscreen"
|
|
385
|
+
className="flex-col"
|
|
386
|
+
onKeyDown={handleKeyboardNavigation}
|
|
387
|
+
showCloseButton={false}
|
|
388
|
+
>
|
|
389
|
+
<DialogTitle className="sr-only">{t('common.settings')}</DialogTitle>
|
|
390
|
+
<DialogDescription className="sr-only">
|
|
391
|
+
{t('common.settings')}
|
|
392
|
+
</DialogDescription>
|
|
393
|
+
<SidebarProvider className="flex h-full min-h-0 items-start">
|
|
394
|
+
<Sidebar
|
|
395
|
+
collapsible="none"
|
|
396
|
+
className="hidden h-full w-72 flex-col border-r bg-muted/30 md:flex"
|
|
397
|
+
>
|
|
398
|
+
<SidebarHeader className="z-10 gap-3 p-4 pb-0">
|
|
399
|
+
<DialogClose asChild>
|
|
400
|
+
<Button
|
|
401
|
+
type="button"
|
|
402
|
+
variant="ghost"
|
|
403
|
+
className="h-9 justify-start px-2 text-muted-foreground"
|
|
404
|
+
>
|
|
405
|
+
<ArrowLeft className="h-4 w-4" />
|
|
406
|
+
{t('settings.back_to_app')}
|
|
407
|
+
</Button>
|
|
408
|
+
</DialogClose>
|
|
409
|
+
<div className="relative">
|
|
410
|
+
<Search className="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
|
411
|
+
<SidebarInput
|
|
412
|
+
ref={desktopSearchInputRef}
|
|
413
|
+
placeholder={t('settings.search_settings_placeholder')}
|
|
414
|
+
className="bg-background pl-8"
|
|
415
|
+
value={searchQuery}
|
|
416
|
+
onChange={(e) => updateSearchQuery(e.target.value)}
|
|
417
|
+
onFocus={ensureSearchEngine}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
</SidebarHeader>
|
|
421
|
+
<SidebarContent className="overflow-y-auto p-4">
|
|
422
|
+
{filteredNavItems.map((group, index) => (
|
|
423
|
+
<Collapsible
|
|
424
|
+
key={`${group.label}-${searchQuery ? 'search' : 'browse'}-${expandAllAccordions ? 'expanded' : 'collapsed'}`}
|
|
425
|
+
defaultOpen={isGroupExpandedByDefault(group.label, index)}
|
|
426
|
+
open={expandAllAccordions ? true : undefined}
|
|
427
|
+
className="group/collapsible"
|
|
428
|
+
>
|
|
429
|
+
<SidebarGroup className="p-0">
|
|
430
|
+
<SidebarGroupLabel
|
|
431
|
+
asChild
|
|
432
|
+
className="group/label w-full cursor-pointer text-sidebar-foreground text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
433
|
+
>
|
|
434
|
+
<CollapsibleTrigger>
|
|
435
|
+
{group.label}
|
|
436
|
+
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
|
437
|
+
</CollapsibleTrigger>
|
|
438
|
+
</SidebarGroupLabel>
|
|
439
|
+
<CollapsibleContent>
|
|
440
|
+
<SidebarGroupContent>
|
|
441
|
+
<SidebarMenu>
|
|
442
|
+
{group.items.map((item) => (
|
|
443
|
+
<SidebarMenuItem key={item.name}>
|
|
444
|
+
<SidebarMenuButton
|
|
445
|
+
disabled={item.disabled}
|
|
446
|
+
isActive={activeTab === item.name}
|
|
447
|
+
onClick={() => {
|
|
448
|
+
if (!item.disabled) {
|
|
449
|
+
selectActiveTab(item.name);
|
|
450
|
+
}
|
|
451
|
+
}}
|
|
452
|
+
className={cn(
|
|
453
|
+
'h-9 w-full justify-start px-2 transition-colors',
|
|
454
|
+
item.disabled &&
|
|
455
|
+
'cursor-not-allowed opacity-50',
|
|
456
|
+
activeTab === item.name
|
|
457
|
+
? 'bg-accent font-medium text-accent-foreground'
|
|
458
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
|
|
459
|
+
)}
|
|
460
|
+
>
|
|
461
|
+
<item.icon className="mr-2 h-4 w-4" />
|
|
462
|
+
<span>{item.label}</span>
|
|
463
|
+
</SidebarMenuButton>
|
|
464
|
+
</SidebarMenuItem>
|
|
465
|
+
))}
|
|
466
|
+
</SidebarMenu>
|
|
467
|
+
</SidebarGroupContent>
|
|
468
|
+
</CollapsibleContent>
|
|
469
|
+
</SidebarGroup>
|
|
470
|
+
</Collapsible>
|
|
471
|
+
))}
|
|
472
|
+
</SidebarContent>
|
|
473
|
+
</Sidebar>
|
|
474
|
+
<main className="flex h-full flex-1 flex-col overflow-hidden bg-background">
|
|
475
|
+
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-6">
|
|
476
|
+
<div className="flex flex-1 items-center gap-2 md:flex-initial">
|
|
477
|
+
{isMobile && (
|
|
478
|
+
<Drawer open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
|
|
479
|
+
<DrawerTrigger asChild>
|
|
480
|
+
<Button
|
|
481
|
+
variant="outline"
|
|
482
|
+
role="combobox"
|
|
483
|
+
aria-expanded={mobileNavOpen}
|
|
484
|
+
className="w-full flex-1 justify-between gap-2"
|
|
485
|
+
>
|
|
486
|
+
{activeItem && (
|
|
487
|
+
<>
|
|
488
|
+
<activeItem.icon className="h-4 w-4 shrink-0" />
|
|
489
|
+
<span className="flex-1 truncate text-left">
|
|
490
|
+
{activeItem.label}
|
|
491
|
+
</span>
|
|
492
|
+
</>
|
|
493
|
+
)}
|
|
494
|
+
<ChevronDown className="ml-1 h-4 w-4 shrink-0 opacity-50" />
|
|
495
|
+
</Button>
|
|
496
|
+
</DrawerTrigger>
|
|
497
|
+
<DrawerContent>
|
|
498
|
+
<DrawerHeader className="sr-only">
|
|
499
|
+
<DrawerTitle>{t('common.settings')}</DrawerTitle>
|
|
500
|
+
<DrawerDescription>
|
|
501
|
+
{t('search.search')}
|
|
502
|
+
</DrawerDescription>
|
|
503
|
+
</DrawerHeader>
|
|
504
|
+
<Command
|
|
505
|
+
className="rounded-none border-0"
|
|
506
|
+
shouldFilter={false}
|
|
507
|
+
>
|
|
508
|
+
<CommandInput
|
|
509
|
+
ref={mobileSearchInputRef}
|
|
510
|
+
value={searchQuery}
|
|
511
|
+
onFocus={ensureSearchEngine}
|
|
512
|
+
onValueChange={updateSearchQuery}
|
|
513
|
+
placeholder={t('settings.search_settings_placeholder')}
|
|
514
|
+
/>
|
|
515
|
+
<CommandList className="max-h-[50vh]">
|
|
516
|
+
<CommandEmpty>
|
|
517
|
+
{t('common.no_results_found')}
|
|
518
|
+
</CommandEmpty>
|
|
519
|
+
{filteredNavItems.map((group) => (
|
|
520
|
+
<CommandGroup key={group.label} heading={group.label}>
|
|
521
|
+
{group.items.map((item) => (
|
|
522
|
+
<CommandItem
|
|
523
|
+
disabled={item.disabled}
|
|
524
|
+
key={item.name}
|
|
525
|
+
value={`${group.label} ${item.label} ${item.description || ''} ${item.keywords?.join(' ') || ''} ${item.aliases?.join(' ') || ''} ${item.searchLabels?.join(' ') || ''}`}
|
|
526
|
+
onSelect={() => {
|
|
527
|
+
if (item.disabled) return;
|
|
528
|
+
selectActiveTab(item.name);
|
|
529
|
+
setMobileNavOpen(false);
|
|
530
|
+
}}
|
|
531
|
+
className={cn(
|
|
532
|
+
'flex items-center gap-2',
|
|
533
|
+
activeTab === item.name && 'bg-accent'
|
|
534
|
+
)}
|
|
535
|
+
>
|
|
536
|
+
<item.icon className="h-4 w-4" />
|
|
537
|
+
<div className="flex flex-col">
|
|
538
|
+
<span>{item.label}</span>
|
|
539
|
+
{item.description && (
|
|
540
|
+
<span className="line-clamp-1 text-muted-foreground text-xs">
|
|
541
|
+
{item.description}
|
|
542
|
+
</span>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
</CommandItem>
|
|
546
|
+
))}
|
|
547
|
+
</CommandGroup>
|
|
548
|
+
))}
|
|
549
|
+
</CommandList>
|
|
550
|
+
</Command>
|
|
551
|
+
</DrawerContent>
|
|
552
|
+
</Drawer>
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
<Breadcrumb className="hidden md:flex">
|
|
556
|
+
<BreadcrumbList>
|
|
557
|
+
<BreadcrumbItem>
|
|
558
|
+
<BreadcrumbLink href="#" className="pointer-events-none">
|
|
559
|
+
{t('common.settings')}
|
|
560
|
+
</BreadcrumbLink>
|
|
561
|
+
</BreadcrumbItem>
|
|
562
|
+
<BreadcrumbSeparator />
|
|
563
|
+
{activeGroup && (
|
|
564
|
+
<>
|
|
565
|
+
<BreadcrumbItem data-testid="settings-active-group-breadcrumb">
|
|
566
|
+
{activeGroupBreadcrumb ?? (
|
|
567
|
+
<BreadcrumbPage className="text-muted-foreground">
|
|
568
|
+
{activeGroup.label}
|
|
569
|
+
</BreadcrumbPage>
|
|
570
|
+
)}
|
|
571
|
+
</BreadcrumbItem>
|
|
572
|
+
<BreadcrumbSeparator />
|
|
573
|
+
</>
|
|
574
|
+
)}
|
|
575
|
+
<BreadcrumbItem>
|
|
576
|
+
<BreadcrumbPage>{activeItem?.label}</BreadcrumbPage>
|
|
577
|
+
</BreadcrumbItem>
|
|
578
|
+
</BreadcrumbList>
|
|
579
|
+
</Breadcrumb>
|
|
580
|
+
</div>
|
|
581
|
+
<DialogClose asChild>
|
|
582
|
+
<Button
|
|
583
|
+
type="button"
|
|
584
|
+
variant="ghost"
|
|
585
|
+
size="icon"
|
|
586
|
+
className="shrink-0 text-muted-foreground md:hidden"
|
|
587
|
+
>
|
|
588
|
+
<X
|
|
589
|
+
className="h-4 w-4"
|
|
590
|
+
data-testid="settings-mobile-close-icon"
|
|
591
|
+
/>
|
|
592
|
+
<span className="sr-only">{t('common.close')}</span>
|
|
593
|
+
</Button>
|
|
594
|
+
</DialogClose>
|
|
595
|
+
</header>
|
|
596
|
+
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-2 md:p-6">
|
|
597
|
+
<div className="mx-auto w-full max-w-4xl space-y-6">
|
|
598
|
+
{showContentHeader && (
|
|
599
|
+
<>
|
|
600
|
+
<div className="space-y-1">
|
|
601
|
+
<h2 className="font-semibold text-lg tracking-tight">
|
|
602
|
+
{activeItem?.label}
|
|
603
|
+
</h2>
|
|
604
|
+
<p className="text-muted-foreground text-sm">
|
|
605
|
+
{activeItem?.description ||
|
|
606
|
+
t('settings.manage_settings', {
|
|
607
|
+
label: activeItem?.label?.toLowerCase() ?? '',
|
|
608
|
+
})}
|
|
609
|
+
</p>
|
|
610
|
+
</div>
|
|
611
|
+
<Separator />
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
614
|
+
{activeTab === 'onboarding_guide' ? onboardingPanel : children}
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
</main>
|
|
618
|
+
</SidebarProvider>
|
|
619
|
+
</DialogContent>
|
|
620
|
+
);
|
|
621
|
+
}
|