<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Porthole

The status spot: a small round window with one line about how current the app's data is, which opens into the facts behind it. It shows one item at a time, an action over an error over working over rest; at rest it says "Up to date" only while every area was verified within its `freshWithinMs`, otherwise the real age, and "Status unknown" when an area was never verified or the service's heartbeat stops. It owns its clock: it looks again only at the one moment the view could change (`nextChangeAt`), so a stuck service can never hold "Up to date", a beat never renders it, and it waits while the page is hidden. It opens into the detail lines while a mouse is over it, and a press keeps it open until a second press, Escape or a press anywhere else. It never takes focus on its own, and speaks only when it changes into an action or an error, at most once every ten seconds. Its state is on `data-porthole-state` for a fault-injection check.

```tsx
import { Porthole } from "@/components/ouro/porthole/porthole";
```

## Example

```tsx
<Porthole source={statusSource} label="Status" onAction={(item, id) => service.act(item.id, id)} onDismiss={(item) => service.dismiss(item.id)} />
```

## Use instead when

- Use `SyncStatus` instead for where one document's own changes are, and `toast()` to confirm a single action.

## Props

Type: `PortholeProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  | Layout only: margins, placement (`ms-auto`). Appearance classes warn in development. |
| `defaultOpen` | `boolean` | `false` | Starts open, showing the detail lines, as on a page about the app's status. |
| `freshWithinMs` | `number` | `60_000` | How recently, in ms, the areas must have been verified for "Up to date", unless an area sets its own `freshWithinMs`. |
| `heartbeatTimeoutMs` | `number` | `5_000` | "Status unknown" once the last beat is older than this many ms. |
| `label` (required) | `string` |  | The status spot's name for screen readers, such as `"Status"`. |
| `labels` | `PortholeWords` |  | Words for other languages. |
| `onAction` | `((item: PortholeItem, actionId: string) => void)` |  | Called when the person presses the shown item's action, with the item and the action's id. Pass it to the service, which changes or removes the item: Porthole never changes an item itself. |
| `onDismiss` | `((item: PortholeItem) => void)` |  | Called when the person dismisses a `dismissable` item. Pass it to the service, which removes the item. |
| `quiet` | `boolean` | `true` | At rest and fresh, shows the light alone and the words on hover or focus, in the same room, so nothing moves. Pass `false` to keep the words showing. The spot's name holds them while they are unseen. |
| `ref` | `Ref<HTMLDivElement>` |  | Reaches the rendered element. |
| `side` | `"top" \| "bottom"` | `"bottom"` | Where the detail lines open: under the spot, or over it for a spot at the foot of a column. |
| `source` (required) | `PortholeSource \| PortholeFixed` |  | Where the status comes from: the app's mirror of its service (`getSnapshot`, `subscribe` and `lastBeatAt`), or a fixed `{ snapshot, lastBeatAt }`. |
| `troubleAfterMs` | `number` | `5_000` | An area in trouble for this many ms stops counting as fresh. |

## Native attributes

Also accepts the attributes in React's `HTMLAttributes` (`id`, `aria-*`, event handlers), passed to the rendered element, except those named on this page.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `items`: Not a prop. Porthole reads items from `source`: `getSnapshot().items`.
- `status`: Not a prop. Porthole decides the status from `source`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `PortholeFixed`

A snapshot that does not change, with the time of its last beat: for a page rendered on the server, a story or a test. It is plain data, so a Server Component can pass it. The server renders it as of its last beat, so a page read later from a cache hydrates without a mismatch; once hydrated, the spot judges it against the clock, so it says "Status unknown" once the beat is older than `heartbeatTimeoutMs`.

```ts
interface PortholeFixed {
  snapshot: PortholeSnapshot;
  lastBeatAt: number;
}
```

### `PortholeWords`

Porthole's words: those of `portholeView`, and the dismiss button's name.

```ts
interface PortholeWords extends PortholeLabels {
  /** The name of the button that dismisses an item. @default "Dismiss" */
  dismiss?: string | undefined;
}
```
