← Prototype · 技能图谱

UI Prototype

UI 原型

Generate several radically different UI variations on a single route, switchable from a floating bottom bar. The user flips between variants in the browser, picks one (or steals bits from each), then throws the rest away.

在单个路由上生成几种截然不同的 UI 变体,通过浮动底部栏切换。用户在浏览器中翻看各个变体,选一个(或从每个变体中借用一些元素),然后扔掉其余的。

If the question is about logic/state rather than what something looks like — wrong branch. Use LOGIC.md.

如果问题是关于逻辑/状态而不是外观——选错分支了。使用 LOGIC.md

When this is the right shape

何时适用

  • "What should this page look like?"
  • "I want to see a few options for this dashboard before committing."
  • "Try a different layout for the settings screen."
  • Any time the user would otherwise spend a day picking between three vague mockups in their head.
  • "这个页面应该长什么样?"
  • "我想在最终确定之前看看这个仪表盘的几个选项。"
  • "为设置页面尝试不同的布局。"
  • 任何用户可能会花一整天在脑子里从三个模糊的 mockup 中做选择的时候。

Two sub-shapes — strongly prefer sub-shape A

两种子形态——强烈推荐子形态 A

A UI prototype is much easier to judge when it's butting up against the rest of the app — real header, real sidebar, real data, real density. A throwaway route on its own is a vacuum: every variant looks fine in isolation. Default to sub-shape A whenever there's a plausible existing page to host the variants. Only reach for sub-shape B if the prototype genuinely has no nearby home.

当 UI 原型紧贴应用的其余部分时,判断起来要容易得多——真实的头部、真实的侧边栏、真实的数据、真实的密度。一个单独的可抛弃路由是一个真空:每个变体在孤立状态下看起来都不错。只要有一个可行的现有页面来承载变体,就默认使用子形态 A。只有在原型确实没有附近的归宿时才选择子形态 B。

Sub-shape A — adjustment to an existing page (preferred)

子形态 A——对现有页面的调整(推荐)

The route already exists. Variants are rendered on the same route, gated by a ?variant= URL search param. The existing data fetching, params, and auth all stay — only the rendering swaps. This is the default; pick it unless there's a specific reason not to.

路由已经存在。变体在同一条路由上渲染,通过 ?variant= URL 搜索参数控制。现有的数据获取、参数和认证都保持不变——只有渲染部分切换。这是默认方式;除非有特定原因,否则选择它。

If the prototype is for something that doesn't yet have a page but would naturally live inside one (a new section of the dashboard, a new card on the settings screen, a new step in an existing flow) — that's still sub-shape A. Mount the variants inside the host page.

如果原型针对的是还没有页面但自然应该存在于某个页面内部的东西(仪表盘的新区域、设置屏幕上的新卡片、现有流程中的新步骤)——那仍然是子形态 A。将变体挂载到宿主页面内。

Sub-shape B — a new page (last resort)

子形态 B——新页面(最后手段)

Only use this when the thing being prototyped genuinely has no existing page to live inside — e.g. an entirely new top-level surface, or a flow that can't be embedded anywhere sensible.

只有当被原型化的东西确实没有现有页面可以容纳时才使用——例如,一个全新的顶层页面,或一个无法嵌入任何合理位置的流程。

Create a throwaway route following whatever routing convention the project already uses — don't invent a new top-level structure. Name it so it's obviously a prototype (e.g. include the word prototype in the path or filename). Same ?variant= pattern.

按照项目已有的路由约定创建可抛弃路由——不要发明新的顶层结构。命名要让它明显是原型(例如在路径或文件名中包含 prototype)。使用相同的 ?variant= 模式。

Before committing to sub-shape B, sanity-check: is there really no existing page this could be embedded in? An empty route hides design problems that a populated one would expose.

在确定使用子形态 B 之前,先验证:真的没有现有页面可以嵌入吗?空路由会隐藏有内容的路由才会暴露的设计问题。

In both sub-shapes the floating bottom bar is identical.

两种子形态的浮动底部栏是相同的。

Process

流程

1. State the question and pick N

1. 陈述问题并选择数量

Default to 3 variants. More than 5 stops being radically different and starts being noise — cap there.

默认为 3 个变体。超过 5 个就不再是"截然不同"而变成噪音了——以此为上限。

Write down the plan in one line, in the prototype's location or a top-of-file comment:

用一行写下计划,放在原型位置或文件顶部注释中:

"Three variants of the settings page, switchable via ?variant=, on the existing /settings route."
"设置页面的三个变体,通过 ?variant= 切换,在现有的 /settings 路由上。"

This works whether the user is here to push back or not.

无论用户是否在旁边提出异议,这个方法都有效。

2. Generate radically different variants

2. 生成截然不同的变体

Draft each variant. Hold each one to:

起草每个变体。遵循以下约束:

  • The page's purpose and the data it has access to.
  • The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever).
  • A clear exported component name, e.g. VariantA, VariantB, VariantC.
  • 页面的目的及其可访问的数据。
  • 项目的组件库/样式系统(TailwindCSS、shadcn、MUI、纯 CSS 等)。
  • 清晰的导出组件名称,例如 VariantAVariantBVariantC

Variants must be structurally different — different layout, different information hierarchy, different primary affordance, not just different colours. Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper. If two drafts come out too similar, redo one with explicit "do not use a card grid" guidance.

变体必须是结构上不同的——不同的布局、不同的信息层次、不同的主要操作入口,而不仅仅是不同的颜色。三个稍作调整的卡片网格不是 UI 原型,那是墙纸。如果两个草案太相似,用明确的"不要使用卡片网格"的指导重做一个。

