@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.
- package/CHANGELOG.md +41 -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/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,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
|
-
<
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|