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