fix(ui): make CardPicker hover cover the whole card and align content left (#19884)

## Summary

Two small visual issues with the shared `CardPicker` (used in the
Enterprise plan modal and the onboarding plan picker):

- Labels like \`Monthly\` / \`Yearly\` were center-aligned inside their
cards while the subtitle (\`\$25 / seat / month\`) stayed left-aligned,
because the underlying \`<button>\` element's default \`text-align:
center\` was leaking into the children.
- The hover background was painted on the same element that owned the
inner padding, so the hover surface didn't visually feel like the whole
card.

This PR:
- Moves the content padding into a new \`StyledCardInner\` so the outer
\`<button>\` is just the card chrome (border + radius + background +
hover).
- Adds \`text-align: left\` so titles align with their subtitles.
- Hoists \`cursor: pointer\` out of \`:hover\` (it should be on by
default for the card).

Affects:
- \`EnterprisePlanModal\` (Settings → Enterprise)
- \`ChooseYourPlanContent\` (onboarding trial picker)
This commit is contained in:
Charles Bochet 2026-04-20 17:00:12 +02:00 committed by GitHub
parent 4ecde0e161
commit 13c4a71594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 34 deletions

View file

@ -9,9 +9,9 @@ export const AgentChatProvider = ({
}) => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
if (!isAiEnabled) {
return <>{children}</>;
}
return <AgentChatProviderContent>{children}</AgentChatProviderContent>;
return (
<AgentChatProviderContent isAiEnabled={isAiEnabled}>
{children}
</AgentChatProviderContent>
);
};

View file

@ -10,20 +10,26 @@ import { Suspense } from 'react';
export const AgentChatProviderContent = ({
children,
isAiEnabled,
}: {
children: React.ReactNode;
isAiEnabled: boolean;
}) => {
return (
<Suspense fallback={null}>
<AgentChatComponentInstanceContext.Provider
value={{ instanceId: 'agentChatComponentInstance' }}
>
<AgentChatThreadInitializationEffect />
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatSessionStartTimeEffect />
<AgentChatStreamingAutoScrollEffect />
{isAiEnabled && (
<>
<AgentChatThreadInitializationEffect />
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatSessionStartTimeEffect />
<AgentChatStreamingAutoScrollEffect />
</>
)}
{children}
</AgentChatComponentInstanceContext.Provider>
</Suspense>

View file

@ -6,6 +6,7 @@ import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { AI_ADMIN_PATH } from '@/settings/admin-panel/ai/constants/AiAdminPath';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
@ -64,6 +65,7 @@ export const SettingsAdminWorkspaceDetail = () => {
);
const currentUser = useAtomStateValue(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
const { enqueueErrorSnackBar } = useSnackBar();
const { updateFeatureFlagState } = useFeatureFlagState();
@ -262,25 +264,34 @@ export const SettingsAdminWorkspaceDetail = () => {
<TableHeader>{t`Feature Flag`}</TableHeader>
<TableHeader align="right">{t`Status`}</TableHeader>
</TableRow>
{workspace.featureFlags?.map((flag) => (
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
key={flag.key}
>
<TableCell>{flag.key}</TableCell>
<TableCell align="right">
{isDefined(flag.key) && (
<Toggle
value={flag.value}
onChange={(newValue) =>
handleFeatureFlagUpdate(flag.key!, newValue)
}
/>
)}
</TableCell>
</TableRow>
))}
{workspace.featureFlags?.map((flag) => {
const currentWorkspaceValue =
currentWorkspace?.id === workspaceId
? currentWorkspace?.featureFlags?.find(
(f) => f.key === flag.key,
)?.value
: undefined;
const displayedValue = currentWorkspaceValue ?? flag.value;
return (
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
key={flag.key}
>
<TableCell>{flag.key}</TableCell>
<TableCell align="right">
{isDefined(flag.key) && (
<Toggle
value={displayedValue}
onChange={(newValue) =>
handleFeatureFlagUpdate(flag.key!, newValue)
}
/>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Section>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { styled } from '@linaria/react';
import React from 'react';
import { themeCssVariables } from '@ui/theme-constants';
import { Radio } from './Radio';
@ -8,16 +8,23 @@ const StyledSubscriptionCardContainer = styled.button`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
cursor: pointer;
display: flex;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
padding: 0;
position: relative;
text-align: left;
width: 100%;
:hover {
cursor: pointer;
background: ${themeCssVariables.background.tertiary};
}
`;
const StyledCardInner = styled.div`
display: flex;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
width: 100%;
`;
const StyledRadioContainer = styled.div`
position: absolute;
right: ${themeCssVariables.spacing[2]};
@ -40,7 +47,7 @@ export const CardPicker = ({
<StyledRadioContainer>
<Radio checked={checked} />
</StyledRadioContainer>
{children}
<StyledCardInner>{children}</StyledCardInner>
</StyledSubscriptionCardContainer>
);
};