fix(front): settings skeleton, app-detail header & empty favorites (#21209)

Three small post-redesign UI fixes. Each is an independent commit, so
they can be split into separate PRs if preferred.

## 1. Settings loading skeleton — match the rounded-card layout

The redesign (#21131) moved settings chrome into a rounded card
(`SettingsPageLayout`: bordered header with breadcrumb + centered title,
optional secondary bar, 760px body), but `SettingsSkeletonLoader` still
rendered the old flat `PageHeader` + `PageBody` — so pages painted as a
full-width flat bar then snapped into the card.

- `SettingsSkeletonLoader` now reproduces the card and **reuses the real
`SettingsPageHeader` + `SettingsPageContainer`**, so the frame aligns by
construction; the card CSS is replicated (not `SettingsPageLayout`) to
avoid the layout's side effects (hotkeys, side panel, info banner).
- It's **composed with `SettingsSectionSkeletonLoader`** so the loading
body is identical whether or not chrome is present. Rule: no chrome on
screen yet → full-page skeleton; chrome already on screen → body-only
`SettingsSectionSkeletonLoader` (the admin Enterprise tab now uses it,
matching its sibling tabs). A short comment on each component documents
this.

## 2. Application detail header — pass a plain title

`SettingsApplicationDetails` / `SettingsAvailableApplicationDetails`
passed a custom `SettingsApplicationDetailTitle` (avatar + name +
multi-line description, fixed width) into `SettingsPageLayout`'s
**centered single-line title slot**, which broke the header. They now
pass the app's display name like every other page. The available-app
"unlisted" notice moves into the body as a reusable `InlineBanner`; the
now-unused `SettingsApplicationDetailTitle` is removed.

## 3. Navigation — hide Favorites when empty

Always rendering the Favorites section (#21087) left a stray "Favorites"
title above Workspace for users with no favorites. It now renders only
when at least one favorite exists (redundant per-child guards dropped).
Note: the "+ add favorite" entry point therefore appears once you have
≥1 favorite; the first favorite is created from a record/view as before.

## Verification
- `nx typecheck twenty-front`  · `oxlint` + `oxfmt --check` on changed
files 
- i18n catalogs intentionally untouched — handled by the repo's separate
i18n pipeline.
This commit is contained in:
Félix Malfait
2026-06-04 09:32:36 +02:00
committed by GitHub
parent e99116b0a5
commit a3a44c8315
6 changed files with 122 additions and 227 deletions
@@ -5,7 +5,7 @@ import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-vari
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { lazy, Suspense } from 'react';
@@ -34,7 +34,7 @@ export const SettingsAdminTabContent = () => {
return <SettingsAdminHealthStatus />;
case SETTINGS_ADMIN_TABS.ENTERPRISE:
return (
<Suspense fallback={<SettingsSkeletonLoader />}>
<Suspense fallback={<SettingsSectionSkeletonLoader />}>
<SettingsEnterprise isAdminPanelTab />
</Suspense>
);
@@ -1,54 +1,70 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
const StyledRoot = styled.div<{ isMobile: boolean }>`
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
padding: ${({ isMobile }) =>
isMobile ? themeCssVariables.spacing[1] : themeCssVariables.spacing[2]};
`;
const StyledCard = styled.div`
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
width: 100%;
`;
const StyledTitleLoaderContainer = styled.div`
margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}
${themeCssVariables.spacing[2]};
`;
export const SettingsSkeletonLoader = () => {
const isMobile = useIsMobile();
const { theme } = useContext(ThemeContext);
return (
<StyledContainer>
<PageHeader
title={
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
width={120}
/>{' '}
</SkeletonTheme>
}
/>
<PageBody>
<StyledTitleLoaderContainer>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
width={200}
/>
</SkeletonTheme>
</StyledTitleLoaderContainer>
</PageBody>
</StyledContainer>
<StyledRoot isMobile={isMobile}>
<StyledCard>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<SettingsPageHeader
links={[
{
children: (
<Skeleton
width={64}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
),
},
]}
title={
<Skeleton
width={120}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
}
/>
<SettingsPageContainer>
<SettingsSectionSkeletonLoader />
</SettingsPageContainer>
</SkeletonTheme>
</StyledCard>
</StyledRoot>
);
};