{
  "$schema": "https://pi-tui.ratstack.sh/schema/pattern.schema.json",
  "aka": [
    "Tabbed view"
  ],
  "animated": false,
  "apis": [
    {
      "name": "ctx.ui.custom",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays"
    },
    {
      "name": "matchesKey",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus"
    },
    {
      "name": "TUI.requestRender",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model"
    },
    {
      "name": "SelectList",
      "url": null
    },
    {
      "name": "truncateToWidth",
      "url": null
    }
  ],
  "applicability": [
    "Use this when one compact dashboard has several related datasets."
  ],
  "category": "lists",
  "categoryLabel": "Lists and pickers",
  "consequences": [
    "Tabs keep related datasets in one compact view.",
    "Only the active tab is visible at a time."
  ],
  "family": "structural",
  "familyLabel": "Structural",
  "id": "tab-deck",
  "implementation": [
    "Request a render after tab or row changes.",
    "Resolve the custom interaction on dismissal."
  ],
  "intent": "Keep separate keyboard-navigable data views behind a tab strip.",
  "jsonUrl": "https://pi-tui.ratstack.sh/patterns/tab-deck.json",
  "kind": "component",
  "knownUses": [
    {
      "example": "usage-extension-tabbed-dashboard",
      "moreRefs": 0,
      "refs": [
        {
          "file": "usage-extension/index.ts",
          "lines": "347-400",
          "sha": "bca5070b",
          "url": "https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/usage-extension/index.ts#L347-L400"
        },
        {
          "file": "usage-extension/index.ts",
          "lines": "526-558",
          "sha": "bca5070b",
          "url": "https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/usage-extension/index.ts#L526-L558"
        }
      ],
      "repo": "pi-extensions",
      "repoUrl": "https://github.com/nicobailon/pi-extensions",
      "title": "Usage extension: navigable tabular dashboard"
    }
  ],
  "linkedFrom": [],
  "links": {
    "agentGuide": "https://pi-tui.ratstack.sh/llms.txt",
    "index": "https://pi-tui.ratstack.sh/patterns.md",
    "skill": "https://pi-tui.ratstack.sh/skills/build-pi-tui-component.md"
  },
  "mdUrl": "https://pi-tui.ratstack.sh/patterns/tab-deck.md",
  "motivation": "The usage extension puts several data views in one keyboard-driven dashboard.",
  "name": "Tab Deck",
  "participants": [
    {
      "name": "ctx.ui.custom",
      "role": "Mounts one interaction and resolves through done.",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays"
    },
    {
      "name": "matchesKey",
      "role": "Recognizes explicit terminal key combinations.",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus"
    },
    {
      "name": "TUI.requestRender",
      "role": "Requests a coalesced redraw after state changes.",
      "url": "https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model"
    },
    {
      "name": "Active tab",
      "role": "Selects which dataset and row highlight are visible.",
      "url": null
    }
  ],
  "pi": "1.0.3",
  "related": [
    {
      "how": "resolves navigation actions",
      "id": "action-compass",
      "jsonUrl": "https://pi-tui.ratstack.sh/patterns/action-compass.json",
      "mdUrl": "https://pi-tui.ratstack.sh/patterns/action-compass.md",
      "name": "Action Compass",
      "url": "https://pi-tui.ratstack.sh/patterns/action-compass/"
    }
  ],
  "sampleCode": {
    "code": "// Tab Deck: preserve each dataset and its selection behind a keyboard-driven tab strip.\nimport { matchesKey, SelectList, truncateToWidth, type Component } from \"@earendil-works/pi-tui\";\nimport { getSelectListTheme } from \"@earendil-works/pi-coding-agent\";\nimport type { PatternStory } from \"../../../src/pattern.ts\";\n\nexport const story: PatternStory = {\n  id: \"tab-deck\", title: \"Tab Deck\", kind: \"component\",\n  apis: [\"matchesKey\", \"SelectList\", \"TUI.requestRender\", \"truncateToWidth\"],\n  states: [\n    { id: \"first\", label: \"First tab\" },\n    { id: \"second\", label: \"Second tab\", steps: [\n      { type: \"keys\", data: \"\\x1b[B\" }, { type: \"keys\", data: \"\\t\" },\n      { type: \"keys\", data: \"\\x1b[B\" },\n    ] },\n  ],\n  setup({ theme, tui }) {\n    const tabs = [\n      { title: \"Tasks\", rows: [\"Render preview · ready\", \"Review sample · waiting\", \"Publish · queued\"] },\n      { title: \"Files\", rows: [\"src/preview.ts · 2 edits\", \"src/tasks.ts · 1 edit\", \"test/preview.ts · unchanged\"] },\n    ].map(tab => ({ ...tab, list: new SelectList(tab.rows.map((label, i) => ({\n      value: String(i), label,\n    })), 4, getSelectListTheme()) }));\n    let activeTab = 0;\n    const component: Component = {\n      invalidate() { tabs.forEach(tab => tab.list.invalidate()); },\n      handleInput(data) {\n        if (matchesKey(data, \"tab\") || matchesKey(data, \"right\")) activeTab = (activeTab + 1) % tabs.length;\n        else if (matchesKey(data, \"left\")) activeTab = (activeTab + tabs.length - 1) % tabs.length;\n        else tabs[activeTab]!.list.handleInput(data);\n        tui.requestRender();\n      },\n      render(width) {\n        const strip = tabs.map((tab, i) => theme.fg(i === activeTab ? \"accent\" : \"muted\",\n          i === activeTab ? \"[\" + tab.title + \"]\" : \" \" + tab.title + \" \")).join(\"  \");\n        const current = tabs[activeTab]!;\n        return [\n          theme.fg(\"accent\", \"Task workspace\"),\n          truncateToWidth(strip, width), theme.fg(\"border\", \"─\".repeat(width)),\n          ...current.list.render(width),\n          theme.fg(\"dim\", truncateToWidth(\"Row \" + (Number(current.list.getSelectedItem()?.value ?? 0) + 1) +\n            \" of \" + current.rows.length + \" · \" + current.title, width)),\n          theme.fg(\"muted\", \"tab / ←→ view · ↑↓ row\"),\n        ];\n      },\n    };\n    return component;\n  },\n};\n",
    "lang": "ts",
    "path": "stories/patterns/structural/tab-deck.ts"
  },
  "states": [
    {
      "dont": false,
      "expectFail": [],
      "frames": [
        {
          "alt": "Tab Deck, First tab state, 40 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-40-dark.9aaa2d7ca516.webp",
          "theme": "dark",
          "w": 800,
          "width": 40
        },
        {
          "alt": "Tab Deck, First tab state, 40 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-40-light.b0312fdc2e56.webp",
          "theme": "light",
          "w": 800,
          "width": 40
        },
        {
          "alt": "Tab Deck, First tab state, 60 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-60-dark.e01b183d2c13.webp",
          "theme": "dark",
          "w": 1200,
          "width": 60
        },
        {
          "alt": "Tab Deck, First tab state, 60 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-60-light.af8173ff1f19.webp",
          "theme": "light",
          "w": 1200,
          "width": 60
        },
        {
          "alt": "Tab Deck, First tab state, 80 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-80-dark.af5115350f14.webp",
          "theme": "dark",
          "w": 1600,
          "width": 80
        },
        {
          "alt": "Tab Deck, First tab state, 80 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-80-light.c4360638b525.webp",
          "theme": "light",
          "w": 1600,
          "width": 80
        },
        {
          "alt": "Tab Deck, First tab state, 120 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-120-dark.2fd0c5d51bf7.webp",
          "theme": "dark",
          "w": 2400,
          "width": 120
        },
        {
          "alt": "Tab Deck, First tab state, 120 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/first-120-light.a6dbe7f8e5f5.webp",
          "theme": "light",
          "w": 2400,
          "width": 120
        }
      ],
      "how": "Show summary rows under the active tab.",
      "id": "first",
      "label": "First tab",
      "problems": []
    },
    {
      "dont": false,
      "expectFail": [],
      "frames": [
        {
          "alt": "Tab Deck, Second tab state, 40 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-40-dark.94d36cb22136.webp",
          "theme": "dark",
          "w": 800,
          "width": 40
        },
        {
          "alt": "Tab Deck, Second tab state, 40 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-40-light.77b7a20eaf9c.webp",
          "theme": "light",
          "w": 800,
          "width": 40
        },
        {
          "alt": "Tab Deck, Second tab state, 60 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-60-dark.838d8ffe852a.webp",
          "theme": "dark",
          "w": 1200,
          "width": 60
        },
        {
          "alt": "Tab Deck, Second tab state, 60 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-60-light.6c43c623dd81.webp",
          "theme": "light",
          "w": 1200,
          "width": 60
        },
        {
          "alt": "Tab Deck, Second tab state, 80 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-80-dark.e7397945c8de.webp",
          "theme": "dark",
          "w": 1600,
          "width": 80
        },
        {
          "alt": "Tab Deck, Second tab state, 80 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-80-light.85621b887a42.webp",
          "theme": "light",
          "w": 1600,
          "width": 80
        },
        {
          "alt": "Tab Deck, Second tab state, 120 columns, dark theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-120-dark.8f5f8e891c02.webp",
          "theme": "dark",
          "w": 2400,
          "width": 120
        },
        {
          "alt": "Tab Deck, Second tab state, 120 columns, light theme, 8 rows",
          "animated": false,
          "animation": null,
          "checks": [
            {
              "evidence": [],
              "name": "width",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "style-leak",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "hard-coded-colour",
              "status": "pass",
              "verdict": "pass"
            },
            {
              "evidence": [],
              "name": "height",
              "status": "pass",
              "verdict": "pass"
            }
          ],
          "h": 352,
          "rows": 8,
          "still": "https://pi-tui.ratstack.sh/frames/tab-deck/second-120-light.2eafc9f0a87e.webp",
          "theme": "light",
          "w": 2400,
          "width": 120
        }
      ],
      "how": "Change tab and show its independent rows and highlight.",
      "id": "second",
      "label": "Second tab",
      "problems": []
    }
  ],
  "structure": "keys -> active tab\nactive tab -> rows + selection",
  "thumbnails": {
    "dark": {
      "animation": null,
      "src": "https://pi-tui.ratstack.sh/thumbs/tab-deck/dark.b8215f419d72.webp"
    },
    "light": {
      "animation": null,
      "src": "https://pi-tui.ratstack.sh/thumbs/tab-deck/light.4a4154ea890c.webp"
    }
  },
  "url": "https://pi-tui.ratstack.sh/patterns/tab-deck/",
  "widths": [
    40,
    60,
    80,
    120
  ]
}
