Fields it adds

FieldTypeRequiredDescription
status string yes Raw business status string (e.g., "active", "past_due", "unpaid"). Resolved against the domain's status map to derive presentation data.
domain StatusDomain yes Domain scope for registry lookup. Determines which status map is consulted (e.g., "subscription" statuses differ from "invoice" statuses).
tone StatusTone (neutral, info, accent, success, warning, critical) no Semantic tone derived from the registry or inferred from token paths. One of: neutral, info, accent, success, warning, critical. Falls back to the defaultTone parameter when unresolvable.
badge_tokens StatusTokenVariants no Resolved badge token sets with subtle and solid emphasis variants. Each variant contains { background, border, foreground } CSS variable references. Structure: { subtle: StatusTokenSet, solid: StatusTokenSet }
banner_tokens StatusTokenVariants no Resolved banner token sets with subtle and solid emphasis variants. Subtle banners use domain-specific tokens when available; solid banners use the tone-based component token set. Structure: { subtle: StatusTokenSet, solid: StatusTokenSet }

What it shows in each context

list
StatusBadge
detail
StatusBadge, Banner
form
StatusBadge
timeline
StatusBadge

Parameters

domains string[], required
Domain identifiers whose statuses this object participates in. Each domain resolves to a scoped status map in the registry (e.g., "subscription", "invoice", "ticket", "user").
toneAliases Record<string, StatusTone>
Custom alias map for inferring tones from token path segments. Merged with built-in aliases (info, positive→success, danger→critical, etc.). Keys are token path segments; values are canonical StatusTone values.
defaultTone StatusTone
Fallback tone applied when a status is not found in the registry or when tone inference from token paths yields no match.

Objects that use it

No shipped object uses this trait.

The trait file

traits/visual/Statusable.trait.yaml
trait:
  name: Statusable
  version: 1.0.0
  description: |
    Semantic status presentation layer that maps business statuses to visual tones,
    design tokens, and badge/banner rendering. Statusable owns the status registry
    pattern: a domain-scoped lookup table that translates raw status strings into
    consistent visual output (tone, label, icon, token sets).

    DISTINCT from Stateful — Stateful governs state machine transitions and guards;
    Statusable governs how status LOOKS. Statusable is the bridge between data-layer
    state and the component layer (Badge, Banner, StatusBadge).

    Core concepts:
    - Status Registry: Map<domain, Map<status, StatusPresentation>>
    - Tone System: Six semantic tones (neutral, info, accent, success, warning, critical)
    - Token Resolution: --cmp-status-{tone}-{surface|border|text} CSS variables
    - Domain Scoping: Each domain (subscription, invoice, ticket, etc.) has its own status map
    - Emphasis Variants: badge.subtle / badge.solid, banner.subtle / banner.solid
  category: visual
  tags:
    - status
    - presentation
    - registry
    - badge
    - banner
    - tone
    - semantic
    - saas

parameters:
  - name: domains
    type: string[]
    required: true
    description: |
      Domain identifiers whose statuses this object participates in.
      Each domain resolves to a scoped status map in the registry
      (e.g., "subscription", "invoice", "ticket", "user").
    default:
      - subscription
  - name: toneAliases
    type: Record<string, StatusTone>
    required: false
    description: |
      Custom alias map for inferring tones from token path segments.
      Merged with built-in aliases (info, positive→success, danger→critical, etc.).
      Keys are token path segments; values are canonical StatusTone values.
    default: {}
  - name: defaultTone
    type: StatusTone
    required: false
    description: |
      Fallback tone applied when a status is not found in the registry
      or when tone inference from token paths yields no match.
    default: neutral
    validation:
      enum:
        - neutral
        - info
        - accent
        - success
        - warning
        - critical

schema:
  status:
    type: string
    required: true
    description: |
      Raw business status string (e.g., "active", "past_due", "unpaid").
      Resolved against the domain's status map to derive presentation data.
  domain:
    type: StatusDomain
    required: true
    description: |
      Domain scope for registry lookup. Determines which status map is consulted
      (e.g., "subscription" statuses differ from "invoice" statuses).
    validation:
      enumFromParameter: domains
  tone:
    type: StatusTone
    required: false
    description: |
      Semantic tone derived from the registry or inferred from token paths.
      One of: neutral, info, accent, success, warning, critical.
      Falls back to the defaultTone parameter when unresolvable.
    validation:
      enum:
        - neutral
        - info
        - accent
        - success
        - warning
        - critical
  badge_tokens:
    type: StatusTokenVariants
    required: false
    description: |
      Resolved badge token sets with subtle and solid emphasis variants.
      Each variant contains { background, border, foreground } CSS variable references.
      Structure: { subtle: StatusTokenSet, solid: StatusTokenSet }
  banner_tokens:
    type: StatusTokenVariants
    required: false
    description: |
      Resolved banner token sets with subtle and solid emphasis variants.
      Subtle banners use domain-specific tokens when available; solid banners
      use the tone-based component token set.
      Structure: { subtle: StatusTokenSet, solid: StatusTokenSet }

