frontend: compose settings frames from CardFrame primitives

SettingsFrame used a hand-rolled <div className="p-5"> body and
SettingsCard was a plain div (no data-slot=card), so the frame's card
styling never applied.

- Add CardFramePanel (data-slot="card-frame-panel") as the padded frame
  body, mirroring the other CardFrame* subcomponents.
- SettingsFrame now composes CardFrameHeader + CardFramePanel (same p-5
  padding as before) instead of a raw div.
- SettingsCard renders a real COSS Card (data-slot=card). Since Card is
  flex flex-col, the horizontal-row call sites (ToggleRow, billing and
  preferences rows) now pass flex-row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-14 21:10:19 +03:00
parent 76da310766
commit 44d653ffbd
4 changed files with 32 additions and 11 deletions
@@ -176,7 +176,7 @@ export function SigningPanel() {
description={t("settings.network.description")} description={t("settings.network.description")}
title={t("settings.network.title")} title={t("settings.network.title")}
> >
<SettingsCard className="flex items-center justify-between gap-4 p-5"> <SettingsCard className="flex flex-row items-center justify-between gap-4 p-5">
<div className="min-w-0 space-y-0.5"> <div className="min-w-0 space-y-0.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
@@ -251,7 +251,7 @@ export function SigningPanel() {
description={t("settings.signing.backupDescription")} description={t("settings.signing.backupDescription")}
title={t("settings.signing.backupTitle")} title={t("settings.signing.backupTitle")}
> >
<SettingsCard className="flex items-center justify-between gap-4 p-4"> <SettingsCard className="flex flex-row items-center justify-between gap-4 p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{t("settings.signing.backupLabel")} {t("settings.signing.backupLabel")}
@@ -5,12 +5,13 @@ import { useState } from "react";
import { Check, Copy } from "lucide-react"; import { Check, Copy } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { import {
Card,
CardFrame, CardFrame,
CardFrameAction, CardFrameAction,
CardFrameDescription, CardFrameDescription,
CardFrameHeader, CardFrameHeader,
CardFramePanel,
CardFrameTitle, CardFrameTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -43,7 +44,7 @@ export function SettingsFrame({
) : null} ) : null}
{action ? <CardFrameAction>{action}</CardFrameAction> : null} {action ? <CardFrameAction>{action}</CardFrameAction> : null}
</CardFrameHeader> </CardFrameHeader>
<div className={cn("p-5", bodyClassName)}>{children}</div> <CardFramePanel className={bodyClassName}>{children}</CardFramePanel>
</CardFrame> </CardFrame>
); );
} }
@@ -74,6 +75,10 @@ export function SettingsSection({
); );
} }
// A card surface used inside settings panels. Rendering a real COSS `Card` (with
// `data-slot="card"`) keeps settings cards consistent with the rest of the app
// and lets them pick up the frame's card treatment. `Card` is `flex flex-col`,
// so row layouts must pass `flex-row` in their className.
export function SettingsCard({ export function SettingsCard({
className, className,
children, children,
@@ -81,11 +86,7 @@ export function SettingsCard({
className?: string; className?: string;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return <Card className={className}>{children}</Card>;
<div className={cn("rounded-2xl border border-border bg-card/30", className)}>
{children}
</div>
);
} }
export function ToggleRow({ export function ToggleRow({
@@ -103,7 +104,7 @@ export function ToggleRow({
onCheckedChange?: (checked: boolean) => void; onCheckedChange?: (checked: boolean) => void;
}) { }) {
return ( return (
<SettingsCard className="flex items-center justify-between gap-4 px-4 py-3.5"> <SettingsCard className="flex flex-row items-center justify-between gap-4 px-4 py-3.5">
<div className="space-y-0.5"> <div className="space-y-0.5">
<p className="text-sm font-medium">{title}</p> <p className="text-sm font-medium">{title}</p>
{description ? ( {description ? (
@@ -375,7 +375,7 @@ export function ProfilePanel() {
description={t("settings.profile.dangerZoneDescription")} description={t("settings.profile.dangerZoneDescription")}
title={t("settings.profile.dangerZone")} title={t("settings.profile.dangerZone")}
> >
<SettingsCard className="flex items-center justify-between gap-4 p-4"> <SettingsCard className="flex flex-row items-center justify-between gap-4 p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{t("settings.profile.deleteAccount")} {t("settings.profile.deleteAccount")}
+20
View File
@@ -136,6 +136,26 @@ export function CardFrameFooter({
}); });
} }
// The padded body of a frame: content that sits between the frame header and
// footer. Mirrors the other CardFrame* subcomponents (useRender + data-slot) so
// a frame can be composed entirely from primitives instead of a raw <div>.
export function CardFramePanel({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("p-5", className),
"data-slot": "card-frame-panel",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardHeader({ export function CardHeader({
className, className,
render, render,