fix(site): align settings header style and paywall spacing (#28418)

Settings pages rendered the premium upsell banner at different vertical
offsets and the IdP sync pages used a lighter heading style than the
rest of the settings UI.

Three causes:

1. The IdP Sync (`/organizations/:org/idp-sync`) and Organization IdP
Sync (`/deployment/idp-org-sync`) pages hand-rolled their headers with a
bare `<h1 className="text-3xl m-0">` (missing `font-semibold`) and a
`flex flex-col gap-12` wrapper (48px above the banner).
2. Workspace Proxies and Appearance wrapped `SettingsHeader` in flex
columns with their own `gap` (`gap-4`, `gap-8`), which stacked on top of
the header's built-in `pb-6`, pushing the banner to 40px and 56px
offsets respectively while Groups sits at 24px.
3. `SettingsHeaderDescription` used `leading-normal` (21px lines) while
the inline `Link` component ("View docs") renders a 24px-tall
`inline-flex leading-6` box, inflating the line box on pages whose
description contains a docs link and shifting content below by ~3px.

Changes:

- Replace the custom header markup on both IdP sync pages with the
shared `SettingsHeader`, `SettingsHeaderTitle`, and
`SettingsHeaderDescription` components; the Export Policy button moves
into the `actions` slot.
- Hoist `SettingsHeader` out of the gap wrappers on Workspace Proxies
and Appearance so the header's `pb-6` is the single source of
header-to-content spacing; the gap now applies only within the entitled
content.
- Set `SettingsHeaderDescription` to `leading-6` so description line
height matches the inline `Link`, whether or not a docs link is present.

All affected pages now start content, including the premium paywall,
24px below the header description.

_Created by Coder Agents on behalf of @tracyjohnsonux._
This commit is contained in:
TJ
2026-08-20 23:08:51 +00:00
committed by GitHub
parent 1c139402d0
commit 3ffb266af6
5 changed files with 102 additions and 92 deletions
@@ -97,7 +97,7 @@ export const SettingsHeaderDescription: FC<SettingsHeaderDescriptionProps> = ({
return (
<p
className={cn(
"m-0 text-content-secondary font-medium leading-normal",
"m-0 text-content-secondary font-medium leading-6",
className,
)}
>
@@ -48,7 +48,7 @@ export const AppearanceSettingsPageView: FC<
const getFieldHelpers = getFormHelpers(form);
return (
<div className="flex flex-col gap-8">
<div>
<SettingsHeader
actions={
<SettingsHeaderDocsLink href={docs("/admin/setup/appearance")} />
@@ -73,7 +73,7 @@ export const AppearanceSettingsPageView: FC<
canViewPremium={canViewPremium}
/>
) : (
<>
<div className="flex flex-col gap-8">
<VerticalForm
onSubmit={form.handleSubmit}
aria-label="Appearance settings"
@@ -122,7 +122,7 @@ export const AppearanceSettingsPageView: FC<
onSaveAppearance({ announcement_banners: announcementBanners })
}
/>
</>
</div>
)}
</div>
);
@@ -9,7 +9,12 @@ import {
} from "#/api/queries/idpsync";
import { Loader } from "#/components/Loader/Loader";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
@@ -63,22 +68,23 @@ const IdpOrgSyncPage: FC = () => {
<>
<title>{pageTitle("Organization IdP Sync")}</title>
<div className="flex flex-col gap-12">
<header className="flex flex-row items-baseline justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-3xl m-0">Organization IdP Sync</h1>
<p className="flex flex-row gap-1 text-sm text-content-secondary font-medium m-0">
Automatically assign users to an organization based on their IdP
claims.
</p>
</div>
<div className="flex flex-row gap-2 items-center">
<SettingsHeaderDocsLink
href={docs("/admin/users/idp-sync#organization-sync")}
/>
<ExportPolicyButton syncSettings={settingsQuery.data} />
</div>
</header>
<div>
<SettingsHeader
actions={
<div className="flex flex-row gap-2 items-center">
<SettingsHeaderDocsLink
href={docs("/admin/users/idp-sync#organization-sync")}
/>
<ExportPolicyButton syncSettings={settingsQuery.data} />
</div>
}
>
<SettingsHeaderTitle>Organization IdP Sync</SettingsHeaderTitle>
<SettingsHeaderDescription>
Automatically assign users to an organization based on their IdP
claims.
</SettingsHeaderDescription>
</SettingsHeader>
{!isIdpSyncEnabled ? (
<PaywallPremium
message="IdP Organization Sync"
@@ -14,7 +14,12 @@ import {
import { organizationRoles } from "#/api/queries/roles";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { useOrganizationSettings } from "#/modules/management/OrganizationSettingsLayout";
@@ -118,75 +123,74 @@ const IdpSyncPage: FC = () => {
<div className="w-full max-w-screen-2xl pb-10">
{title}
<div className="flex flex-col gap-12">
<header className="flex flex-row items-baseline justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-3xl m-0">IdP Sync</h1>
<p className="flex flex-row gap-1 text-sm text-content-secondary font-medium m-0">
Automatically assign groups or roles to a user based on their IdP
claims.
</p>
</div>
<SettingsHeader
actions={
<SettingsHeaderDocsLink href={docs("/admin/users/idp-sync")} />
</header>
{!isIdpSyncEnabled ? (
<PaywallPremium
message="IdP Sync"
description="Auto-sync groups & roles from your IdP."
features={[
"Sync groups & roles automatically",
"Configured per organization",
"No manual user assignment",
"Works with your OIDC provider",
]}
canViewPremium={permissions.viewAllLicenses}
/>
) : (
<IdpSyncPageView
tab={tab}
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
claimFieldValues={fieldValuesQuery.data}
groups={groupsQuery.data}
groupsMap={groupsMap}
roles={rolesQuery.data}
organization={organization}
onGroupSyncFieldChange={setGroupField}
onRoleSyncFieldChange={setRoleField}
error={error}
onSubmitGroupSyncSettings={async (data) => {
const mutation = patchGroupSyncSettingsMutation.mutateAsync(data);
toast.promise(mutation, {
loading: "Updating IdP group sync settings...",
success: "IdP group sync settings updated.",
error: (error) => ({
message: getErrorMessage(
error,
"Failed to update IdP group sync settings.",
),
}
>
<SettingsHeaderTitle>IdP Sync</SettingsHeaderTitle>
<SettingsHeaderDescription>
Automatically assign groups or roles to a user based on their IdP
claims.
</SettingsHeaderDescription>
</SettingsHeader>
{!isIdpSyncEnabled ? (
<PaywallPremium
message="IdP Sync"
description="Auto-sync groups & roles from your IdP."
features={[
"Sync groups & roles automatically",
"Configured per organization",
"No manual user assignment",
"Works with your OIDC provider",
]}
canViewPremium={permissions.viewAllLicenses}
/>
) : (
<IdpSyncPageView
tab={tab}
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
claimFieldValues={fieldValuesQuery.data}
groups={groupsQuery.data}
groupsMap={groupsMap}
roles={rolesQuery.data}
organization={organization}
onGroupSyncFieldChange={setGroupField}
onRoleSyncFieldChange={setRoleField}
error={error}
onSubmitGroupSyncSettings={async (data) => {
const mutation = patchGroupSyncSettingsMutation.mutateAsync(data);
toast.promise(mutation, {
loading: "Updating IdP group sync settings...",
success: "IdP group sync settings updated.",
error: (error) => ({
message: getErrorMessage(
error,
"Failed to update IdP group sync settings.",
),
description: getErrorDetail(error),
}),
});
}}
onSubmitRoleSyncSettings={async (data) => {
try {
await patchRoleSyncSettingsMutation.mutateAsync(data);
toast.success("IdP Role sync settings updated.");
} catch (error) {
toast.error(
getErrorMessage(
error,
"Failed to update IdP role sync settings.",
),
{
description: getErrorDetail(error),
}),
});
}}
onSubmitRoleSyncSettings={async (data) => {
try {
await patchRoleSyncSettingsMutation.mutateAsync(data);
toast.success("IdP Role sync settings updated.");
} catch (error) {
toast.error(
getErrorMessage(
error,
"Failed to update IdP role sync settings.",
),
{
description: getErrorDetail(error),
},
);
}
}}
/>
)}
</div>
},
);
}
}}
/>
)}
</div>
);
};
@@ -45,7 +45,7 @@ export const WorkspaceProxyView: FC<WorkspaceProxyViewProps> = ({
permissions,
}) => {
return (
<div className="flex flex-col gap-4">
<div>
<SettingsHeader
actions={
<SettingsHeaderDocsLink
@@ -73,7 +73,7 @@ export const WorkspaceProxyView: FC<WorkspaceProxyViewProps> = ({
canViewPremium={permissions.viewAllLicenses}
/>
) : (
<>
<div className="flex flex-col gap-4">
{Boolean(getWorkspaceProxiesError) && (
<ErrorAlert error={getWorkspaceProxiesError} />
)}
@@ -96,7 +96,7 @@ export const WorkspaceProxyView: FC<WorkspaceProxyViewProps> = ({
/>
</TableBody>
</Table>
</>
</div>
)}
</div>
);