Skip to content
Dethink Components
Workspace · Settings

Northstar Operations

Manage workspace configuration, seats, billing, and security for your team.

Current plan · Growthnorthstar.dev

Workspace settings

Dense forms with calm grouping and a visible save state.

General
Identity and defaults for new projects.
Default regionUsed for new projects.
Feature controls
Toggle workspace-wide capabilities.
Feature controls

Enable command suggestions and run summaries.

Allow admins to export signed audit trails.

Approvals
Guardrails for seat provisioning.

Requests above this number require owner approval.

Autosave enabled

Usage this cycle

Resets Aug 1 · seats, spend, and API volume against your plan limits.

Seats used38 of 50 seats
Monthly spend$1,248 of $1,500 budget
API calls612k of 1M included

Invoices

Billing history stays a semantic table for audit exports.

InvoiceDateAmountStatusDownload
INV-2026-07Jul 1, 2026$1,248.00Paid
INV-2026-06Jun 1, 2026$1,109.00Paid
INV-2026-05May 1, 2026$982.00Paid

Recipe details

SettingsIntermediateSettingsBillingFormsDanger zone

Settings and billing

An account settings page with profile fields, feature toggles, plan selection, invoice rows, usage controls, and a danger zone.

Source

Copy the whole example or lift the component compositions into your own app. Recipes are showcase examples, not registry blocks.

examples/recipes/settings-and-billing.tsx
"use client";

import { useState } from "react";
import type { CSSProperties } from "react";
import {
  AlertTriangle,
  Building2,
  Check,
  CreditCard,
  Download,
  Gauge,
  ShieldCheck,
  Sparkles,
  Wallet,
  Zap,
} from "lucide-react";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
  Button,
  Callout,
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
  Field,
  FieldContent,
  FieldControl,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
  Form,
  Input,
  NumberInput,
  RadioGroup,
  RadioGroupItem,
  Select,
  SelectItem,
  Switch,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
  Progress,
  type ProgressTone,
} from "@dethink/components";
import type { RecipePreviewProps } from "@/lib/recipe-presentation";

// ----------------------------------------------------------------------------
// Decorative layers (token-only, aria-hidden, non-interactive)
// ----------------------------------------------------------------------------

const washStyle: CSSProperties = {
  backgroundImage: [
    "radial-gradient(52% 40% at 0% 0%, color-mix(in oklab, var(--dt-color-primary) 12%, transparent), transparent 70%)",
    "radial-gradient(48% 38% at 100% 2%, color-mix(in oklab, var(--dt-color-info) 10%, transparent), transparent 72%)",
    "radial-gradient(60% 52% at 94% 104%, color-mix(in oklab, var(--dt-color-success) 8%, transparent), transparent 74%)",
    "linear-gradient(to bottom, color-mix(in oklab, var(--dt-color-foreground) 2.5%, transparent), transparent 28%)",
  ].join(", "),
};

// ----------------------------------------------------------------------------
// Domain data
// ----------------------------------------------------------------------------

const invoices = [
  {
    id: "INV-2026-07",
    date: "Jul 1, 2026",
    amount: "$1,248.00",
    status: "Paid",
  },
  {
    id: "INV-2026-06",
    date: "Jun 1, 2026",
    amount: "$1,109.00",
    status: "Paid",
  },
  { id: "INV-2026-05", date: "May 1, 2026", amount: "$982.00", status: "Paid" },
];

type UsageMeter = {
  key: string;
  label: string;
  detail: string;
  value: number;
  tone: ProgressTone;
  icon: typeof Gauge;
};

const usageMeters: UsageMeter[] = [
  {
    key: "seats",
    label: "Seats used",
    detail: "38 of 50 seats",
    value: 76,
    tone: "primary",
    icon: Building2,
  },
  {
    key: "spend",
    label: "Monthly spend",
    detail: "$1,248 of $1,500 budget",
    value: 83,
    tone: "warning",
    icon: Wallet,
  },
  {
    key: "api",
    label: "API calls",
    detail: "612k of 1M included",
    value: 61,
    tone: "info",
    icon: Zap,
  },
];

