# Secret Field

A read-only secret, such as an API key, hidden until shown, with buttons to copy and regenerate it.

Source: [secret-field.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/secret-field.tsx)

```tsx
import { SecretField } from "@/components/ui/secret-field";

export default function SecretFieldBasic() {
  return (
    <SecretField
      label="Secret key"
      value="acme_live_7f3a9c2e81d4b05a"
      visiblePrefix={10}
      className="max-w-sm"
    />
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/secret-field
```

## Usage

```tsx
import { SecretField } from "@/components/ui/secret-field";

export function SecretKey() {
  return <SecretField label="Secret key" value="acme_live_7f3a9c2e81d4b05a" />;
}
```

Secret Field is a [Copy Field](https://ui.jarv.is/components/copy-field.md) that hides the value until the eye button is
pressed. Copying always copies the whole value.

For password entry, use an [Input](https://ui.jarv.is/components/input.md) with `type="password"`. If the browser should
never see the secret at all, don't send it to the page.

## Examples

### Visible characters

`visibleSuffix` and `visiblePrefix` set how many characters stay visible at each end while the
secret is hidden.

```tsx
import { SecretField } from "@/components/ui/secret-field";

export default function SecretFieldVisibleCharacters() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <SecretField label="Webhook signing secret" value="acme_whk_3c91e07b5fa24d68" />
      <SecretField
        label="Publishable key"
        value="acme_pub_5d2a8e61c09f47b3"
        visiblePrefix={9}
        visibleSuffix={6}
      />
      <SecretField label="Database password" value="correct-horse-battery" visibleSuffix={0} />
    </div>
  );
}
```

### Regenerate

`onRegenerate` adds a button for making a new secret. Set `regenerating` while it's in progress.
`revealed` and `onRevealedChange` control whether the value is shown.

```tsx
import * as React from "react";

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { SecretField } from "@/components/ui/secret-field";

function newKey() {
  const bytes = crypto.getRandomValues(new Uint8Array(8));
  return `acme_live_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}

export default function SecretFieldRegenerate() {
  const [key, setKey] = React.useState("acme_live_7f3a9c2e81d4b05a");
  const [revealed, setRevealed] = React.useState(false);
  const [confirming, setConfirming] = React.useState(false);
  const [regenerating, setRegenerating] = React.useState(false);

  function regenerate() {
    setConfirming(false);
    setRegenerating(true);
    // Stands in for the request that makes a new key on your server.
    setTimeout(() => {
      setKey(newKey());
      setRevealed(true);
      setRegenerating(false);
    }, 1200);
  }

  return (
    <>
      <SecretField
        label="Secret key"
        value={key}
        visiblePrefix={10}
        revealed={revealed}
        onRevealedChange={setRevealed}
        onRegenerate={() => setConfirming(true)}
        regenerating={regenerating}
        className="max-w-sm"
      />
      <AlertDialog open={confirming} onOpenChange={setConfirming}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Regenerate the secret key?</AlertDialogTitle>
            <AlertDialogDescription>
              Apps using the current key stop working until you give them the new one.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction variant="destructive" onClick={regenerate}>
              Regenerate
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}
```

## Props

The file also exports `maskSecret(value, visiblePrefix, visibleSuffix)`, which masks a secret as a
string, such as for a table of keys.

| Part | Extends |
| --- | --- |
| `SecretField` | `<div>` |

### SecretField

| Prop                | Type                            | Description                                                                                                |
| ------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `value`             | `string`                        | The secret. Copying always copies all of it, even while it's hidden.                                       |
| `label?`            | `React.ReactNode`               | Optional small label above the row.                                                                        |
| `visiblePrefix?`    | `number`                        | How many of its first characters stay visible while it's hidden, such as 10 for `acme_live_`. Default: `0` |
| `visibleSuffix?`    | `number`                        | How many of its last characters stay visible while it's hidden. Default: `4`                               |
| `revealed?`         | `boolean`                       | Whether the whole secret shows.                                                                            |
| `defaultRevealed?`  | `boolean`                       | Default: `false`                                                                                           |
| `onRevealedChange?` | `((revealed: boolean) => void)` |                                                                                                            |
| `timeout?`          | `number`                        | How long the copied or failed state lasts, in ms.                                                          |
| `onCopy?`           | `((value: string) => void)`     |                                                                                                            |
| `onRegenerate?`     | `(() => void)`                  | Adds a button that calls this to make a new secret. Confirm first if it revokes this one.                  |
| `regenerating?`     | `boolean`                       | Disables the regenerate button and spins its icon while a new secret is made. Default: `false`             |
