# SOR Config 01 - Source mapping editor (`sor-config-01`) - SignalOS page block

> Maps a record to an external source: which connection to read, which fields identify and date it, and which source status values roll up into which outcome. Fully generic over the field and category keys the caller defines.

- **Version:** 0.1.1
- **Kind:** block (group: sor-config) · **Category:** pages
- **Install:** `npx shadcn@4.1.2 add @signalos/sor-config-01`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/button, @signalos/input, @signalos/skeleton, @signalos/combobox, @signalos/confirm-dialog, @signalos/data-primitives, @signalos/page-header, @signalos/status-badge, @signalos/text-field
- **npm dependencies:** lucide-react@^1.7.0
- **Files installed:** `src/components/blocks/sor-config-01/SorConfig01.tsx`, `src/components/blocks/sor-config-01/SorConfig01.types.ts`, `src/components/blocks/sor-config-01/SorConfig01.constants.ts`, `src/components/blocks/sor-config-01/SorConfig01.utils.ts`, `src/components/blocks/sor-config-01/components/InlineNotice.tsx`, `src/components/blocks/sor-config-01/components/FieldSuggestions.tsx`, `src/components/blocks/sor-config-01/components/MappingFieldsCard.tsx`, `src/components/blocks/sor-config-01/components/StatusMappingCard.tsx`, `src/components/blocks/sor-config-01/components/ProposalDiffCard.tsx`, `src/components/blocks/sor-config-01/components/RemoveMappingSection.tsx`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

```tsx
// after: npx shadcn@4.1.2 add @signalos/sor-config-01
// mount the pulled page in a route, e.g. src/app/sor-config/page.tsx
import { SorConfig01 } from "@/components/blocks/sor-config-01/SorConfig01"

export default function Page() {
  return <SorConfig01 />
}
```

## Props

### `SorConfig01LinkProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `href` | `string` | yes | - |  |
| `className` | `string \| undefined` | no | - |  |
| `children` | `ReactNode` | yes | - |  |

### `ConnectionLike`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes | - |  |
| `name` | `string` | yes | - |  |

### `SorConfig01Check`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `message` | `ReactNode` | yes | - |  |

### `SorConfig01FieldSpec`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `TFieldKey` | yes | - |  |
| `label` | `string` | yes | - |  |
| `placeholder` | `string \| undefined` | no | - |  |
| `required` | `boolean \| undefined` | no | `false` |  |
| `helper` | `ReactNode` | no | - |  |

### `SorConfig01CategorySpec`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `TCategoryKey` | yes | - |  |
| `label` | `ReactNode` | yes | - | Display label, e.g. "Resolved". |
| `reward` | `ReactNode` | yes | - | Score or weight shown beside the label, e.g. "+1.0". |
| `tone` | `"neutral" \| "positive" \| "negative" \| "partial" \| undefined` | no | `"neutral"` |  |
| `placeholder` | `string \| undefined` | no | - |  |

### `SorConfig01Diff`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `field` | `string` | yes | - |  |
| `label` | `string` | yes | - |  |
| `draftValue` | `string` | yes | - |  |
| `proposedValue` | `string` | yes | - |  |

### `SorConfig01ApiError`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `kind` | `"outage" \| "refused" \| "absent" \| "unknown"` | yes | - | `outage` - the source could not be reached, offers a retry. `refused` - the source was reached and rejected the request. `absent` - the requested record does not exist. `unknown` - anything else. |
| `detail` | `ReactNode` | yes | - |  |

### `SorConfig01SaveOutcome`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `ok` | `boolean` | yes | - |  |
| `droppedFieldLabels` | `string[]` | yes | - | Field labels that did not round-trip when `ok` is false. |