semantics:
  status:
    semantic_type: status.presentation.status
    token_mapping: tokenMap(status.registry.*)
    ui_hints:
      component: StatusBadge
      registryLookup: getStatusPresentation(domain, status)
  domain:
    semantic_type: status.presentation.domain
    token_mapping: tokenMap(status.domains.*)
    ui_hints:
      component: DomainSelector
      scopedTo: domains
  tone:
    semantic_type: status.presentation.tone
    token_mapping: tokenMap(status.tone.*)
    ui_hints:
      component: ToneBadge
      toneValues:
        - neutral
        - info
        - accent
        - success
        - warning
        - critical
  badge_tokens:
    semantic_type: status.presentation.badge
    token_mapping: tokenMap(cmp.status.{tone}.*)
    ui_hints:
      component: Badge
      emphasisVariants:
        - subtle
        - solid
  banner_tokens:
    semantic_type: status.presentation.banner
    token_mapping: tokenMap(cmp.status.{tone}.*)
    ui_hints:
      component: Banner
      emphasisVariants:
        - subtle
        - solid

view_extensions:
  # StatusBadge rather than Badge, for the reason lifecycle/Supersedable already records: the
  # canonical Badge contract carries no field directive, so a Badge authored with these props is
  # refused by the target contracts. Measured in s204-m02 against code.generate rather than assumed —
  # Badge reds on THREE of the four props below (statusField, domainField and showIcon) in both React
  # and Vue, while StatusBadge takes all four with zero errors. No object composes Statusable's list
  # extension today, so this sat unexercised as a latent refusal rather than a visible break; every
  # other context in this trait already authors StatusBadge.
  list:
    - component: StatusBadge
      position: after
      props:
        statusField: status
        domainField: domain
        emphasis: subtle
        showIcon: true
  detail:
    - component: StatusBadge
      position: main
      priority: 80
      props:
        statusField: status
        domainField: domain
        emphasis: subtle
        showIcon: true
    - component: Banner
      position: top
      priority: 90
      props:
        statusField: status
        domainField: domain
        emphasis: subtle
        showIcon: true
      conditions:
        tones:
          - warning
          - critical
  form:
    - component: StatusBadge
      position: top
      props:
        statusField: status
        domainField: domain
        emphasis: subtle
        readOnly: true
  timeline:
    - component: StatusBadge
      props:
        statusField: status
        domainField: domain
        emphasis: subtle
        showIcon: true
        compact: true

tokens:
  # Badge tone tokens — pattern: --cmp-status-{tone}-{surface|border|text}
  cmp.status.neutral.surface: "var(--sys-surface-neutral)"
  cmp.status.neutral.border: "var(--sys-border-neutral)"
  cmp.status.neutral.text: "var(--sys-text-neutral)"
  cmp.status.info.surface: "var(--sys-surface-info)"
  cmp.status.info.border: "var(--sys-border-info)"
  cmp.status.info.text: "var(--sys-text-info)"
  cmp.status.accent.surface: "var(--sys-surface-accent)"
  cmp.status.accent.border: "var(--sys-border-accent)"
  cmp.status.accent.text: "var(--sys-text-accent)"
  cmp.status.success.surface: "var(--sys-surface-success)"
  cmp.status.success.border: "var(--sys-border-success)"
  cmp.status.success.text: "var(--sys-text-success)"
  cmp.status.warning.surface: "var(--sys-surface-warning)"
  cmp.status.warning.border: "var(--sys-border-warning)"
  cmp.status.warning.text: "var(--sys-text-warning)"
  cmp.status.critical.surface: "var(--sys-surface-critical)"
  cmp.status.critical.border: "var(--sys-border-critical)"
  cmp.status.critical.text: "var(--sys-text-critical)"
  # Banner fallback tokens
  cmp.banner.background: "var(--sys-surface-raised)"
  cmp.banner.border: "var(--sys-border-default)"
  cmp.banner.text: "var(--sys-text-default)"

# Statusable can work standalone with static status strings.
# When composed with Stateful, status values are driven by the state machine.
# Stateful is optional — not listed as a dep to avoid required-dep enforcement.
dependencies: []

metadata:
  created: "2026-02-28"
  owners:
    - design@oods.systems
    - engineering@oods.systems
  maturity: experimental
  accessibility:
    keyboard: Badge and Banner elements are not interactive; no keyboard handling required.
    screenreader: |
      StatusBadge announces status label and tone as a live region.
      Banner uses role="status" with aria-live="polite" for critical/warning tones.
  regionsUsed:
    - list
    - detail
    - form
    - timeline
  examples:
    - Subscription
    - Invoice
    - Ticket
    - User
  references:
    - "objects/provenance.v1.json#/references/ref-ca663a7c5731 — StatusPresentation, StatusTone, tone inference, domain maps"
    - "objects/provenance.v1.json#/references/ref-24923e661958 — 7 sub-stories across 5 domains"
    - "objects/provenance.v1.json#/references/ref-be33a98177f7 — domain status token map consumed by registry"
    - "objects/provenance.v1.json#/references/ref-c373015a9045 — StatusableViewData, StatusTransitionEvent interfaces"
    - "objects/provenance.v1.json#/references/ref-cc4cadcf82d5 — formatStatus, resolveDisplayName utilities"