@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
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import {
3
+ type AnchorHTMLAttributes,
4
+ type ComponentType,
5
+ createContext,
6
+ useContext,
7
+ } from 'react';
8
+ export interface NotificationRuntimeValue {
9
+ params: { wsId?: string | string[]; locale?: string | string[] };
10
+ pathname: string;
11
+ router: { push: (url: string) => void; refresh: () => void };
12
+ Link: ComponentType<
13
+ AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }
14
+ >;
15
+ realtime: boolean;
16
+ }
17
+ export const NotificationRuntime =
18
+ createContext<NotificationRuntimeValue | null>(null);
19
+ export function useNotificationRuntime() {
20
+ const value = useContext(NotificationRuntime);
21
+ if (!value) throw new Error('NotificationRuntime is required');
22
+ return value;
23
+ }
@@ -1,8 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { MessageSquareText, Play, RotateCcw } from '@tuturuuu/icons';
4
3
  import { updateConnectedOnboardingProgress } from '@tuturuuu/internal-api/onboarding';
5
- import { Button } from '@tuturuuu/ui/button';
6
4
  import {
7
5
  getLaunchableApp,
8
6
  getLaunchableAppByHostname,
@@ -10,6 +8,7 @@ import {
10
8
  } from '@tuturuuu/utils/launchable-apps';
11
9
  import { useTranslations } from 'next-intl';
12
10
  import { useState } from 'react';
11
+ import { SatelliteOnboardingSettingsPanel } from './satellite-onboarding-settings-panel';
13
12
 
14
13
  function currentAppSlug() {
15
14
  if (typeof window === 'undefined') return 'platform';
@@ -73,59 +72,13 @@ export function OnboardingSettingsPanel() {
73
72
  );
74
73
  };
75
74
 
76
- const actions = [
77
- {
78
- description: t('replay_app_description'),
79
- icon: Play,
80
- label: t('replay_app'),
81
- onClick: replayApp,
82
- pending: pending === 'replay',
83
- },
84
- {
85
- description: t('restart_journey_description'),
86
- icon: RotateCcw,
87
- label: t('restart_journey'),
88
- onClick: restartJourney,
89
- pending: pending === 'restart',
90
- },
91
- {
92
- description: t('feedback_description'),
93
- icon: MessageSquareText,
94
- label: t('feedback'),
95
- onClick: shareFeedback,
96
- pending: false,
97
- },
98
- ];
99
-
100
75
  return (
101
- <div className="space-y-3">
102
- {actions.map((action) => (
103
- <div
104
- key={action.label}
105
- className="flex flex-col gap-4 rounded-xl border p-4 sm:flex-row sm:items-center sm:justify-between"
106
- >
107
- <div className="flex gap-3">
108
- <div className="grid size-9 shrink-0 place-items-center rounded-lg border bg-muted/40">
109
- <action.icon className="size-4" />
110
- </div>
111
- <div>
112
- <p className="font-medium text-sm">{action.label}</p>
113
- <p className="mt-1 max-w-xl text-muted-foreground text-sm">
114
- {action.description}
115
- </p>
116
- </div>
117
- </div>
118
- <Button
119
- type="button"
120
- variant="outline"
121
- disabled={action.pending}
122
- onClick={action.onClick}
123
- >
124
- <action.icon className="size-4" />
125
- {action.label}
126
- </Button>
127
- </div>
128
- ))}
129
- </div>
76
+ <SatelliteOnboardingSettingsPanel
77
+ t={t}
78
+ pending={pending}
79
+ replayApp={replayApp}
80
+ restartJourney={restartJourney}
81
+ shareFeedback={shareFeedback}
82
+ />
130
83
  );
131
84
  }
