news 2026/9/16 23:30:47

Kaneo 原型变体切换器 The Picker:从 Markup、CSS 到行为契约的完整实现规范

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kaneo 原型变体切换器 The Picker:从 Markup、CSS 到行为契约的完整实现规范

Kaneo 原型变体切换器 The Picker:从 Markup、CSS 到行为契约的完整实现规范

【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app

在 Kaneo(开源项目管理应用)的设计工程技能库中,prototype技能负责把一个 UI 片段(一个 Toast、一张定价卡、一个按住删除按钮)做成多个真正不同的版本,并放在可视化切换器背后让用户实时翻看、挑出最合适的一个。而这个"切换器"本身——PICKER.md 所定义的The Picker——是一份被刻意冻结的规格:它是一颗悬浮在页面底部中央的深色玻璃胶囊,外观不是设计决策,而是评审工具台(harness)的一部分。读完本文,你将掌握这颗 Picker 的完整 Markup、CSS 样式表、四条硬性规则、行为契约与参考接线代码,并理解它如何与 prototype 技能的 Phase 4 工作流(skills/prototype/SKILL.md)衔接,以及如何在 React 等框架中按同一行为契约做惯用化移植。

Picker 在设计工程流程中的定位

在展开代码之前,先明确它在整个技能生态里的角色。根据 skills/prototype/SKILL.md,prototype 是一个"发散(divergence)"技能:它只做一件事——把一段被描述的 UI 构建成几个真正不同的版本,放到可视化切换器后面,让用户实时翻看并选出赢家。它不做已有 UI 的评审(那是 skills/review-animations/SKILL.md)、不为已有动效制定修复计划(那是 skills/improve-animations/SKILL.md)、也不挑选依赖库(那是pick-ui-library)。

在 Phase 4「构建 Picker 工具台」中,SKILL.md 明确指示:Picker 的 Markup、样式、键盘接线和位置来自 PICKER.md,逐字照抄(verbatim)。PICKER.md 的开篇第一句话就划定了边界:

The picker's appearance isnot a design decision— it is this spec.

也就是说,Picker 的外观不是被评审的对象,它是评审工具。因此它必须跨项目保持完全一致——永远读起来像"工具台镶边(harness chrome)",而不是被评审设计的一部分。不得用项目的 token、字体或配色去重新装饰它。

一个关键的设计选择是:它是一颗**深色玻璃(dark glass)**悬浮胶囊,底部居中。深色玻璃在浅色和深色页面上都能叠得住,因此它不需要感知主题(not theme-aware)。

Markup 结构:五个元素的职责拆解

Picker 的 DOM 结构是固定的,原样照抄即可:

