{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"trace-trail","type":"registry:component","title":"Trace Trail","description":"One line of provenance that opens into the whole action history.","dependencies":[],"categories":["agentic","provenance","audit","automation","breadcrumb"],"files":[{"path":"components/lab/trace-trail/trace-trail.tsx","target":"components/lab/trace-trail/trace-trail.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useId, useState } from \"react\";\nimport \"./trace-trail.css\";\n\nexport type TraceStatus = \"ok\" | \"warn\" | \"failed\";\n\nexport type TraceStep = {\n  id: string;\n  actor: string;\n  action: string;\n  detail?: string;\n  at?: string;\n  source?: { label: string; href?: string };\n  status?: TraceStatus;\n};\n\ntype TraceTrailProps = {\n  /** The ordered chain: who did what, in order. */\n  steps: TraceStep[];\n  /**\n   * One-line collapsed text. Without it the step actions are joined\n   * with a quiet arrow, which is the honest default for a trail.\n   */\n  summary?: string;\n  /** Start expanded. */\n  defaultOpen?: boolean;\n  /** Show each step's timestamp. */\n  showTimes?: boolean;\n  /** Called when a step's actor + action are activated. */\n  onStepSelect?: (step: TraceStep) => void;\n  className?: string;\n};\n\nconst STATUS_WORD: Record<TraceStatus, string> = {\n  ok: \"ok\",\n  warn: \"warning\",\n  failed: \"failed\",\n};\n\n/** failed beats warn beats ok; a missing status counts as ok. */\nfunction worstStatus(steps: TraceStep[]): TraceStatus {\n  let worst: TraceStatus = \"ok\";\n  for (const step of steps) {\n    const status = step.status ?? \"ok\";\n    if (status === \"failed\") return \"failed\";\n    if (status === \"warn\") worst = \"warn\";\n  }\n  return worst;\n}\n\n/**\n * TRACE TRAIL — one line of provenance that opens into the chain.\n *\n * Collapsed it is a single button: a status dot derived from the worst\n * step, the actions joined by arrows, a step count and a chevron.\n * Expanded it is the ordered list itself — actor, action, detail, time\n * and the source each step used. Automated work should be able to say\n * where it came from without leaving the sentence being read.\n *\n * Expansion animates grid-template-rows and is instant under\n * prefers-reduced-motion. The closed panel is inert, so its links and\n * buttons never take focus while collapsed.\n */\nexport function TraceTrail({\n  steps,\n  summary,\n  defaultOpen = false,\n  showTimes = true,\n  onStepSelect,\n  className,\n}: TraceTrailProps) {\n  const [open, setOpen] = useState(defaultOpen);\n  const [lastDefaultOpen, setLastDefaultOpen] = useState(defaultOpen);\n  const panelId = useId();\n\n  // Uncontrolled, but a host that changes defaultOpen (a preview\n  // control, a reset to defaults) should see the trail follow.\n  if (defaultOpen !== lastDefaultOpen) {\n    setLastDefaultOpen(defaultOpen);\n    setOpen(defaultOpen);\n  }\n\n  if (steps.length === 0) {\n    return (\n      <div className={`tt-trail tt-trail-empty${className ? ` ${className}` : \"\"}`}>\n        No recorded steps.\n      </div>\n    );\n  }\n\n  const worst = worstStatus(steps);\n  const breadcrumb =\n    summary ?? steps.map((step) => step.action).join(\" → \");\n\n  return (\n    <div className={`tt-trail${className ? ` ${className}` : \"\"}`} data-open={open}>\n      <button\n        type=\"button\"\n        className=\"tt-toggle\"\n        aria-expanded={open}\n        aria-controls={panelId}\n        onClick={() => setOpen((current) => !current)}\n      >\n        <span className=\"tt-dot\" data-status={worst} aria-hidden=\"true\" />\n        <span className=\"tt-sr\">Status: {STATUS_WORD[worst]}.</span>\n        <span className=\"tt-summary\" title={breadcrumb}>\n          {breadcrumb}\n        </span>\n        <span className=\"tt-count\">\n          {steps.length} {steps.length === 1 ? \"step\" : \"steps\"}\n        </span>\n        <svg\n          className=\"tt-chevron\"\n          viewBox=\"0 0 16 16\"\n          width=\"12\"\n          height=\"12\"\n          aria-hidden=\"true\"\n          focusable=\"false\"\n        >\n          <path\n            d=\"M4 6.25 8 10.25 12 6.25\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.6\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </button>\n\n      <div\n        className=\"tt-panel\"\n        id={panelId}\n        data-open={open}\n        aria-hidden={!open}\n        inert={!open}\n      >\n        <div className=\"tt-panel-inner\">\n          <ol className=\"tt-steps\">\n            {steps.map((step) => {\n              const status = step.status ?? null;\n              const statusWord =\n                status === \"failed\" || status === \"warn\"\n                  ? STATUS_WORD[status]\n                  : null;\n              const hasMeta = (showTimes && Boolean(step.at)) || Boolean(step.source);\n\n              const text = (\n                <>\n                  <span className=\"tt-actor\">{step.actor}</span>\n                  <span className=\"tt-action\">{step.action}</span>\n                </>\n              );\n\n              return (\n                <li className=\"tt-step\" key={step.id} data-status={status ?? \"none\"}>\n                  <span className=\"tt-marker\" aria-hidden=\"true\" />\n                  <div className=\"tt-step-body\">\n                    <div className=\"tt-step-line\">\n                      {statusWord ? (\n                        <span className=\"tt-status\" data-status={status}>\n                          {statusWord}\n                        </span>\n                      ) : null}\n                      {onStepSelect ? (\n                        <button\n                          type=\"button\"\n                          className=\"tt-step-button\"\n                          onClick={() => onStepSelect(step)}\n                        >\n                          {text}\n                        </button>\n                      ) : (\n                        <span className=\"tt-step-text\">{text}</span>\n                      )}\n                    </div>\n\n                    {step.detail ? <p className=\"tt-detail\">{step.detail}</p> : null}\n\n                    {hasMeta ? (\n                      <p className=\"tt-meta\">\n                        {showTimes && step.at ? (\n                          <span className=\"tt-time\" title={step.at}>\n                            {step.at}\n                          </span>\n                        ) : null}\n                        {step.source ? (\n                          step.source.href ? (\n                            <a\n                              className=\"tt-source\"\n                              href={step.source.href}\n                              target=\"_blank\"\n                              rel=\"noreferrer\"\n                            >\n                              {step.source.label}\n                            </a>\n                          ) : (\n                            <span className=\"tt-source tt-source-static\">\n                              {step.source.label}\n                            </span>\n                          )\n                        ) : null}\n                      </p>\n                    ) : null}\n                  </div>\n                </li>\n              );\n            })}\n          </ol>\n        </div>\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/lab/trace-trail/trace-trail.css","target":"components/lab/trace-trail/trace-trail.css","type":"registry:file","content":"/* ============================================================\n   TRACE TRAIL — self-contained styles.\n\n   Every token is scoped to the root with a fallback, so the\n   component carries its own look into any codebase that has\n   never heard of this stylesheet.\n   ============================================================ */\n\n.tt-trail {\n  --tt-ink: #141418;\n  --tt-muted: #5f5f68;\n  --tt-faint: #8c8c94;\n  --tt-line: rgba(20, 20, 24, 0.1);\n  --tt-line-soft: rgba(20, 20, 24, 0.06);\n  --tt-rail: rgba(20, 20, 24, 0.18);\n  --tt-surface: #ffffff;\n  --tt-surface-hover: rgba(20, 20, 24, 0.035);\n  --tt-ok: #137a4e;\n  --tt-warn: #8a5300;\n  --tt-danger: #b3261e;\n  --tt-focus: #4f27e0;\n  --tt-mono: ui-monospace, \"SF Mono\", \"Cascadia Mono\", Consolas, monospace;\n  --tt-ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n  border: 1px solid var(--tt-line);\n  border-radius: 12px;\n  background: var(--tt-surface);\n  color: var(--tt-ink);\n  font-size: 0.8125rem;\n  line-height: 1.5;\n}\n\n.tt-trail,\n.tt-trail *,\n.tt-trail *::before,\n.tt-trail *::after {\n  box-sizing: border-box;\n}\n\n.tt-trail-empty {\n  padding: 10px 12px;\n  color: var(--tt-muted);\n}\n\n/* ---- the collapsed line ------------------------------------ */\n\n.tt-toggle {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  margin: 0;\n  padding: 9px 11px;\n  border: 0;\n  border-radius: 11px;\n  background: transparent;\n  color: inherit;\n  font: inherit;\n  text-align: left;\n  cursor: pointer;\n}\n\n.tt-toggle:hover {\n  background: var(--tt-surface-hover);\n}\n\n.tt-toggle:focus-visible {\n  outline: 2px solid var(--tt-focus);\n  outline-offset: 2px;\n}\n\n.tt-trail[data-open=\"true\"] .tt-toggle {\n  border-radius: 11px 11px 0 0;\n}\n\n.tt-dot {\n  flex: none;\n  width: 8px;\n  height: 8px;\n  border-radius: 999px;\n  background: var(--tt-faint);\n  box-shadow: 0 0 0 3px rgba(20, 20, 24, 0.05);\n}\n\n.tt-dot[data-status=\"ok\"] {\n  background: var(--tt-ok);\n  box-shadow: 0 0 0 3px rgba(19, 122, 78, 0.12);\n}\n\n.tt-dot[data-status=\"warn\"] {\n  background: var(--tt-warn);\n  box-shadow: 0 0 0 3px rgba(138, 83, 0, 0.14);\n}\n\n.tt-dot[data-status=\"failed\"] {\n  background: var(--tt-danger);\n  box-shadow: 0 0 0 3px rgba(179, 38, 30, 0.14);\n}\n\n.tt-summary {\n  flex: 1 1 auto;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.tt-count {\n  flex: none;\n  font-family: var(--tt-mono);\n  font-size: 0.6875rem;\n  font-variant-numeric: tabular-nums;\n  letter-spacing: 0.02em;\n  color: var(--tt-faint);\n  white-space: nowrap;\n}\n\n.tt-chevron {\n  flex: none;\n  color: var(--tt-faint);\n  transition: transform 200ms var(--tt-ease);\n}\n\n.tt-trail[data-open=\"true\"] .tt-chevron {\n  transform: rotate(180deg);\n}\n\n/* ---- the expanded trail ------------------------------------ */\n\n.tt-panel {\n  display: grid;\n  grid-template-rows: 0fr;\n  transition: grid-template-rows 240ms var(--tt-ease);\n}\n\n.tt-panel[data-open=\"true\"] {\n  grid-template-rows: 1fr;\n}\n\n.tt-panel-inner {\n  min-height: 0;\n  overflow: hidden;\n}\n\n.tt-panel[data-open=\"true\"] .tt-panel-inner {\n  border-top: 1px solid var(--tt-line-soft);\n}\n\n.tt-steps {\n  margin: 0;\n  padding: 6px 12px 10px 10px;\n  list-style: none;\n}\n\n.tt-step {\n  position: relative;\n  display: grid;\n  grid-template-columns: 16px minmax(0, 1fr);\n  gap: 10px;\n  padding: 8px 0 10px;\n}\n\n/* the rail: a marker per step, joined downwards to the next one */\n.tt-step::before {\n  content: \"\";\n  position: absolute;\n  left: 8px;\n  top: 21px;\n  bottom: -3px;\n  width: 1px;\n  background: var(--tt-rail);\n}\n\n.tt-step:last-child::before {\n  display: none;\n}\n\n.tt-marker {\n  position: relative;\n  z-index: 1;\n  justify-self: center;\n  align-self: start;\n  width: 9px;\n  height: 9px;\n  margin-top: 5px;\n  border: 1px solid var(--tt-rail);\n  border-radius: 999px;\n  background: var(--tt-faint);\n  box-shadow: 0 0 0 2.5px var(--tt-surface);\n}\n\n.tt-step[data-status=\"ok\"] .tt-marker {\n  background: var(--tt-ok);\n  border-color: var(--tt-ok);\n}\n\n.tt-step[data-status=\"warn\"] .tt-marker {\n  background: var(--tt-warn);\n  border-color: var(--tt-warn);\n}\n\n.tt-step[data-status=\"failed\"] .tt-marker {\n  background: var(--tt-danger);\n  border-color: var(--tt-danger);\n}\n\n.tt-step-body {\n  min-width: 0;\n}\n\n.tt-step-line {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: baseline;\n  gap: 4px 7px;\n}\n\n.tt-step-text {\n  min-width: 0;\n}\n\n.tt-actor {\n  font-weight: 600;\n}\n\n.tt-status {\n  flex: none;\n  padding: 1px 6px;\n  border: 1px solid currentColor;\n  border-radius: 999px;\n  font-family: var(--tt-mono);\n  font-size: 0.625rem;\n  line-height: 1.5;\n  letter-spacing: 0.08em;\n  text-transform: uppercase;\n}\n\n.tt-step[data-status=\"failed\"] .tt-status {\n  color: var(--tt-danger);\n}\n\n.tt-step[data-status=\"warn\"] .tt-status {\n  color: var(--tt-warn);\n}\n\n/* When a host wants steps to act, actor + action become one button. */\n.tt-step-button {\n  display: inline-flex;\n  flex-wrap: wrap;\n  align-items: baseline;\n  gap: 5px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  border-radius: 4px;\n  background: none;\n  color: inherit;\n  font: inherit;\n  text-align: left;\n  cursor: pointer;\n}\n\n.tt-step-button:hover .tt-actor,\n.tt-step-button:hover .tt-action {\n  text-decoration: underline;\n  text-decoration-color: var(--tt-faint);\n  text-underline-offset: 2px;\n}\n\n.tt-step-button:focus-visible {\n  outline: 2px solid var(--tt-focus);\n  outline-offset: 2px;\n}\n\n.tt-detail {\n  margin: 3px 0 0;\n  color: var(--tt-muted);\n  overflow-wrap: anywhere;\n}\n\n.tt-meta {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: baseline;\n  gap: 3px 10px;\n  margin: 4px 0 0;\n}\n\n.tt-time {\n  font-family: var(--tt-mono);\n  font-size: 0.6875rem;\n  font-variant-numeric: tabular-nums;\n  color: var(--tt-faint);\n  white-space: nowrap;\n}\n\n.tt-source {\n  font-family: var(--tt-mono);\n  font-size: 0.6875rem;\n  color: var(--tt-muted);\n  overflow-wrap: anywhere;\n}\n\na.tt-source {\n  color: var(--tt-focus);\n  text-decoration: underline;\n  text-decoration-color: rgba(79, 39, 224, 0.35);\n  text-underline-offset: 2px;\n}\n\na.tt-source:hover {\n  text-decoration-color: currentColor;\n}\n\na.tt-source:focus-visible {\n  outline: 2px solid var(--tt-focus);\n  outline-offset: 2px;\n  border-radius: 2px;\n}\n\n/* The component ships its own sr-only, so it never depends on the host. */\n.tt-sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  clip-path: inset(50%);\n  white-space: nowrap;\n  border: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .tt-panel,\n  .tt-chevron {\n    transition: none;\n  }\n}\n"}]}