{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dev-1",
  "title": "Dev 1: developers page",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A developers page: a 14-day request chart with errors stacked, success rate and median time, API keys shown in full once and revoked behind a confirmation, and a request log with filter, responses and copy as curl.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/alert-dialog.json",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/blur-fade.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/json-viewer.json",
    "https://ui.ballmac.com/r/number-ticker.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/return-focus.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/dev-1/dev-1.tsx",
      "content": "// Ballmac UI: Dev 1. https://ui.ballmac.com/blocks/dev-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight, CircleAlert, CircleCheck, CircleX, KeyRound, Loader2, Plus, Search, ShieldAlert, TerminalSquare, TriangleAlert } from \"lucide-react\"\n\nimport { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from \"@/components/ballmac/alert-dialog\"\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { BlurFade } from \"@/components/ballmac/blur-fade\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { JsonViewer } from \"@/components/ballmac/json-viewer\"\nimport { NumberTicker } from \"@/components/ballmac/number-ticker\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { useReturnFocus } from \"@/lib/ballmac/return-focus\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Dev1Scope = \"read\" | \"write\" | \"admin\"\ntype Dev1Env = \"test\" | \"live\"\n\ntype Dev1Key = {\n  id: string\n  name: string\n  scope: Dev1Scope\n  /** The last four characters, the only part that is ever shown again. */\n  last4: string\n  /** Already written for people (\"12 Mar 2026\"). */\n  created: string\n  /** Already written for people (\"2 hours ago\"). Leave out for a key that was never used. */\n  lastUsed?: string\n}\n\ntype Dev1Request = {\n  id: string\n  method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"\n  path: string\n  status: number\n  /** Milliseconds. */\n  ms: number\n  /** Already written for people (\"2 min ago\"). */\n  time: string\n  ip?: string\n  /** The name of the key the request used. */\n  keyName?: string\n  response?: unknown\n}\n\ntype Dev1Usage = {\n  /** One entry per day, oldest first. */\n  days: { label: string; ok: number; errors: number }[]\n  /** Median response time in milliseconds. */\n  p50: number\n}\n\ntype Dev1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    keys?: Dev1Key[]\n    requests?: Dev1Request[]\n    usage?: Dev1Usage\n    /** The environment shown at first. */\n    defaultEnv?: Dev1Env\n    onEnvChange?: (env: Dev1Env) => void\n    /** The address the API lives at, used in the curl commands. */\n    baseUrl?: string\n    /**\n     * Creates a key. Resolve with its id and the full secret, which is shown once; throw to show the reason. Defaults to a\n     * simulated key so the page works without a backend.\n     */\n    onCreateKey?: (input: { name: string; scope: Dev1Scope; env: Dev1Env }) => Promise<{ id: string; secret: string; last4?: string }>\n    /** Called after Revoke is confirmed. */\n    onRevokeKey?: (id: string) => void | Promise<void>\n    /** The id of a request that is open at first. */\n    defaultOpenRequest?: string\n  }\n\nconst sampleKeys: Dev1Key[] = [\n  { id: \"k1\", name: \"Production server\", scope: \"write\", last4: \"a1b2\", created: \"12 Mar 2026\", lastUsed: \"2 hours ago\" },\n  { id: \"k2\", name: \"Analytics (read only)\", scope: \"read\", last4: \"9f3c\", created: \"2 Jan 2026\", lastUsed: \"yesterday\" },\n  { id: \"k3\", name: \"CI deploy\", scope: \"admin\", last4: \"77de\", created: \"30 Oct 2025\" },\n]\n\nconst sampleUsage: Dev1Usage = {\n  p50: 84,\n  days: [\n    [\"Oct 4\", 3120, 24], [\"Oct 5\", 3410, 31], [\"Oct 6\", 3290, 19], [\"Oct 7\", 2810, 22], [\"Oct 8\", 2630, 15], [\"Oct 9\", 3560, 40], [\"Oct 10\", 4010, 38],\n    [\"Oct 11\", 3890, 29], [\"Oct 12\", 4210, 61], [\"Oct 13\", 4090, 34], [\"Oct 14\", 3320, 25], [\"Oct 15\", 2980, 17], [\"Oct 16\", 3870, 28], [\"Oct 17\", 4380, 52],\n  ].map(([label, ok, errors]) => ({ label: label as string, ok: ok as number, errors: errors as number })),\n}\n\nconst sampleRequests: Dev1Request[] = [\n  { id: \"req_8f2a91\", method: \"POST\", path: \"/v1/charges\", status: 201, ms: 142, time: \"12 sec ago\", ip: \"203.0.113.24\", keyName: \"Production server\", response: { id: \"ch_3Nf2\", amount: 12900, currency: \"usd\", status: \"succeeded\" } },\n  { id: \"req_8f2a90\", method: \"GET\", path: \"/v1/customers/cus_91\", status: 200, ms: 38, time: \"41 sec ago\", ip: \"203.0.113.24\", keyName: \"Production server\", response: { id: \"cus_91\", email: \"jordan@acme.com\", balance: 0 } },\n  { id: \"req_8f2a8f\", method: \"POST\", path: \"/v1/refunds\", status: 402, ms: 96, time: \"3 min ago\", ip: \"198.51.100.7\", keyName: \"Production server\", response: { error: { type: \"card_error\", code: \"charge_already_refunded\", message: \"This charge has already been refunded.\" } } },\n  { id: \"req_8f2a8e\", method: \"GET\", path: \"/v1/events?limit=50\", status: 200, ms: 61, time: \"6 min ago\", ip: \"198.51.100.19\", keyName: \"Analytics (read only)\", response: { data: [], has_more: false } },\n  { id: \"req_8f2a8d\", method: \"DELETE\", path: \"/v1/webhooks/we_12\", status: 204, ms: 54, time: \"14 min ago\", ip: \"203.0.113.24\", keyName: \"Production server\" },\n  { id: \"req_8f2a8c\", method: \"POST\", path: \"/v1/charges\", status: 500, ms: 2140, time: \"22 min ago\", ip: \"203.0.113.24\", keyName: \"Production server\", response: { error: { type: \"api_error\", message: \"Something went wrong on our side. The request was not processed.\" } } },\n  { id: \"req_8f2a8b\", method: \"GET\", path: \"/v1/invoices\", status: 401, ms: 12, time: \"31 min ago\", ip: \"192.0.2.88\", response: { error: { type: \"authentication_error\", message: \"No valid API key was provided.\" } } },\n  { id: \"req_8f2a8a\", method: \"PATCH\", path: \"/v1/customers/cus_91\", status: 200, ms: 77, time: \"48 min ago\", ip: \"203.0.113.24\", keyName: \"Production server\", response: { id: \"cus_91\", name: \"Jordan Lee\" } },\n]\n\nconst methodTone: Record<Dev1Request[\"method\"], string> = {\n  GET: \"bg-chart-1/20\",\n  POST: \"bg-chart-2/20\",\n  PUT: \"bg-chart-3/25\",\n  PATCH: \"bg-chart-3/25\",\n  DELETE: \"bg-destructive/15\",\n}\nconst statusText: Record<number, string> = { 200: \"OK\", 201: \"Created\", 204: \"No content\", 400: \"Bad request\", 401: \"Unauthorized\", 402: \"Request failed\", 403: \"Forbidden\", 404: \"Not found\", 429: \"Too many requests\", 500: \"Server error\", 502: \"Bad gateway\" }\nconst scopeCopy: Record<Dev1Scope, string> = { read: \"Can read data. Cannot change anything.\", write: \"Can read and change data.\", admin: \"Full access, including keys and webhooks. Keep it out of apps.\" }\n\nfunction StatusChip({ status }: { status: number }) {\n  const ok = status < 400\n  const warn = status >= 400 && status < 500\n  const Icon = ok ? CircleCheck : warn ? TriangleAlert : CircleX\n  return (\n    <span className=\"inline-flex items-center gap-1.5 text-xs whitespace-nowrap tabular-nums\">\n      <Icon aria-hidden=\"true\" className={cn(\"size-3.5\", ok ? \"text-chart-2\" : warn ? \"text-chart-3\" : \"text-destructive\")} />\n      <span className=\"font-medium\">{status}</span>\n      <span className=\"text-muted-foreground hidden sm:inline\">{statusText[status] ?? \"\"}</span>\n    </span>\n  )\n}\n\nfunction DevSkeleton() {\n  return (\n    <div className=\"grid gap-6\" aria-hidden=\"true\">\n      <Skeleton className=\"h-72 rounded-2xl\" />\n      <Skeleton className=\"h-64 rounded-2xl\" />\n      <Skeleton className=\"h-80 rounded-2xl\" />\n    </div>\n  )\n}\n\nfunction Card({ id, title, description, action, children }: { id: string; title: string; description?: string; action?: React.ReactNode; children: React.ReactNode }) {\n  return (\n    <section aria-labelledby={id} className=\"bg-card rounded-2xl border\">\n      <header className=\"flex flex-wrap items-start gap-3 border-b p-5 pb-4\">\n        <div className=\"min-w-0 flex-1 basis-64\">\n          <h2 id={id} className=\"text-sm font-semibold\">\n            {title}\n          </h2>\n          {description ? <p className=\"text-muted-foreground mt-0.5 text-xs text-pretty\">{description}</p> : null}\n        </div>\n        {action}\n      </header>\n      {children}\n    </section>\n  )\n}\n\n/**\n * A developers page: how the API is doing (a 14-day chart with errors stacked, success rate, median time), API keys that are shown in\n * full exactly once and revoked behind a confirmation, and a request log with a filter, expandable responses and copy as curl.\n */\nfunction Dev1({\n  keys: keysProp = sampleKeys,\n  requests = sampleRequests,\n  usage = sampleUsage,\n  defaultEnv = \"test\",\n  onEnvChange,\n  baseUrl = \"https://api.acme.com\",\n  onCreateKey,\n  onRevokeKey,\n  defaultOpenRequest,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Dev1Props) {\n  const id = React.useId()\n  const [env, setEnv] = React.useState<Dev1Env>(defaultEnv)\n  const [keys, setKeys] = React.useState(keysProp)\n  React.useEffect(() => setKeys(keysProp), [keysProp])\n  const [creating, setCreating] = React.useState(false)\n  const [name, setName] = React.useState(\"\")\n  const [scope, setScope] = React.useState<Dev1Scope>(\"read\")\n  const [saving, setSaving] = React.useState(false)\n  const [createError, setCreateError] = React.useState<string>()\n  const [fresh, setFresh] = React.useState<{ name: string; secret: string } | null>(null)\n  const [revoke, setRevoke] = React.useState<Dev1Key | null>(null)\n  const returnFocus = useReturnFocus(!!revoke)\n  const [filter, setFilter] = React.useState<\"all\" | \"errors\">(\"all\")\n  const [query, setQuery] = React.useState(\"\")\n  const [open, setOpen] = React.useState<string | null>(defaultOpenRequest ?? null)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const made = React.useRef(0)\n  const ready = state === \"ready\" || state === \"empty\"\n  const log = state === \"empty\" ? [] : requests\n  const prefix = env === \"live\" ? \"sk_live_\" : \"sk_test_\"\n\n  const total = usage.days.reduce((s, d) => s + d.ok + d.errors, 0)\n  const errors = usage.days.reduce((s, d) => s + d.errors, 0)\n  const rate = total ? ((total - errors) / total) * 100 : 100\n  const peak = Math.max(1, ...usage.days.map((d) => d.ok + d.errors))\n  const busiest = usage.days.reduce((a, d) => (d.ok + d.errors > a.ok + a.errors ? d : a), usage.days[0] ?? { label: \"\", ok: 0, errors: 0 })\n\n  const q = query.trim().toLowerCase()\n  const shown = log.filter((r) => (filter === \"all\" || r.status >= 400) && (!q || r.path.toLowerCase().includes(q) || r.method.toLowerCase().includes(q) || String(r.status).includes(q)))\n  const errorCount = log.filter((r) => r.status >= 400).length\n  const curl = (r: Dev1Request) => `curl ${baseUrl}${r.path} \\\\\\n  -X ${r.method} \\\\\\n  -H \"Authorization: Bearer $API_KEY\"`\n  const firstCurl = `curl ${baseUrl}/v1/charges \\\\\\n  -X POST \\\\\\n  -H \"Authorization: Bearer $API_KEY\" \\\\\\n  -d amount=2000 -d currency=usd`\n\n  async function create(e: React.FormEvent) {\n    e.preventDefault()\n    if (!name.trim()) return setCreateError(\"Give the key a name so you can recognise it later.\")\n    setSaving(true)\n    setCreateError(undefined)\n    try {\n      const result: { id: string; secret: string; last4?: string } = onCreateKey\n        ? await onCreateKey({ name: name.trim(), scope, env })\n        : await new Promise<{ id: string; secret: string }>((r) => {\n            made.current += 1\n            setTimeout(() => r({ id: `new-${made.current}`, secret: `${prefix}${\"x7Kq2MvR9tLpA4dZ8nWcB3yF\".slice(0, 20)}${String(made.current).padStart(4, \"0\")}` }), 700)\n          })\n      setKeys((l) => [{ id: result.id, name: name.trim(), scope, last4: result.last4 ?? result.secret.slice(-4), created: \"Just now\" }, ...l])\n      setFresh({ name: name.trim(), secret: result.secret })\n      setName(\"\")\n      setScope(\"read\")\n      setCreating(false)\n      setAnnounce(`Key ${name.trim()} created. Copy it now.`)\n    } catch (err) {\n      setCreateError(err instanceof Error && err.message ? err.message : \"The key could not be created. Try again.\")\n    } finally {\n      setSaving(false)\n    }\n  }\n\n  const content = (\n    <div className=\"grid grid-cols-1 gap-6\">\n      <BlurFade direction=\"up\" offset={10} duration={0.45}>\n        <Card id={`${id}-usage`} title=\"API requests\" description=\"The last 14 days. Errors are stacked in red at the bottom of each bar.\">\n          <div className=\"grid gap-6 p-5 lg:grid-cols-[minmax(0,1fr)_14rem]\">\n            <div>\n              <div className=\"flex items-baseline gap-3\">\n                <p className=\"text-4xl font-semibold tracking-tight tabular-nums\">\n                  <NumberTicker value={total} />\n                </p>\n                <p className=\"text-muted-foreground text-sm\">requests</p>\n              </div>\n              <div role=\"img\" aria-label={`${new Intl.NumberFormat(\"en-US\").format(total)} requests in ${usage.days.length} days. The busiest day was ${busiest.label} with ${new Intl.NumberFormat(\"en-US\").format(busiest.ok + busiest.errors)}.`} className=\"mt-5 flex h-52 items-end gap-1.5 sm:gap-2\">\n                {usage.days.map((d) => {\n                  const all = d.ok + d.errors\n                  return (\n                    <div key={d.label} className=\"group/bar relative flex h-full min-w-0 flex-1 flex-col justify-end\">\n                      <div className=\"pointer-events-none absolute bottom-full start-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded-lg border bg-popover px-2.5 py-1.5 text-xs whitespace-nowrap shadow-md group-hover/bar:block rtl:translate-x-1/2\">\n                        <p className=\"font-medium\">{d.label}</p>\n                        <p className=\"text-muted-foreground tabular-nums\">\n                          {new Intl.NumberFormat(\"en-US\").format(all)} requests · {d.errors} errors\n                        </p>\n                      </div>\n                      <div className=\"hover:opacity-80 flex flex-col justify-end overflow-hidden rounded-t-md transition-opacity\" style={{ height: `${Math.max(4, (all / peak) * 100)}%` }}>\n                        <div className=\"bg-chart-1\" style={{ flexGrow: d.ok }} />\n                        <div className=\"bg-destructive min-h-[3px]\" style={{ flexGrow: Math.max(d.errors * 6, d.errors ? 0.01 : 0), flexBasis: 0 }} />\n                      </div>\n                    </div>\n                  )\n                })}\n              </div>\n              <div aria-hidden=\"true\" className=\"text-muted-foreground mt-2 flex justify-between text-[11px] tabular-nums\">\n                <span>{usage.days[0]?.label}</span>\n                <span>{usage.days[usage.days.length - 1]?.label}</span>\n              </div>\n              <div className=\"sr-only\">\n<table>\n                <caption>Requests per day</caption>\n                <thead>\n                  <tr>\n                    <th scope=\"col\">Day</th>\n                    <th scope=\"col\">Successful</th>\n                    <th scope=\"col\">Errors</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {usage.days.map((d) => (\n                    <tr key={d.label}>\n                      <th scope=\"row\">{d.label}</th>\n                      <td>{d.ok}</td>\n                      <td>{d.errors}</td>\n                    </tr>\n                  ))}\n                </tbody>\n              </table>\n</div>\n            </div>\n            <dl className=\"grid content-start gap-3 sm:grid-cols-3 lg:grid-cols-1\">\n              {[\n                { label: \"Success rate\", value: `${rate.toFixed(1)}%`, hint: rate >= 99 ? \"Healthy\" : \"Worth a look\" },\n                { label: \"Median time\", value: `${usage.p50} ms`, hint: \"p50\" },\n                { label: \"Errors\", value: new Intl.NumberFormat(\"en-US\").format(errors), hint: \"Last 14 days\" },\n              ].map((s) => (\n                <div key={s.label} className=\"bg-muted/50 rounded-xl p-4\">\n                  <dt className=\"text-muted-foreground text-xs\">{s.label}</dt>\n                  <dd className=\"mt-1 text-2xl font-semibold tracking-tight tabular-nums\">{s.value}</dd>\n                  <dd className=\"text-muted-foreground mt-0.5 text-xs\">{s.hint}</dd>\n                </div>\n              ))}\n            </dl>\n          </div>\n        </Card>\n      </BlurFade>\n\n      <BlurFade direction=\"up\" offset={10} duration={0.45} delay={0.08}>\n        <Card\n          id={`${id}-keys`}\n          title=\"API keys\"\n          description={`Keys for ${env === \"live\" ? \"live\" : \"test\"} mode. A key is shown in full once, when you create it.`}\n          action={\n            !creating ? (\n              <Button size=\"sm\" onClick={() => setCreating(true)}>\n                <Plus aria-hidden=\"true\" />\n                Create key\n              </Button>\n            ) : null\n          }\n        >\n          {fresh ? (\n            <div role=\"alert\" className=\"bg-chart-3/10 border-chart-3/30 m-5 mb-0 rounded-xl border p-4\">\n              <p className=\"flex items-center gap-2 text-sm font-semibold\">\n                <ShieldAlert aria-hidden=\"true\" className=\"size-4\" />\n                Copy “{fresh.name}” now\n              </p>\n              <p className=\"text-muted-foreground mt-1 text-xs text-pretty\">This is the only time the full key is shown. Store it somewhere safe; if you lose it, create a new one.</p>\n              <div className=\"bg-background mt-3 flex items-center gap-2 rounded-lg border p-1.5 ps-3\">\n                <code dir=\"ltr\" className=\"min-w-0 flex-1 truncate text-start font-mono text-[13px]\">\n                  {fresh.secret}\n                </code>\n                <CopyButton value={fresh.secret} label=\"Copy\" copiedLabel=\"Copied\" variant=\"outline\" size=\"sm\" />\n              </div>\n              <Button variant=\"ghost\" size=\"sm\" className=\"mt-2\" onClick={() => setFresh(null)}>\n                I have saved it\n              </Button>\n            </div>\n          ) : null}\n          {creating ? (\n            <form onSubmit={create} className=\"m-5 mb-0 grid gap-4 rounded-xl border p-4\" noValidate>\n              <div className=\"grid gap-1.5\">\n                <label htmlFor={`${id}-name`} className=\"text-sm font-medium\">\n                  Name\n                </label>\n                <input\n                  id={`${id}-name`}\n                  autoFocus\n                  value={name}\n                  onChange={(e) => setName(e.target.value)}\n                  placeholder=\"Production server\"\n                  aria-invalid={!!createError || undefined}\n                  aria-describedby={createError ? `${id}-name-error` : undefined}\n                  className=\"border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive h-9 rounded-md border px-3 text-sm outline-none focus-visible:ring-[3px]\"\n                />\n                {createError ? (\n                  <p id={`${id}-name-error`} role=\"alert\" className=\"text-destructive text-xs\">\n                    {createError}\n                  </p>\n                ) : null}\n              </div>\n              <div className=\"grid gap-1.5\">\n                <span id={`${id}-scope`} className=\"text-sm font-medium\">\n                  Access\n                </span>\n                <SegmentedControl value={scope} onValueChange={(v) => setScope(v as Dev1Scope)} aria-labelledby={`${id}-scope`} size=\"sm\">\n                  <SegmentedControlItem value=\"read\">Read</SegmentedControlItem>\n                  <SegmentedControlItem value=\"write\">Write</SegmentedControlItem>\n                  <SegmentedControlItem value=\"admin\">Admin</SegmentedControlItem>\n                </SegmentedControl>\n                <p className=\"text-muted-foreground text-xs\">{scopeCopy[scope]}</p>\n              </div>\n              <div className=\"flex gap-2\">\n                <Button type=\"submit\" size=\"sm\" disabled={saving}>\n                  {saving ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : <KeyRound aria-hidden=\"true\" />}\n                  {saving ? \"Creating…\" : \"Create key\"}\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  disabled={saving}\n                  onClick={() => {\n                    setCreating(false)\n                    setCreateError(undefined)\n                  }}\n                >\n                  Cancel\n                </Button>\n              </div>\n            </form>\n          ) : null}\n          <ul aria-label=\"API keys\" className=\"divide-y p-2\">\n            {keys.map((k) => (\n              <li key={k.id} className=\"flex flex-wrap items-center gap-x-4 gap-y-2 px-3 py-3.5\">\n                <span aria-hidden=\"true\" className=\"bg-muted flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4\">\n                  <KeyRound />\n                </span>\n                <div className=\"min-w-0 flex-1 basis-52\">\n                  <p className=\"flex flex-wrap items-center gap-2 text-sm font-medium\">\n                    {k.name}\n                    <Badge variant=\"outline\" status={k.scope === \"admin\" ? \"warning\" : undefined} dot={false} className=\"font-normal capitalize\">\n                      {k.scope}\n                    </Badge>\n                  </p>\n                  <p dir=\"ltr\" className=\"text-muted-foreground mt-0.5 text-start font-mono text-xs\">\n                    {prefix}••••••••{k.last4}\n                  </p>\n                </div>\n                <p className=\"text-muted-foreground text-xs tabular-nums\">\n                  Created {k.created} · {k.lastUsed ? `used ${k.lastUsed}` : \"never used\"}\n                </p>\n                <Button variant=\"outline\" size=\"sm\" onClick={() => setRevoke(k)} aria-label={`Revoke ${k.name}`}>\n                  Revoke\n                </Button>\n              </li>\n            ))}\n            {!keys.length ? <li className=\"text-muted-foreground px-3 py-8 text-center text-sm\">No keys yet. Create one to make your first request.</li> : null}\n          </ul>\n        </Card>\n      </BlurFade>\n\n      <BlurFade direction=\"up\" offset={10} duration={0.45} delay={0.16}>\n        <Card\n          id={`${id}-log`}\n          title=\"Recent requests\"\n          description=\"Newest first. Open one to see the response, or copy it as a curl command.\"\n          action={\n            log.length ? (\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <div className=\"bg-background focus-within:ring-ring/50 flex h-8 items-center gap-2 rounded-md border px-2 focus-within:ring-[3px]\">\n                  <Search aria-hidden=\"true\" className=\"text-muted-foreground size-3.5\" />\n                  <input type=\"search\" aria-label=\"Filter requests\" placeholder=\"Filter\" value={query} onChange={(e) => setQuery(e.target.value)} className=\"placeholder:text-muted-foreground w-24 bg-transparent text-xs outline-none sm:w-32\" />\n                </div>\n                <SegmentedControl value={filter} onValueChange={(v) => setFilter(v as typeof filter)} aria-label=\"Show\" size=\"sm\">\n                  <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n                  <SegmentedControlItem value=\"errors\">Errors {errorCount}</SegmentedControlItem>\n                </SegmentedControl>\n              </div>\n            ) : null\n          }\n        >\n          {log.length ? (\n            <ul aria-label=\"Requests\" className=\"divide-y\">\n              {shown.map((r) => {\n                const isOpen = open === r.id\n                return (\n                  <li key={r.id}>\n                    <button\n                      type=\"button\"\n                      aria-expanded={isOpen}\n                      aria-controls={`${id}-${r.id}`}\n                      onClick={() => setOpen(isOpen ? null : r.id)}\n                      className=\"hover:bg-accent/40 focus-visible:ring-ring/50 grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 px-5 py-3 text-start outline-none focus-visible:ring-[3px] focus-visible:ring-inset md:grid-cols-[4.5rem_minmax(0,1fr)_9rem_4.5rem_6rem_auto]\"\n                    >\n                      <span className={cn(\"rounded px-1.5 py-0.5 text-center font-mono text-[11px] font-semibold\", methodTone[r.method])}>{r.method}</span>\n                      <span dir=\"ltr\" className=\"min-w-0 truncate text-start font-mono text-[13px]\">\n                        {r.path}\n                      </span>\n                      <span className=\"max-md:order-last max-md:col-span-2 max-md:col-start-2\">\n                        <StatusChip status={r.status} />\n                      </span>\n                      <span className=\"text-muted-foreground hidden text-end text-xs tabular-nums md:block\">{r.ms} ms</span>\n                      <span className=\"text-muted-foreground hidden text-end text-xs md:block\">{r.time}</span>\n                      <ChevronRight aria-hidden=\"true\" className={cn(\"text-muted-foreground size-4 transition-transform duration-150 motion-reduce:transition-none\", isOpen ? \"rotate-90\" : \"rtl:rotate-180\")} />\n                    </button>\n                    {isOpen ? (\n                      <div id={`${id}-${r.id}`} className=\"bg-muted/30 grid gap-4 border-t px-5 py-4 lg:grid-cols-[14rem_minmax(0,1fr)]\">\n                        <div className=\"grid content-start gap-3\">\n                          <dl className=\"grid gap-2 text-xs\">\n                            {[\n                              [\"Request ID\", r.id],\n                              [\"Took\", `${r.ms} ms`],\n                              [\"When\", r.time],\n                              ...(r.keyName ? [[\"Key\", r.keyName]] : []),\n                              ...(r.ip ? [[\"IP address\", r.ip]] : []),\n                            ].map(([k, v]) => (\n                              <div key={k} className=\"flex justify-between gap-3\">\n                                <dt className=\"text-muted-foreground\">{k}</dt>\n                                <dd dir=\"ltr\" className=\"min-w-0 truncate text-end font-mono\">\n                                  {v}\n                                </dd>\n                              </div>\n                            ))}\n                          </dl>\n                          <CopyButton value={curl(r)} label=\"Copy as curl\" copiedLabel=\"Copied\" variant=\"outline\" size=\"sm\" className=\"justify-self-start\" />\n                        </div>\n                        <div>\n                          <p className=\"text-muted-foreground mb-1.5 text-xs font-medium\">Response</p>\n                          {r.response !== undefined ? <JsonViewer value={r.response} label=\"Response body\" defaultExpandedDepth={2} /> : <p className=\"text-muted-foreground text-xs\">No body. The server answered {r.status} {statusText[r.status] ?? \"\"}.</p>}\n                        </div>\n                      </div>\n                    ) : null}\n                  </li>\n                )\n              })}\n              {!shown.length ? <li className=\"text-muted-foreground px-5 py-10 text-center text-sm\">No requests match.</li> : null}\n            </ul>\n          ) : (\n            <div className=\"px-6 py-12 text-center\">\n              <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n                <TerminalSquare className=\"size-6\" />\n              </span>\n              <h3 className=\"mt-4 text-base font-semibold tracking-tight\">{emptyState?.title ?? \"No requests yet\"}</h3>\n              <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-sm text-sm text-pretty\">{emptyState?.description ?? \"Make your first request with a test key and it will appear here within a few seconds.\"}</p>\n              <div className=\"bg-background mx-auto mt-5 w-full max-w-lg min-w-0 rounded-xl border text-start\">\n                <pre dir=\"ltr\" className=\"overflow-x-auto p-4 font-mono text-xs leading-relaxed\">\n                  {firstCurl}\n                </pre>\n                <div className=\"flex justify-end border-t p-2\">\n                  <CopyButton value={firstCurl} label=\"Copy\" copiedLabel=\"Copied\" variant=\"ghost\" size=\"sm\" />\n                </div>\n              </div>\n              {emptyState?.action}\n            </div>\n          )}\n        </Card>\n      </BlurFade>\n    </div>\n  )\n\n  return (\n    <section data-slot=\"dev-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8 flex flex-wrap items-center gap-4\">\n        <div className=\"min-w-0 flex-1 basis-64\">\n          <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em]\">\n            Developers\n          </h1>\n          <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">Keys, usage and a log of what your code sent.</p>\n        </div>\n        <SegmentedControl\n          value={env}\n          onValueChange={(v) => {\n            setEnv(v as Dev1Env)\n            onEnvChange?.(v as Dev1Env)\n          }}\n          aria-label=\"Mode\"\n          disabled={!ready}\n        >\n          <SegmentedControlItem value=\"test\">Test mode</SegmentedControlItem>\n          <SegmentedControlItem value=\"live\">\n            <span aria-hidden=\"true\" className=\"bg-chart-2 me-1.5 inline-block size-1.5 rounded-full\" />\n            Live mode\n          </SegmentedControlItem>\n        </SegmentedControl>\n      </header>\n      {env === \"live\" && ready ? (\n        <p role=\"note\" className=\"bg-chart-3/10 border-chart-3/30 mb-6 flex items-start gap-3 rounded-xl border px-4 py-3 text-sm text-pretty\">\n          <CircleAlert aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n          You are looking at live mode. Requests with these keys change real customer data.\n        </p>\n      ) : null}\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading developer data\" loading={<DevSkeleton />} error={{ title: errorState?.title ?? \"Developer data could not be loaded\", description: errorState?.description ?? \"Your keys are safe and nothing changed. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n\n      <AlertDialog open={!!revoke} onOpenChange={(o) => !o && setRevoke(null)}>\n        <AlertDialogContent {...returnFocus}>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Revoke “{revoke?.name}”?</AlertDialogTitle>\n            <AlertDialogDescription>Anything using this key stops working straight away. This cannot be undone, but you can create a new key.</AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              onClick={async () => {\n                if (!revoke) return\n                await onRevokeKey?.(revoke.id)\n                setKeys((l) => l.filter((x) => x.id !== revoke.id))\n                setAnnounce(`${revoke.name} revoked`)\n              }}\n            >\n              Revoke key\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Dev1, type Dev1Props, type Dev1Key, type Dev1Request, type Dev1Usage, type Dev1Scope, type Dev1Env }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/dev-1/dev-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "developer",
      "api",
      "api keys",
      "request log",
      "usage",
      "test mode",
      "live mode",
      "curl",
      "logs",
      "dashboard",
      "platform"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "A developer dashboard page. <Dev1 keys={[{ id, name, scope: 'read' | 'write' | 'admin', last4, created, lastUsed }]} usage={{ days: [{ label, ok, errors }], p50 }} requests={[{ id, method, path, status, ms, time, ip, keyName, response }]} baseUrl onCreateKey={async ({ name, scope, env }) => ({ id, secret })} onRevokeKey onEnvChange defaultEnv />. The secret returned by onCreateKey is shown once in a copy banner and only its last four characters are kept in the list.",
      "whenToUse": [
        "The developer or API section of a platform product",
        "Letting customers manage keys and debug requests themselves"
      ],
      "whenNotToUse": [
        "Showing one secret in a settings form (use api-key-field)",
        "Webhook endpoints and deliveries (use webhook-card)",
        "Server logs for operators (use log-stream)"
      ],
      "composesWith": [
        "settings-4",
        "settings-5",
        "app-shell-1",
        "agent-3"
      ],
      "a11y": [
        {
          "keys": "Chart",
          "action": "An image with a text summary, plus a hidden table with the numbers for each day; bars never carry meaning by color alone"
        },
        {
          "keys": "Create key",
          "action": "The new key is announced and shown in an alert with a Copy button; Revoke asks first in an alert dialog"
        },
        {
          "keys": "Enter / Space on a request",
          "action": "Opens its response (aria-expanded); status is an icon, the number and words, not color alone"
        }
      ],
      "customization": [
        "baseUrl for the curl commands; defaultEnv: 'test' | 'live' (live shows a warning and sk_live_ prefixes)",
        "onCreateKey must call your backend: the secret must never be derivable on the client",
        "defaultOpenRequest opens a request at first"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "dev-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton of the usage chart, the keys and the request log under the real heading."
      },
      {
        "id": "empty",
        "title": "No requests yet",
        "example": "dev-1-empty",
        "trigger": "state=\"empty\" (or requests={[]})",
        "note": "Explains that requests appear within seconds and gives a ready curl command to make the first one, with a copy button."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "dev-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying keys are safe and nothing changed, with Try again (onRetry)."
      }
    ],
    "examples": [
      "dev-1-demo",
      "dev-1-live",
      "dev-1-empty",
      "dev-1-loading",
      "dev-1-error"
    ],
    "url": "https://ui.ballmac.com/components/dev-1"
  },
  "categories": [
    "blocks",
    "developer"
  ],
  "type": "registry:block"
}