<nav class="proto-picker" aria-label="Prototype variants"> <span class="proto-picker-highlight" aria-hidden="true"></span> <button class="proto-picker-item">.proto-picker { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 2147483647; display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: 999px; background: rgba(10, 10, 10, 0.82); -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset, 0 8px 24px rgba(0, 0, 0, 0.24), 0 2px 6px rgba(0, 0, 0, 0.12); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 13px; line-height: 1; -webkit-font-smoothing: antialiased; user-select: none; -webkit-user-select: none; } .proto-picker-highlight { position: absolute; top: 4px; left: 0; height: 28px; border-radius: 999px; background: rgba(255, 255, 255, 0.12); will-change: transform; } /* The slide is enabled only after first paint (data-ready), so load doesn't animate. */ .proto-picker[data-ready] .proto-picker-highlight { transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1), width 250ms cubic-bezier(0.23, 1, 0.32, 1); } @media (prefers-reduced-motion: reduce) { .proto-picker[data-ready] .proto-picker-highlight { transition: none; } } .proto-picker-item { position: relative; /* sits above the highlight */ display: flex; align-items: center; height: 28px; padding: 0 12px; border: 0; border-radius: 999px; background: transparent; color: rgba(255, 255, 255, 0.55); font: inherit; cursor: pointer; transition: color 150ms ease-out; } .proto-picker-item:hover { color: rgba(255, 255, 255, 0.85); } .proto-picker-item:active { transform: scale(0.97); } .proto-picker-item:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.4); outline-offset: 2px; } .proto-picker-item[data-active] { color: #fff; } .proto-picker-divider { width: 1px; height: 16px; margin: 0 4px; background: rgba(255, 255, 255, 0.12); } .proto-picker-replay { padding: 0 10px; font-size: 14px; } .proto-picker[data-position="top"] { bottom: auto; top: 24px; }

值得逐段说明的设计意图:

  • 定位与层级position: fixed+bottom: 24px+left: 50%+translateX(-50%)实现底部居中;z-index: 2147483647是 32 位有符号整数的最大值,保证 Picker 永远悬浮在任何内容之上,不会被子元素的层叠上下文盖住。
  • 深色玻璃质感background: rgba(10, 10, 10, 0.82)backdrop-filter: blur(12px) saturate(1.4)形成磨砂玻璃;-webkit-前缀保证 Safari 兼容。三层box-shadow分别提供 1px 内描边、24px 大阴影与 6px 小阴影,塑造胶囊的悬浮立体感。
  • 字体独立性font-family使用系统字体栈,font-size: 13px,明确不继承项目字体——这正呼应了"不得用项目 tokens 重装饰"的规则。
  • 滑动高亮:高亮块position: absolute、高 28px、圆角胶囊,背景为半透明白(rgba(255,255,255,0.12))。它通过transform: translateX()width在按钮之间滑动。
  • data-ready 门控:滑动 transition仅在[data-ready]属性出现后才启用(该属性由 JS 在首次绘制后通过双重requestAnimationFrame添加)。这样页面加载时高亮块直接落在初始位置,不会从左上角"滑"进来。
  • reduced-motionprefers-reduced-motion: reduce时关闭 transition。这与仓库动画标准(skills/review-animations/STANDARDS.md)中"尊重减少动效偏好"的原则一致。
  • 按压反馈:activetransform: scale(0.97),是 skills/animate/SKILL.md 中按钮按压反馈标准的直接应用(100–160ms、scale(0.95–0.98))。
  • 无障碍焦点:focus-visible使用 2px 半透明白描边,键盘用户能看到焦点环。
  • 唯一允许的位置修改data-position="top"会把胶囊移到顶部(top: 24px),用于变体本身占据屏幕底部中央(Toast 堆栈、底部抽屉、Dock)的场景,避免 Picker 遮住被评审的工作内容。

四条硬性规则

  1. 逐字照抄(Verbatim):这些值就是规格本身。不得使用项目字体、品牌色,不做主题切换,不加额外的阴影或边框。
  2. 高亮滑动、变体切换保持瞬时:活动药丸在按钮之间以 250ms 强缓出曲线动画(作为 Picker 自身的空间反馈);但被预览的变体切换没有任何过渡。这里width的 transition 是对 transform/opacity 规则的刻意例外:该元素高 28px、绝对定位、没有布局依赖,绘制成本可忽略。
  3. 唯一允许的修改:如果某个变体占据屏幕底部中央(Toast 堆栈、底部抽屉、Dock),设置data-position="top",让 Picker 永不遮住工作内容。除此之外,任何东西都不得移动或改变。
  4. 重放按钮是条件渲染的:仅当至少一个变体有值得重新触发的入场或状态动画时才渲染重放按钮及其分隔符;纯静态对比则得到一颗更短的胶囊。

行为契约:固定不变的交互语义

无论工具台用什么框架渲染,行为契约是固定的:

  • 键盘:数字键1–N/切换变体;R重放。当焦点位于 input、textarea、select 或 contenteditable 中,或按住修饰键(meta/ctrl/alt)时,忽略键盘事件。
  • 点击:点击某项即切换到它;任何时刻恰好一项携带data-activearia-current="true",高亮块滑向它。
  • URL 持久化:选择通过 URL 参数(?v=2)在刷新后保持,缺省回退到变体 1。高亮块初始位置不带动画(data-ready在首次绘制后添加)。
  • 重挂载语义:切换会重挂载变体(因此入场动画会重新执行);按重放键只重挂载、不切换。

最后一条是整个 Picker 的核心机制:切换即重挂载(re-mount),从而让每个变体的入场动画都能真实重演——这正是"评审动效"这一用途的基础。

参考接线:JavaScript 实现详解

以下是独立 HTML 分支的逐字参考实现;在框架中保持相同行为但用惯用化表达(详见下一节):

// `variants` is an array of render functions, one per variant, in picker order. const stage = document.getElementById('stage'); const picker = document.querySelector('.proto-picker'); const highlight = picker.querySelector('.proto-picker-highlight'); const items = [...picker.querySelectorAll('.proto-picker-item:not(.proto-picker-replay)')]; const replay = picker.querySelector('.proto-picker-replay'); let current = 0; function moveHighlight() { const el = items[current]; highlight.style.width = el.offsetWidth + 'px'; highlight.style.transform = `translateX(${el.offsetLeft}px)`; } function mount(i) { stage.innerHTML = ''; // Clear first, render next frame, so entrance animations re-run. requestAnimationFrame(() => { stage.innerHTML = variants[i](); }); } function setActive(i) { if (i < 0 || i >= variants.length) return; current = i; items.forEach((el, j) => { el.toggleAttribute('data-active', j === i); if (j === i) el.setAttribute('aria-current', 'true'); else el.removeAttribute('aria-current'); }); moveHighlight(); const url = new URL(location); url.searchParams.set('v', i + 1); history.replaceState(null, '', url); mount(i); } items.forEach((el, i) => el.addEventListener('click', () => setActive(i))); replay?.addEventListener('click', () => mount(current)); window.addEventListener('resize', moveHighlight); document.addEventListener('keydown', (e) => { if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable) return; if (e.metaKey || e.ctrlKey || e.altKey) return; const num = parseInt(e.key, 10); if (num >= 1 && num <= variants.length) setActive(num - 1); else if (e.key === 'ArrowRight') setActive((current + 1) % variants.length); else if (e.key === 'ArrowLeft') setActive((current - 1 + variants.length) % variants.length); else if (e.key === 'r' || e.key === 'R') mount(current); }); setActive((parseInt(new URLSearchParams(location.search).get('v'), 10) || 1) - 1); // Enable the slide only after first paint, so load doesn't animate. requestAnimationFrame(() => requestAnimationFrame(() => picker.setAttribute('data-ready', '')));

关键实现要点:

  • moveHighlight():直接用offsetWidthoffsetLeft同步高亮块的宽与位。由于高亮是绝对定位、项目是相对定位(position: relative,CSS 注释明确说明"坐在高亮之上"),offsetLeft相对 Picker 容器计算是准确的。窗口resize时也要重算。
  • mount(i):先清空舞台,再在下一帧渲染——这就是"入场动画重新执行"的秘诀。清空后 DOM 已移除旧节点,下一帧插入新节点时浏览器会将其视为全新元素,CSS transition 与@keyframes才会从头触发。
  • setActive(i):越界保护 → 更新current→ 通过toggleAttribute维护data-activearia-current的"恰好一项"不变量 → 移动高亮 → 用history.replaceState写入?v=N(替换而非压栈,避免污染浏览器历史)→ 重挂载变体。
  • 键盘处理器:先排除 input/textarea/select/contenteditable 与修饰键,再处理数字键、方向键(带模运算回绕)与R键。这与行为契约逐条对应。
  • 初始化:从?v=读取序号(缺省 1),并执行双重requestAnimationFrame后设置data-ready,确保加载时高亮不滑动。

框架移植:保持行为契约,做惯用化表达

PICKER.md 对框架分支给出的指导是:保持相同行为,但用框架惯用法表达。具体对应关系:

独立 HTML 写法框架惯用法
stage.innerHTML = variants[i]()组件状态驱动渲染(state),而非 innerHTML 字符串拼接
requestAnimationFrame清空再渲染keyed 重挂载(如 React 中给变体根元素设置变化的key,强制 React 卸载旧子树、挂载新子树,使入场动效重跑)
手动查询offsetWidth/offsetLeft移动高亮ref + layout effect(如 React 的useLayoutEffect)在 DOM 布局后测量并设置高亮位置

需要注意保持不变的:类名proto-picker*、结构顺序、data-active/aria-current语义、data-ready门控、键盘契约、URL 持久化。任何框架适配都不得改变这些可观察行为。

与 prototype 技能工作流的衔接

在 skills/prototype/SKILL.md 的 Phase 4 中,Picker 工具的搭建分两条分支:

  • 有 dev server 的项目内:建一个隔离路由或页面(/prototypes/<slug>,或框架等价物),每个变体一个文件 + 一个小型工具台文件。任何东西都不得从原型面导入生产代码(对应 Hard Rule 1:探索期间绝不触碰生产代码)。
  • 无项目 / 纯静态场景:一个自包含的 HTML 文件(内联 CSS/JS),用户可直接在浏览器中打开。

工具台在 Picker 之外还有一个硬性要求:一次只渲染一个变体、全尺寸、放在真实的周边环境中——Toast 需要背后有一页内容,卡片需要有兄弟元素,按钮需要有一个表单。并排缩略图会扭曲间距与比例,绝不在邮票大小的尺寸上评审 UI。切换是瞬时的——翻看是 100+/会话的高频动作,按频率规则,变体切换不带任何动画。

整个工作流(Phase 1 范围界定 → Phase 2 侦察 → Phase 3 选择方向 → Phase 4 构建工具台 → Phase 5 验证与交接 → Phase 6 提升选中项)中,Picker 始终是"评审工具台"而非"参赛者";选赢家后,按 Hard Rule 5 清理原型面。技能的清单(skills-lock.json)表明这套技能源自 Emil Kowalski 的设计工程技能集,与仓库内 skills/animate/SKILL.md(动效构建)、skills/review-animations/SKILL.md(动效评审)等技能共享同一套缓动曲线、时长预算与 reduced-motion 原则——Picker 中的cubic-bezier(0.23, 1, 0.32, 1)正是仓库标准里的--ease-out强缓出曲线,scale(0.97)按压反馈与 sub-300ms 时长也都与标准表(skills/review-animations/STANDARDS.md)逐项吻合。

小结

The Picker 是一份"冻结规格":Markup、CSS、行为契约与接线代码逐字照抄,唯一可变的是变体名称、数量以及极少数场景下的data-position="top"。它刻意不感知主题、不继承项目设计 token,以恒定不变的"工具台镶边"外观保证评审的公平性;同时,它内部的每次滑动、每次重挂载、每次 URL 写入都精确服务于一个目标——让用户能在一屏之内、纯键盘或纯鼠标地,真实、公平地比较多个 UI 方向。理解它的每条规则,也就理解了 prototype 技能所追求的"发散而不失工艺"的设计工程方法论。

【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 23:29:42

RISC-V五级流水线CPU设计:从源码到仿真的完整拆解

简介&#xff1a;基于RISC-V的五级流水线CPU设计源码与项目说明包&#xff0c;面向计算机体系结构学习者、数字IC设计初学者及课程设计人员&#xff0c;提供一套可直接运行和验证的CPU完整实现。包内共有98个文件&#xff0c;涵盖27个Verilog源码文件&#xff08;包括IFU、IDU、…

作者头像 李华
网站建设 2026/9/16 23:29:29

嵌入式固件下载全链路解析:从JTAG/SWD到OTA升级

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:28:49

Python模块执行机制与__name__变量解析

1. Python模块执行机制解析在Python中&#xff0c;if __name__ "__main__"这个看似简单的条件判断语句&#xff0c;实际上承载着Python模块系统的核心设计哲学。要真正理解它的含义&#xff0c;我们需要从Python的模块加载机制说起。1.1 Python模块的双重身份每个Py…

作者头像 李华
网站建设 2026/9/16 23:28:00

Java Web新闻系统实战:JSP+Servlet+MySQL完整闭环

简介&#xff1a;本资源是一套完整的Java毕业设计项目——新闻发布及管理系统&#xff0c;面向计算机专业本科生、Java初学者及课程设计实践者&#xff0c;解决新闻内容发布、用户交互与后台管理等典型Web系统开发需求。压缩包共55个文件&#xff0c;含40张系统界面截图&#x…

作者头像 李华
网站建设 2026/9/16 23:27:50

PBR渲染中的几何遮蔽函数实现与性能对比

1. 项目概述在计算机图形学领域&#xff0c;实现真实感光照一直是核心挑战之一。PBR&#xff08;基于物理的渲染&#xff09;作为当前主流渲染技术&#xff0c;其核心在于准确模拟光线与材质表面的物理交互过程。而几何遮蔽&#xff08;Geometric Occlusion&#xff09;作为PBR…

作者头像 李华