diff --git a/packages/components/src/components/loro-sidebar.tsx b/packages/components/src/components/loro-sidebar.tsx index fc3707548..9f8a97828 100644 --- a/packages/components/src/components/loro-sidebar.tsx +++ b/packages/components/src/components/loro-sidebar.tsx @@ -64,6 +64,19 @@ import { useStableNow } from '@/hooks/use-stable-now'; export type LoroSidebarNavKey = 'home' | 'archive' | 'tasks'; +/** One utility in the footer rail. `filter` is the mobile-only organize/scope + * popover; the other three are the footer's icon buttons. */ +export type LoroSidebarFooterItem = 'settings' | 'help' | 'archive' | 'filter'; + +/** Every footer utility, which is what the footer renders when a host says + * nothing. Declared once so the default cannot drift from the union. */ +export const LORO_SIDEBAR_FOOTER_ITEMS: readonly LoroSidebarFooterItem[] = [ + 'settings', + 'help', + 'archive', + 'filter', +]; + export type LoroSidebarChatScope = 'my' | 'team'; export type LoroSidebarOrganizeMode = SidebarOrganizeMode; @@ -167,6 +180,32 @@ export interface LoroSidebarProps { afterSessionListContent?: ReactNode; bottomFloatingContent?: ReactNode; + /** + * Suppresses the workspace-identity header row. A host that already renders + * its own workspace header — an embedder mounting the sidebar body inside an + * existing shell — would otherwise stack two of them. + */ + hideHeader?: boolean; + /** + * Suppresses the footer utility rail (settings, help, archive, and on mobile + * the filter popover). Same reason as {@link hideHeader}: a host that serves + * those entries from its own chrome would otherwise render them twice. On + * mobile this also removes the only filter trigger, so a host that hides the + * footer owns the organize/scope controls too. + */ + hideFooter?: boolean; + /** + * Which footer utilities render, when the footer renders at all. + * + * {@link hideFooter} is all-or-nothing, and a host that serves its own + * settings and its own help — but NOT its own archive — has no way to keep the + * one entry it wants. Listing the items is that way: the default is every + * item, so a host that says nothing renders exactly what it rendered before. + * Ignored while {@link hideFooter} is set, which stays the shorter spelling + * for "none of them". + */ + footerItems?: readonly LoroSidebarFooterItem[]; + repoSections?: LoroSidebarRepoSection[]; chats?: LoroSidebarChatItem[]; sessionListProps?: SessionListProps; @@ -631,6 +670,9 @@ export const LoroSidebar = memo(function LoroSidebar({ desktopFilterPlaceholder, afterSessionListContent, bottomFloatingContent, + hideHeader = false, + hideFooter = false, + footerItems = LORO_SIDEBAR_FOOTER_ITEMS, repoSections = defaultRepoSections, chats = defaultChats, sessionListProps, @@ -873,118 +915,120 @@ export const LoroSidebar = memo(function LoroSidebar({ !isMobile && 'p-[2px]' )} > -
- {workspaceSwitcherEnabled ? ( - -
- - - -
- - {userEmail} - - - {workspaces.length > 0 ? ( - <> - - {mergedLabels.switchWorkspace} - - onWorkspaceSelected?.(value)} + {hideHeader ? null : ( +
+ {workspaceSwitcherEnabled ? ( + +
+ + + +
+ + {userEmail} + - onCreateWorkspaceClicked?.()}> - - {mergedLabels.createWorkspace} - - onInviteClicked?.()}> - - {mergedLabels.inviteMembers} - - onLinkRepoClicked?.()}> - - {mergedLabels.connectGithubRepo} - - -
- ) : ( -
-
- {workspaceIdentity} + {workspaces.length > 0 ? ( + <> + + {mergedLabels.switchWorkspace} + + onWorkspaceSelected?.(value)} + > + {workspaces.map((ws) => ( + + + {ws.name} + {ws.planTier ? ( + + {ws.planTier === 'enterprise' + ? mergedLabels.planEnterprise + : mergedLabels.planPlus} + + ) : null} + + ))} + + + + ) : null} + + onCreateWorkspaceClicked?.()}> + + {mergedLabels.createWorkspace} + + onInviteClicked?.()}> + + {mergedLabels.inviteMembers} + + onLinkRepoClicked?.()}> + + {mergedLabels.connectGithubRepo} + + + + ) : ( +
+
+ {workspaceIdentity} +
-
- )} - {/* Collapse toggle anchored to the header's top-right corner. - `top-2` centers the h-7 button inside the standard h-11 header. - On macOS Electron the header is taller (`h-[72px] pt-7`) and its - top sits 11px below the window top (card `mt-2` + 1px border + - inner `p-[2px]`); `-top-0.5` then puts the button center at - 11 - 2 + 14 = 23px, exactly on the traffic-light centerline - (`trafficLightPosition.y` 16 + 7px radius in - apps/electron/src/main/window.ts) — and level with the - collapsed-state expand button (`top-[9px]` in - web-chat-landing-screen.tsx), so the control stays put across - collapse/expand. */} - {!isMobile && onRequestCollapse ? ( - - ) : null} -
+ )} + {/* Collapse toggle anchored to the header's top-right corner. + `top-2` centers the h-7 button inside the standard h-11 header. + On macOS Electron the header is taller (`h-[72px] pt-7`) and its + top sits 11px below the window top (card `mt-2` + 1px border + + inner `p-[2px]`); `-top-0.5` then puts the button center at + 11 - 2 + 14 = 23px, exactly on the traffic-light centerline + (`trafficLightPosition.y` 16 + 7px radius in + apps/electron/src/main/window.ts) — and level with the + collapsed-state expand button (`top-[9px]` in + web-chat-landing-screen.tsx), so the control stays put across + collapse/expand. */} + {!isMobile && onRequestCollapse ? ( + + ) : null} +
+ )}
) : null} -
-
- - - + {hideFooter ? null : ( +
+
+ {footerItems.includes('settings') ? ( + + + + ) : null} - - - - - - - - onDocsClicked?.()}> - - {mergedLabels.docs} - - onJoinCommunityClicked?.()}> - - {mergedLabels.joinCommunity} - - onFeedbackClicked?.()}> - - {mergedLabels.feedback} - - onBugReportClicked?.()}> - - {mergedLabels.bugReport} - - - + {footerItems.includes('help') ? ( + + + + + + + + onDocsClicked?.()}> + + {mergedLabels.docs} + + onJoinCommunityClicked?.()}> + + {mergedLabels.joinCommunity} + + onFeedbackClicked?.()}> + + {mergedLabels.feedback} + + onBugReportClicked?.()}> + + {mergedLabels.bugReport} + + + + ) : null} - - - -
+ {footerItems.includes('archive') ? ( + + + + ) : null} +
- {isMobile ? ( - - ) : null} -
+ {isMobile && footerItems.includes('filter') ? ( + + ) : null} +
+ )}
);