### `SorConfig01Props`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `recordName` | `ReactNode` | yes | - | Name of the record this mapping belongs to, e.g. a pipeline name. |
| `isMapped` | `boolean \| undefined` | no | `false` |  |
| `savedCaption` | `ReactNode` | no | - | Pre-formatted "Last saved …" caption; omit to hide. |
| `backHref` | `string` | yes | - |  |
| `backLabel` | `ReactNode` | no | - |  |
| `LinkComponent` | `ComponentType<SorConfig01LinkProps> \| undefined` | no | - | Router link renderer for the back link; a plain `<a>` by default. |
| `connections` | `ConnectionLike[]` | yes | - |  |
| `connectionId` | `string` | yes | - |  |
| `onConnectionChange` | `(id: string) => void` | yes | - |  |
| `isConnectionMissing` | `boolean \| undefined` | no | `false` |  |
| `connectionSearchPlaceholder` | `string \| undefined` | no | - |  |
| `onPropose` | `() => void` | yes | - |  |
| `isProposing` | `boolean \| undefined` | no | `false` |  |
| `canPropose` | `boolean \| undefined` | no | `false` |  |
| `proposeError` | `SorConfig01ApiError \| null \| undefined` | no | - |  |
| `isProposalWrongConnection` | `boolean \| undefined` | no | - | True once a proposal exists but points at a different connection than the form. |
| `onClearFields` | `(() => void) \| undefined` | no | - |  |
| `proposalSummary` | `ReactNode` | no | - |  |
| `isProposalUnverified` | `boolean \| undefined` | no | `false` |  |
| `proposalUnverifiedDetail` | `ReactNode` | no | - |  |
| `isProposalBlocked` | `boolean \| undefined` | no | `false` |  |
| `proposalBlockedReason` | `ReactNode` | no | - |  |
| `proposalDiff` | `SorConfig01Diff[] \| undefined` | no | - |  |
| `onUseProposal` | `(() => void) \| undefined` | no | - |  |
| `fields` | `SorConfig01FieldSpec<TFieldKey>[]` | yes | - |  |
| `fieldValues` | `Record<TFieldKey, string>` | yes | - |  |
| `onFieldChange` | `(key: TFieldKey, value: string) => void` | yes | - |  |
| `fieldErrors` | `Partial<Record<TFieldKey, ReactNode>> \| undefined` | no | - |  |
| `fieldSuggestions` | `Partial<Record<TFieldKey, string[]>> \| undefined` | no | - | Catalog-derived value suggestions per field, keyed by field key. |
| `multiValueFieldKeys` | `TFieldKey[] \| undefined` | no | - | Field keys whose value is a comma-separated list, e.g. free-text search fields. |
| `categories` | `SorConfig01CategorySpec<TCategoryKey>[]` | yes | - |  |
| `categoryValues` | `Record<TCategoryKey, string>` | yes | - |  |
| `onCategoryChange` | `(key: TCategoryKey, value: string) => void` | yes | - |  |
| `inFlightValuesByCategory` | `Partial<Record<TCategoryKey, string[]>> \| undefined` | no | - | Values that look like they mean "in progress", shown as a warning per category. |
| `observedValues` | `string[] \| undefined` | no | - | Every value actually observed in a sample of source records. |
| `observedValuesCaption` | `ReactNode` | no | - |  |
| `blockingChecks` | `SorConfig01Check[] \| undefined` | no | - | Local, blocking checks - shown once `onSave` has been attempted. |
| `verifyBlockingChecks` | `SorConfig01Check[] \| undefined` | no | - | Checks a verification pass ran against real records refused. |
| `isVerifyUnverified` | `boolean \| undefined` | no | `false` |  |
| `verifyUnverifiedDetail` | `ReactNode` | no | - |  |
| `warningChecks` | `SorConfig01Check[] \| undefined` | no | - |  |
| `warningsCaption` | `ReactNode` | no | - |  |
| `unsavedFieldLabels` | `string[] \| undefined` | no | - | Coordinates the draft has changed but not yet saved. |
| `onSave` | `() => void` | yes | - |  |
| `isSaving` | `boolean \| undefined` | no | `false` |  |
| `isVerifying` | `boolean \| undefined` | no | `false` |  |
| `canSave` | `boolean \| undefined` | no | `true` |  |
| `saveError` | `SorConfig01ApiError \| null \| undefined` | no | - |  |
| `verifyError` | `SorConfig01ApiError \| null \| undefined` | no | - |  |
| `saveOutcome` | `SorConfig01SaveOutcome \| null \| undefined` | no | - |  |
| `savedDetectorCount` | `number \| undefined` | no | - |  |
| `onReset` | `() => void` | yes | - |  |
| `canReset` | `boolean \| undefined` | no | `false` |  |
| `resetLabel` | `ReactNode` | no | - |  |
| `onRemove` | `(() => void) \| undefined` | no | - |  |
| `isRemoving` | `boolean \| undefined` | no | `false` |  |
| `removeError` | `SorConfig01ApiError \| null \| undefined` | no | - |  |
| `removeConfirmDescription` | `ReactNode` | no | - |  |
| `removeConfirmLabel` | `ReactNode` | no | - |  |
| `isLoading` | `boolean \| undefined` | no | `false` |  |
| `loadError` | `SorConfig01ApiError \| null \| undefined` | no | - |  |
| `className` | `string \| undefined` | no | - |  |
| `data-testid` | `string \| undefined` | no | - | Test identifier rendered as `data-testid` on the root element. |

## Changelog

# sor-config-01

## 0.1.1

- Initial release: a source-mapping editor page - connection picker, propose
  flow with a field-by-field diff against the current draft, generic field
  and status/category mapping cards, save/reset/remove actions, and every
  check and async state as a prop. Fully controlled: the caller owns the
  draft, the checks, and the field/category vocabulary via generic
  `TFieldKey`/`TCategoryKey` type parameters - no fetching, no domain types.
