{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-state",
  "type": "registry:ui",
  "title": "Data State",
  "description": "The single conditional swap point for a data surface, and the place the design system says what an ABSENCE is.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "feedback",
    "primitive"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/skeleton.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/data-state.tsx",
      "target": "components/ui/data-state.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/data-state v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/data-state\n// What changed since: https://ds.interlace.tools/c/data-state#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — DataState\n *\n * The single conditional swap point for a data surface, and the place the\n * design system says what an ABSENCE is.\n *\n * It started as the four-state ladder every fetch site otherwise re-writes:\n *\n *   {isLoading ? <Skeleton /> :\n *    error    ? <ErrorState /> :\n *    !data?.length ? <EmptyState /> :\n *    <ListOfThings items={data} />}\n *\n * becomes:\n *\n *   <DataState\n *     loading={isLoading}\n *     error={error}\n *     empty={!data?.length}\n *     skeletonVariant=\"article-card\"\n *     emptyState={<EmptyState>No articles yet</EmptyState>}\n *   >\n *     {() => <ListOfThings items={data!} />}\n *   </DataState>\n *\n * The render-prop child only runs when the surface has real data, so consumers\n * can `data!.map(...)` inside without a null guard.\n *\n * ## Four states was not enough, and the missing five were the interesting ones\n *\n * Six published artifacts were catalogued for phase 10, and every one of them\n * had hand-rolled a vocabulary this component did not have. Not decoration —\n * distinctions that change what the reader is entitled to conclude:\n *\n *   - **`not-counted`** — no run happened. Drawn as a diagonal hatch, because\n *     a run that returned zero and a run that never happened must not look the\n *     same. This is the state `charts/scale.ts` has always encoded as `null`.\n *   - **`not-applicable`** — the metric has no meaning for this subject. It\n *     recedes: it was never going to have a value, so it should not compete\n *     with the cells that do.\n *   - **`partial`** — some sources did not report. Every count below is a\n *     FLOOR, not a total. Ranked above `truncated` because it is invisible:\n *     a reader can see a list stop, but cannot see a source that never replied.\n *   - **`truncated`** — the list is cut. Never a denominator.\n *   - **`first-measurement`** — a reading exists, a prior does not. This is the\n *     one absence that gets the accent colour and a dashed outline, because it\n *     is the only one a reader can act on. It exists so a metric with no prior\n *     shows \"first measurement\" instead of `+0%`.\n *\n * The union, its precedence, and every announcement live in the pure companion\n * `data-state-model.ts` — no React, exhaustively testable, shipped inside this\n * registry item so a `shadcn add` gets the contract and not just the chrome.\n *\n * ## Precedence, and why the loser is not thrown away\n *\n * `DATA_STATES` is the precedence order. Two rules the audit named: **error\n * beats empty** (a failed fetch is a different message, not \"nothing found\"),\n * and **truncated is not empty** (truncated does not replace the body at all).\n *\n * States co-occur. A partially-covered list that is also truncated is wrong\n * twice, so `resolveDataState` returns the winner AND the qualifiers, and the\n * announcement says both. A resolver that returned one name would silently\n * drop the second fact — which is the bug this component exists to prevent.\n *\n * ## The hatch must not exist only in pixels\n *\n * A diagonal hatch a screen reader cannot perceive keeps the \"no run\" /\n * \"measured zero\" distinction for sighted readers and destroys it for everyone\n * else — a worse outcome than not drawing it. Every state therefore carries a\n * sentence (`announceDataState`), `loading` is `role=\"status\"`, `error` is\n * `role=\"alert\"`, and `DataStateBadge` renders its glyph `aria-hidden` with\n * the sentence beside it.\n *\n * ## MIN_VIEWPORT — 320\n *\n * `DataState` inherits the min-viewport of whichever child it renders and\n * declares none of its own. `DataStateBadge` is an inline chip that wraps with\n * its line; it never introduces horizontal overflow.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends prop bag                 | `Omit<ComponentProps<'div'>, 'children'>` + `DataStateFlags` |\n * | R6   | data-slot on root                | `data-slot=\"data-state\"` + `data-state` + `data-qualifiers`  |\n * | R7   | className merged + ...rest       | `cn(className)` + `{...props}`                              |\n * | R8   | No `isXxx`; explicit booleans    | one boolean per absence — they co-occur, so no enum          |\n * | R10  | Composition seam (children fn)   | `children: (data: T) => ReactNode`                          |\n * | R13  | Ecosystem first                  | No state machine — an ordered array IS the precedence        |\n * | R18  | Tailwind only                    | Zero inline `style`; the hatch is a Tailwind arbitrary bg    |\n * | R19  | Tokens only                      | `--viz-axis` / `--muted-foreground` / `--primary` / `--destructive` |\n * | R20  | AA contrast                      | contrast table in `data-state-model.ts`                      |\n * | R25  | Server component                 | Pure render — no hooks                                       |\n * | R26  | A11y                             | role=status/alert, aria-busy, sentence per state             |\n */\n\nimport { cn } from '@/lib/utils';\nimport {\n  announceDataState,\n  presentationFor,\n  resolveDataState,\n  type AnnouncementOptions,\n  type DataStateFlags,\n  type DataStateName,\n} from '@/components/ui/data-state-model';\nimport { Skeleton, type SkeletonVariant } from '@/components/ui/skeleton';\n\nexport const MIN_VIEWPORT = 320 as const;\n\n// ─────────────────────────────────────────────────────────────────\n// DataStateBadge — one state, painted and announced\n// ─────────────────────────────────────────────────────────────────\n\nexport interface DataStateBadgeProps\n  extends Omit<React.ComponentProps<'span'>, 'children'> {\n  state: DataStateName;\n  /** Context for the spoken sentence — noun, shown count, coverage, reason. */\n  announce?: AnnouncementOptions;\n  /**\n   * Visible label override. The SENTENCE is not overridable — a caller who\n   * shortens \"not counted\" to \"n/c\" must not also be able to shorten what a\n   * screen reader hears down to nothing.\n   */\n  label?: React.ReactNode;\n  /** Hide the visible label and keep only the glyph. For dense cells. */\n  glyphOnly?: boolean;\n}\n\n/**\n * The inline chip for one state.\n *\n * Three carriers, in descending order of reliability: the **sentence**\n * (`sr-only`, always present), the **word**, and the **texture** — hatch for\n * \"no run happened\", dashed border for \"not yet real\". Texture is last on\n * purpose: it is the one that disappears in greyscale and for a screen reader,\n * which is exactly the argument `Delta` makes about colour.\n */\nexport const DataStateBadge = React.forwardRef<\n  HTMLSpanElement,\n  DataStateBadgeProps\n>(function DataStateBadge(\n  { state, announce, label, glyphOnly = false, className, ...props },\n  ref,\n) {\n  const presentation = presentationFor(state);\n  const sentence = announceDataState(state, announce);\n\n  return (\n    <span\n      ref={ref}\n      data-slot=\"data-state-badge\"\n      data-state={state}\n      data-emphasis={presentation.emphasis}\n      data-hatch={presentation.hatch || undefined}\n      className={cn(\n        // `w-fit`: this is an inline chip, but it is routinely dropped into a\n        // `flex-col` (a StatStrip cell, a notice row), where the default\n        // `align-items: stretch` blows it to the full column width and the\n        // border reads as a field rather than a tag. jsdom cannot see that —\n        // it reports every box as 0×0.\n        'inline-flex w-fit max-w-full items-center gap-1 whitespace-nowrap rounded-sm border bg-background px-1.5 py-0.5 align-middle font-mono text-ui-sm leading-none',\n        presentation.chip,\n        className,\n      )}\n      {...props}\n    >\n      {/* The hatch is a swatch BESIDE the words, never a background under\n          them: painted on the chip it ran diagonals straight through\n          \"not counted\". A texture and a glyph competing for the same 12px is\n          two marks saying one thing badly, so a state has one or the other. */}\n      {presentation.swatch ? (\n        <span\n          aria-hidden\n          data-slot=\"data-state-swatch\"\n          className={cn(\n            'size-3 shrink-0 rounded-[2px] border',\n            presentation.swatch,\n          )}\n        />\n      ) : presentation.glyph ? (\n        <span aria-hidden className=\"leading-none\">\n          {presentation.glyph}\n        </span>\n      ) : null}\n      {glyphOnly ? null : (\n        <span aria-hidden className=\"truncate\">\n          {label ?? presentation.short}\n        </span>\n      )}\n      <span className=\"sr-only\">{sentence}</span>\n    </span>\n  );\n});\n\n// ─────────────────────────────────────────────────────────────────\n// DataState — the switch\n// ─────────────────────────────────────────────────────────────────\n\ninterface DataStateProps<T = unknown>\n  extends Omit<React.ComponentProps<'div'>, 'children' | 'defaultValue'>,\n    DataStateFlags {\n  /**\n   * The data to render. Only narrows the children's typed parameter — the\n   * decision to render children is driven by the flags, not by this value.\n   */\n  data?: T;\n  /** Loading-state UI. Defaults to a single full-width Skeleton rect. */\n  skeleton?: React.ReactNode;\n  /** Optional shortcut: pick a `<Skeleton variant>` instead of passing a node. */\n  skeletonVariant?: SkeletonVariant;\n  /** Error-state UI. Defaults to a minimal `role=\"alert\"` line. */\n  errorState?: React.ReactNode;\n  /** Empty-state UI. Defaults to a minimal muted line. */\n  emptyState?: React.ReactNode;\n  /**\n   * UI for the two \"there was never going to be a value here\" states.\n   * Defaults to a `DataStateBadge`, which is usually the right answer — these\n   * states belong in a cell, not in a full-page panel.\n   */\n  notApplicableState?: React.ReactNode;\n  notCountedState?: React.ReactNode;\n  /** Context for every announcement this instance emits. */\n  announce?: AnnouncementOptions;\n  /**\n   * Render badges for qualifying states (`partial` / `truncated` /\n   * `first-measurement`) above the body. The sr-only announcement is emitted\n   * either way — set this to `false` only when the surrounding surface already\n   * shows the same badge, never to make the caveat go away.\n   */\n  notice?: boolean;\n  /**\n   * Idle render. Receives the (narrowed-non-null) `data` value. Runs whenever\n   * the resolved state does not replace the body — which includes the\n   * qualifying states, because those annotate real data rather than hide it.\n   */\n  children: (data: T) => React.ReactNode;\n}\n\nfunction DataState<T>({\n  loading,\n  error,\n  empty,\n  partial,\n  truncated,\n  notApplicable,\n  notCounted,\n  firstMeasurement,\n  data,\n  skeleton,\n  skeletonVariant,\n  errorState,\n  emptyState,\n  notApplicableState,\n  notCountedState,\n  announce,\n  notice = true,\n  children,\n  className,\n  ...props\n}: DataStateProps<T>) {\n  const resolved = resolveDataState(\n    {\n      loading,\n      error,\n      empty,\n      partial,\n      truncated,\n      notApplicable,\n      notCounted,\n      firstMeasurement,\n    },\n    announce,\n  );\n\n  const replacement = REPLACEMENTS[resolved.state]?.({\n    skeleton,\n    skeletonVariant,\n    errorState,\n    emptyState,\n    notApplicableState,\n    notCountedState,\n    announce,\n  });\n\n  return (\n    <div\n      data-slot=\"data-state\"\n      data-state={resolved.state}\n      // Serialised so a test, an audit script or a consumer's CSS can see the\n      // facts the single `data-state` winner does not carry.\n      data-qualifiers={resolved.qualifiers.join(' ') || undefined}\n      aria-busy={resolved.state === 'loading' || undefined}\n      className={cn(className)}\n      {...props}\n    >\n      {resolved.replaces ? (\n        replacement\n      ) : (\n        <>\n          {notice && resolved.active[0] !== 'idle' ? (\n            <p\n              data-slot=\"data-state-notice\"\n              className=\"mb-2 flex flex-wrap items-center gap-1\"\n            >\n              {resolved.active.map((state) => (\n                <DataStateBadge key={state} state={state} announce={announce} />\n              ))}\n            </p>\n          ) : (\n            // The caveat is never optional for a screen reader, only its\n            // visible chip is. Without this branch `notice={false}` would\n            // silently downgrade the announcement to nothing.\n            <span className=\"sr-only\">{resolved.announcement}</span>\n          )}\n          {children(data as T)}\n        </>\n      )}\n    </div>\n  );\n}\nDataState.displayName = 'DataState';\n\n/**\n * What each REPLACING state renders when the caller supplies nothing.\n *\n * A lookup rather than a nested ternary: the previous four-state ladder was\n * already three levels deep and nine states would have made it unreadable, and\n * an object keyed by the union is what makes a missing arm a type error.\n */\ntype ReplacementSlots = Pick<\n  DataStateProps,\n  | 'skeleton'\n  | 'skeletonVariant'\n  | 'errorState'\n  | 'emptyState'\n  | 'notApplicableState'\n  | 'notCountedState'\n  | 'announce'\n>;\n\nconst REPLACEMENTS: Partial<\n  Record<DataStateName, (slots: ReplacementSlots) => React.ReactNode>\n> = {\n  loading: ({ skeleton, skeletonVariant }) =>\n    skeleton ?? <Skeleton variant={skeletonVariant ?? 'rect'} />,\n  error: ({ errorState, announce }) =>\n    errorState ?? (\n      <p role=\"alert\" className=\"font-body text-ui text-destructive\">\n        {announceDataState('error', announce)}\n      </p>\n    ),\n  empty: ({ emptyState, announce }) =>\n    emptyState ?? (\n      <p className=\"font-body text-ui text-muted-foreground\">\n        {announceDataState('empty', announce)}\n      </p>\n    ),\n  'not-applicable': ({ notApplicableState, announce }) =>\n    notApplicableState ?? (\n      <DataStateBadge state=\"not-applicable\" announce={announce} />\n    ),\n  'not-counted': ({ notCountedState, announce }) =>\n    notCountedState ?? (\n      <DataStateBadge state=\"not-counted\" announce={announce} />\n    ),\n};\n\nexport { DataState };\nexport type { DataStateProps };\nexport * from '@/components/ui/data-state-model';\n"
    },
    {
      "path": "registry/interlace-ui/data-state-model.ts",
      "target": "components/ui/data-state-model.ts",
      "type": "registry:ui",
      "content": "export const DATA_STATES = [\n  'loading',\n  'error',\n  'not-applicable',\n  'not-counted',\n  'empty',\n  'partial',\n  'truncated',\n  'first-measurement',\n  'idle',\n] as const;\n\nexport type DataStateName = (typeof DATA_STATES)[number];\n\n/**\n * States that REPLACE the body — there is no value to render underneath them.\n *\n * The complement (`partial`, `truncated`, `first-measurement`) qualifies a body\n * that does render. `idle` is neither, and is excluded from both.\n */\nexport const REPLACING_STATES = new Set<DataStateName>([\n  'loading',\n  'error',\n  'not-applicable',\n  'not-counted',\n  'empty',\n]);\n\n/** States that annotate a body which still renders. */\nexport const QUALIFYING_STATES = new Set<DataStateName>([\n  'partial',\n  'truncated',\n  'first-measurement',\n]);\n\n/** True when this state swaps out the content rather than annotating it. */\nexport const replacesBody = (state: DataStateName): boolean =>\n  REPLACING_STATES.has(state);\n\n/**\n * The caller's flags, one per absence.\n *\n * `error` is `unknown` rather than `boolean` so a caught value can be passed\n * through untouched — the value is never rendered from here, only its\n * truthiness is read.\n *\n * Deliberately NOT a single `state` enum prop: the whole point is that these\n * co-occur. A partially-covered, truncated list is two facts, and an enum\n * would force the caller to pick one and drop the other on the floor.\n */\nexport interface DataStateFlags {\n  loading?: boolean;\n  error?: unknown;\n  empty?: boolean;\n  /** Some sources did not report. Every count below is a FLOOR, not a total. */\n  partial?: boolean;\n  /** The list is cut short. It must never become a denominator. */\n  truncated?: boolean;\n  /** The metric has no meaning for this subject. Not zero — inapplicable. */\n  notApplicable?: boolean;\n  /** No run happened. Not zero — unmeasured. This is the hatch. */\n  notCounted?: boolean;\n  /** A reading exists but no prior does. Never render this as `+0%`. */\n  firstMeasurement?: boolean;\n}\n\n/** Flag key → state name, in the same order as `DATA_STATES`. */\nconst FLAG_ORDER: readonly (readonly [keyof DataStateFlags, DataStateName])[] = [\n  ['loading', 'loading'],\n  ['error', 'error'],\n  ['notApplicable', 'not-applicable'],\n  ['notCounted', 'not-counted'],\n  ['empty', 'empty'],\n  ['partial', 'partial'],\n  ['truncated', 'truncated'],\n  ['firstMeasurement', 'first-measurement'],\n];\n\n/**\n * Context for the spoken sentence.\n *\n * Every field is optional, and every announcement is a complete sentence\n * without any of them — a component that forgets to pass `noun` still\n * announces something true, just less specific.\n */\nexport interface AnnouncementOptions {\n  /** What is missing — \"articles\", \"downloads\", \"runs\". */\n  noun?: string;\n  /** How many rows the truncated list actually shows. */\n  shown?: number;\n  /** How coverage is incomplete — \"4 of 9 sources reported\". */\n  coverage?: string;\n  /** Why this is not applicable — \"repository has no test suite\". */\n  reason?: string;\n}\n\n/**\n * The sentence a screen reader hears for one state.\n *\n * A hatch pattern that exists only in pixels is invisible to a screen reader,\n * which defeats the entire point of distinguishing \"no run\" from \"zero\" —\n * the distinction would survive for sighted users and vanish for everyone\n * else. Every state therefore owes a sentence, and the sentence says what the\n * absence MEANS rather than naming the state.\n */\nexport function announceDataState(\n  state: DataStateName,\n  options: AnnouncementOptions = {},\n): string {\n  const { noun, shown, coverage, reason } = options;\n  const subject = noun ?? 'data';\n\n  switch (state) {\n    case 'loading':\n      return `Loading ${subject}.`;\n    case 'error':\n      return `${capitalise(subject)} could not be loaded.`;\n    case 'not-applicable':\n      return reason\n        ? `Not applicable: ${reason}.`\n        : `Not applicable. No value is possible here.`;\n    // \"This is not a zero\" is doing real work: without it a listener has no\n    // way to tell an unmeasured cell from a measured zero, which is the exact\n    // confusion the hatch exists to prevent for sighted readers.\n    case 'not-counted':\n      return `Not counted. No measurement was taken; this is not a zero.`;\n    case 'empty':\n      return `No ${subject}.`;\n    case 'partial':\n      return coverage\n        ? `Partial coverage: ${coverage}. Every count is a floor, not a total.`\n        : `Partial coverage. Every count is a floor, not a total.`;\n    case 'truncated':\n      return shown === undefined\n        ? `Truncated list. The total is unknown; do not use this as a denominator.`\n        : `Truncated list: showing ${shown.toLocaleString()} of an unknown total. ` +\n            `Do not use this as a denominator.`;\n    case 'first-measurement':\n      return `First measurement. There is no prior reading to compare against.`;\n    /* istanbul ignore next -- exhaustive; `idle` is the only remaining member */\n    default:\n      return '';\n  }\n}\n\nconst capitalise = (value: string): string =>\n  value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);\n\n/** What `resolveDataState` returns. */\nexport interface ResolvedDataState {\n  /** The winner by precedence — what a single-slot surface should render. */\n  state: DataStateName;\n  /** Every active state, in precedence order. `['idle']` when none fired. */\n  active: DataStateName[];\n  /** `active` minus the winner. The facts a one-winner resolver would lose. */\n  qualifiers: DataStateName[];\n  /** True when the winner swaps out the body rather than annotating it. */\n  replaces: boolean;\n  /** Winner sentence followed by every qualifier sentence. */\n  announcement: string;\n}\n\n/**\n * Resolve a flag bag into a state, its qualifiers, and one spoken sentence.\n *\n * The two rules the phase-10 audit called out by name both fall out of the\n * array order and are pinned by tests: **error beats empty** (a failed fetch\n * is a different message, not \"nothing found\"), and **truncated is not empty**\n * (they are separate members, and truncated does not replace the body at all).\n */\nexport function resolveDataState(\n  flags: DataStateFlags = {},\n  options: AnnouncementOptions = {},\n): ResolvedDataState {\n  const active = FLAG_ORDER.filter(([key]) => Boolean(flags[key])).map(\n    ([, state]) => state,\n  );\n\n  if (active.length === 0) {\n    return {\n      state: 'idle',\n      active: ['idle'],\n      qualifiers: [],\n      replaces: false,\n      announcement: '',\n    };\n  }\n\n  const [state, ...qualifiers] = active;\n  return {\n    state,\n    active,\n    qualifiers,\n    replaces: replacesBody(state),\n    announcement: [state, ...qualifiers]\n      .map((name) => announceDataState(name, options))\n      .join(' '),\n  };\n}\n\n// ── Presentation ────────────────────────────────────────────────────────────\n\n/**\n * How loud a state is allowed to be.\n *\n *   - `recede` — the *unwritten* family: a thing that was never going to have\n *     a value. It should not compete with the data around it.\n *   - `muted`  — the *unmeasured* family: a real gap, worth noticing, not\n *     worth alarming about.\n *   - `accent` — the *ungated* family: actionable. Something a reader can go\n *     and change. This is the one that earns colour.\n *   - `danger` — the request failed.\n */\nexport type DataStateEmphasis = 'recede' | 'muted' | 'accent' | 'danger';\n\n/**\n * The diagonal hatch. **No run happened.**\n *\n * Written as a Tailwind arbitrary background-image rather than a CSS class in\n * `styles/` so the whole vocabulary ships inside the registry item — a\n * consumer who runs `npx shadcn add @interlace/data-state` gets the hatch,\n * not a reference to a stylesheet they do not have. `image:` is the explicit\n * type hint; without it Tailwind has to guess the property from a `repeating-\n * linear-gradient(...)` value.\n *\n * `--viz-axis` and not `--viz-grid`: the grid token is documented as\n * decorative (1.37:1) and must never be the sole carrier of a value. The hatch\n * IS the value here, so it uses the axis token, measured at 3.49:1 light /\n * 3.83:1 dark — clearing WCAG 2.2 SC 1.4.11 for non-text content.\n */\nexport const HATCH_CLASS =\n  'bg-[image:repeating-linear-gradient(45deg,var(--viz-axis)_0,var(--viz-axis)_1px,transparent_1px,transparent_5px)]';\n\n/** The same hatch, quieter, for states that recede rather than report. */\nexport const HATCH_CLASS_FAINT =\n  'bg-[image:repeating-linear-gradient(45deg,var(--viz-grid)_0,var(--viz-grid)_1px,transparent_1px,transparent_5px)]';\n\n/**\n * Everything a surface needs to paint one state.\n *\n * `glyph` and `short` are BOTH here because the two carriers serve different\n * widths: a meter cell has room for a glyph, a stat strip has room for a word.\n * Neither is ever the only carrier — `announceDataState` is.\n */\nexport interface DataStatePresentation {\n  /** One-character mark. Always `aria-hidden`; the sentence carries meaning. */\n  glyph: string;\n  /** Terse visible label, lower case, for a chip. */\n  short: string;\n  /** Diagonal hatch — no run happened. */\n  hatch: boolean;\n  /** Dashed outline — planned, or not yet approached. Solid means real. */\n  dashed: boolean;\n  emphasis: DataStateEmphasis;\n  /**\n   * Tailwind classes for the chip SURFACE. Never the hatch.\n   *\n   * The first browser pass painted `HATCH_CLASS` on the chip itself and the\n   * diagonals ran straight through \"not counted\", which is a legibility bug\n   * axe cannot see and jsdom cannot render — it reports every box as 0×0 and\n   * resolves no Tailwind at all. The hatch moved to `swatch`, a leading block\n   * that carries the texture beside the words instead of behind them.\n   */\n  chip: string;\n  /**\n   * Classes for the leading hatch swatch, or `''` when this state has none.\n   * Replaces `glyph` when present — a texture and a character competing for\n   * the same 12px is two marks saying one thing badly.\n   */\n  swatch: string;\n}\n\n/**\n * The state → pixels table.\n *\n * Read the `hatch` / `dashed` / `emphasis` columns down and the doctrine is\n * visible: `not-counted` is the only hatch that reports (a run that did not\n * happen), `not-applicable` hatches faintly and recedes (it was never going\n * to happen), and `first-measurement` is the only absence that gets the accent\n * — because it is the only one the reader can act on, by measuring again\n * tomorrow.\n *\n * Contrast, measured against `--background` (see COLOR_PHILOSOPHY.md):\n *\n * | emphasis | token                | Light   | Dark    | Floor |\n * | -------- | -------------------- | ------- | ------- | ----- |\n * | recede   | `--muted-foreground` |  5.66:1 |  6.29:1 | 4.5:1 |\n * | muted    | `--muted-foreground` |  5.66:1 |  6.29:1 | 4.5:1 |\n * | accent   | `--primary`          |  8.80:1 |  9.12:1 | 4.5:1 |\n * | danger   | `--destructive`      |  8.31:1 | 10.43:1 | 4.5:1 |\n * | (hatch)  | `--viz-axis` on bg   |  3.49:1 |  3.83:1 | 3:1 (SC 1.4.11, non-text) |\n */\nexport const DATA_STATE_PRESENTATION: Record<\n  DataStateName,\n  DataStatePresentation\n> = {\n  loading: {\n    glyph: '',\n    short: 'loading',\n    hatch: false,\n    dashed: false,\n    emphasis: 'muted',\n    chip: 'border-border text-muted-foreground',\n    swatch: '',\n  },\n  error: {\n    glyph: '!',\n    short: 'error',\n    hatch: false,\n    dashed: false,\n    emphasis: 'danger',\n    chip: 'border-destructive/40 text-destructive',\n    swatch: '',\n  },\n  'not-applicable': {\n    glyph: '',\n    short: 'n/a',\n    hatch: true,\n    dashed: false,\n    emphasis: 'recede',\n    chip: 'border-border/60 text-muted-foreground',\n    swatch: `border-border/60 ${HATCH_CLASS_FAINT}`,\n  },\n  'not-counted': {\n    glyph: '',\n    short: 'not counted',\n    hatch: true,\n    dashed: false,\n    emphasis: 'muted',\n    chip: 'border-border text-muted-foreground',\n    swatch: `border-border ${HATCH_CLASS}`,\n  },\n  empty: {\n    glyph: '—',\n    short: 'none',\n    hatch: false,\n    dashed: false,\n    emphasis: 'muted',\n    chip: 'border-border text-muted-foreground',\n    swatch: '',\n  },\n  partial: {\n    glyph: '≥',\n    short: 'partial',\n    hatch: false,\n    dashed: true,\n    emphasis: 'muted',\n    chip: 'border-dashed border-border text-muted-foreground',\n    swatch: '',\n  },\n  truncated: {\n    glyph: '⋯',\n    short: 'truncated',\n    hatch: false,\n    dashed: true,\n    emphasis: 'muted',\n    chip: 'border-dashed border-border text-muted-foreground',\n    swatch: '',\n  },\n  'first-measurement': {\n    glyph: '·',\n    short: 'first measurement',\n    hatch: false,\n    dashed: true,\n    emphasis: 'accent',\n    chip: 'border-dashed border-primary/50 text-primary',\n    swatch: '',\n  },\n  idle: {\n    glyph: '',\n    short: '',\n    hatch: false,\n    dashed: false,\n    emphasis: 'muted',\n    chip: '',\n    swatch: '',\n  },\n};\n\n/** Presentation lookup. A function so callers do not index a frozen map by hand. */\nexport const presentationFor = (\n  state: DataStateName,\n): DataStatePresentation => DATA_STATE_PRESENTATION[state];\n\n/**\n * @interlace/ui — the DataState vocabulary (pure)\n *\n * Nothing in this file imports React, touches the DOM, or renders anything.\n * It is the part of the absence contract that can be *proved* — the union, the\n * precedence order, the announcement each state owes a screen reader, and the\n * class set that paints it — so `data-state.tsx`, `stat-strip.tsx` and\n * `meter.tsx` all resolve absence through the same three functions rather than\n * each re-deriving it from a boolean ladder.\n *\n * ## Why absence is a vocabulary and not a placeholder\n *\n * `charts/scale.ts` already encodes one half of this: `null` is *unmeasured*,\n * never zero, and `numeric()` drops it rather than coercing it, because\n * averaging over an invented zero silently manufactures data. That is a\n * statement about arithmetic. This file is the same statement about pixels.\n *\n * Six published artifacts were catalogued for phase 10 and every one of them\n * had hand-rolled some version of this, because the distinctions are real and\n * no component library ships them:\n *\n *   - a diagonal **hatch** means *no run happened* — which is a different fact\n *     from a run that returned zero, and a very different fact from a run that\n *     is still going;\n *   - a **dashed** outline means *planned / not yet approached*; solid means\n *     *real*;\n *   - `not counted`, `authority`, `visibility`, `dormant` are legitimate\n *     non-numeric values a cell can hold, and rendering any of them as `0`\n *     is a lie the reader cannot detect;\n *   - two absences that recede vs. accent on purpose: an *unwritten* thing is\n *     quiet, an *ungated* thing is actionable.\n *\n * The rules that fall out, verbatim from the corpus: **never render a missing\n * prior as 0**; **a truncated list must never be a denominator**; when coverage\n * is partial, **treat every count as a floor**.\n *\n * ## Two kinds of state, which is what makes the precedence tractable\n *\n * REPLACING states answer \"there is nothing here to show\" — the body is\n * swapped for the state itself. QUALIFYING states answer \"there IS something\n * here, and here is what is wrong with it\" — the body renders, annotated.\n *\n * A resolver that returns one winner throws away the second fact. So\n * `resolveDataState` returns the winner **and** every other active state as\n * `qualifiers`, and the announcement concatenates them. A partial-coverage\n * result that is also truncated says so twice, because it is wrong twice.\n *\n * ## Precedence\n *\n * `DATA_STATES` **is** the precedence order — lowest index wins. Reading down\n * the array is reading the rule, so the two cannot drift apart.\n *\n *   1. `loading`            nothing is known yet; every other flag is stale.\n *   2. `error`              a failed fetch must never read as \"no data\".\n *   3. `not-applicable`     the metric has no meaning here. Any number, `0`\n *                           included, would be a category error.\n *   4. `not-counted`        measurable in principle, deliberately not tallied.\n *                           `0` here invents a measurement nobody took.\n *   5. `empty`              a complete result with nothing in it. The only\n *                           absence that is a real, observed zero-length.\n *   6. `partial`            coverage is incomplete. Ranked above `truncated`\n *                           because it is INVISIBLE: the reader can see a list\n *                           stop, but cannot see a source that never reported.\n *   7. `truncated`          the list is cut. Not a denominator.\n *   8. `first-measurement`  a value exists, no prior does. Never `+0%`.\n *   9. `idle`              data is real and complete. The resting state.\n */\n\n/**\n * Every state, in precedence order. Lowest index wins.\n *\n * `idle` is last and is not an absence — it is the absence of absence, kept in\n * the union so a caller can exhaustively switch and so the resolver always has\n * something to return.\n */\n"
    }
  ],
  "meta": {
    "tier": "primitive",
    "client": false,
    "minViewport": 320,
    "loading": false,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/data-state\n\nInstalled to `components/ui/data-state.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/data-state';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/data-state\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
