@tuturuuu/ui 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/biome.json +1 -1
  3. package/package.json +95 -49
  4. package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
  5. package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
  7. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
  8. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
  9. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
  10. package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
  11. package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
  12. package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
  13. package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
  14. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
  15. package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
  16. package/src/components/ui/custom/nav-link.tsx +7 -7
  17. package/src/components/ui/custom/nav.tsx +15 -63
  18. package/src/components/ui/custom/notification-card.tsx +361 -0
  19. package/src/components/ui/custom/notification-list.tsx +178 -0
  20. package/src/components/ui/custom/notification-popover-client.tsx +12 -780
  21. package/src/components/ui/custom/notification-runtime.tsx +23 -0
  22. package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
  23. package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
  24. package/src/components/ui/custom/satellite-brand.tsx +122 -0
  25. package/src/components/ui/custom/satellite-content.tsx +148 -0
  26. package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
  27. package/src/components/ui/custom/satellite-language-item.tsx +26 -0
  28. package/src/components/ui/custom/satellite-navigation.tsx +86 -0
  29. package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
  30. package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
  31. package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
  32. package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
  33. package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
  34. package/src/components/ui/custom/satellite-shell.tsx +50 -0
  35. package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
  36. package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
  37. package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
  38. package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
  39. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
  40. package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
  41. package/src/components/ui/custom/sidebar-context.tsx +5 -14
  42. package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
  43. package/src/components/ui/custom/structure.tsx +14 -0
  44. package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
  45. package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
  46. package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
  47. package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
  48. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
  49. package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
  50. package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
  51. package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
  52. package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
  53. package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
  54. package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
  56. package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
  57. package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
  58. package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
  59. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
  60. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
  61. package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
  62. package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
  63. package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
  64. package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
  65. package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
  66. package/src/components/ui/pwa-status.test.tsx +53 -0
  67. package/src/components/ui/pwa-status.tsx +113 -0
  68. package/src/components/ui/report-problem-dialog-content.tsx +350 -0
  69. package/src/components/ui/report-problem-dialog.tsx +6 -975
  70. package/src/components/ui/report-problem-model.ts +133 -0
  71. package/src/components/ui/report-problem-mutation.ts +122 -0
  72. package/src/components/ui/save-notification.test.tsx +36 -0
  73. package/src/components/ui/save-notification.tsx +17 -0
  74. package/src/components/ui/use-report-problem.ts +488 -0
  75. package/src/declarations.d.ts +2 -0
  76. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
  77. package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
  78. package/src/hooks/calendar-date-utils.ts +11 -0
  79. package/src/hooks/calendar-event-lookup.test.ts +77 -0
  80. package/src/hooks/calendar-event-lookup.ts +47 -0
  81. package/src/hooks/calendar-range-cache.ts +52 -0
  82. package/src/hooks/pwa-install-prompt.ts +44 -0
  83. package/src/hooks/use-calendar-sync.tsx +23 -100
  84. package/src/hooks/use-calendar.tsx +21 -77
  85. package/src/hooks/use-notifications.ts +9 -24
  86. package/src/hooks/use-open-initial-calendar-event.ts +27 -0
  87. package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
  88. package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
  89. package/src/hooks/use-workspace-presence.ts +0 -18
  90. package/src/lib/tasks-app-url.test.ts +25 -0
  91. 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 type { createSettingsSearchEngine } from './settings-dialog-search';
69
- import { loadSettingsSearchEngine } from './settings-dialog-search-loader';
70
-
71
- export interface SettingsNavItem {
72
- name: string;
73
- label: string;
74
- icon: ComponentType<{ className?: string }>;
75
- description?: string;
76
- hideContentHeader?: boolean;
77
- disabled?: boolean;
78
- keywords?: string[];
79
- aliases?: string[];
80
- searchLabels?: string[];
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
- <DialogContent
375
- presentation="fullscreen"
376
- className="flex-col"
377
- onKeyDown={handleKeyboardNavigation}
378
- showCloseButton={false}
379
- >
380
- <DialogTitle className="sr-only">{t('common.settings')}</DialogTitle>
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.defaultPrevented ||
190
+ isShortcutEventIgnored(event) ||
200
191
  event.key.toLowerCase() !== 'b' ||
201
192
  !(event.metaKey || event.ctrlKey) ||
202
193
  event.shiftKey ||