# Semantic badge

One value from a vocabulary as a badge: an icon and a word, never a colour alone. Seven vocabularies are built in, their values those of the kit's own types, so a badge agrees with the item beside it: `severity` (as `FindingData`), `risk`, `effort`, `confidence` (as `Confidence`), `provenance` (as `AiContentLabel`'s involvement), `autonomy` (`R1` to `R5`, with `ReviewLadder`'s names) and `classification`. Add your own with `VocabulariesProvider`.

A `null` value says "Unknown"; nothing is guessed. A value the vocabulary doesn't have is drawn as its own text, in an outline badge with no icon, and warns in development, so a typo or a new status shows rather than hides.

- Registry name: `semantic-badge` (`@candor/semantic-badge`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/semantic-badge

## Install

```bash
npx candor-ui add semantic-badge
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/semantic-badge`.
It brings in: badge, bidi, locale, tokens, utils.
npm packages: @base-ui/react@^1.9.0, lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { CircleCheck, Hourglass, OctagonX } from "lucide-react"
import { SemanticBadge, VocabulariesProvider } from "@/components/candor/semantic-badge"

// Built-in vocabularies, an unknown value, and an app's own vocabulary through the provider.
export default function Example() {
  return (
    <VocabulariesProvider
      vocabularies={{
        delivery: {
          waiting: { label: "Waiting on review", icon: Hourglass, tone: "warning" },
          blocked: { label: "Blocked", icon: OctagonX, tone: "destructive" },
          done: { label: "Done", icon: CircleCheck, tone: "success" },
        },
      }}
    >
      <div className="flex max-w-xl flex-wrap gap-2">
        <SemanticBadge vocabulary="severity" value="serious" />
        <SemanticBadge vocabulary="risk" value="medium" />
        <SemanticBadge vocabulary="effort" value="small" />
        <SemanticBadge vocabulary="confidence" value="low" />
        <SemanticBadge vocabulary="provenance" value="generated" />
        <SemanticBadge vocabulary="autonomy" value="R3" />
        <SemanticBadge vocabulary="classification" value="confidential" />
        <SemanticBadge vocabulary="risk" value={null} />
        <SemanticBadge vocabulary="delivery" value="blocked" />
        <SemanticBadge vocabulary="delivery" value="waiting" size="sm" />
      </div>
    </VocabulariesProvider>
  )
}
```

## <SemanticBadge>

Props:

- `vocabulary: string`: Which vocabulary the value is from: a built-in one (`severity`, `risk`, `effort`, `confidence`, `provenance`, `autonomy`, `classification`) or one given by `VocabulariesProvider`.
- `value: string | null`: The value. `null` when it isn't known, which the badge says as "Unknown".
- `size?: "sm" | "default" | undefined` (default `"default"`): `sm` for dense rows and tables.
- `render?: useRender.RenderProp | undefined`: The element to draw instead of a `<span>`, with the badge's styles.
- `className?: string | undefined`: Classes for the badge, applied last.

## <VocabulariesProvider>

Adds an app's own vocabularies, and values to the built-in ones, for every `SemanticBadge` inside: a delivery status, a ticket priority. Each value is a word with an optional icon and tone. A built-in value's word can't be changed here (LocaleProvider's `strings` does that, under `semanticBadge`), so a severity reads the same everywhere. Nested providers add to the ones around them.

Props:

- `vocabularies: Vocabularies`: The vocabularies to add, by name.
- `children?: React.ReactNode | undefined`: The part of the app that uses them.

## Values

### semanticVocabularies

The built-in vocabularies' icons and tones; their words are `semanticBadgeStrings`.

```ts
const semanticVocabularies: {
  severity: {
    critical: {
      icon: LucideIcon;
      tone: "destructive";
    };
    serious: {
      icon: LucideIcon;
      tone: "destructive";
    };
    warning: {
      icon: LucideIcon;
      tone: "warning";
    };
    info: {
      icon: LucideIcon;
      tone: "secondary";
    };
  };
  risk: {
    high: {
      icon: LucideIcon;
      tone: "destructive";
    };
    medium: {
      icon: LucideIcon;
      tone: "warning";
    };
    low: {
      icon: LucideIcon;
      tone: "secondary";
    };
  };
  effort: {
    small: {
      icon: LucideIcon;
      tone: "secondary";
    };
    medium: {
      icon: LucideIcon;
      tone: "secondary";
    };
    large: {
      icon: LucideIcon;
      tone: "secondary";
    };
  };
  confidence: {
    high: {
      icon: LucideIcon;
      tone: "success";
    };
    medium: {
      icon: LucideIcon;
      tone: "secondary";
    };
    low: {
      icon: LucideIcon;
      tone: "warning";
    };
    unknown: {
      icon: LucideIcon;
      tone: "outline";
    };
  };
  provenance: {
    generated: {
      icon: LucideIcon;
      tone: "accent";
    };
    modified: {
      icon: LucideIcon;
      tone: "accent";
    };
    none: {
      icon: LucideIcon;
      tone: "secondary";
    };
    unknown: {
      icon: LucideIcon;
      tone: "outline";
    };
  };
  autonomy: {
    R1: {
      icon: LucideIcon;
      tone: "secondary";
    };
    R2: {
      icon: LucideIcon;
      tone: "secondary";
    };
    R3: {
      icon: LucideIcon;
      tone: "secondary";
    };
    R4: {
      icon: LucideIcon;
      tone: "accent";
    };
    R5: {
      icon: LucideIcon;
      tone: "attention";
    };
  };
  classification: {
    public: {
      icon: LucideIcon;
      tone: "secondary";
    };
    internal: {
      icon: LucideIcon;
      tone: "secondary";
    };
    confidential: {
      icon: LucideIcon;
      tone: "warning";
    };
    restricted: {
      icon: LucideIcon;
      tone: "destructive";
    };
  };
}
```

## Strings (`semanticBadge`)

The fixed words, English by default. Override any of them under `semanticBadge` in `<LocaleProvider strings={{ semanticBadge: { … } }}>`; the table is exported as `semanticBadgeStrings`.

- `unknown`: Unknown
- `severity.critical`: Critical
- `severity.serious`: Serious
- `severity.warning`: Warning
- `severity.info`: Note
- `risk.high`: High risk
- `risk.medium`: Medium risk
- `risk.low`: Low risk
- `effort.small`: Small effort
- `effort.medium`: Medium effort
- `effort.large`: Large effort
- `confidence.high`: High confidence
- `confidence.medium`: Medium confidence
- `confidence.low`: Low confidence
- `confidence.unknown`: Confidence unknown
- `provenance.generated`: AI-generated
- `provenance.modified`: AI-modified
- `provenance.none`: No AI used
- `provenance.unknown`: AI involvement not recorded
- `autonomy.R1`: Advise
- `autonomy.R2`: Draft
- `autonomy.R3`: Build & hold
- `autonomy.R4`: Ship with a window
- `autonomy.R5`: Full delegation
- `classification.public`: Public
- `classification.internal`: Internal
- `classification.confidential`: Confidential
- `classification.restricted`: Restricted

## Types

### SemanticTone

A badge's tone: one of `Badge`'s variants. The tone is never the meaning on its own; every value has a word.

```ts
"default" | "secondary" | "outline" | "accent" | "success" | "warning" | "destructive" | "attention" | "running"
```

### VocabularyValue

One value of a vocabulary: its word, and the icon and tone it is drawn with.

```ts
{
  /** The value's word, as the app names it: "Blocked". */
  label: string;
  /** The icon drawn before the word. Left out, the word stands alone. */
  icon?: LucideIcon | undefined;
  /** The badge's tone. Default `secondary`. */
  tone?: SemanticTone | undefined;
}
```

### Vocabularies

An app's own vocabularies, by name, each a record of its values: `{ delivery: { blocked: { label: "Blocked", … } } }`.

```ts
Record<string, Record<string, VocabularyValue>>
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
