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。
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。
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。将变体挂载到宿主页面内。
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.
两种子形态的浮动底部栏是相同的。
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/settingsroute."
"设置页面的三个变体,通过?variant=切换,在现有的/settings路由上。"
This works whether the user is here to push back or not.
无论用户是否在旁边提出异议,这个方法都有效。
Draft each variant. Hold each one to:
起草每个变体。遵循以下约束:
VariantA, VariantB, VariantC.VariantA、VariantB、VariantC。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 原型,那是墙纸。如果两个草案太相似,用明确的"不要使用卡片网格"的指导重做一个。
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> 下的可抛弃路由挂载相同的切换器。
A small fixed-position bar at the bottom-centre of the screen with three pieces:
屏幕底部中央的一个小型固定位置栏,包含三部分:
B — Sidebar layout.B — Sidebar layout。Behaviour:
行为:
router.replace on Next, navigate on React Router, etc) so the variant is shareable and reload-stable.← and → arrow keys also cycle. Don't intercept arrow keys when an <input>, <textarea>, or [contenteditable] is focused.process.env.NODE_ENV !== 'production' or an equivalent check, so a stray prototype merge can't ship the bar to users.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 所在的位置。
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 的侧边栏"——那就是他们真正想要的设计。
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:
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 分支中的变体组件和切换器会迅速腐烂,让后来的读者困惑。
<Header> is fine; a shared <Layout> defeats the point. Each variant should be free to throw out the layout.<Header> 是可以的;共享 <Layout> 就失去了意义。每个变体应该可以自由抛弃布局。