type Plan = {
  value: string;
  label: string;
  price: string;
  cadence: string;
  feature: string;
};

const plans: Plan[] = [
  {
    value: "starter",
    label: "Starter",
    price: "$29",
    cadence: "/ seat · mo",
    feature: "Core workspace, 3 projects, community support.",
  },
  {
    value: "growth",
    label: "Growth",
    price: "$79",
    cadence: "/ seat · mo",
    feature: "SAML, audit exports, priority support, unlimited projects.",
  },
  {
    value: "enterprise",
    label: "Enterprise",
    price: "Custom",
    cadence: "annual",
    feature: "SCIM, dedicated region, SLA, and named CSM.",
  },
];

// ----------------------------------------------------------------------------
// Presentational bits (token-only)
// ----------------------------------------------------------------------------

function SectionHeading({
  icon: Icon,
  title,
  description,
}: {
  icon: typeof Gauge;
  title: string;
  description: string;
}) {
  return (
    <div className="flex items-start gap-2.5">
      <span
        aria-hidden="true"
        className="bg-primary/10 text-primary ring-primary/20 mt-0.5 grid size-7 shrink-0 place-items-center rounded-md ring-1"
      >
        <Icon className="size-4" />
      </span>
      <div className="min-w-0">
        <div className="text-foreground text-sm font-semibold">{title}</div>
        <div className="text-muted-foreground text-xs leading-5">
          {description}
        </div>
      </div>
    </div>
  );
}

// ----------------------------------------------------------------------------

