feat: add multi-organization dropdown for organization navigation (#40967)
This commit is contained in:
parent
24ec7954a2
commit
bb1c055126
|
|
@ -19,3 +19,9 @@ export const updateOrganizationConfig = (
|
|||
type: ReduxActionTypes.UPDATE_ORGANIZATION_CONFIG,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const fetchMyOrganizations = () => {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,8 +20,20 @@ export interface UpdateOrganizationConfigRequest {
|
|||
apiConfig?: AxiosRequestConfig;
|
||||
}
|
||||
|
||||
export type FetchMyOrganizationsResponse = ApiResponse<{
|
||||
organizations: Organization[];
|
||||
}>;
|
||||
|
||||
export interface Organization {
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
organizationUrl: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export class OrganizationApi extends Api {
|
||||
static tenantsUrl = "v1/tenants";
|
||||
static meUrl = "v1/users/me";
|
||||
|
||||
static async fetchCurrentOrganizationConfig(): Promise<
|
||||
AxiosPromise<FetchCurrentOrganizationConfigResponse>
|
||||
|
|
@ -41,6 +53,12 @@ export class OrganizationApi extends Api {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
static async fetchMyOrganizations(): Promise<
|
||||
AxiosPromise<FetchMyOrganizationsResponse>
|
||||
> {
|
||||
return Api.get(`${OrganizationApi.meUrl}/organizations`);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrganizationApi;
|
||||
|
|
|
|||
|
|
@ -1229,6 +1229,8 @@ const OrganizationActionTypes = {
|
|||
FETCH_PRODUCT_ALERT_INIT: "FETCH_PRODUCT_ALERT_INIT",
|
||||
FETCH_PRODUCT_ALERT_SUCCESS: "FETCH_PRODUCT_ALERT_SUCCESS",
|
||||
UPDATE_PRODUCT_ALERT_CONFIG: "UPDATE_PRODUCT_ALERT_CONFIG",
|
||||
FETCH_MY_ORGANIZATIONS_INIT: "FETCH_MY_ORGANIZATIONS_INIT",
|
||||
FETCH_MY_ORGANIZATIONS_SUCCESS: "FETCH_MY_ORGANIZATIONS_SUCCESS",
|
||||
};
|
||||
|
||||
const OrganizationActionErrorTypes = {
|
||||
|
|
@ -1236,6 +1238,7 @@ const OrganizationActionErrorTypes = {
|
|||
"FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR",
|
||||
UPDATE_ORGANIZATION_CONFIG_ERROR: "UPDATE_ORGANIZATION_CONFIG_ERROR",
|
||||
FETCH_PRODUCT_ALERT_FAILED: "FETCH_PRODUCT_ALERT_FAILED",
|
||||
FETCH_MY_ORGANIZATIONS_ERROR: "FETCH_MY_ORGANIZATIONS_ERROR",
|
||||
};
|
||||
|
||||
const AnalyticsActionTypes = {
|
||||
|
|
|
|||
|
|
@ -2717,3 +2717,5 @@ export const MULTI_ORG_FOOTER_CREATE_ORG_LEFT_TEXT = () =>
|
|||
"Looking to create one?";
|
||||
export const MULTI_ORG_FOOTER_CREATE_ORG_RIGHT_TEXT = () =>
|
||||
"Create an organization";
|
||||
|
||||
export const PENDING_INVITATIONS = () => "Pending invitations";
|
||||
|
|
|
|||
|
|
@ -99,8 +99,11 @@ import {
|
|||
getIsFetchingApplications,
|
||||
} from "ee/selectors/selectedWorkspaceSelectors";
|
||||
import {
|
||||
getIsFetchingMyOrganizations,
|
||||
getMyOrganizations,
|
||||
getOrganizationPermissions,
|
||||
shouldShowLicenseBanner,
|
||||
activeOrganizationId,
|
||||
} from "ee/selectors/organizationSelectors";
|
||||
import { getWorkflowsList } from "ee/selectors/workflowSelectors";
|
||||
import {
|
||||
|
|
@ -141,6 +144,10 @@ import {
|
|||
} from "git";
|
||||
import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
|
||||
import { trackCurrentDomain } from "utils/multiOrgDomains";
|
||||
import OrganizationDropdown from "components/OrganizationDropdown";
|
||||
import { fetchMyOrganizations } from "ee/actions/organizationActions";
|
||||
import type { Organization } from "ee/api/OrganizationApi";
|
||||
import { useIsCloudBillingEnabled } from "hooks";
|
||||
|
||||
function GitModals() {
|
||||
const isGitModEnabled = useGitModEnabled();
|
||||
|
|
@ -434,25 +441,45 @@ export const submitCreateWorkspaceForm = async (data: any, dispatch: any) => {
|
|||
};
|
||||
|
||||
export interface LeftPaneProps {
|
||||
isBannerVisible?: boolean;
|
||||
isFetchingWorkspaces: boolean;
|
||||
workspaces: Workspace[];
|
||||
activeOrganizationId?: string;
|
||||
activeWorkspaceId?: string;
|
||||
isBannerVisible?: boolean;
|
||||
isFetchingOrganizations: boolean;
|
||||
isFetchingWorkspaces: boolean;
|
||||
organizations: Organization[];
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
export function LeftPane(props: LeftPaneProps) {
|
||||
const {
|
||||
activeOrganizationId,
|
||||
activeWorkspaceId,
|
||||
isBannerVisible = false,
|
||||
isFetchingOrganizations,
|
||||
isFetchingWorkspaces,
|
||||
organizations = [],
|
||||
workspaces = [],
|
||||
} = props;
|
||||
const isMobile = useIsMobileDevice();
|
||||
const isCloudBillingEnabled = useIsCloudBillingEnabled();
|
||||
|
||||
if (isMobile) return null;
|
||||
|
||||
return (
|
||||
<LeftPaneWrapper isBannerVisible={isBannerVisible}>
|
||||
{isCloudBillingEnabled &&
|
||||
!isFetchingOrganizations &&
|
||||
organizations.length > 0 && (
|
||||
<OrganizationDropdown
|
||||
organizations={organizations}
|
||||
selectedOrganization={
|
||||
organizations.find(
|
||||
(organization) =>
|
||||
organization.organizationId === activeOrganizationId,
|
||||
) || organizations[0]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<LeftPaneSection
|
||||
heading={createMessage(WORKSPACES_HEADING)}
|
||||
isBannerVisible={isBannerVisible}
|
||||
|
|
@ -992,6 +1019,9 @@ export const ApplictionsMainPage = (props: any) => {
|
|||
const isHomePage = useRouteMatch("/applications")?.isExact;
|
||||
const isLicensePage = useRouteMatch("/license")?.isExact;
|
||||
const isBannerVisible = showBanner && (isHomePage || isLicensePage);
|
||||
const organizations = useSelector(getMyOrganizations);
|
||||
const isFetchingOrganizations = useSelector(getIsFetchingMyOrganizations);
|
||||
const currentOrganizationId = useSelector(activeOrganizationId);
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -1013,6 +1043,10 @@ export const ApplictionsMainPage = (props: any) => {
|
|||
workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMyOrganizations());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveWorkspaceId(
|
||||
workspaceIdFromQueryParams
|
||||
|
|
@ -1056,9 +1090,12 @@ export const ApplictionsMainPage = (props: any) => {
|
|||
return (
|
||||
<PageWrapper displayName="Applications">
|
||||
<LeftPane
|
||||
activeOrganizationId={currentOrganizationId}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
isBannerVisible={isBannerVisible}
|
||||
isFetchingOrganizations={isFetchingOrganizations}
|
||||
isFetchingWorkspaces={isFetchingWorkspaces}
|
||||
organizations={organizations}
|
||||
workspaces={workspaces}
|
||||
/>
|
||||
<MediaQuery maxWidth={MOBILE_MAX_WIDTH}>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
createBrandColorsFromPrimaryColor,
|
||||
} from "utils/BrandingUtils";
|
||||
import { createReducer } from "utils/ReducerUtils";
|
||||
import type { Organization } from "ee/api/OrganizationApi";
|
||||
|
||||
export interface OrganizationReduxState<T> {
|
||||
displayName?: string;
|
||||
|
|
@ -21,6 +22,8 @@ export interface OrganizationReduxState<T> {
|
|||
instanceId: string;
|
||||
tenantId: string;
|
||||
isWithinAnOrganization: boolean;
|
||||
myOrganizations: Organization[];
|
||||
isFetchingMyOrganizations: boolean;
|
||||
}
|
||||
|
||||
export const defaultBrandingConfig = {
|
||||
|
|
@ -43,6 +46,8 @@ export const initialState: OrganizationReduxState<any> = {
|
|||
instanceId: "",
|
||||
tenantId: "",
|
||||
isWithinAnOrganization: false,
|
||||
myOrganizations: [],
|
||||
isFetchingMyOrganizations: false,
|
||||
};
|
||||
|
||||
export const handlers = {
|
||||
|
|
@ -113,6 +118,32 @@ export const handlers = {
|
|||
...state,
|
||||
isLoading: false,
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT]: (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
state: OrganizationReduxState<any>,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingMyOrganizations: true,
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS]: (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
state: OrganizationReduxState<any>,
|
||||
action: ReduxAction<Organization[]>,
|
||||
) => ({
|
||||
...state,
|
||||
myOrganizations: action.payload,
|
||||
isFetchingMyOrganizations: false,
|
||||
}),
|
||||
[ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR]: (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
state: OrganizationReduxState<any>,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingMyOrganizations: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export default createReducer(initialState, handlers);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import {
|
|||
} from "ee/constants/ReduxActionConstants";
|
||||
import { call, put } from "redux-saga/effects";
|
||||
import type { APIResponseError, ApiResponse } from "api/ApiResponses";
|
||||
import type { UpdateOrganizationConfigRequest } from "ee/api/OrganizationApi";
|
||||
import type {
|
||||
FetchMyOrganizationsResponse,
|
||||
UpdateOrganizationConfigRequest,
|
||||
} from "ee/api/OrganizationApi";
|
||||
import { OrganizationApi } from "ee/api/OrganizationApi";
|
||||
import { validateResponse } from "sagas/ErrorSagas";
|
||||
import { safeCrashAppRequest } from "actions/errorActions";
|
||||
|
|
@ -158,3 +161,26 @@ export function* updateOrganizationConfigSaga(
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchMyOrganizationsSaga() {
|
||||
try {
|
||||
const response: FetchMyOrganizationsResponse = yield call(
|
||||
OrganizationApi.fetchMyOrganizations,
|
||||
);
|
||||
const isValidResponse: boolean = yield validateResponse(response);
|
||||
|
||||
if (isValidResponse) {
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS,
|
||||
payload: response.data,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,3 +67,15 @@ export const isFreePlan = (state: DefaultRootState) => true;
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const isWithinAnOrganization = (state: DefaultRootState) => true;
|
||||
|
||||
export const getMyOrganizations = (state: DefaultRootState) => {
|
||||
return state.organization?.myOrganizations || [];
|
||||
};
|
||||
|
||||
export const getIsFetchingMyOrganizations = (state: DefaultRootState) => {
|
||||
return state.organization?.isFetchingMyOrganizations || false;
|
||||
};
|
||||
|
||||
export const activeOrganizationId = (state: DefaultRootState) => {
|
||||
return state.organization?.tenantId;
|
||||
};
|
||||
|
|
|
|||
205
app/client/src/components/OrganizationDropdown/index.tsx
Normal file
205
app/client/src/components/OrganizationDropdown/index.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { Avatar, Icon } from "@appsmith/ads";
|
||||
import type { Organization } from "ee/api/OrganizationApi";
|
||||
import { createMessage, PENDING_INVITATIONS } from "ee/constants/messages";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DropdownContainer,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
MenuItem,
|
||||
MenuItemIcon,
|
||||
MenuItemText,
|
||||
SectionDivider,
|
||||
SectionHeader,
|
||||
TriggerContent,
|
||||
TriggerText,
|
||||
} from "./styles";
|
||||
|
||||
export interface PendingInvitation {
|
||||
id: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
export interface OrganizationDropdownProps {
|
||||
"data-testid"?: string;
|
||||
organizations: Organization[];
|
||||
selectedOrganization: Organization;
|
||||
}
|
||||
|
||||
const OrganizationDropdown: React.FC<OrganizationDropdownProps> = ({
|
||||
"data-testid": testId,
|
||||
organizations = [],
|
||||
selectedOrganization,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const safeOrganizations = organizations || [];
|
||||
const activeOrganizations = safeOrganizations.filter(
|
||||
(org) => org.state === "ACTIVE",
|
||||
);
|
||||
const pendingInvitations = safeOrganizations.filter(
|
||||
(org) => org.state === "INVITED",
|
||||
);
|
||||
|
||||
const generateInitials = (name: string): string => {
|
||||
if (!name) return "";
|
||||
|
||||
return name.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback((organization: Organization) => {
|
||||
if (organization.organizationUrl) {
|
||||
const url = `https://${organization.organizationUrl}`;
|
||||
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
triggerRef.current?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const displayText = selectedOrganization?.organizationName;
|
||||
|
||||
const renderOrgAvatar = (orgName: string) => {
|
||||
return (
|
||||
<Avatar
|
||||
firstLetter={generateInitials(orgName)}
|
||||
label={generateInitials(orgName)}
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownContainer data-testid={testId} ref={dropdownRef}>
|
||||
<DropdownTrigger
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
aria-label={`Current organization: ${displayText}`}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
>
|
||||
<TriggerContent>
|
||||
{renderOrgAvatar(displayText)}
|
||||
<TriggerText>{displayText}</TriggerText>
|
||||
</TriggerContent>
|
||||
<Icon name="dropdown" size="md" />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownMenu aria-label="Organizations" isOpen={isOpen} role="listbox">
|
||||
{activeOrganizations
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const aIsSelected =
|
||||
a.organizationId === selectedOrganization?.organizationId;
|
||||
|
||||
const bIsSelected =
|
||||
b.organizationId === selectedOrganization?.organizationId;
|
||||
|
||||
if (aIsSelected && !bIsSelected) return -1;
|
||||
|
||||
if (!aIsSelected && bIsSelected) return 1;
|
||||
|
||||
return 0;
|
||||
})
|
||||
.map((org) => {
|
||||
const isSelected =
|
||||
org.organizationId === selectedOrganization?.organizationId;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
aria-selected={isSelected}
|
||||
isSelected={isSelected}
|
||||
key={org.organizationId}
|
||||
onClick={!isSelected ? () => handleSelect(org) : undefined}
|
||||
onKeyDown={
|
||||
!isSelected
|
||||
? (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelect(org);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
role="option"
|
||||
tabIndex={0}
|
||||
>
|
||||
{renderOrgAvatar(org.organizationName)}
|
||||
<MenuItemText>
|
||||
{org.organizationName}{" "}
|
||||
{org.organizationId ===
|
||||
selectedOrganization?.organizationId && "(current)"}
|
||||
</MenuItemText>
|
||||
|
||||
{!isSelected && (
|
||||
<MenuItemIcon
|
||||
className="hover-icon color-fg-muted"
|
||||
name="share-box-line"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{pendingInvitations.length > 0 && (
|
||||
<>
|
||||
<SectionDivider />
|
||||
<SectionHeader>{createMessage(PENDING_INVITATIONS)}</SectionHeader>
|
||||
{pendingInvitations.map((invitation) => (
|
||||
<MenuItem
|
||||
key={invitation.organizationId}
|
||||
onClick={() => handleSelect(invitation)}
|
||||
role="option"
|
||||
>
|
||||
{renderOrgAvatar(invitation.organizationName)}
|
||||
<MenuItemText>{invitation.organizationName}</MenuItemText>
|
||||
|
||||
<MenuItemIcon
|
||||
className="hover-icon color-fg-muted"
|
||||
name="share-box-line"
|
||||
size="md"
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</DropdownContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationDropdown;
|
||||
130
app/client/src/components/OrganizationDropdown/styles.ts
Normal file
130
app/client/src/components/OrganizationDropdown/styles.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { Icon } from "@appsmith/ads";
|
||||
import styled from "styled-components";
|
||||
|
||||
export const DropdownContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: var(--ads-v2-spaces-3) 0;
|
||||
`;
|
||||
|
||||
export const DropdownTrigger = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: var(--ads-v2-spaces-3) var(--ads-v2-spaces-4);
|
||||
background: var(--ads-v2-color-bg);
|
||||
border: 1px solid var(--ads-v2-color-border);
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--ads-v2-color-fg);
|
||||
transition: all 0.2s ease;
|
||||
min-height: 40px;
|
||||
`;
|
||||
|
||||
export const TriggerContent = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const TriggerText = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
export const DropdownMenu = styled.div<{ isOpen: boolean }>`
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--ads-v2-color-bg);
|
||||
padding: var(--ads-v2-spaces-2);
|
||||
border: 1px solid var(--ads-v2-color-border);
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
box-shadow: var(--ads-v2-shadow-popovers);
|
||||
z-index: var(--ads-v2-z-index-7);
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
|
||||
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
|
||||
transform: ${({ isOpen }) => (isOpen ? "translateY(0)" : "translateY(-8px)")};
|
||||
transition: all 0.2s ease;
|
||||
`;
|
||||
|
||||
export const MenuItem = styled.div<{
|
||||
isSelected?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
padding: var(--ads-v2-spaces-3) var(--ads-v2-spaces-4);
|
||||
margin-bottom: var(--ads-v2-spaces-2);
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
cursor: ${({ isSelected }) => (isSelected ? "default" : "pointer")};
|
||||
font-size: 14px;
|
||||
color: var(--ads-v2-color-fg);
|
||||
background: ${({ isSelected }) =>
|
||||
isSelected ? "var(--ads-v2-color-bg-muted)" : "transparent"};
|
||||
|
||||
.hover-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${({ isSelected }) =>
|
||||
isSelected
|
||||
? "var(--ads-v2-color-bg-muted)"
|
||||
: "var(--ads-v2-color-bg-subtle)"};
|
||||
|
||||
.hover-icon {
|
||||
opacity: ${({ isSelected }) => (isSelected ? 0 : 1)};
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: ${({ isSelected }) =>
|
||||
isSelected
|
||||
? "var(--ads-v2-color-bg-muted)"
|
||||
: "var(--ads-v2-color-bg-subtle)"};
|
||||
|
||||
.hover-icon {
|
||||
opacity: ${({ isSelected }) => (isSelected ? 0 : 1)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MenuItemIcon = styled(Icon)`
|
||||
color: var(--ads-v2-color-fg-muted);
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const MenuItemText = styled.span`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export const SectionDivider = styled.div`
|
||||
height: 1px;
|
||||
background: var(--ads-v2-color-border);
|
||||
margin: var(--ads-v2-spaces-2) 0;
|
||||
`;
|
||||
|
||||
export const SectionHeader = styled.div`
|
||||
padding: var(--ads-v2-spaces-2) var(--ads-v2-spaces-4);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ads-v2-color-fg-muted);
|
||||
`;
|
||||
Loading…
Reference in New Issue
Block a user