@@ -0,0 +1,212 @@
1
+ 'use client';
2
+
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { Check, ExternalLink, Trash2, User, Users } from '@tuturuuu/icons';
5
+ import type { WebAccountSummary } from '@tuturuuu/internal-api/auth';
6
+ import {
7
+ listWebAccountsWithInternalApi,
8
+ removeWebAccountWithInternalApi,
9
+ switchWebAccountWithInternalApi,
10
+ } from '@tuturuuu/internal-api/auth';
11
+ import {
12
+ DropdownMenuItem,
13
+ DropdownMenuLabel,
14
+ DropdownMenuPortal,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuSub,
17
+ DropdownMenuSubContent,
18
+ DropdownMenuSubTrigger,
19
+ } from '@tuturuuu/ui/dropdown-menu';
20
+
21
+ interface SatelliteAccountSwitcherMenuProps {
22
+ centralUrl: string;
23
+ workspaceId?: string;
24
+ currentRoute: string;
25
+ t: (
26
+ key:
27
+ | 'accounts'
28
+ | 'current_account'
29
+ | 'manage_accounts'
30
+ | 'please_wait'
31
+ | 'remove_account'
32
+ | 'switch_account'
33
+ | 'unknown_user'
34
+ ) => string;
35
+ onAccountChanged?: () => void;
36
+ }
37
+
38
+ const accountQueryKey = (centralUrl: string) =>
39
+ ['satellite', 'web-accounts', centralUrl] as const;
40
+
41
+ function accountLabel(account: WebAccountSummary, fallback: string) {
42
+ return (
43
+ account.metadata.displayName?.trim() || account.email?.trim() || fallback
44
+ );
45
+ }
46
+
47
+ export function webAccountSettingsUrl(
48
+ centralUrl: string,
49
+ workspaceId?: string
50
+ ) {
51
+ let end = centralUrl.length;
52
+ while (end > 0 && centralUrl[end - 1] === '/') end -= 1;
53
+
54
+ const baseUrl = centralUrl.slice(0, end);
55
+ if (!workspaceId) return `${baseUrl}/add-account`;
56
+
57
+ return `${baseUrl}/${encodeURIComponent(workspaceId)}?settingsDialog=open&settingsTab=accounts`;
58
+ }
59
+
60
+ export function SatelliteAccountSwitcherMenu({
61
+ centralUrl,
62
+ workspaceId,
63
+ currentRoute,
64
+ t,
65
+ onAccountChanged = () => window.location.reload(),
66
+ }: SatelliteAccountSwitcherMenuProps) {
67
+ const queryClient = useQueryClient();
68
+ const queryKey = accountQueryKey(centralUrl);
69
+ const accountsQuery = useQuery({
70
+ enabled: Boolean(centralUrl),
71
+ queryFn: () => listWebAccountsWithInternalApi({ baseUrl: centralUrl }),
72
+ queryKey,
73
+ retry: 1,
74
+ staleTime: 30_000,
75
+ });
76
+ const invalidateAccounts = () => queryClient.invalidateQueries({ queryKey });
77
+ const switchMutation = useMutation({
78
+ mutationFn: (accountId: string) =>
79
+ switchWebAccountWithInternalApi(
80
+ accountId,
81
+ {
82
+ currentRoute,
83
+ },
84
+ { baseUrl: centralUrl }
85
+ ),
86
+ onSuccess: async (response) => {
87
+ await invalidateAccounts();
88
+ if (response.success) onAccountChanged();
89
+ },
90
+ });
91
+ const removeMutation = useMutation({
92
+ mutationFn: (accountId: string) =>
93
+ removeWebAccountWithInternalApi(accountId, { baseUrl: centralUrl }),
94
+ onSuccess: async (response, accountId) => {
95
+ await invalidateAccounts();
96
+ if (
97
+ response.success &&
98
+ accountId === accountsQuery.data?.activeAccountId
99
+ ) {
100
+ onAccountChanged();
101
+ }
102
+ },
103
+ });
104
+ const accounts = accountsQuery.data?.accounts ?? [];
105
+ const activeAccountId = accountsQuery.data?.activeAccountId ?? null;
106
+ const isMutating = switchMutation.isPending || removeMutation.isPending;
107
+
108
+ if (accountsQuery.isLoading) {
109
+ return (
110
+ <DropdownMenuItem disabled>
111
+ <Users className="h-4 w-4 text-dynamic-orange" />
112
+ <span>{t('please_wait')}</span>
113
+ </DropdownMenuItem>
114
+ );
115
+ }
116
+
117
+ if (accounts.length === 0) {
118
+ return (
119
+ <DropdownMenuItem asChild>
120
+ <a
121
+ href={webAccountSettingsUrl(centralUrl, workspaceId)}
122
+ target="_blank"
123
+ rel="noopener noreferrer"
124
+ className="cursor-pointer"
125
+ >
126
+ <Users className="h-4 w-4 text-dynamic-orange" />
127
+ <span>{t('manage_accounts')}</span>
128
+ </a>
129
+ </DropdownMenuItem>
130
+ );
131
+ }
132
+
133
+ return (
134
+ <DropdownMenuSub>
135
+ <DropdownMenuSubTrigger>
136
+ <Users className="h-4 w-4 text-dynamic-orange" />
137
+ <span>{t('switch_account')}</span>
138
+ </DropdownMenuSubTrigger>
139
+ <DropdownMenuPortal>
140
+ <DropdownMenuSubContent sideOffset={4} className="w-72">
141
+ <DropdownMenuLabel>{t('accounts')}</DropdownMenuLabel>
142
+ {accounts.map((account) => {
143
+ const isActive = account.id === activeAccountId;
144
+ const label = accountLabel(account, t('unknown_user'));
145
+
146
+ return (
147
+ <DropdownMenuItem
148
+ key={account.id}
149
+ disabled={isActive || isMutating}
150
+ onSelect={() => {
151
+ if (!isActive) switchMutation.mutate(account.id);
152
+ }}
153
+ >
154
+ <User className="h-4 w-4" />
155
+ <span className="min-w-0 flex-1">
156
+ <span className="block truncate">{label}</span>
157
+ {account.email && (
158
+ <span className="block truncate text-xs opacity-70">
159
+ {account.email}
160
+ </span>
161
+ )}
162
+ </span>
163
+ {isActive && <Check className="h-4 w-4" />}
164
+ </DropdownMenuItem>
165
+ );
166
+ })}
167
+ <DropdownMenuSeparator />
168
+ <DropdownMenuSub>
169
+ <DropdownMenuSubTrigger disabled={isMutating}>
170
+ <Trash2 className="h-4 w-4 text-dynamic-red" />
171
+ <span>{t('remove_account')}</span>
172
+ </DropdownMenuSubTrigger>
173
+ <DropdownMenuPortal>
174
+ <DropdownMenuSubContent sideOffset={4} className="w-72">
175
+ {accounts.map((account) => {
176
+ const isActive = account.id === activeAccountId;
177
+ const label = accountLabel(account, t('unknown_user'));
178
+
179
+ return (
180
+ <DropdownMenuItem
181
+ key={account.id}
182
+ disabled={isActive || isMutating}
183
+ onSelect={() => {
184
+ if (!isActive) removeMutation.mutate(account.id);
185
+ }}
186
+ >
187
+ <Trash2 className="h-4 w-4 text-dynamic-red" />
188
+ <span className="truncate">
189
+ {isActive ? t('current_account') : label}
190
+ </span>
191
+ </DropdownMenuItem>
192
+ );
193
+ })}
194
+ </DropdownMenuSubContent>
195
+ </DropdownMenuPortal>
196
+ </DropdownMenuSub>
197
+ <DropdownMenuItem asChild>
198
+ <a
199
+ href={webAccountSettingsUrl(centralUrl, workspaceId)}
200
+ target="_blank"
201
+ rel="noopener noreferrer"
202
+ className="cursor-pointer"
203
+ >
204
+ <ExternalLink className="h-4 w-4" />
205
+ <span>{t('manage_accounts')}</span>
206
+ </a>
207
+ </DropdownMenuItem>
208
+ </DropdownMenuSubContent>
209
+ </DropdownMenuPortal>
210
+ </DropdownMenuSub>
211
+ );
212
+ }
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Building2 as BuildingIcon,
5
+ ChevronDown,
6
+ EyeDashed as EyeDashedIcon,
7
+ } from '@tuturuuu/icons';
8
+ import { Button } from '@tuturuuu/ui/button';
9
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
10
+ import { cn } from '@tuturuuu/utils/format';
11
+ import type { ComponentProps, ComponentType, ReactNode } from 'react';
12
+
13
+ export type ShellLinkComponent = ComponentType<
14
+ ComponentProps<'a'> & { href: string }
15
+ >;
16
+ export const ShellAnchor: ShellLinkComponent = (props) => <a {...props} />;
17
+
18
+ interface WorkspaceSelectVisibilityToggleProps {
19
+ hideLabel: string;
20
+ onToggle: () => void;
21
+ showLabel: string;
22
+ visible: boolean;
23
+ }
24
+
25
+ export function WorkspaceSelectVisibilityToggle({
26
+ hideLabel,
27
+ onToggle,
28
+ showLabel,
29
+ visible,
30
+ }: WorkspaceSelectVisibilityToggleProps) {
31
+ const label = visible ? hideLabel : showLabel;
32
+
33
+ return (
34
+ <Tooltip>
35
+ <TooltipTrigger asChild>
36
+ <Button
37
+ aria-controls="sidebar-workspace-selector"
38
+ aria-expanded={visible}
39
+ aria-label={label}
40
+ className="size-8 shrink-0 text-muted-foreground shadow-none hover:text-foreground"
41
+ data-slot="workspace-select-visibility-toggle"
42
+ onClick={onToggle}
43
+ size="icon"
44
+ type="button"
45
+ variant="ghost"
46
+ >
47
+ {visible ? (
48
+ <EyeDashedIcon aria-hidden className="size-4" />
49
+ ) : (
50
+ <BuildingIcon aria-hidden className="size-4" />
51
+ )}
52
+ </Button>
53
+ </TooltipTrigger>
54
+ <TooltipContent side="bottom">{label}</TooltipContent>
55
+ </Tooltip>
56
+ );
57
+ }
58
+
59
+ export interface SatelliteBrandProps {
60
+ LinkComponent?: ShellLinkComponent;
61
+ logo: ReactNode;
62
+ appName: string;
63
+ actions?: ReactNode;
64
+ appHref: string;
65
+ centralHref: string;
66
+ className?: string;
67
+ launcherLabel?: string;
68
+ onAppClick?: () => void;
69
+ }
70
+
71
+ export function SatelliteBrand({
72
+ LinkComponent: Link = ShellAnchor,
73
+ logo,
74
+ appName,
75
+ actions,
76
+ appHref,
77
+ centralHref,
78
+ className,
79
+ launcherLabel = 'Open apps',
80
+ onAppClick,
81
+ }: SatelliteBrandProps) {
82
+ const appControl = onAppClick ? (
83
+ <button
84
+ aria-haspopup="dialog"
85
+ aria-label={launcherLabel}
86
+ className="group flex min-w-0 items-center gap-1.5 rounded-lg px-1.5 py-1 font-semibold text-lg tracking-tight transition hover:bg-foreground/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
87
+ onClick={onAppClick}
88
+ type="button"
89
+ >
90
+ <span className="truncate">{appName}</span>
91
+ <ChevronDown
92
+ aria-hidden
93
+ className="size-4 shrink-0 text-muted-foreground"
94
+ />
95
+ </button>
96
+ ) : (
97
+ <Link
98
+ className="min-w-0 truncate rounded-md font-semibold text-lg tracking-tight transition hover:text-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
99
+ href={appHref}
100
+ >
101
+ {appName}
102
+ </Link>
103
+ );
104
+
105
+ return (
106
+ <div className={cn('flex min-w-0 flex-1 items-center gap-2.5', className)}>
107
+ <Link
108
+ aria-label="Tuturuuu"
109
+ className="flex size-9 shrink-0 items-center justify-center rounded-xl transition hover:bg-foreground/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
110
+ href={centralHref}
111
+ >
112
+ {logo}
113
+ </Link>
114
+ <span
115
+ aria-hidden
116
+ className="h-5 w-px shrink-0 self-center rounded-full bg-foreground/10"
117
+ />
118
+ {appControl}
119
+ {actions ? <div className="ml-auto flex-none">{actions}</div> : null}
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import type { NavLink } from '@tuturuuu/ui/custom/navigation';
4
+ import { cn } from '@tuturuuu/utils/format';
5
+ import {
6
+ type ComponentType,
7
+ type Dispatch,
8
+ type ReactNode,
9
+ type SetStateAction,
10
+ Suspense,
11
+ } from 'react';
12
+ import type {
13
+ NavigationState,
14
+ WorkspaceSelectRenderer,
15
+ } from './satellite-shell-utils';
16
+ import { SidebarWorkspaceSelectSection } from './satellite-workspace-section';
17
+
18
+ export interface SatelliteContentProps {
19
+ Navigation: ComponentType<{
20
+ wsId: string;
21
+ isCollapsed: boolean;
22
+ links: (NavLink | null)[];
23
+ onClick: () => void;
24
+ onSubMenuClick: (links: (NavLink | null)[], title: string) => void;
25
+ }>;
26
+ backButton: NavLink;
27
+ currentTitle?: string;
28
+ extraContent?: ReactNode;
29
+ filteredCurrentLinks: (NavLink | null)[];
30
+ isCollapsed: boolean;
31
+ navState: NavigationState;
32
+ setIsCollapsed: Dispatch<SetStateAction<boolean>>;
33
+ setNavState: Dispatch<SetStateAction<NavigationState>>;
34
+ workspaceSelect?: WorkspaceSelectRenderer;
35
+ workspaceSelectVisible: boolean;
36
+ wsId: string;
37
+ }
38
+
39
+ export function SatelliteContent({
40
+ Navigation: Nav,
41
+ backButton,
42
+ currentTitle,
43
+ extraContent,
44
+ filteredCurrentLinks,
45
+ isCollapsed,
46
+ navState,
47
+ setIsCollapsed,
48
+ setNavState,
49
+ workspaceSelect,
50
+ workspaceSelectVisible,
51
+ wsId,
52
+ }: SatelliteContentProps) {
53
+ const handleSubMenuClick = (
54
+ newLinks: (NavLink | null)[],
55
+ parentTitle: string
56
+ ) => {
57
+ setNavState((prevState) => ({
58
+ currentLinks: newLinks,
59
+ direction: 'forward',
60
+ history: [...prevState.history, prevState.currentLinks],
61
+ titleHistory: [...prevState.titleHistory, parentTitle],
62
+ }));
63
+ };
64
+
65
+ const closeOnMobile = () => {
66
+ if (window.innerWidth < 768) setIsCollapsed(true);
67
+ };
68
+
69
+ return (
70
+ <div className="relative flex h-full min-h-0 flex-col overflow-hidden">
71
+ <SidebarWorkspaceSelectSection
72
+ visible={Boolean(workspaceSelect && workspaceSelectVisible)}
73
+ >
74
+ {workspaceSelect ? (
75
+ <Suspense
76
+ fallback={
77
+ <div className="h-8 w-full animate-pulse rounded-md bg-foreground/5" />
78
+ }
79
+ >
80
+ {workspaceSelect({ isCollapsed, standalone: true })}
81
+ </Suspense>
82
+ ) : null}
83
+ </SidebarWorkspaceSelectSection>
84
+ <div
85
+ key={navState.history.length}
86
+ className={cn(
87
+ 'min-h-0 flex-1 transition-transform duration-300 ease-in-out',
88
+ navState.direction === 'forward'
89
+ ? 'slide-in-from-right animate-in'
90
+ : 'slide-in-from-left animate-in'
91
+ )}
92
+ >
93
+ {navState.history.length === 0 ? (
94
+ <div className="flex h-full min-h-0 flex-col">
95
+ <div
96
+ className={cn(
97
+ 'scrollbar-none overflow-y-auto',
98
+ extraContent ? 'shrink-0' : 'min-h-0 flex-1'
99
+ )}
100
+ >
101
+ <Nav
102
+ isCollapsed={isCollapsed}
103
+ links={filteredCurrentLinks}
104
+ onClick={closeOnMobile}
105
+ onSubMenuClick={handleSubMenuClick}
106
+ wsId={wsId}
107
+ />
108
+ </div>
109
+ {extraContent ? (
110
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
111
+ {extraContent}
112
+ </div>
113
+ ) : null}
114
+ </div>
115
+ ) : (
116
+ <>
117
+ <Nav
118
+ isCollapsed={isCollapsed}
119
+ links={[backButton]}
120
+ onClick={() => null}
121
+ onSubMenuClick={() => null}
122
+ wsId={wsId}
123
+ />
124
+ {!isCollapsed && currentTitle ? (
125
+ <div className="p-2 pt-0">
126
+ <h2 className="line-clamp-1 px-2 font-semibold text-muted-foreground text-sm uppercase tracking-wide">
127
+ {currentTitle}
128
+ </h2>
129
+ </div>
130
+ ) : null}
131
+ {!isCollapsed ? <div className="mx-4 my-1 border-b" /> : null}
132
+ {filteredCurrentLinks.length > 0 ? (
133
+ <div className="scrollbar-none min-h-0 overflow-y-auto">
134
+ <Nav
135
+ isCollapsed={isCollapsed}
136
+ links={filteredCurrentLinks}
137
+ onClick={closeOnMobile}
138
+ onSubMenuClick={handleSubMenuClick}
139
+ wsId={wsId}
140
+ />
141
+ </div>
142
+ ) : null}
143
+ </>
144
+ )}
145
+ </div>
146
+ </div>
147
+ );
148
+ }