|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import type { DesktopUpdateState } from '@sim/desktop-bridge' |
4 | | -import { |
5 | | - Chip, |
6 | | - chipContentLabelClass, |
7 | | - chipPrimaryFillTokens, |
8 | | - chipVariants, |
9 | | - cn, |
10 | | - DropdownMenu, |
11 | | - DropdownMenuContent, |
12 | | - DropdownMenuItem, |
13 | | - DropdownMenuItemLabel, |
14 | | - DropdownMenuSeparator, |
15 | | - DropdownMenuTrigger, |
16 | | - OverflowText, |
17 | | - Skeleton, |
18 | | -} from '@sim/emcn' |
19 | | -import { BookOpen, Download, HelpCircle, Settings } from '@sim/emcn/icons' |
20 | | -import { SlackIcon } from '@/components/icons' |
21 | | -import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link' |
22 | | -import { getDesktopUpdates } from '@/lib/desktop' |
| 3 | +import type { ComponentProps } from 'react' |
| 4 | +import { useRouter } from 'next/navigation' |
23 | 5 | import { organizationRoutes } from '@/lib/navigation/paths' |
24 | | -import { getUserColor } from '@/lib/workspaces/colors' |
25 | 6 | import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider' |
26 | | -import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components' |
27 | | -import { |
28 | | - SIDEBAR_ITEM_GAP_CLASS, |
29 | | - SIDEBAR_RAIL_CHIP_CLASS, |
30 | | -} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' |
31 | | -import { useUserProfile } from '@/hooks/queries/user-profile' |
32 | | -import { useDesktopUpdateState } from '@/hooks/use-desktop-update-state' |
| 7 | +import { SidebarFooter } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer' |
33 | 8 |
|
34 | | -function hasAvailableDesktopUpdate(state: DesktopUpdateState): boolean { |
35 | | - return state.status === 'available' || state.status === 'downloading' || state.status === 'ready' |
36 | | -} |
37 | | - |
38 | | -function desktopUpdateActionLabel(state: DesktopUpdateState): string { |
39 | | - if (state.status === 'downloading') { |
40 | | - return state.percent === undefined |
41 | | - ? 'Downloading update…' |
42 | | - : `Downloading update ${state.percent}%` |
43 | | - } |
44 | | - return state.status === 'ready' ? 'Restart to update' : 'Update' |
45 | | -} |
46 | | - |
47 | | -/** Compact primary update circle using the same footprint as the surrounding sidebar icons. */ |
48 | | -function DesktopUpdateIcon({ className }: { className?: string }) { |
49 | | - return ( |
50 | | - <div |
51 | | - className={cn( |
52 | | - className, |
53 | | - 'flex size-[17px] shrink-0 items-center justify-center rounded-full', |
54 | | - chipPrimaryFillTokens |
55 | | - )} |
56 | | - > |
57 | | - {/* Download's default viewBox is asymmetric around its paths. Center the |
58 | | - artwork itself, not merely its SVG box, inside the avatar-sized circle. */} |
59 | | - <Download className='size-[11px]' viewBox='-1.75 -1.75 24 24' /> |
60 | | - </div> |
61 | | - ) |
62 | | -} |
| 9 | +interface OrganizationFooterProps |
| 10 | + extends Omit< |
| 11 | + ComponentProps<typeof SidebarFooter>, |
| 12 | + 'accountSettingsHref' | 'onOpenAccountSettings' | 'navigationLinks' |
| 13 | + > {} |
63 | 14 |
|
64 | | -interface OrganizationFooterProps { |
65 | | - /** |
66 | | - * True while the scroll region above still hides rows beyond its bottom edge — |
67 | | - * the same test the divider under the pinned nav applies at the top. The bar's |
68 | | - * top rule is drawn only then, so a list that fits meets the footer with no line. |
69 | | - */ |
70 | | - showDivider: boolean |
71 | | - isCollapsed: boolean |
72 | | - showCollapsedTooltips: boolean |
73 | | - onOpenDocs: () => void |
74 | | - onJoinSlack: () => void |
75 | | -} |
76 | | - |
77 | | -/** |
78 | | - * Pinned bottom bar of the organization sidebar: the viewer's avatar and name, |
79 | | - * which open their account settings, plus a help menu. Same two elements and the |
80 | | - * same layout as the workspace footer — expanded they share one row with help hard |
81 | | - * right, collapsed they stack as icon chips with help on top. |
82 | | - * |
83 | | - * Collapsed reverses the flex direction instead of reordering the DOM, which keeps |
84 | | - * both elements (and the help menu's trigger) alive across a toggle. |
85 | | - */ |
86 | | -export function OrganizationFooter({ |
87 | | - showDivider, |
88 | | - isCollapsed, |
89 | | - showCollapsedTooltips, |
90 | | - onOpenDocs, |
91 | | - onJoinSlack, |
92 | | -}: OrganizationFooterProps) { |
| 15 | +export function OrganizationFooter(props: OrganizationFooterProps) { |
93 | 16 | const { organization } = useOrganizationContext() |
94 | | - const { data: profile } = useUserProfile() |
95 | | - const updateState = useDesktopUpdateState() |
96 | | - |
97 | | - const name = profile ? profile.name?.trim() || profile.email : '' |
98 | | - const updateAvailable = hasAvailableDesktopUpdate(updateState) |
99 | | - |
100 | | - const handleUpdateSelect = () => { |
101 | | - const updates = getDesktopUpdates() |
102 | | - if (updateState.status === 'ready') { |
103 | | - updates?.install() |
104 | | - } else if (updateState.status === 'available') { |
105 | | - updates?.check() |
106 | | - } |
107 | | - } |
108 | | - |
109 | | - /** |
110 | | - * Plain `img`/`div` rather than the emcn `Avatar`, whose Radix root renders a |
111 | | - * `<span>` — and globals fade every `span` in the collapsed rail to `opacity: 0`, |
112 | | - * which would blank the avatar exactly where it is the only thing left to see. |
113 | | - */ |
114 | | - const avatar = !profile ? ( |
115 | | - <Skeleton className='size-[16px] shrink-0 rounded-full' /> |
116 | | - ) : profile.image ? ( |
117 | | - <img |
118 | | - src={profile.image} |
119 | | - alt='' |
120 | | - referrerPolicy='no-referrer' |
121 | | - className='size-[16px] shrink-0 rounded-full object-cover' |
122 | | - /> |
123 | | - ) : ( |
124 | | - <div |
125 | | - className='flex size-[16px] shrink-0 items-center justify-center rounded-full text-[9px] text-white leading-none' |
126 | | - style={{ backgroundColor: getUserColor(profile.id) }} |
127 | | - > |
128 | | - {name.charAt(0).toUpperCase()} |
129 | | - </div> |
130 | | - ) |
131 | | - |
132 | | - /** |
133 | | - * Expanded, the chip hugs its content (`max-w-full` so a long name truncates |
134 | | - * rather than overflowing); collapsed, `fullWidth` fills the narrow rail and |
135 | | - * `min-w-0` lets the hidden label give up its box so the chip never overflows it. |
136 | | - * The name is the button's accessible name — no `aria-label`, which would |
137 | | - * override the visible text. |
138 | | - */ |
139 | | - const profileMenu = ( |
140 | | - <DropdownMenu> |
141 | | - <SidebarTooltip label={name} enabled={showCollapsedTooltips && Boolean(name)}> |
142 | | - <DropdownMenuTrigger asChild> |
143 | | - <button |
144 | | - type='button' |
145 | | - data-item-id='profile' |
146 | | - className={cn( |
147 | | - chipVariants({ fullWidth: isCollapsed }), |
148 | | - isCollapsed ? 'min-w-0' : 'max-w-full', |
149 | | - SIDEBAR_RAIL_CHIP_CLASS |
150 | | - )} |
151 | | - > |
152 | | - {avatar} |
153 | | - {profile ? ( |
154 | | - <OverflowText |
155 | | - label={name} |
156 | | - className={cn('sidebar-collapse-hide flex-1', chipContentLabelClass)} |
157 | | - tooltipEnabled={!isCollapsed && !showCollapsedTooltips} |
158 | | - focusTarget='nearest-interactive' |
159 | | - /> |
160 | | - ) : ( |
161 | | - /* Fixed width — the chip hugs its content, so a flexible bar would collapse to nothing. */ |
162 | | - <Skeleton className='sidebar-collapse-hide h-[14px] w-[96px] rounded-sm' /> |
163 | | - )} |
164 | | - </button> |
165 | | - </DropdownMenuTrigger> |
166 | | - </SidebarTooltip> |
167 | | - <DropdownMenuContent align='start' side='top' sideOffset={4}> |
168 | | - <DropdownMenuItem asChild> |
169 | | - <SettingsGuardedLink |
170 | | - href={organizationRoutes(organization.id).settingsSection('general')} |
171 | | - > |
172 | | - <Settings className='size-[14px]' /> |
173 | | - <DropdownMenuItemLabel label='Settings' /> |
174 | | - </SettingsGuardedLink> |
175 | | - </DropdownMenuItem> |
176 | | - </DropdownMenuContent> |
177 | | - </DropdownMenu> |
178 | | - ) |
179 | | - |
180 | | - /** |
181 | | - * One node across both states; only `fullWidth` changes, so the same Radix menu |
182 | | - * survives the transition. `shrink-0` keeps the chip off the avatar while the rail |
183 | | - * is briefly narrower than the row — the aside's clip hides it until there is room. |
184 | | - */ |
185 | | - const helpMenu = ( |
186 | | - <DropdownMenu> |
187 | | - <SidebarTooltip |
188 | | - label={updateAvailable ? 'Help — update available' : 'Help'} |
189 | | - enabled={showCollapsedTooltips} |
190 | | - > |
191 | | - <DropdownMenuTrigger asChild> |
192 | | - <Chip |
193 | | - data-item-id='help' |
194 | | - aria-label={updateAvailable ? 'Help, update available' : 'Help'} |
195 | | - leftIcon={updateAvailable ? DesktopUpdateIcon : HelpCircle} |
196 | | - fullWidth={isCollapsed} |
197 | | - className={cn('shrink-0', SIDEBAR_RAIL_CHIP_CLASS)} |
198 | | - /> |
199 | | - </DropdownMenuTrigger> |
200 | | - </SidebarTooltip> |
201 | | - {/* Anchored to whichever edge the trigger sits on, so the menu never overhangs the rail. */} |
202 | | - <DropdownMenuContent align={isCollapsed ? 'start' : 'end'} side='top' sideOffset={4}> |
203 | | - {updateAvailable && ( |
204 | | - <> |
205 | | - <DropdownMenuItem |
206 | | - onSelect={handleUpdateSelect} |
207 | | - disabled={updateState.status === 'downloading'} |
208 | | - > |
209 | | - <img src='/favicon/favicon-32x32.png' alt='' className='size-[14px] rounded-[3px]' /> |
210 | | - {desktopUpdateActionLabel(updateState)} |
211 | | - </DropdownMenuItem> |
212 | | - <DropdownMenuSeparator /> |
213 | | - </> |
214 | | - )} |
215 | | - <DropdownMenuItem onSelect={onOpenDocs}> |
216 | | - <BookOpen className='size-[14px]' /> |
217 | | - Docs |
218 | | - </DropdownMenuItem> |
219 | | - <DropdownMenuItem onSelect={onJoinSlack}> |
220 | | - <SlackIcon className='size-[14px]' /> |
221 | | - Join Slack |
222 | | - </DropdownMenuItem> |
223 | | - </DropdownMenuContent> |
224 | | - </DropdownMenu> |
225 | | - ) |
| 17 | + const router = useRouter() |
| 18 | + const accountSettingsHref = organizationRoutes(organization.id).settingsSection('general') |
226 | 19 |
|
227 | 20 | return ( |
228 | | - <div |
229 | | - className={cn( |
230 | | - 'flex shrink-0 border-t px-2 pt-[9px] pb-2 transition-colors duration-150', |
231 | | - !showDivider && 'border-transparent', |
232 | | - isCollapsed ? cn(SIDEBAR_ITEM_GAP_CLASS, 'flex-col-reverse') : 'items-center' |
233 | | - )} |
234 | | - > |
235 | | - {/* Expanded, claims the row's free width so the help button lands hard right. |
236 | | - `flex` makes the inline-flex chip a flex item, so the wrapper is exactly the |
237 | | - chip's 30px rather than a line box padded by the strut's half-leading. */} |
238 | | - <div className={cn('flex min-w-0', !isCollapsed && 'flex-1')}>{profileMenu}</div> |
239 | | - {helpMenu} |
240 | | - </div> |
| 21 | + <SidebarFooter |
| 22 | + {...props} |
| 23 | + accountSettingsHref={accountSettingsHref} |
| 24 | + onOpenAccountSettings={() => router.push(accountSettingsHref)} |
| 25 | + navigationLinks={[]} |
| 26 | + /> |
241 | 27 | ) |
242 | 28 | } |
0 commit comments