diff --git a/frontend/components/settings/settings-parts.tsx b/frontend/components/settings/settings-parts.tsx
index 4b30423..0d5d06f 100644
--- a/frontend/components/settings/settings-parts.tsx
+++ b/frontend/components/settings/settings-parts.tsx
@@ -1,7 +1,7 @@
"use client";
import type { ReactNode } from "react";
-import { useState } from "react";
+import { createContext, useContext, useState } from "react";
import { Check, Copy } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -13,9 +13,15 @@ import {
CardFrameHeader,
CardFrameTitle,
} from "@/components/ui/card";
+import { Frame, FramePanel } from "@/components/ui/frame";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
+// Whether the enclosing frame is a COSS "Separated Panels" tray. When true,
+// SettingsCard/ToggleRow render as a FramePanel (a distinct bordered card on the
+// muted tray) instead of a fused CardFrame Card.
+const SeparatedFrameContext = createContext(false);
+
// A settings section rendered inside the COSS "frame" surface: a titled header
// above one or more cards.
//
@@ -43,15 +49,36 @@ export function SettingsFrame({
children: ReactNode;
className?: string;
/**
- * COSS "Separated Panels": add a 1rem gap so sibling panels read as distinct
- * cards instead of one flush joined list. The `gap-4` matches CardFrame's
- * built-in `--clip-top/--clip-bottom: -1rem`, which keeps each panel's rounded
- * corners clipping correctly across the gap. Leave off for a joined list.
+ * COSS "Separated Panels": render a muted Frame tray whose children are
+ * distinct bordered FramePanels spaced apart (the real coss.com/ui/frame
+ * look), instead of the fused CardFrame surface. Leave off for a joined list.
*/
separated?: boolean;
}) {
+ if (separated) {
+ return (
+
+
+
+
{title}
+ {description ? (
+
{description}
+ ) : null}
+
+ {action ?
{action}
: null}
+
+
+ {children}
+
+
+ );
+ }
+
return (
-
+
{title}
{description ? (
@@ -105,6 +132,16 @@ export function SettingsCard({
className?: string;
children: ReactNode;
}) {
+ const separated = useContext(SeparatedFrameContext);
+ if (separated) {
+ // A distinct panel on the Frame tray. `flex flex-col` mirrors Card's default
+ // layout; callers that need a row (ToggleRow) override with `flex-row`.
+ return (
+
+ {children}
+
+ );
+ }
return {children};
}
diff --git a/frontend/components/ui/frame.tsx b/frontend/components/ui/frame.tsx
new file mode 100644
index 0000000..1af8ee9
--- /dev/null
+++ b/frontend/components/ui/frame.tsx
@@ -0,0 +1,94 @@
+import type React from "react";
+
+import { cn } from "@/lib/utils";
+
+// COSS "Frame" primitive (coss.com/ui/docs/components/frame). A Frame is a muted
+// tray that holds one or more FramePanels; multiple panels are "Separated
+// Panels" — spaced apart with `mt-1` by default (via the adjacent-sibling
+// selector below), each a self-contained bordered card. This is the opposite of
+// `CardFrame` in card.tsx, which fuses its cards into one flush surface.
+
+export function Frame({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FramePanel({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameTitle({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}