export function SettingsAndBillingRecipe({
  presentation = "embedded",
}: RecipePreviewProps) {
  const [saved, setSaved] = useState<null | string>(null);
  const [plan, setPlan] = useState("growth");
  const fullPage = presentation === "full-page";
  const currentPlanLabel =
    plans.find((candidate) => candidate.value === plan)?.label ?? "Growth";

  return (
    <div
      data-recipe-surface="settings-and-billing"
      className={`relative ${
        fullPage ? "min-h-[calc(100dvh-7rem)] p-4 sm:p-6 lg:p-8" : ""
      }`}
    >
      <span
        aria-hidden="true"
        style={washStyle}
        className="pointer-events-none absolute -inset-x-6 -top-6 bottom-0 -z-10"
      />

      <div className="space-y-5">
        {/* Header band */}
        <div className="flex flex-wrap items-start justify-between gap-4">
          <div className="flex items-center gap-3.5">
            <span
              aria-hidden="true"
              className="border-border/60 text-foreground/90 grid size-12 shrink-0 place-items-center rounded-xl border text-sm font-semibold shadow-sm"
              style={{
                backgroundColor:
                  "color-mix(in oklab, var(--dt-color-primary) 18%, var(--dt-color-background))",
              }}
            >
              NO
            </span>
            <div className="space-y-1.5">
              <span className="text-muted-foreground inline-flex items-center gap-2 text-[0.7rem] font-semibold tracking-[0.18em] uppercase">
                <Sparkles
                  className="text-primary size-3.5"
                  aria-hidden="true"
                />
                Workspace · Settings
              </span>
              <h2 className="font-heading text-2xl font-semibold tracking-tight">
                Northstar Operations
              </h2>
              <p className="text-muted-foreground max-w-md text-sm leading-6">
                Manage workspace configuration, seats, billing, and security for
                your team.
              </p>
            </div>
          </div>

          <div className="flex flex-col items-start gap-2 ps-[3.875rem] sm:items-end sm:ps-0 sm:pt-6">
            <span
              aria-live="polite"
              aria-atomic="true"
              data-settings-current-plan
              className="bg-primary/15 text-primary ring-primary/25 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset"
            >
              <CreditCard className="size-3.5" aria-hidden="true" />
              Current plan · {currentPlanLabel}
            </span>
            <span className="text-muted-foreground inline-flex items-center gap-1.5 text-xs">
              <Building2 className="size-3.5" aria-hidden="true" />
              northstar.dev
            </span>
          </div>
        </div>

        <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_22rem]">
          <div className="min-w-0 space-y-5">
            {/* Settings card */}
            <Card
              data-settings-panel="workspace"
              shadow="sm"
              className="ring-border/60 relative overflow-hidden ring-1 backdrop-blur-sm"
            >
              <CardHeader>
                <CardTitle className="flex items-center gap-2 text-base">
                  <Building2
                    className="text-primary size-4"
                    aria-hidden="true"
                  />
                  Workspace settings
                </CardTitle>
                <CardDescription>
                  Dense forms with calm grouping and a visible save state.
                </CardDescription>
              </CardHeader>
              <CardContent>
                <Form
                  spacing="md"
                  onSubmit={(event) => {
                    event.preventDefault();
                    setSaved("All changes saved");
                  }}
                >
                  {/* General section */}
                  <div className="space-y-4">
                    <SectionHeading
                      icon={Building2}
                      title="General"
                      description="Identity and defaults for new projects."
                    />
                    <div className="grid gap-4 sm:grid-cols-2">
                      <Field id="settings-name">
                        <FieldLabel>Workspace name</FieldLabel>
                        <FieldControl asChild>
                          <Input defaultValue="Northstar Operations" />
                        </FieldControl>
                      </Field>
                      <Select
                        label="Default region"
                        defaultValue="eu-west"
                        description="Used for new projects."
                      >
                        <SelectItem value="us-east">US East</SelectItem>
                        <SelectItem value="eu-west">EU West</SelectItem>
                        <SelectItem value="ap-south">AP South</SelectItem>
                      </Select>
                    </div>
                  </div>

                  <div className="border-border/60 border-t" />

                  {/* Feature controls */}
                  <FieldSet className="grid gap-4">
                    <SectionHeading
                      icon={Zap}
                      title="Feature controls"
                      description="Toggle workspace-wide capabilities."
                    />
                    <FieldLegend className="sr-only">
                      Feature controls
                    </FieldLegend>
                    <FieldGroup className="gap-3">
                      <Field
                        id="settings-ai"
                        orientation="horizontal"
                        className="border-border/60 bg-background/60 rounded-lg border p-3"
                      >
                        <FieldControl asChild>
                          <Switch defaultChecked name="ai-assistant" />
                        </FieldControl>
                        <FieldContent>
                          <FieldLabel>AI assistant</FieldLabel>
                          <FieldDescription>
                            Enable command suggestions and run summaries.
                          </FieldDescription>
                        </FieldContent>
                      </Field>
                      <Field
                        id="settings-audit"
                        orientation="horizontal"
                        className="border-border/60 bg-background/60 rounded-lg border p-3"
                      >
                        <FieldControl asChild>
                          <Switch defaultChecked name="audit-export" />
                        </FieldControl>
                        <FieldContent>
                          <FieldLabel>Audit export</FieldLabel>
                          <FieldDescription>
                            Allow admins to export signed audit trails.
                          </FieldDescription>
                        </FieldContent>
                      </Field>
                    </FieldGroup>
                  </FieldSet>

                  <div className="border-border/60 border-t" />

                  {/* Seat limit */}
                  <div className="space-y-4">
                    <SectionHeading
                      icon={Gauge}
                      title="Approvals"
                      description="Guardrails for seat provisioning."
                    />
                    <Field id="settings-seat-limit">
                      <FieldLabel>Seat approval limit</FieldLabel>
                      <FieldControl asChild>
                        <NumberInput
                          defaultValue={42}
                          min={1}
                          max={500}
                          name="seat-limit"
                          type="number"
                        />
                      </FieldControl>
                      <FieldDescription>
                        Requests above this number require owner approval.
                      </FieldDescription>
                    </Field>
                  </div>

                  <div className="border-border/60 flex flex-wrap items-center justify-between gap-3 border-t pt-4">
                    <output className="text-sm">
                      {saved ? (
                        <span className="text-foreground/90 inline-flex items-center gap-2">
                          <span
                            aria-hidden="true"
                            className="bg-success grid size-4 shrink-0 place-items-center rounded-full"
                          >
                            <Check className="text-background size-2.5" />
                          </span>
                          {saved}
                        </span>
                      ) : (
                        <span className="text-muted-foreground inline-flex items-center gap-2">
                          <span
                            aria-hidden="true"
                            className="bg-muted-foreground/50 size-2 shrink-0 rounded-full"
                          />
                          Autosave enabled
                        </span>
                      )}
                    </output>
                    <Button type="submit">Save settings</Button>
                  </div>
                </Form>
              </CardContent>
            </Card>

            {/* Usage / billing summary */}
            <Card
              shadow="sm"
              className="ring-border/60 ring-1 backdrop-blur-sm"
            >
              <CardHeader>
                <CardTitle className="flex items-center gap-2 text-base">
                  <Gauge className="text-primary size-4" aria-hidden="true" />
                  Usage this cycle
                </CardTitle>
                <CardDescription>
                  Resets Aug 1 · seats, spend, and API volume against your plan
                  limits.
                </CardDescription>
              </CardHeader>
              <CardContent className="space-y-5">
                {usageMeters.map((meter) => {
                  const Icon = meter.icon;
                  return (
                    <div key={meter.key} className="space-y-2">
                      <div className="flex items-center justify-between gap-3">
                        <span className="text-foreground/90 inline-flex items-center gap-2 text-sm font-medium">
                          <Icon
                            className="text-muted-foreground size-4"
                            aria-hidden="true"
                          />
                          {meter.label}
                        </span>
                        <span className="text-muted-foreground text-xs tabular-nums">
                          {meter.detail}
                        </span>
                      </div>
                      <Progress
                        size="sm"
                        value={meter.value}
                        tone={meter.tone}
                        aria-label={`${meter.label}: ${meter.detail}`}
                      />
                    </div>
                  );
                })}
              </CardContent>
            </Card>

            {/* Invoices */}
            <Card
              shadow="sm"
              className="ring-border/60 ring-1 backdrop-blur-sm"
            >
              <CardHeader>
                <CardTitle className="flex items-center gap-2 text-base">
                  <CreditCard
                    className="text-primary size-4"
                    aria-hidden="true"
                  />
                  Invoices
                </CardTitle>
                <CardDescription>
                  Billing history stays a semantic table for audit exports.
                </CardDescription>
              </CardHeader>
              <CardContent>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead>Invoice</TableHead>
                      <TableHead>Date</TableHead>
                      <TableHead align="end">Amount</TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead align="end">
                        <span className="sr-only">Download</span>
                      </TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {invoices.map((invoice) => (
                      <TableRow
                        key={invoice.id}
                        className="hover:bg-muted/40 motion-safe:transition-colors"
                      >
                        <TableCell className="font-mono text-sm">
                          {invoice.id}
                        </TableCell>
                        <TableCell className="text-muted-foreground">
                          {invoice.date}
                        </TableCell>
                        <TableCell align="end" className="tabular-nums">
                          {invoice.amount}
                        </TableCell>
                        <TableCell>
                          <span className="bg-success/15 text-success ring-success/25 inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[0.7rem] font-semibold ring-1 ring-inset">
                            <span
                              aria-hidden="true"
                              className="bg-success size-1.5 rounded-full"
                            />
                            {invoice.status}
                          </span>
                        </TableCell>
                        <TableCell align="end">
                          <Button
                            variant="ghost"
                            size="sm"
                            leftIcon={<Download />}
                            aria-label={`Download ${invoice.id}`}
                          >
                            Download
                          </Button>
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </CardContent>
            </Card>
          </div>

          <aside className="min-w-0 space-y-5">
            {/* Plan selection */}
            <Card
              data-settings-panel="plan"
              shadow="sm"
              className="ring-border/60 ring-1 backdrop-blur-sm"
            >
              <CardHeader>
                <CardTitle className="flex items-center gap-2 text-base">
                  <CreditCard
                    className="text-primary size-5"
                    aria-hidden="true"
                  />
                  Plan
                </CardTitle>
                <CardDescription>
                  Switch plans with real, keyboard-accessible radios.
                </CardDescription>
              </CardHeader>
              <CardContent>
                <FieldSet>
                  <FieldLegend className="sr-only">Billing plan</FieldLegend>
                  <RadioGroup value={plan} onValueChange={setPlan} name="plan">
                    <FieldGroup className="gap-3">
                      {plans.map((item) => {
                        const selected = plan === item.value;
                        return (
                          <Field
                            key={item.value}
                            id={`settings-plan-${item.value}`}
                            orientation="horizontal"
                            className={`rounded-lg border p-3 motion-safe:transition-colors ${
                              selected
                                ? "border-primary ring-primary/40 bg-primary/5 ring-1"
                                : "border-border/60 bg-background/60 hover:border-border"
                            }`}
                          >
                            <FieldControl asChild>
                              <RadioGroupItem value={item.value} />
                            </FieldControl>
                            <FieldContent>
                              <div className="flex items-baseline justify-between gap-2">
                                <FieldLabel>{item.label}</FieldLabel>
                                <span className="text-foreground shrink-0 text-sm font-semibold whitespace-nowrap tabular-nums">
                                  {item.price}
                                  <span className="text-muted-foreground ml-1 text-[0.7rem] font-normal">
                                    {item.cadence}
                                  </span>
                                </span>
                              </div>
                              <FieldDescription>
                                {item.feature}
                              </FieldDescription>
                            </FieldContent>
                          </Field>
                        );
                      })}
                    </FieldGroup>
                  </RadioGroup>
                </FieldSet>
              </CardContent>
            </Card>

            {/* Security callout */}
            <Callout
              icon={<ShieldCheck />}
              title="Security review complete"
              description="SAML, SCIM, and audit exports are enabled for this workspace."
              tone="success"
            />

            {/* Danger zone */}
            <Card
              shadow="sm"
              className="border-destructive/40 ring-destructive/20 relative overflow-hidden ring-1"
            >
              <span
                aria-hidden="true"
                className="pointer-events-none absolute inset-0"
                style={{
                  backgroundImage:
                    "radial-gradient(80% 60% at 100% 0%, color-mix(in oklab, var(--dt-color-destructive) 8%, transparent), transparent 72%)",
                }}
              />
              <CardHeader className="relative">
                <CardTitle className="text-destructive flex items-center gap-2 text-base">
                  <AlertTriangle className="size-4" aria-hidden="true" />
                  Danger zone
                </CardTitle>
                <CardDescription>
                  Destructive actions require explicit confirmation.
                </CardDescription>
              </CardHeader>
              <CardContent className="relative space-y-3">
                <p className="text-muted-foreground text-sm leading-6">
                  Suspending removes member access immediately. Billing and
                  audit logs remain available to owners.
                </p>
                <AlertDialog>
                  <AlertDialogTrigger variant="destructive">
                    <AlertTriangle className="size-4" aria-hidden="true" />
                    Suspend workspace
                  </AlertDialogTrigger>
                  <AlertDialogContent>
                    <AlertDialogHeader>
                      <AlertDialogTitle>Suspend workspace?</AlertDialogTitle>
                      <AlertDialogDescription>
                        Users will lose access immediately. Billing and audit
                        logs remain available to owners.
                      </AlertDialogDescription>
                    </AlertDialogHeader>
                    <AlertDialogFooter>
                      <AlertDialogCancel>Keep active</AlertDialogCancel>
                      <AlertDialogAction variant="destructive">
                        Suspend
                      </AlertDialogAction>
                    </AlertDialogFooter>
                  </AlertDialogContent>
                </AlertDialog>
              </CardContent>
            </Card>
          </aside>
        </div>
      </div>
    </div>
  );
}