3. Wire them together

3. 将它们连接起来

Create a single switcher component on the route:

在路由上创建一个切换器组件:

// pseudo-code — adapt to the project's framework
const variant = searchParams.get('variant') ?? 'A';
return (
  <>
    {variant === 'A' && <VariantA {...data} />}
    {variant === 'B' && <VariantB {...data} />}
    {variant === 'C' && <VariantC {...data} />}
    <PrototypeSwitcher variants={['A','B','C']} current={variant} />
  </>
);
// 伪代码——适配到项目的框架
const variant = searchParams.get('variant') ?? 'A';
return (
  <>
    {variant === 'A' && <VariantA {...data} />}
    {variant === 'B' && <VariantB {...data} />}
    {variant === 'C' && <VariantC {...data} />}
    <PrototypeSwitcher variants={['A','B','C']} current={variant} />
  </>
);

For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant.

对于子形态 A(现有页面):在切换器之上保留所有现有的数据获取;只有渲染的子树按变体变化。

For sub-shape B (new page): the throwaway route under /prototype/<name> mounts the same switcher.

对于子形态 B(新页面):/prototype/<name> 下的可抛弃路由挂载相同的切换器。

4. Build the floating switcher

4. 构建浮动切换器

A small fixed-position bar at the bottom-centre of the screen with three pieces:

屏幕底部中央的一个小型固定位置栏,包含三部分:

  • Left arrow — cycles to the previous variant (wraps around).
  • Variant label — shows the current variant key and, if the variant exports a name, that name too. e.g. B — Sidebar layout.
  • Right arrow — cycles forward (wraps around).
  • 左箭头——循环到前一个变体(可循环)。
  • 变体标签——显示当前变体键,如果变体导出了名称,也显示名称。例如 B — Sidebar layout
  • 右箭头——向前循环(可循环)。

Behaviour:

行为:

  • Clicking an arrow updates the URL search param (use the framework's router — router.replace on Next, navigate on React Router, etc) so the variant is shareable and reload-stable.
  • Keyboard: and arrow keys also cycle. Don't intercept arrow keys when an <input>, <textarea>, or [contenteditable] is focused.
  • Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated.
  • Hidden in production builds — gate on process.env.NODE_ENV !== 'production' or an equivalent check, so a stray prototype merge can't ship the bar to users.
  • 点击箭头更新 URL 搜索参数(使用框架的路由器——Next 的 router.replace、React Router 的 navigate 等),使变体可共享且刷新稳定。
  • 键盘: 方向键也可循环。当 <input><textarea>[contenteditable] 获得焦点时不要拦截方向键。
  • 在视觉上与页面区分开来(例如高对比度药丸形状、细微阴影),使其明显不是被评估设计的一部分。
  • 在生产构建中隐藏——通过 process.env.NODE_ENV !== 'production' 或等效检查来控制,这样意外的原型合并不会把切换栏推送给用户。

Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project.

将切换器放在一个共享组件中,这样两种子形态都可以复用。放在项目中共享 UI 所在的位置。

5. Hand it over

5. 交出去

Surface the URL (and the ?variant= keys). The user will flip through whenever they get to it. The interesting feedback is usually "I want the header from B with the sidebar from C" — that's the actual design they want.

提供 URL(以及 ?variant= 键)。用户会在有空时翻看。有趣的反馈通常是"我想要 B 的头部和 C 的侧边栏"——那就是他们真正想要的设计。

6. Capture the answer and clean up

6. 记录答案并清理

Once a variant has won, capture the answer — which variant and why — then capture the prototype the way the [SKILL](SKILL.html) describes. Fold the winner into the real code and move the rest onto the throwaway branch, not into main:

一旦某个变体胜出,先捕获答案——哪个变体以及为什么——然后按 [SKILL](SKILL.html) 所述方式捕获原型。将胜出者合入真实代码,其余部分移到一次性分支,而不是 main:

  • Sub-shape A — fold the winner into the existing page; drop the losing variants and the switcher from main.
  • Sub-shape B — promote the winning variant to a real route; drop the throwaway route and the switcher from main.
  • 子形态 A——将胜出者合入现有页面;把落选的变体和切换器从 main 中去掉。
  • 子形态 B——将胜出的变体提升为真实路由;把可抛弃路由和切换器从 main 中去掉。

The full set of variants is the primary source, so it lands on the throwaway branch, not the bin — variant components and the switcher left in the main branch rot fast and confuse the next reader.

完整的变体集合是一手来源,所以它落在一次性分支上,而非垃圾桶——留在 main 分支中的变体组件和切换器会迅速腐烂,让后来的读者困惑。

Anti-patterns

反模式

  • Variants that differ only in colour or copy. That's a tweak, not a prototype. Real variants disagree about structure.
  • Sharing too much code between variants. A shared <Header> is fine; a shared <Layout> defeats the point. Each variant should be free to throw out the layout.
  • Wiring variants to real mutations. Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
  • Promoting the prototype directly to production. The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in.
  • 仅颜色或文案不同的变体。 那只是微调,不是原型。真正的变体在结构上有分歧。
  • 在变体之间共享太多代码。 共享 <Header> 是可以的;共享 <Layout> 就失去了意义。每个变体应该可以自由抛弃布局。
  • 将变体连接到真实的修改操作。 只读原型就很好。如果变体需要修改数据,指向一个桩——问题是"这应该长什么样",而不是"后端能否工作"。
  • 直接将原型提升到生产环境。 变体代码是在原型约束下编写的(无测试、最少错误处理)。在合入时适当重写。