{
  "_meta": {
    "title": "Page Recipes — AI 机读页面级组合配方",
    "purpose": "component-affordances.json 是组件级「单个组件内置什么能力」的机读层；本文件是页面级「多个组件怎么组合成一个完整页面骨架」的机读层——L5 AIC-01 / L6 PAT-01 的首个实例。AI 生成产品页面（如数据表页）前，先在这里查槽位组合 + 状态→驱动 prop/slot 的映射，再决定怎么拼装 canonical 组件；不含任何产品语义（无具体列名/文案/业务状态命名）。",
    "is_source_of_truth": true,
    "hand_authored": true,
    "registered_in": "docs/PROJECT_MAP.md §2 数据来源",
    "source_spec": "docs/superpowers/specs/2026-07-22-table-data-grid-spec-design.md §3.3",
    "schema_file": "figma-data/page-recipes.schema.json（结构真源；闸 = pnpm audit:page-recipes，L4 pre-commit + L5 pr-checks 双挂）",
    "related": {
      "component_affordances": "docs/internal/component-affordances.json（组件级能力 + code_props/features）",
      "canonical_components": "src/canonical/*.vue"
    },
    "schema_note": "每条 recipe：id + summary + slots[]（每个槽位是否必需、对应哪个 canonical 组件或 null=纯布局占位）+ states[]（呈现态 → 由哪个 prop/slot 驱动，不含行为）+ cellRendering（可选；Table 列 cell 数据描述符怎么把行数据映射成单元格内 TVU 视觉，仅呈现）+ behaviorOwnedByApp（true = 排序/选择/翻页/单元格点击等运行时行为完全由消费方 app 实现，组件与本配方都不管理）+ layoutTokens（引用 src/tokens/variables.css 既有 spacing token，不新造数值）。slots[].position 只写一个确定位置；「窄屏改到别处」填 responsive { stacksBelow: 裸 --bp-* token 名, stackedPosition }，不许写成 \"left (or top)\" 那种条件式散文（说了会变、没说在哪变，AI 解析不出切换点，闸 S5 核）。stacksBelow 刻意不是 var(--x)：CSS @media 吃不了 var()，消费方要的是数值，走 ./tokens/js 与 ./tokens DTCG 出口解析。当前唯一用到的 --bp-lg (1100px) 取自仓库里唯一的同形态现存实例——playground/docs/docs.css:918 让 .canonical-shell 从三栏塌成单栏、侧栏由左侧固定改为顶部静态；照 DG-1「取实测现值、不发明新值」办（INFRA-F68 DG-1 后续，2026-08-03）。该值是 owner 可一句话改的单点：纯 JSON 契约、零渲染，改一处即生效。"
  },
  "recipes": [
    {
      "id": "data-table-page",
      "summary": "数据表页面骨架：可选 toolbar + Table + 下方 Pagination。Table 的空/加载/选中/排序四态均为呈现层 prop/slot 映射，行内单元格可用 column.cell 数据描述符渲染状态 pill / 操作按钮 / 链接 / 图标；这些都不内置任何行为——排序触发、选中集变更、翻页请求（数据获取）、单元格点击处理完全由消费方 app 实现。",
      "slots": [
        {
          "slot": "toolbar",
          "required": false,
          "component": null,
          "position": "above table",
          "description": "app 自定义区域（搜索框、筛选器、批量操作按钮等）。本 DS 无对应 canonical 组件，纯布局占位——内容与产品语义完全由消费方决定。"
        },
        {
          "slot": "table",
          "required": true,
          "component": "Table",
          "position": "core",
          "description": "canonical Table：columns + data 驱动渲染；loading / selectedKeys+rowKey / column.sortOrder / #empty 具名槽控制四种呈现态（见下方 states）。"
        },
        {
          "slot": "pagination",
          "required": false,
          "component": "Pagination",
          "position": "below table",
          "description": "canonical Pagination，紧跟 table 下方；total/pageSize 驱动页数计算，当前页走 v-model（update:modelValue）。翻页触发后重新请求/切片数据由 app 实现，Pagination 本身不持有数据。"
        }
      ],
      "states": [
        {
          "state": "empty",
          "when": "data 为空数组且未处于 loading",
          "driven_by": "slot:empty",
          "note": "Table #empty 具名槽；未投影内容时的默认 fallback = TvuLocale.tableEmpty（默认 \"No data\"）。文案走 locale 契约，不写死业务词。"
        },
        {
          "state": "loading",
          "when": "app 发起数据请求、尚未返回",
          "driven_by": "prop:loading",
          "note": "Table.loading=true 时渲染半透明 overlay + 内联 CSS spinner（code-first，因暂无独立 Spin 组件）。app 自行决定何时置 true/false。"
        },
        {
          "state": "selected",
          "when": "app 记录了当前被选中的行",
          "driven_by": "prop:selectedKeys+rowKey",
          "note": "Table 按 rowKey 取值匹配 selectedKeys 做行高亮（.tbl-row--selected），仅视觉、无勾选列。选中集的增删由 app 管理，Table 不维护任何内部选中状态。"
        },
        {
          "state": "sorted",
          "when": "某一列当前处于排序状态",
          "driven_by": "column.sortOrder",
          "note": "TableColumn.sortOrder = 'asc' | 'desc' | null，表头渲染方向指示图标（复用 registry action/sorting）。Table 不排序数据、无点击排序行为——点击触发、重新请求/客户端排序完全由 app 实现。"
        }
      ],
      "cellRendering": {
        "driven_by": "column.cell",
        "summary": "TableColumn.cell 是一个纯数据描述符（可 JSON 序列化，无函数），把该列的行数据映射成单元格内的真 TVU 视觉。仅呈现层，Table 不因此新增任何 emits/状态。默认（无 cell 或 kind='text'）= 纯文本，向后兼容。",
        "kinds": [
          { "kind": "text", "renders": "纯文本（默认）", "shape": "{ kind:'text' }" },
          { "kind": "pill", "renders": "PillStatus 状态药丸", "shape": "{ kind:'pill', map?:{ [rawValue]: { status?:'On-air'|'Preview'|'Analyzing'|'Inactive', active?:boolean, label?:string } } }", "note": "按该列的原始值查 map 决定状态色/文案；未命中则以原始值为 label。" },
          { "kind": "button", "renders": "单个 Button", "shape": "{ kind:'button', label?, actionKey?, color?, fill?, size?, iconName? }", "note": "label 缺省用原始值。" },
          { "kind": "actions", "renders": "一组 Button（操作列）", "shape": "{ kind:'actions', items:[{ label?, actionKey, color?, fill?, size?, iconName? }] }" },
          { "kind": "link", "renders": "themed <a>", "shape": "{ kind:'link', href?, hrefKey?, label?, actionKey? }", "note": "href 缺省依次取 hrefKey 指向的字段、再取原始值。" },
          { "kind": "icon", "renders": "registry Icon", "shape": "{ kind:'icon', name?, map?:{ [rawValue]:iconName } }", "note": "图标名走 registry（如 action/sorting、edit/copy），不内联自画 SVG。" }
        ],
        "interactionDelegation": "button / actions / link 类元素渲染时带 data-cell-action=<actionKey> + data-row-key=<rowKey 值>。Table 不发事件；app 在 Table 元素上挂一个委派 click 监听，用 event.composedPath()（不是 e.target——shadow DOM 会把 target retarget 到宿主）找到带 data-cell-action 的节点，读 action + rowKey 决定业务动作。",
        "note": "rich cell 是 Figma 无源的合法 code-first（divergence table-cell-descriptor-code-first-2026-07-24）；描述符是数据不是渲染函数，故可跨 Vue-npm + CE/React 双出口以 DOM property 传入。"
      },
      "behaviorOwnedByApp": true,
      "behaviorNote": "本配方与 Table/Pagination 组件本身都只负责『此刻是什么状态、由哪个 prop/slot 驱动其视觉』。排序触发、行选中变更、翻页请求（数据获取或客户端切片）、单元格 action/link 的点击处理等运行时行为一律由消费方 app 实现；组件无 emits、无内部状态、无 v-model 用于这些行为。",
      "layoutTokens": {
        "toolbarToTableGap": "var(--sp-m)",
        "tableToPaginationGap": "var(--sp-m)"
      }
    },
    {
      "id": "entity-form-page",
      "summary": "实体表单页骨架：可选页头 + Form 容器包 N 个 FormItem + 底部主/次操作 + 可选提交结果反馈。四态（pristine / field-invalid / submitting / submit-result）全是呈现层 prop 映射；校验规则由 Form 引擎按 rules 判定，但提交请求、成功后跳转、服务端错误映射回字段、异步/远程校验一律由消费方 app 实现。",
      "slots": [
        {
          "slot": "header",
          "required": false,
          "component": null,
          "position": "above form",
          "description": "app 自定义页头区域（标题、面包屑、说明文字）。本 DS 无对应组件，纯布局占位——内容与产品语义完全由消费方决定。"
        },
        {
          "slot": "form",
          "required": true,
          "component": "Form",
          "position": "core",
          "description": "canonical Form：model（必填）+ rules 驱动校验，另有 labelWidth / layout / disabled；暴露 validate / validateField / resetFields / clearValidate 四个方法供 app 调用。⚠️ 字段之间的纵向间距由 Form 自己承担（.tvu-form 内建 gap: var(--sp-m)），app 不要再额外加——故 layoutTokens 里没有 fieldToFieldGap。"
        },
        {
          "slot": "field",
          "required": true,
          "component": "FormItem",
          "position": "inside form, repeated",
          "description": "canonical FormItem 逐字段重复：prop 指向 model 里的字段路径，label 走 prop 或 #label 具名槽，控件放默认槽（InputBoxFilled / SelectBoxFilled / Radio / CheckBox / Switch 等）。error 与 status 是错误态的呈现开关；在 Form 内由引擎按 rules 自动写入，独立使用时由 app 直接传。"
        },
        {
          "slot": "actions",
          "required": true,
          "component": "Button",
          "position": "below form",
          "description": "提交 / 取消操作区。主操作 fill=filling + color=green，次操作 fill=ghost + color=gray 1（同 PopupBox footer 的用法；已删除的 variant/size 死 prop 不要再用）。"
        },
        {
          "slot": "feedback",
          "required": false,
          "component": "Message",
          "position": "page-level, above form",
          "description": "提交结果反馈。canonical Message 承担视觉（status 语义色 + size + closable），何时显示、显示什么文案由 app 决定。"
        }
      ],
      "states": [
        {
          "state": "pristine",
          "when": "model 为初值且未触发过任何校验",
          "driven_by": "prop:model",
          "note": "字段无错误态、无红边框；FormItem 不显示 error message。app 可调 resetFields 回到此态。"
        },
        {
          "state": "field-invalid",
          "when": "某字段校验失败",
          "driven_by": "FormItem.error",
          "note": "FormItem 红边框 + 独立 error message 行。在 Form 内由引擎按 rules 写入 error（等价 status='Error'），独立使用时 app 直接传 error 字符串。⚠️ label 保持灰、不染红——CANONICAL-019 曾把 label 染红，已被 live Figma 1923:49069 实证推翻并 revert，别再改回去。"
        },
        {
          "state": "submitting",
          "when": "app 已发起提交请求、尚未返回",
          "driven_by": "Button.status",
          "note": "主操作按钮传 status='loading' 进 loading 呈现态。⚠️ Button 没有 loading 这个 prop——loading 是 status 枚举的一个值（与 icon='loading' 配合）。DS 不管理请求生命周期，只提供该呈现态。"
        },
        {
          "state": "submit-result",
          "when": "提交已返回成功或失败",
          "driven_by": "Message.status",
          "note": "成功/失败用 Message 的 status 语义色 + 文案传达，渲染在 feedback 槽里；spinner / 图标动效交 dev 实现，DS 不内置结果动画。"
        }
      ],
      "behaviorOwnedByApp": true,
      "behaviorNote": "本配方与 Form/FormItem/Button/Message 都只负责『此刻是什么状态、由哪个 prop/slot 驱动其视觉』。校验规则的判定由 Form 引擎承担（二值 pass/fail），但触发提交、发请求、成功后跳转、把服务端错误映射回字段、异步/远程校验、validating 中间态一律由消费方 app 实现（owner 2026-07-22 明确裁剪这些运行时余项：属 app 层逻辑，烘进组件是冗余）。",
      "layoutTokens": {
        "headerToFormGap": "var(--sp-l)",
        "formToActionsGap": "var(--sp-l)",
        "feedbackToFormGap": "var(--sp-m)"
      }
    },
    {
      "id": "master-detail-page",
      "summary": "主从详情页骨架：可选面包屑 + 主列表（Table）+ 详情区（纯布局占位）+ 详情内可选分段（Tab）。五态全是呈现层 prop/slot 映射；选中哪一项、详情数据怎么取、切段后加载什么、路由怎么变一律由消费方 app 实现——Table 不维护任何内部选中状态。",
      "slots": [
        {
          "slot": "breadcrumb",
          "required": false,
          "component": "Breadcrumb",
          "position": "top",
          "description": "canonical Breadcrumb 只是容器——本体零 props，仅转发 attrs + 默认槽；层级信息全在其内的 BreadcrumbItem：href 给链接、state（current | Default | disabled | hover）标当前位、separatorType（arrow | slash）+ showSeparator 控分隔符。点击跳转由 app 接路由，Breadcrumb 自身不管导航。"
        },
        {
          "slot": "master",
          "required": true,
          "component": "Table",
          "position": "left",
          "responsive": {
            "stacksBelow": "--bp-lg",
            "stackedPosition": "top"
          },
          "description": "主列表。canonical Table 按 rowKey 取值匹配 selectedKeys 做行高亮来表达「当前选中项」，loading 表达取数中，#empty 具名槽表达列表空态。选中集由 app 持有——Table 无勾选列、无 emits、不维护内部选中状态。"
        },
        {
          "slot": "detail",
          "required": true,
          "component": null,
          "position": "right",
          "responsive": {
            "stacksBelow": "--bp-lg",
            "stackedPosition": "below master"
          },
          "description": "详情区容器，纯布局占位——DS 不规定详情字段，内部内容由 app 用 canonical 控件自由组合；空态也在这里由 app 自己渲染（见 states 的 detail-empty）。"
        },
        {
          "slot": "detail-sections",
          "required": false,
          "component": "Tab",
          "position": "inside detail",
          "description": "详情内分段，两种用法二选一：canonical Tab 走槽位组合（内部放 TabItem），canonical TabList 走 items[] 数据驱动（{ label, value, disabled? }）。两者当前段都由 modelValue 走 v-model 驱动、都发 change（跨 CE 边界已随 v1.1.0 ship），且渲染同一个 Figma Tab/List 节点。切段后加载哪份数据由 app 决定。"
        }
      ],
      "states": [
        {
          "state": "master-loading",
          "when": "主列表数据正在请求中",
          "driven_by": "prop:loading",
          "note": "Table.loading=true 渲染半透明 overlay + 内联 spinner；详情区保持上一次内容还是转空态由 app 决定。DS 不管理请求生命周期。"
        },
        {
          "state": "master-empty",
          "when": "主列表 data 为空数组且未处于 loading",
          "driven_by": "slot:empty",
          "note": "Table #empty 具名槽；未投影内容时 fallback = TvuLocale.tableEmpty（默认 \"No data\"）。与 nothing-selected 是两回事——此态是「没有可选的项」，那态是「有项但一个都没选」。"
        },
        {
          "state": "nothing-selected",
          "when": "主列表有数据、但 app 尚未给出任何选中项",
          "driven_by": "prop:selectedKeys",
          "note": "selectedKeys 为空数组 → 主列表无高亮行；详情区显示引导性空态（app 在 detail 槽内自渲染，或用 Message 承担视觉）。"
        },
        {
          "state": "selected",
          "when": "app 记录了当前被选中的行",
          "driven_by": "prop:selectedKeys+rowKey",
          "note": "Table 按 rowKey 取值匹配 selectedKeys 做行高亮（.tbl-row--selected），仅视觉映射、不含选中行为；详情区渲染对应内容。单选与多选的差别只在 app 往 selectedKeys 放几个键。"
        },
        {
          "state": "detail-empty",
          "when": "已选中某项、但该项没有详情数据",
          "driven_by": "slot:detail",
          "note": "空态由 app 在 detail 槽内自己渲染——detail 是纯布局占位，DS 不提供 detail 级 #empty 槽（对比 master 侧可直接用 Table 自带的 #empty）。"
        }
      ],
      "behaviorOwnedByApp": true,
      "behaviorNote": "本配方与 Table/Breadcrumb/Tab 都只负责『此刻是什么状态、由哪个 prop/slot 驱动其视觉』。选中集变更、详情数据获取、切段后取数、路由同步、面包屑跳转一律由消费方 app 实现：Table 不维护内部选中状态，Breadcrumb 不管导航，Tab 只发 change 不取数。",
      "layoutTokens": {
        "breadcrumbToBodyGap": "var(--sp-m)",
        "masterToDetailGap": "var(--sp-l)",
        "detailSectionsToContentGap": "var(--sp-m)"
      }
    }
  ]
}
