---
phase: 03-dialog-feedback
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - app/layout.tsx
autonomous: true
requirements:
  - CRED-FE-05
must_haves:
  truths:
    - "调用 toast.success(...) / toast.error(...) 后屏幕能看到 Sonner 通知"
    - "Toaster 在所有路由下均可用（挂在 RootLayout 顶层）"
    - "不破坏现有 RootLayout 渲染（children 仍正常显示）"
  artifacts:
    - path: "app/layout.tsx"
      provides: "RootLayout 注入 <Toaster /> portal"
      contains: "Toaster"
  key_links:
    - from: "app/layout.tsx"
      to: "@/components/ui/sonner"
      via: "import { Toaster }"
      pattern: "from \"@/components/ui/sonner\""
---

<objective>
在 `app/layout.tsx` 的 `<body>` 内挂载 Sonner `<Toaster />`，让全局 `toast.success(...)` / `toast.error(...)` 命令式调用真正能在屏幕上显示。

**Purpose**：仓库 9 处 `toast(...)` 调用当前全部是 dead code（仅 `components/ui/sonner.tsx` 定义了 Toaster 包装但**从未挂载**），不挂载 Phase 3 的成功 / 失败反馈完全静默；这是 Phase 3 业务功能跑通的硬前置。

**Output**：修改后的 `app/layout.tsx`，新增 1 行 import + 1 个 JSX 元素挂载点。
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-dialog-feedback/03-CONTEXT.md
@.planning/phases/03-dialog-feedback/03-RESEARCH.md
@CLAUDE.md
@app/layout.tsx
@components/ui/sonner.tsx

<interfaces>
<!-- Sonner Toaster 包装组件签名（从 components/ui/sonner.tsx 提取） -->
<!-- 直接 import 即可使用，无 props 也能渲染（已封装 next-themes 主题适配）。 -->

From components/ui/sonner.tsx:
```typescript
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster: ({ ...props }: ToasterProps) => JSX.Element
export { Toaster }
```

调用形态：`<Toaster />`（无 props 即可，theme 内部已读 next-themes context；本仓库未挂 ThemeProvider，会回退到默认 "system"，无错误）
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1：在 RootLayout 挂载 Sonner Toaster</name>
  <files>app/layout.tsx</files>
  <read_first>
    必读 `app/layout.tsx` 全文（仅 20 行）确认当前结构。当前内容：

    ```tsx
    import type { Metadata } from 'next'
    import './globals.css'

    export const metadata: Metadata = {
      title: 'v0 App',
      description: 'Created with v0',
      generator: 'v0.dev',
    }

    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode
    }>) {
      return (
        <html lang="en">
          <body>{children}</body>
        </html>
      )
    }
    ```
  </read_first>
  <action>
**精确改动 2 处**：

**改动 1**：在 `import './globals.css'` 之后追加 1 行 import：

```tsx
import { Toaster } from '@/components/ui/sonner'
```

**改动 2**：把 `<body>{children}</body>` 改为 `<body>{children}<Toaster /></body>`（即在 `{children}` 之后、`</body>` 之前插入 `<Toaster />`）。

**最终全文（应该是这样）**：

```tsx
import type { Metadata } from 'next'
import './globals.css'
import { Toaster } from '@/components/ui/sonner'

export const metadata: Metadata = {
  title: 'v0 App',
  description: 'Created with v0',
  generator: 'v0.dev',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  )
}
```

