feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary
Two intertwined streams of work:
### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.
### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.
- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.
### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.
## Test plan
### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active
### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200
### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px
### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type HeroPlayButtonProps = {
|
||||
onClick?: () => void;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledButton = styled.button`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
height: 44px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition:
|
||||
transform 120ms ease-out,
|
||||
background-color 120ms ease-out;
|
||||
width: 44px;
|
||||
|
||||
&:hover {
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${themeCssVariables.border.color.blue};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeroPlayButton = ({
|
||||
onClick,
|
||||
ariaLabel = 'Play video',
|
||||
className,
|
||||
}: HeroPlayButtonProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
className={className}
|
||||
>
|
||||
<IconPlayerPlay size={theme.icon.size.md} stroke={theme.icon.stroke.md} />
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
@@ -9,10 +9,8 @@ import { LayoutCustomizationBar } from '@/layout-customization/components/Layout
|
||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||
import { PageDragDropProvider } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProvider';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { BackgroundMockNavigationDrawer } from '@/sign-in-background-mock/components/BackgroundMockNavigationDrawer';
|
||||
import { Suspense, lazy, useContext } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
const BackgroundMockPage = lazy(() =>
|
||||
import('@/sign-in-background-mock/components/BackgroundMockPage').then(
|
||||
@@ -21,13 +19,11 @@ const BackgroundMockPage = lazy(() =>
|
||||
);
|
||||
import { useShowFullscreen } from '@/ui/layout/fullscreen/hooks/useShowFullscreen';
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { styled } from '@linaria/react';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useScreenSize } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
const StyledLayout = styled.div`
|
||||
background: ${themeCssVariables.background.noisy};
|
||||
display: flex;
|
||||
@@ -43,14 +39,13 @@ const StyledLayout = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPageContainerBase = styled.div`
|
||||
const StyledPageContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
`;
|
||||
const StyledPageContainer = motion.create(StyledPageContainerBase);
|
||||
|
||||
const StyledNavigationDrawerWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
@@ -65,11 +60,8 @@ const StyledMainContainer = styled.div`
|
||||
|
||||
export const DefaultLayout = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
const useShowFullScreen = useShowFullscreen();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,21 +70,7 @@ export const DefaultLayout = () => {
|
||||
<AppErrorBoundary FallbackComponent={AppFullScreenErrorFallback}>
|
||||
<InformationBannerIsImpersonating />
|
||||
<LayoutCustomizationBar />
|
||||
<StyledPageContainer
|
||||
animate={{
|
||||
marginLeft:
|
||||
isSettingsPage && !isMobile && !useShowFullScreen
|
||||
? (windowsWidth -
|
||||
(OBJECT_SETTINGS_WIDTH +
|
||||
NAVIGATION_DRAWER_CONSTRAINTS.default +
|
||||
76)) /
|
||||
2
|
||||
: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
<StyledPageContainer>
|
||||
<PageDragDropProvider>
|
||||
{!showAuthModal && <KeyboardShortcutMenu />}
|
||||
{showAuthModal ? (
|
||||
|
||||
+38
-11
@@ -1,4 +1,5 @@
|
||||
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
|
||||
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
|
||||
import {
|
||||
Breadcrumb,
|
||||
type BreadcrumbProps,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type JSX, type ReactNode } from 'react';
|
||||
import { PageBody } from './PageBody';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -20,12 +20,31 @@ type SubMenuTopBarContainerProps = {
|
||||
tag?: JSX.Element;
|
||||
};
|
||||
|
||||
// Cards, forms, and tables inside the white panel are centered in a fixed
|
||||
// max-width column so they don't sprawl on large displays. The white panel
|
||||
// itself spans edge-to-edge; only the content is constrained.
|
||||
const SETTINGS_CONTENT_MAX_WIDTH = 760;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
// flex: 1 + min-height: 0 keep the vertical-scroll chain intact: PagePanel's
|
||||
// own overflow handling sits one level up and depends on its children
|
||||
// participating in the flex height calculation rather than collapsing to
|
||||
// content height.
|
||||
const StyledBodyContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: ${SETTINGS_CONTENT_MAX_WIDTH}px;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span<{ reserveTitleSpace?: boolean }>`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
@@ -53,16 +72,24 @@ export const SubMenuTopBarContainer = ({
|
||||
<PageHeader title={<Breadcrumb links={links} />}>
|
||||
{actionButton}
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<InformationBannerWrapper />
|
||||
{(isDefined(title) || reserveTitleSpace === true) && (
|
||||
<StyledTitle reserveTitleSpace={reserveTitleSpace}>
|
||||
{title}
|
||||
{tag}
|
||||
</StyledTitle>
|
||||
)}
|
||||
{children}
|
||||
</PageBody>
|
||||
{/*
|
||||
MainContainerLayoutWithSidePanel is the same wrapper the App's record
|
||||
pages use: it renders the page body on the left and SidePanelForDesktop
|
||||
on the right. Hosting it here lets the AI chat side panel (and any
|
||||
other side-panel page) open in settings exactly as it does in the App.
|
||||
*/}
|
||||
<MainContainerLayoutWithSidePanel>
|
||||
<StyledBodyContentWrapper>
|
||||
<InformationBannerWrapper />
|
||||
{(isDefined(title) || reserveTitleSpace === true) && (
|
||||
<StyledTitle reserveTitleSpace={reserveTitleSpace}>
|
||||
{title}
|
||||
{tag}
|
||||
</StyledTitle>
|
||||
)}
|
||||
{children}
|
||||
</StyledBodyContentWrapper>
|
||||
</MainContainerLayoutWithSidePanel>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { type ReactNode } from 'react';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
type CollapsibleNavigationDrawerSectionProps = {
|
||||
// Unique id used to persist the open/closed state in localStorage. Pass
|
||||
// a namespaced value (e.g. 'settings/User') so unrelated sections in
|
||||
// different drawers don't share state.
|
||||
sectionId: string;
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
// Optional wrapper around the section title (e.g. AdvancedSettingsWrapper
|
||||
// for advanced-mode-only sections). Receives the title node and returns
|
||||
// the wrapped node.
|
||||
wrapTitle?: (titleNode: ReactNode) => ReactNode;
|
||||
};
|
||||
|
||||
// One-stop section component for any drawer that wants the main-app's
|
||||
// collapsible section behavior: click the title to collapse / expand,
|
||||
// animated height transition, persisted open state, chevron-on-hover.
|
||||
// Use this instead of stitching together NavigationDrawerSection +
|
||||
// NavigationDrawerSectionTitle + AnimatedExpandableContainer by hand at
|
||||
// every call site.
|
||||
export const CollapsibleNavigationDrawerSection = ({
|
||||
sectionId,
|
||||
label,
|
||||
children,
|
||||
wrapTitle,
|
||||
}: CollapsibleNavigationDrawerSectionProps) => {
|
||||
const { toggleNavigationSection, isNavigationSectionOpen } =
|
||||
useNavigationSection(sectionId);
|
||||
|
||||
const titleNode = (
|
||||
<NavigationDrawerSectionTitle
|
||||
label={label}
|
||||
onClick={toggleNavigationSection}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
{wrapTitle ? wrapTitle(titleNode) : titleNode}
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{children}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
+3
-6
@@ -52,7 +52,6 @@ const StyledAnimatedContainer = styled.div<{
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div<{
|
||||
isSettings?: boolean;
|
||||
isExpanded?: boolean;
|
||||
}>`
|
||||
box-sizing: border-box;
|
||||
@@ -60,10 +59,8 @@ const StyledContainer = styled.div<{
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
height: 100%;
|
||||
padding: ${({ isSettings }) =>
|
||||
isSettings
|
||||
? `${themeCssVariables.spacing[3]} 0 ${themeCssVariables.spacing[4]} 0`
|
||||
: `${themeCssVariables.spacing[3]} 0 ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[2]}`};
|
||||
padding: ${themeCssVariables.spacing[3]} 0 ${themeCssVariables.spacing[4]}
|
||||
${themeCssVariables.spacing[2]};
|
||||
width: ${({ isExpanded }) =>
|
||||
isExpanded ? `var(${NAVIGATION_DRAWER_WIDTH_VAR})` : '100%'};
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
@@ -123,7 +120,7 @@ export const NavigationDrawer = ({
|
||||
isExpanded={isExpanded}
|
||||
isResizing={isResizing}
|
||||
>
|
||||
<StyledContainer isSettings={isSettingsDrawer} isExpanded={isExpanded}>
|
||||
<StyledContainer isExpanded={isExpanded}>
|
||||
{!isMobile && isSettingsDrawer && title ? (
|
||||
<NavigationDrawerBackButton title={title} />
|
||||
) : (
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ const StyledContainer = styled.div`
|
||||
flex-direction: row;
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
justify-content: space-between;
|
||||
padding-left: ${themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
export const NavigationDrawerBackButton = ({
|
||||
|
||||
+9
-15
@@ -1,34 +1,28 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFixedContainer = styled.div<{
|
||||
isSettings?: boolean;
|
||||
isMobile?: boolean;
|
||||
}>`
|
||||
padding-left: ${({ isSettings, isMobile }) =>
|
||||
isSettings || isMobile ? themeCssVariables.spacing[5] : '0'};
|
||||
padding-right: ${({ isSettings, isMobile }) =>
|
||||
isMobile
|
||||
? themeCssVariables.spacing[5]
|
||||
: isSettings
|
||||
? themeCssVariables.spacing[8]
|
||||
: '0'};
|
||||
// Mobile keeps the touch-friendly horizontal padding; on desktop the container
|
||||
// is edge-to-edge and the child supplies its own padding.
|
||||
const StyledFixedContainer = styled.div<{ isMobile?: boolean }>`
|
||||
padding-left: ${({ isMobile }) =>
|
||||
isMobile ? themeCssVariables.spacing[5] : '0'};
|
||||
padding-right: ${({ isMobile }) =>
|
||||
isMobile ? themeCssVariables.spacing[5] : '0'};
|
||||
`;
|
||||
|
||||
export const NavigationDrawerFixedContent = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const isSettingsDrawer = useIsSettingsDrawer();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<StyledFixedContainer isSettings={isSettingsDrawer} isMobile={isMobile}>
|
||||
<StyledFixedContainer isMobile={isMobile}>
|
||||
<NavigationDrawerSection>{children}</NavigationDrawerSection>
|
||||
</StyledFixedContainer>
|
||||
);
|
||||
|
||||
+34
@@ -52,6 +52,10 @@ export type NavigationDrawerItemProps = {
|
||||
onClick?: () => void;
|
||||
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
|
||||
iconColor?: string | null;
|
||||
// Wrap the plain icon in a soft grey tile (no border) — used by the
|
||||
// settings drawer so its icons read as a uniform group without picking
|
||||
// up TintedIconTile's bordered colored treatment.
|
||||
withIconBackground?: boolean;
|
||||
active?: boolean;
|
||||
modifier?: NavigationDrawerItemModifier;
|
||||
rightOptions?: ReactNode;
|
||||
@@ -202,6 +206,21 @@ const StyledIcon = styled.div`
|
||||
margin-right: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
// Soft grey background-only tile (no border) used by the settings drawer.
|
||||
// Sized one step larger than the icon so the icon sits with a couple of
|
||||
// pixels of breathing room on every side. radius.md matches the rest of
|
||||
// the App's small-card / tile language; radius.sm read as sharp squares.
|
||||
const StyledIconBackgroundTile = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.tertiary};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
justify-content: center;
|
||||
width: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledRightOptionsContainer = styled.div`
|
||||
align-items: center;
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
@@ -243,6 +262,7 @@ export const NavigationDrawerItem = ({
|
||||
indentationLevel = DEFAULT_INDENTATION_LEVEL,
|
||||
Icon,
|
||||
iconColor,
|
||||
withIconBackground = false,
|
||||
to,
|
||||
onClick,
|
||||
active,
|
||||
@@ -347,6 +367,20 @@ export const NavigationDrawerItem = ({
|
||||
<StyledIcon>
|
||||
<TintedIconTile Icon={Icon} color={iconColor} />
|
||||
</StyledIcon>
|
||||
) : withIconBackground ? (
|
||||
<StyledIcon>
|
||||
<StyledIconBackgroundTile>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={
|
||||
showBreadcrumb && !isExpanded
|
||||
? theme.font.color.light
|
||||
: 'currentColor'
|
||||
}
|
||||
/>
|
||||
</StyledIconBackgroundTile>
|
||||
</StyledIcon>
|
||||
) : (
|
||||
<StyledIcon>
|
||||
<Icon
|
||||
|
||||
+5
-6
@@ -11,11 +11,10 @@ const StyledItemsContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledScrollableInnerContainer = styled.div<{ isMobile?: boolean }>`
|
||||
const StyledScrollableMobileInnerContainer = styled.div`
|
||||
height: 100%;
|
||||
padding-left: ${themeCssVariables.spacing[5]};
|
||||
padding-right: ${({ isMobile }) =>
|
||||
isMobile ? themeCssVariables.spacing[5] : themeCssVariables.spacing[8]};
|
||||
padding-right: ${themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
export const NavigationDrawerScrollableContent = ({
|
||||
@@ -34,10 +33,10 @@ export const NavigationDrawerScrollableContent = ({
|
||||
defaultEnableXScroll={false}
|
||||
>
|
||||
<StyledItemsContainer>
|
||||
{isSettingsDrawer || isMobile ? (
|
||||
<StyledScrollableInnerContainer isMobile={isMobile}>
|
||||
{isMobile ? (
|
||||
<StyledScrollableMobileInnerContainer>
|
||||
{children}
|
||||
</StyledScrollableInnerContainer>
|
||||
</StyledScrollableMobileInnerContainer>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
|
||||
+4
-8
@@ -5,22 +5,19 @@ import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledSection = styled.div<{ isSettingsDrawer?: boolean }>`
|
||||
margin-bottom: ${({ isSettingsDrawer }) =>
|
||||
isSettingsDrawer ? themeCssVariables.spacing[3] : '0'};
|
||||
const StyledSection = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionInnerContainerMinusScrollPadding = styled.div<{
|
||||
isMobile: boolean;
|
||||
isSettingsDrawer: boolean;
|
||||
isMainNavCollapsed: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
width: ${({ isMobile, isSettingsDrawer, isMainNavCollapsed }) =>
|
||||
isMobile || isSettingsDrawer || isMainNavCollapsed
|
||||
width: ${({ isMobile, isMainNavCollapsed }) =>
|
||||
isMobile || isMainNavCollapsed
|
||||
? '100%'
|
||||
: `calc(100% - ${themeCssVariables.spacing[2]})`};
|
||||
`;
|
||||
@@ -41,10 +38,9 @@ export const NavigationDrawerSection = ({
|
||||
!isSettingsDrawer && !isMobile && !isNavigationDrawerExpanded;
|
||||
|
||||
return (
|
||||
<StyledSection isSettingsDrawer={isSettingsDrawer} className={className}>
|
||||
<StyledSection className={className}>
|
||||
<StyledSectionInnerContainerMinusScrollPadding
|
||||
isMobile={isMobile}
|
||||
isSettingsDrawer={isSettingsDrawer}
|
||||
isMainNavCollapsed={isMainNavCollapsed}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user