**严格约束**：
- **不**挂 Radix Toast `<Toaster />`（来自 `@/components/ui/toaster`）—— 那是另一套实现，与 Sonner 不通信；本 phase 锁定 Sonner（CONTEXT D-Toast 决策）
- **不**新增 ThemeProvider / next-themes 包装 —— `components/ui/sonner.tsx:9` 有 `useTheme()` fallback 到 `"system"`，无 ThemeProvider 也能跑（本仓库目前确实没挂 ThemeProvider）
- **不**改 `metadata` / `html lang` / `globals.css` import 顺序
- **不**给 RootLayout 加 `"use client"` —— `<Toaster />` 自身就是 client component（`components/ui/sonner.tsx:1` 顶部已 `"use client"`），React Server Component 可以直接渲染 client child，无需 RootLayout 自己 client 化
- 这是本 phase 唯一改动 `app/layout.tsx` 的 task，**不**在此挂任何其他 provider
  </action>
  <verify>
    <automated>
      # Windows PowerShell（项目不含 .eslintrc*，lint 跳过沿用 Phase 1+2 判定）
      cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin

      # A 段：tsc 整体 + 反向断言（必须 0 条指向 app/layout.tsx）
      npx tsc --noEmit 2>&1 | Select-String -Pattern 'app/layout\.tsx|app\\layout\.tsx'
      # 期望：无任何输出（0 条新错误指向本文件）

      # B 段：grep 验证 import + Toaster 元素都已落地（PowerShell Select-String）
      Select-String -Path 'app/layout.tsx' -Pattern 'from "@/components/ui/sonner"'
      # 期望：1 行命中 import 行
      Select-String -Path 'app/layout.tsx' -Pattern '<Toaster\s*/>'
      # 期望：1 行命中 <Toaster /> 标签

      # C 段：lockfile 未动（不引入新依赖）—— Sonner 已在 deps（^1.7.1）
      git diff --stat HEAD -- package.json yarn.lock package-lock.json pnpm-lock.yaml
      # 期望：0 行（无 diff）
    </automated>
  </verify>
  <done>
    - `app/layout.tsx` 包含 `import { Toaster } from "@/components/ui/sonner"` 一行
    - `<body>` 内 `{children}` 之后渲染 `<Toaster />`
    - `npx tsc --noEmit` 输出过滤 `app/layout.tsx` 后 **0 条新错误**（67 条存量错误与本 task 无关）
    - 4 个 manifest+lockfile 在 git diff 中 0 行 diff（不引入新依赖）
  </done>
</task>

</tasks>

<verification>
**Phase 3 Plan 1 整体验证**（Plan 内已涵盖，此处汇总）：

1. **类型检查**：`npx tsc --noEmit` exit 非 0（67 条存量错误，与本 phase 无关），但 `Select-String` 过滤 `app/layout.tsx` 命中 0 行
2. **挂载位置正确**：grep `<Toaster />` 命中且位于 `<body>` 内、`{children}` 之后（不是 `<head>` 内、不在 children 之前）
3. **不动 lockfile**：`git diff --stat HEAD -- package.json *.lock` 输出 0 行
4. **lint 跳过**：项目无 `.eslintrc*` / `eslint-config-next`，沿用 Phase 1+2 判定（不阻塞）

**关键失败模式**（如果出现，回头修）：
- 如果挂在 `<html>` 之外或 `<head>` 内 → 渲染失败 → 修
- 如果误用 `import { Toaster } from "@/components/ui/toaster"`（Radix Toast）→ 与 Sonner toast() 不通信 → 修
- 如果给 layout.tsx 加了 `"use client"` → 改回 RSC（无必要）
</verification>

<success_criteria>
- [ ] `app/layout.tsx` import 块包含 `from "@/components/ui/sonner"`
- [ ] `app/layout.tsx` `<body>` 内含 `<Toaster />` 元素
- [ ] `npx tsc --noEmit` 过滤后 0 条新错误指向 `app/layout.tsx`
- [ ] 4 个 lockfile 在 git diff 中 0 行 diff
- [ ] Plan 03-02 的 toast 调用上线后能在屏幕显示（本 plan 单独无法跑通端到端，由 03-02 联动验证）
</success_criteria>

<output>
完成后创建 `.planning/phases/03-dialog-feedback/03-01-SUMMARY.md`，按 `$HOME/.claude/get-shit-done/templates/summary.md` 格式记录：
- 改动文件清单（1 个文件）
- import + JSX 两处具体行号
- tsc 过滤结果 + lockfile diff 结果
- 阻塞 / 非阻塞结论
- 下一步：执行 Plan 03-02
</output>
