lov-frontend-design
把需求、现有界面与真实素材落实为有辨识度、交互一致的前端,覆盖页面、组件、导航、GSAP 展示及图片音视频。触发:设计前端、界面不够专业、制作交互案例集;design or refine a frontend UI.
Install
npx skills add https://github.com/lovstudio/skills/tree/main/skills/frontend-design
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
git clone https://github.com/lovstudio/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.
README
界面设计师 · Interface Designer
手工川工作室维护的前端设计 Skill:把需求、真实内容和现有代码变成有辨识度、 交互一致、支持图片音视频的可用界面。
适用场景
- 页面、组件、工具栏、设置与导航设计。
- 修复图标、按钮、状态和响应式之间的交互不一致。
- 制作 GSAP 交互地图、教学展示与多媒体案例集。
- 只读审查现有界面的内容、操作与真实访问路径。
完整品牌定位与营销落地页优先使用 lov-oh-my-landingpage;性能架构、API 请求封装、
纯 CSS 清理和独立剪辑不由本 Skill 接管。
工作室版本增加了什么
| 关注点 | 具体约定 |
|---|---|
| 产品语义 | 链接、动作、状态、切换各用合适控件,同级操作保持一致 |
| 内容适配 | 根据受众、任务与素材决定风格,保留既有设计系统 |
| 图片与音视频 | 来源、加载、错误、实际播放和替代内容分别验收 |
| GSAP | 关系重排与演示,包含减少动效、清理和失败时可读路径 |
| 站点集成 | 桌面、手机、语言和身份分支一起核查,实际点击目标路由 |
| 可信证据 | 历史案例、新版本演练、静态检查与线上结果分别记录 |
工作室是维护者,不是所有作品的默认品牌。用户可以明确要求专题自由选风格。
这是对本地 frontend-design 0.2.0 方法的独立整理,来源说明见
Provenance。旧版安装保留,新版使用独立 ID。
安装
官网安装命令:
npx lovstudio skills add frontend-design
需要手动管理源码时,将源码保存在自己的 Skill 源目录,设置 SKILL_SOURCE
为该目录、SKILL_INSTALL_DIR 为共享 Skill 安装目录后执行:
mkdir -p "$SKILL_INSTALL_DIR"
ln -s "$SKILL_SOURCE" "$SKILL_INSTALL_DIR/lov-frontend-design"
安装目标必须空闲,不能覆盖旧文件。宿主适配入口使用相对链接指向共享安装位。 也可以通过统一目录指定 Skill ID 安装:
npx skills add lovstudio/skills --skill lov-frontend-design -g -y
使用
“用 lov-frontend-design 把这些真实案例做成适合课堂展示的交互页面,支持图片、音频和 视频,并用 GSAP 解释能力分布。风格服从内容。”
“用 lov-frontend-design 只读检查官网资源入口,验证电脑和手机上的标签、键盘与跳转。”
“Design this settings toolbar with consistent control semantics and accessible states.”
Profile 与依赖
每次读取 skill.yaml 的 user-profile/v1。品牌、语言、项目路径与偏好从
当前请求、项目和共享 Profile 解析;不写死维护者环境。只有明确长期偏好才持久保存。
python3 scripts/profile_store.py read --skill-id lov-frontend-design --pretty
可见文案依赖 lov-branding-consistency。主体为 instruction-first,不绑定前端框架。
使用项目已有工具链;运行验收需浏览器工具,Profile/Skill 校验需 Python 3.8+ 与 PyYAML。
GSAP 只在任务需要时使用;安装依赖不是此 Skill 固定副作用。
验证与案例
python3 scripts/validate_skill.py .
完整验收见 Acceptance。 真实案例 收录能力地图与官网入口,并区分旧任务证据和新版本复核。 Skill Card 提供能力维度与限制;定价卡 记录免费依据。
本 Skill 免费提供源码与方法,媒体生成和运行工具的成本另计。 版本与安装入口以 官网目录 和 源码 Release 为准。 渠道发布记录与发行定价卡由 Publisher 在源码之外管理。
Skill manifest
界面设计师 · Interface Designer
把内容、视觉和交互落实为可用的界面。目标受众应能看懂、能操作、能完成任务,同时 获得与内容相称的审美体验。直接实现修改请求;用户明确要求审查时保持只读。
这是手工川工作室维护的通用前端设计方法。维护者身份不决定作品品牌:优先遵守当前 请求与项目设计系统;独立专题可以采用适合内容的视觉方向,不把工作室配色、Logo、 字体或个人路径写成所有用户的默认值。
Triggers
Activate when
- “用手工川工作室版本设计这个前端,直接实现。”
- “这个工具栏能用,但按钮、图标和状态不像一个专业产品。”
- “把真实案例做成支持图片、音频和视频的交互展示,使用 GSAP。”
- “为现有官网增加资源入口,并验证桌面和手机的访问路径。”
- “Design or refine this frontend UI with the existing product conventions.”
- “Audit this interactive case collection without changing the site.”
Do not activate when
- 只创建或安装 Skill,或只发布已完成页面:使用相应创建、发布能力。
- 核心任务是品牌定位、商业叙事和营销转化:优先已有 Landing Page 能力。
- 只调后端并发、启动架构、请求封装、CSS 清理或独立媒体剪辑:交给对应能力。
Runtime context
每次读取 skill.yaml 和 Profile contract。从当前请求、
项目现状、Skill records、共享 preferences、brand/user Profile、安全默认值解析上下文。
环境变量可指定 Profile 来源;显式请求始终优先。仅消费 Manifest 声明的字段。
python3 scripts/profile_store.py read --skill-id lov-frontend-design --pretty
只有用户明确表达跨任务偏好时才用 profile_store.py record --confirm 保存,随后报告
保存路径。单次“无需遵循品牌限制”留在当次上下文,不推断成永久品牌偏好。
秘密、素材原件和个人绝对路径不得写入可分发 Skill。
References by need
- 所有任务:Interaction contract。
- 新视觉方向或整体重设计:Art direction。
- 动态图表、GSAP、图片、音频、视频或演示模式:Motion and media。
- 交付前:Acceptance。
- 选择相邻能力时:Skill composition。
- 来源与历史案例:Provenance。
Workflow
1. 找到真实界面与验收路径
读取项目规则、Git 状态、目标路由、相邻页面、共享组件和已有数据来源,保留其他工作。 区分产品小改、完整界面设计和独立展示。新增导航不必重做品牌策略;全新展示需要先 理解受众和内容结构。工作量随范围变化。
内部明确受众、打开时已知信息、主要任务、核心内容、输入输出、技术限制、风格约束、 资产真伪与最终访问路径。从上下文能确认的直接决定,只问会改变结果的缺口。 使用宿主真实工具,不假定拥有浏览器、媒体生成或部署能力。
2. 设计理解顺序与操作语义
确定第一屏让人理解什么,主体如何展开证据,最后如何继续行动。长内容采用逐层 展开、检索、筛选、索引或具体案例,不把章节机械排成同尺寸卡片。
按 Interaction contract 区分导航、动作、状态和切换。
主要控件有明确输入、状态、结果和失败后的下一步,复用现有组件与状态模型。
调用 lov-branding-consistency 审校标题、按钮、说明和 Caption,保留来源文本与数据。
需要长篇正文时接受经确认的稿件,不把前端设计自动扩展成研究或文章创作。
3. 按情境确定视觉方向
已有产品沿用 token、组件、字体和布局习惯,把新意放在内容组织与细节上。 新作品用一句话说明这个方向为什么适合内容与受众,再确定排版、密度、色彩、素材 处理和一项可记忆的视觉特征。装饰不能争夺图表、证据与主要动作的注意力。
标题、正文、数据、注释承担不同排版角色;检查中文断行、数字、长英文和真实宽度。 字体选择服从可读性、授权、加载与项目现状,不因反模板化而全局替换字体。
4. 实现完整用户路径
使用现有技术栈和依赖。独立静态展示可采用 HTML/CSS/JS,不为样式增加应用框架。 相同动作组重复出现时复用公共实现。数据来源真实,快照标注范围与日期,不把历史 样本数量冒充当前全部能力。
涉及媒体时完整读取 Motion and media。GSAP 服务于 关系变化、叙事推进与演示,不作为基本内容可见性的唯一开关。主要操作都有真实结果。
集成站点同时检查桌面导航、移动端、身份分支、语言与目标路由。独立 HTML、下载和 浏览器目的地采用合适的原生链接,避免客户端路由误接管。仅在当前授权范围内部署, 设计完成和线上发布分别验收。
5. 在真实界面检查并修复
按 Acceptance 覆盖本次涉及的状态与操作,以真实输入走完 主要路径。先解决内容、交互和可访问性,再调整动效与细节;检查最窄支持宽度和常用 桌面。浏览器不可用时报告静态结果与运行待验项,不声称点击、播放或辅助技术已通过。
保存必要截图、测量、检查命令或可回读产物。只读审查列出核实的问题及影响;修改 请求落实修复并复验。证据不完整时标记具体缺口,不写虚构评分。
6. 简洁交付
给出实际页面或文件、主要变化、已验证路径与剩余限制。页面不展示内部 brief、设计 自评、Agent 操作记录或“专业制作”等自我说明。可复用经验更新最窄参考与真实案例, 不把单个作品的风格升级成普遍规则。
Output contract
- 页面或组件的项目原生代码,以及必要的真实素材引用。
- 明确的内容层级、控件文案和一致的操作状态。
- 媒体文件或经核实的地址、用途与来源记录。
- 相称的构建检查、浏览器验证与未验证项。
- 准确区分本地与线上状态;Skill 公开分发交给发布能力。
Files (skills)
-
cases
-
evidence
-
atlas-historical.json 976 B
{ "url": "https://lovstudio.ai/skill-atlas", "errors": [], "failedRequests": [], "checks": [ "HTTP 200; correct source commit; 244 entries; 6 cases", "GSAP layout switch and category highlighting", "CDN image gallery and full-resolution event poster", "Original and synthesized audio playback", "Full video playback and seek through Release Range requests", "Index search and classroom presentation controls", "390 px mobile layout without horizontal page overflow", "Visible entry on official Skills page" ], "sourceCommit": "83e41a909e9e4be98f248117b598ccd12466bb2a", "originalAudio": { "duration": 28.905333, "time": 1.892606, "error": null }, "cloneAudio": { "duration": 28.9, "time": 1.434295, "error": null }, "video": { "duration": 358.549333, "time": 121.976749, "width": 1280, "height": 720, "error": null }, "csp": [], "completedAt": "2026-09-06T02:39:55.367Z" } -
local-acceptance.md 1.7 KB
# 本地验收记录 日期:2026-09-06。目标版本:lov-frontend-design 0.3.0。 - 结构:标准 YAML、允许的 frontmatter、版本、必需卡片、引用与源码卫生通过 Creator 校验。 - 安装:共享 canonical 入口指向本 Skill 真源,Codex 与 Claude Code 通过相对链接指向共享入口。 - Profile:在隔离临时 Profile 验证读取、写入、其他 namespace 保留、revision 递增及秘密字段拒绝;未写入真实用户 Profile。 - 触发演练:按 README 的中文只读导航检查指令,实际完成桌面四语、键盘和 390/320 px 手机入口检查,结果见 navigation-replay.json。 - 非触发审查:“只发布已完成页面”归发布流程;“只优化后端并发”归性能架构;“创建 Skill”归 Creator。此项为路由契约人工核查,未声称宿主自动选择器已跑测试。 - 历史材料:两份先前线上记录作为历史案例,未冒充新版本生成成果。 - 验收时序:初次检查在界面尚未稳定时遇到焦点恢复异步和菜单重新挂载。等待真实焦点状态及页面加载稳定后重新检查;最终六个入口场景均无需重开菜单。 - 覆盖限制:没有已登录账户、屏幕阅读器或非 Chromium 测试;本次没有重跑媒体播放、离线或全部减少动效场景。历史媒体证据单独归档。 - 只读发现:320×740 视口下,长菜单的登录入口底部超过视口,overflow-y 为 hidden,滚轮后仍未完整露出;能力地图入口本身可用,未修改网站。见 narrow-menu-finding.json。 这份记录不证明任意新前端都能自动达到同样结果。每次交付仍需按具体界面实际验收。 -
narrow-menu-finding.json 823 B
{ "checkedAt": "2026-09-06T06:46:29.379Z", "url": "https://lovstudio.ai/design", "viewport": { "width": 320, "height": 740 }, "loginBefore": { "x": 93.2672119140625, "y": 711.4755249023438, "width": 205.465576171875, "height": 30.72381591796875 }, "loginAfter": { "x": 89.79358673095703, "y": 726.554931640625, "width": 212.41283416748047, "height": 31.7626953125 }, "menu": { "overflowY": "hidden", "clientHeight": 681, "scrollHeight": 681, "scrollTop": 0 }, "loginBottomAfter": 758.317626953125, "entryVerifiedSeparately": true, "status": "open-finding", "observation": "The long mobile menu places Login below the 740 px viewport; a wheel gesture inside the menu did not expose it.", "scope": "Read-only evidence; no website edits." } -
navigation-historical.json 648 B
{ "checkedAt": "2026-09-06T02:53:29.360Z", "results": [ { "viewport": { "width": 1440, "height": 1000 }, "label": "Skills 能力地图与案例集", "menu": "资源", "href": "/skill-atlas", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244, "labelFits": true }, { "viewport": { "width": 390, "height": 844 }, "label": "Skills 能力地图与案例集", "menu": "资源", "href": "/skill-atlas", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244, "labelFits": true } ] } -
navigation-replay.json 2.7 KB
{ "skill": "lov-frontend-design", "version": "0.3.0", "mode": "read-only runtime replay", "activation": "用 lov-frontend-design 只读检查官网资源入口,验证电脑和手机上的标签、键盘与跳转。", "checkedAt": "2026-09-06T06:44:29.202Z", "records": [ { "locale": "zh-CN", "viewport": { "width": 1440, "height": 1000 }, "label": "Skills 能力地图与案例集", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "keyboard; Escape restores focus", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 }, { "locale": "en", "viewport": { "width": 1440, "height": 1000 }, "label": "Skills Atlas & Case Studies", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "mouse", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 }, { "locale": "ja", "viewport": { "width": 1440, "height": 1000 }, "label": "Skills 能力マップと事例集", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "mouse", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 }, { "locale": "th", "viewport": { "width": 1440, "height": 1000 }, "label": "แผนที่ Skills และกรณีศึกษา", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "mouse", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 }, { "locale": "zh-CN", "viewport": { "width": 390, "height": 844 }, "label": "Skills 能力地图与案例集", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "touch", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 }, { "locale": "zh-CN", "viewport": { "width": 320, "height": 740 }, "label": "Skills 能力地图与案例集", "href": "/skill-atlas", "labelFits": true, "menuReopened": false, "activation": "touch", "finalUrl": "https://lovstudio.ai/skill-atlas", "mapNodes": 244 } ], "limitations": [ "Authenticated navigation branches inspected in source only; no signed-in runtime test.", "No screen-reader or non-Chromium browser test.", "Media playback is covered by separately archived historical evidence, not this navigation replay." ] } -
resources-1440.png 563.7 KB · in bundle
-
resources-320.png 72.8 KB · in bundle
-
resources-390.png 120.5 KB · in bundle
-
-
cases.json 4.7 KB
[ { "type": "case", "id": "skill-atlas-exhibit", "title": "Skill 能力地图与多媒体案例集", "description": "面向学员同步 Skill 探索的真实展示,采用能力分布、时间线与案例逐层展开。作品早于本 Skill 创建,本条为历史案例归档。", "input": { "items": [ "真实 Skill 清单、先前能力分布分析与一年探索材料", "用户要求适合展示、清晰详实,并支持图片、音频、视频", "用户允许该专题不遵循手工川品牌设计约束" ] }, "prompt": "基于以上分析,以及过去一年的聊天记录进行深挖,结合 gsap,制作适合展示、清晰详实的《手工川 Skill 能力地图与案例集》,风格视内容而选择,重点支持图片、音频、视频。", "prompt_fidelity": "由原始请求压缩的最小 brief,非逐字转录。", "output": { "items": [ "公开的能力地图与案例集", "244 个地图节点、6 个案例和可检索索引", "GSAP 布局切换、图片放大、原声与合成声音对比、视频播放及课堂演示" ], "url": "https://lovstudio.ai/skill-atlas" }, "evidence": { "kind": "historical-task-before-skill-creation", "record": "cases/evidence/atlas-historical.json", "source_commit": "83e41a909e9e4be98f248117b598ccd12466bb2a", "date": "2026-09-06", "limitations": [ "不声称该作品由 lov-frontend-design 0.3.0 新生成。", "归档运行记录不证明所有可访问性、字幕与离线要求均已通过。" ] } }, { "type": "case", "id": "resources-navigation", "title": "官网资源菜单中的能力地图入口", "description": "为已上线专题补齐真实导航入口,覆盖桌面与移动端。原始实现属于历史任务,新版本复核独立记录。", "input": { "items": [ "已上线的 /skill-atlas 专题", "官网桌面资源菜单和两种移动端身份分支", "简体中文、英文、日文、泰文四种现有语言" ] }, "prompt": "应该在官网增加入口: 导航 - 资源 - Skills 能力地图与案例集", "output": { "items": [ "桌面资源菜单及移动端菜单增加原生链接", "中文标签为 Skills 能力地图与案例集,补齐四种翻译", "验证 1440 px 桌面与 390 px 手机点击后加载 244 个地图节点" ] }, "evidence": { "kind": "historical-task-before-skill-creation", "record": "cases/evidence/navigation-historical.json", "website_commit": "2a003a934580aaee5e9c58f5a32131ad70fe665a", "date": "2026-09-06", "limitations": [ "已登录移动端只核对源码,未用真实登录账户执行。" ] } }, { "type": "case", "id": "frontend-design-030-replay", "title": "工作室版本 0.3.0 的只读导航演练", "description": "新 Skill 按自身的交互与验收约定复核已部署页面,实际执行键盘、鼠标和触控。本次不修改网站。", "input": { "items": [ "实际官网设计页的资源导航及 /skill-atlas", "四种现有语言和 1440、390、320 px 宽度", "本 Skill 的 interaction 与 acceptance 参考文档" ] }, "prompt": "用 lov-frontend-design 只读检查官网资源入口,验证电脑和手机上的标签、键盘与跳转。", "output": { "items": [ "四种语言的桌面菜单均显示完整标签并到达能力地图", "中文桌面 Escape 关闭后焦点恢复,Enter 可打开并选择入口", "390 px 和 320 px 手机触控到达能力地图,各加载 244 个节点", "标明未登录分支为运行验证,已登录分支为源码检查;未进行跨浏览器或屏幕阅读器测试", "发现 320×740 视口下长菜单的登录条目底部超出视口,菜单 overflow-y 为 hidden,滚轮未使其完整可见;本次只读,未改官网。" ] }, "gallery": [ "cases/evidence/resources-1440.png", "cases/evidence/resources-390.png", "cases/evidence/resources-320.png" ], "evidence": { "kind": "new-skill-read-only-runtime-replay", "record": "cases/evidence/navigation-replay.json", "skill_version": "0.3.0", "checked_at": "2026-09-06T06:44:29.202Z", "limitations": [ "Authenticated navigation branches inspected in source only; no signed-in runtime test.", "No screen-reader or non-Chromium browser test.", "Media playback is covered by separately archived historical evidence, not this navigation replay." ], "open_finding": "cases/evidence/narrow-menu-finding.json" } } ]
-
-
references
-
acceptance.md 2.6 KB
# Acceptance 检查与修改相称。静态 Skill 校验不等于前端能力通过完整产品验收。 ## 内容与实现 仅看最终界面,受众能否理解内容、价值和下一步?来源、图例、证据和控件各尽其职吗? 没参加聊天的人能否理解所有指代?装饰是否遮挡真实内容? 运行项目适当的 lint、类型检查、测试或构建,不为微小文案改动建测试框架。核对资源、 路由、数据、布局和依赖,保留无关工作。先修复原因再复测,不通过放宽断言掩盖失败。 ## 真实交互 使用已有实例、线上页面或允许的本地文件。没有实例且项目禁止自行启动时保留静态 结果和具体运行待验项。优先专用浏览器或 headless 自动化,不抢占用户前台。 - 实际打开菜单、选择条目并核对最终 URL 与内容,不只匹配 href。 - 键盘覆盖本次适用的 Tab、Enter、Space、Escape、方向键与焦点恢复,检查弹层滚动。 - 输入、切换、加载、失败、禁用与选中按范围验证。 - 检查常用桌面 1280–1440 px、约 390 px 手机及最窄支持容器;无产品约定时用 320 px 作为窄屏检查点,保存实际断行、溢出和可操作性证据。 - 新导航核对全部现有语言与身份分支。无测试账户时只读检查登录分支,明确静态 验证范围,不伪装身份制造通过结果。 - 折叠可改变展示位置,不能改变含义。长菜单需要确认滚动和底部入口可达。 ## 动效与媒体 仅在涉及时检查连续切换、减少动态效果、资源失败、图片放大、音频时间增长、视频 首帧与拖动,必要时核实线上 Range/206。转录、字幕和来源另行回读,结构不代表准确。 ## 证据等级 | 状态 | 可声称的结果 | | --- | --- | | 静态检查 | 代码、声明或资源可解析 | | 构建通过 | 对应工具编译或检查成功 | | 运行已验 | 指定宽度、状态与实际操作通过 | | 线上已验 | 指定生产 URL 与版本上的路径通过 | | 历史案例 | 可追溯的真实旧任务,不证明新版本重新完成 | | 未验证 | 明确缺口,不能并入全部通过 | 证据带日期、环境、输入、结果与限制,不提供无测量依据的综合分。新 Skill 可以复核 已有页面,但不能声称创造了历史产物;归档证据与新一轮运行分别记录。 ## 交付 给出真实产物、主要变化、已做检查和限制。部署是独立状态,未收到发布请求时完成 本地结果,不把写入官网或远程仓库当作默认设计步骤。 -
art-direction.md 2.5 KB
# Art direction 从内容推导风格。以下是判断方法,不是每个作品都要填满的模板。 | 场景 | 首要约束 | 创作空间 | | --- | --- | --- | | 已有业务产品 | 操作习惯、设计系统、信息密度 | 层级、反馈、布局与数据呈现 | | 新产品界面 | 核心任务、频率与设备 | 建立 token 与统一控件语法 | | 展示、教学或案例集 | 理解顺序、证据与讲解场景 | 编辑排版、图形关系、叙事与媒体节奏 | 用户允许跳出品牌限制时放开作品风格,仍保持来源、语义、可读性与可访问性。 工作室维护者身份不意味着每个作品都使用相同配色或人物形象。 ## 把审美要求变成具体行为 在内部说明谁在什么场景使用、最先理解什么、内容多密、真实素材适合如何展示,以及 哪项视觉特征帮助理解。完整新作可比较几个合适方向;局部修改沿用现有方向即可。 - 惊艳:一项解释内容关系的视觉主张,例如同一批节点随维度切换重新编组。 - 清晰:入口、图例、单位、来源、交互提示和主要结论能独立理解。 - 详实:有层级的事实与案例、检索和展开,不把所有材料堆到首屏。 ## 排版与空间 标题、正文、数据、注释分别设计字号、行距、字重与宽度,复用少量明确层级。 检查真实中文长标题、混排 Skill ID、数字和标点;使用合适中文字体与回退。 不因字体流行而禁止使用,也不为独特性全站换字。已有字体属于产品一致性。 正文宽度适合阅读,数据区可提高密度,但保持标签、单位与留白。非对称、错位、大字、 纹理与光晕都应帮助内容。图表先确定数据语义,相同对象跨视图保持身份连续,不用 装饰性坐标、无依据数字或伪评分制造研究感。 ## 色彩与素材 使用项目 semantic token 或独立作品的 CSS 变量。正文、次要信息、交互、选中与错误 有稳定颜色角色;分类颜色附文字或形状,深浅主题检查实际对比。 真实截图、作品与案例输出优先。统一容器时不裁掉代码、图表或作品关键内容。 Logo 使用确认过的原件,生成图像不能冒充案例证据。 ## 去模板化 移走渐变、大标题和卡片边框后,界面还具有自己的内容结构吗?所有章节是否变成 同一个网格、所有强调是否同样响亮?若是,先重新安排内容再调整样式。 目标是形式与任务有因果关系,不是每次刻意换风格。 -
interaction.md 2.6 KB
# Interaction contract 先确认相邻界面约定,再选控件。一个控制元素必须有稳定的主要意图。 | 意图 | 控件语义 | 验收重点 | | --- | --- | --- | | 前往页面、文件或站点 | 有真实 href 的链接 | 目标、返回、键盘激活与浏览器行为 | | 保存、生成、复制等动作 | button | 真实结果,清楚的等待与失败反馈 | | 状态、环境或版本 | 文本、Badge | 不伪装成可点击按钮 | | 固定、静音等持久开关 | switch 或 aria-pressed button | 状态持续可见,不能只靠 Toast | | 切换内容区域 | tabs 或选择控件 | 焦点、选中项与内容关系 | | 危险操作 | 明确对象与后果的动作 | 遵守授权、确认与恢复约定 | ## 动作组与状态 - 同级操作有相同高度、命名方式和视觉权重。紧凑且图标明确时可统一图标按钮;含义 模糊时使用文字。不要让某个同级动作随屏宽独自变换控件语法。 - 图标按钮必须有 accessible name;Tooltip 支持鼠标与键盘,但不能成为手机端理解 操作的唯一方式。导航、状态、普通动作、开关与删除操作合理分组。 - 验证实际存在的 default、hover、focus-visible、pressed、loading、disabled 与错误 状态;不为静态链接虚构危险或加载测试。弹层关闭后焦点回到触发点。 - 重复动作组复用公共交互和可访问性实现,不把所有控件强行抽象成同一个组件。 ## 输入与弹层 保留值、标签、错误关联、提交和撤销语义。中文 composition 期间不把 Enter 误判为 提交。远程检索处理取消与过期响应,后台刷新不让已有结果突然归零。 Dialog、Popover、Portal 组合用真实滚轮与键盘验证,不能只检查 overflow。 ## 导航与展示 - 菜单说明所在类别,条目说明目的地。新资源覆盖桌面、移动端、适用身份分支和语言。 长标签可换行或调整容器,不靠难读的小字号解决。 - 应用路由沿用路由器;独立 HTML、下载与外部 URL 保留原生链接。 - 实际打开菜单、选择条目并核对最终内容。截图里存在链接不证明点击可用。初次 hydration 或身份加载造成菜单重建时记录时序,复测稳定界面,不掩盖持续失败。 - 图形地图有可搜索或遍历的文本路径。hover 提示有触摸、点击或焦点入口,重要信息 不只依赖颜色、悬停或运动。 视觉更新不能破坏表单和路由契约,不用无 href 的 div 模拟链接,不把主要动作藏在 仅 hover 可见的区域,也不在手机端改变动作结果。 -
motion-and-media.md 3.7 KB
# Motion and media 动效解释变化,媒体承载证据。先保证静态内容可读、主要路径可用,再增强表现。 ## GSAP 用户指定 GSAP 时使用项目实际版本;否则先考虑已有工具与 CSS。需要新 API 时查当前 官方文档,不凭记忆假定插件可用,不为简单 hover 增加依赖。 - 适合关系重排、分阶段揭示、可控时间线与演示导航。动画反映应用状态,不以回调 作为唯一状态来源;快速切换会更新或终止旧动画,避免叠加竞争。 - timeline/context 有明确作用域;卸载清理动画、ScrollTrigger、监听器与资源。 - 媒体、字体或容器变尺寸后按需更新尺寸与滚动计算,避免无界刷新。 - 优先合理使用 transform、opacity。节点跨布局保留标识,图形有可达文本路径。 - prefers-reduced-motion 下直接切换终态,关闭非必要循环与视差。内容和控制仍 可用,JS/CDN 失败时不能整页停在 opacity: 0。 - 不劫持普通滚动,不把读完动画作为访问条件;演示模式可以随时退出。 2026-09-06 核对的官方参考:作用域和清理见 [gsap.context](https://gsap.com/docs/v3/GSAP/gsap.context%28%29/),响应式与减少动效处理见 [gsap.matchMedia](https://gsap.com/docs/v3/GSAP/gsap.matchMedia%28%29/)。实际实现仍核对项目安装版本。 ## 媒体清单 按实际文件记录角色、相对路径或已核实 URL、来源、真实或合成属性、格式、尺寸或 时长、适用的 poster/alt/caption/transcript、权利备注与读取结果。 清单是制作记录,只把帮助观众理解或完成归属的部分放入页面。 聊天、人物声音和项目材料遵循明确公开范围;不把全部聊天记录、令牌、临时签名、 私人路径或原始数据库带进页面。发布授权只覆盖明确目标与载荷。 ## 图片 - 确定是证据、解释还是装饰,据此写 alt;装饰图使用空 alt。 - 明确尺寸或 aspect-ratio,按视口选分辨率,延迟加载非首屏图片。 - 放大层支持键盘、关闭与焦点恢复,完整高分辨率作品可达。 - 读取失败有替代信息,离线文件使用可解析的相对路径。 ## 音频 - 使用原生 controls 或同等完整的自定义控件,波形出现不等于可播放。 - 真实声音与合成声音分别标注,对比时明确选择,避免同时发声。 - 验证播放时间增长、暂停、结束和错误,不能只检查文件存在或 HTTP 200。 - 语音提供可靠文字等价信息;缺少转录时标明,不编造逐字稿。 - 不自动有声播放,切换案例或退出演示按约定暂停并释放资源。 ## 视频 - 配置可靠 poster、controls、移动端 playsinline,不默认预载全部大视频。 - 验证首帧、画面尺寸、真实播放、暂停与跳转;部署后检查大文件读取与 Range/206, 定位 MIME、重定向、CSP 或 CORS 的实际失败。 - 按内容提供字幕或文字替代。横竖视频保留比例,手机上操作可达。 - 第三方播放器检查隐私、键盘与失败回退,错误提供实际可行的下一步。 ## 展示与分发 投屏使用足够字号、当前位置和手动推进;快捷键不占用表单及中文组合输入。全屏需 用户手势,拒绝全屏时普通布局仍可演示,退出可发现。切换案例停止上一个案例的声音, 保留普通阅读与检索路径。 按请求选择在线或离线。包内包含实际引用文件与必要许可信息,下载和分发各自遵守 来源授权;不能下载远端媒体绕过宿主显示限制。线上验最终域名播放与跳转,离线验 断网可读与外部资源缺失表现。未做的跨浏览器、字幕、辅助技术检查明确标记。 -
provenance.md 1.2 KB
# Provenance ## 来源与版本 2026-09-06 核查时,本地 `frontend-design` 已标注作者 lovstudio、版本 0.2.0,Changelog 记录 2026-07-18 增加控件语义、动作组一致性与交互验收。其安装没有独立工作室真源、 Profile、案例和分发卡片。 本包 0.3.0 以独立编写的工作流承接方法,加入内容适配、GSAP、真实媒体、导航集成与 可追溯验收。原包声明的 LICENSE.txt 在所检查安装目录缺失;本包不复制其全文,也 不把该许可声明当作已核验授权。新编写指令与配套文件使用本包 LICENSE,Creator 生成工具保留许可归属。旧包正文、安装和发布渠道均保留。 ## 案例诚实性 能力地图与官网入口在本 Skill 创建前已经完成,是方法归纳的真实历史材料,不是 0.3.0 的新生成成果。归档证据有日期,本版本重新演练单独保存。示例站点和第三方 作品的权利不因案例记录而改变,本包 MIT 不覆盖这些素材。 ## 可移植性 工作室名称说明维护来源。运行时从项目、请求或 Profile 获取品牌信息。某个专题 无需遵循手工川品牌限制是当次要求,不自动变成所有用户的永久偏好。 -
skill-card-standard.md 660 B
# LovStudio Skill Card standard `skill-card.yaml` follows the minimum release-record idea of NVIDIA Skill Cards: description, owner, license/terms, use case, deployment, requirements, risks/mitigations, references, output contract, version, and ethical considerations. LovStudio adds evidence that helps a user decide whether the Skill is credible: 1. A real user case with Input → Prompt → Output. 2. A dimension map with named evidence, not an unexplained score. 3. A pricing basis, including the free boundary and review trigger. 4. Explicit paid and free distribution states. Never claim a case, score, channel, or price that has not been verified. -
skill-composition.md 2.4 KB
# Skill Group Composition ## Nearby Skills Inspected 2026-09-06 根据实际入口的输入、输出与非触发条件核查。 | Skill | 分类 | 实际边界 | | --- | --- | --- | | frontend-design 0.2.0 | overlap | 已有视觉与产品交互方法,工作室整理版承接这项结果 | | lov-frontend-design | core atom | 需求、代码与素材进入,输出可用前端及验收 | | lov-branding-consistency | upstream atom | 可见文案的语境门禁,唯一声明的外部文案依赖 | | lov-oh-my-landingpage | overlap | 品牌定位、商业叙事与转化为主时直接使用它 | | lov-app-professional-design | not composed | 启动、缓存、数据桥与并发性能架构,排除仅视觉改动 | | lov-better-css | not composed | CSS/Tailwind 清理,不能代替内容与交互设计 | | lov-frontend | not composed | uni-app 网关请求封装,不是通用设计 | ## Atomic Handoffs 1. `lov-branding-consistency` 接收文案、受众与品牌上下文,交还文案或删除决策;它负责 语境,本 Skill 负责落入组件后的排版、状态与交互。 2. Landing Page 已完成策略时可交接确认过的叙事、资产与约束。只有另有具体组件任务 才使用本 Skill,不为一个完整落地页重复运行两套总控。 3. 核实的图像、音视频与正文以文件、元数据、使用范围交接。媒体制作与研究不成为 隐藏 sibling 依赖,缺失能力按宿主解决或明确缺口。 4. 用户要求发布时交接源码、依赖、资产与验收给项目发布流程;渠道结果由发布流程 验收,不能从本地构建推断已上线。 ## Overlap Decisions 整理现有 `frontend-design` 方法,采用 `lov-frontend-design` ID、独立真源与 Profile。 保留旧安装;需要工作室维护版时明确选择新 ID。新包不依赖旧目录,不声称其他项目 官方发行。来源和许可边界见 `references/provenance.md`。 不扩展性能架构 Skill 或 uni-app 网关模块,二者名字相近但输出不同。完整品牌转化链 仍使用原 Landing Page Kit,局部产品界面与教学展示由本 Skill 负责。 ## Composition Decision Single Skill,instruction-first。语义、视觉、媒体与验收共同完成一个前端结果,按需 参考足够,不拆独立安装阶段。Profile 与校验脚本是确定性配套工具,不据此拆 Kit。 除可见文案门禁外不声明外部 sibling 必需依赖。 -
user-profile.md 2.7 KB
# User Profile contract Every Skill created by Skill Creator declares `user-profile/v1` in `skill.yaml`. The contract connects independent sessions to one user-owned JSON Profile while keeping the Skill source portable across users and brands. ## Shared shape The host supplies the Profile through `SKILL_PROFILE_PATH` (or the runtime's configured profile path). The stable shared scopes are: - `user`: user identity, language, timezone, and other personal working defaults. - `brand`: public brand facts, site, logo, tone, profile, and design guidance. - `workspace`: project roots and output locations. - `preferences`: shared preference values when the host stores them in the Profile. - `skills.<skill_id>.profile`: Skill-specific defaults. - `skills.<skill_id>.records`: durable decisions and preferences learned from direct user statements for this Skill. The Profile may also use the runtime's canonical `identity` fields. Manifest field aliases bridge `identity.*` and the portable `user.*` / `brand.*` names. ## Read on every run 1. Read the current request and project context. 2. Read the shared Profile and the `skills.<skill_id>` namespace. 3. Resolve values in this order: current request, project context, Skill records, shared preferences, shared user/brand Profile, safe defaults. 4. Keep `profile_scope` and field provenance available for the final result. Do not copy resolved personal paths, brand values, or private records into the committed Skill source. ## Persist directly stated values When the user explicitly gives a value meant to survive later sessions, save it immediately after the user statement and report the canonical path: ```bash python3 scripts/profile_store.py record \ --skill-id lov-example \ --path records.subtitle_level \ --value '"cet4"' \ --confirm ``` For shared facts, use `--path brand.<field>` or `--path user.<field>`. The script writes JSON atomically, preserves unrelated Profile data, increments a numeric Profile revision when present, and never echoes the stored value. Inferred information, credentials, tokens, cookies, and secret-like fields stay out of durable records. If the user has not stated that a value should persist, keep it in the current request context. ## Read the connected context ```bash python3 scripts/profile_store.py read \ --skill-id lov-example \ --pretty ``` The result contains `user`, `brand`, `workspace`, `preferences`, `skill`, and `records` scopes. A host using `skill-runtime/v1` also returns the same binding as `profile_scope` and `profile_contract`. ## Compatibility `--user-config` remains accepted by the Creator as a compatibility flag for old invocations. The Profile contract is now always generated; users do not choose an initialization mode.
-
-
scripts
-
profile_store.py 8.3 KB
#!/usr/bin/env python3 """Read and persist a Skill's user-owned profile context. The command keeps shared identity and brand facts in the profile root while putting Skill-specific durable records under ``skills.<skill_id>.records``. Writes require an explicit confirmation flag and use an atomic replacement. """ from __future__ import annotations import argparse import json import os import re import sys import tempfile from pathlib import Path from typing import Any, Iterable USER_PROFILE_SCHEMA = "user-profile/v1" SKILL_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") SAFE_ROOTS = {"user", "brand", "workspace", "preferences"} SENSITIVE_PARTS = {"token", "secret", "password", "credential", "cookie", "api_key", "apikey"} def config_dir() -> Path: configured = os.environ.get("SKILLS_CONFIG_DIR") if configured: return Path(os.path.expandvars(configured)).expanduser() xdg = os.environ.get("XDG_CONFIG_HOME") if xdg: return Path(os.path.expandvars(xdg)).expanduser() / "agent-skills" return Path.home() / ".config" / "agent-skills" def profile_path(explicit: Path | None = None) -> Path: if explicit: return explicit.expanduser() configured = ( os.environ.get("SKILL_PROFILE_PATH") or os.environ.get("SKILLS_PROFILE_PATH") ) if configured: return Path(os.path.expandvars(configured)).expanduser() candidates = ( Path.home() / ".lovstudio" / "skills" / "profile.json", Path.home() / ".skill-publisher" / "skills" / "profile.json", config_dir() / "profile.json", ) return next((candidate for candidate in candidates if candidate.exists()), candidates[-1]) def read_profile(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise ValueError(f"profile cannot be read: {path}: {exc}") from exc if not isinstance(value, dict): raise ValueError("profile root must be an object") return value def profile_skeleton(skill_id: str) -> dict[str, Any]: return { "schema": "skill-profile/v1", "profile_id": f"profile-{skill_id}", "revision": 1, "status": "draft", "identity": {}, "purpose": {}, "brand": {}, "user": {}, "workspace": {}, "skills": {}, "extensions": {}, } def atomic_write(path: Path, value: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) mode = 0o600 if path.exists(): mode = path.stat().st_mode & 0o777 temporary_name: str | None = None try: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False, ) as handle: temporary_name = handle.name handle.write(json.dumps(value, ensure_ascii=False, indent=2) + "\n") handle.flush() os.fsync(handle.fileno()) os.chmod(temporary_name, mode) os.replace(temporary_name, path) finally: if temporary_name: temporary = Path(temporary_name) if temporary.exists(): temporary.unlink() def set_path(root: dict[str, Any], parts: list[str], value: Any) -> None: current: dict[str, Any] = root for part in parts[:-1]: child = current.get(part) if not isinstance(child, dict): child = {} current[part] = child current = child current[parts[-1]] = value def target_parts(skill_id: str, requested_path: str) -> tuple[list[str], str]: if not SKILL_ID_RE.fullmatch(skill_id): raise ValueError("skill_id must be kebab-case") path = requested_path.strip().strip(".") if not path: raise ValueError("path is required") parts = [part for part in path.split(".") if part] if any(not re.fullmatch(r"[A-Za-z0-9_-]+", part) for part in parts): raise ValueError("path contains an invalid segment") if any(part.casefold() in SENSITIVE_PARTS for part in parts): raise ValueError("profile records do not accept secret-like fields") if parts[0] == "records": target = ["skills", skill_id, "records", *parts[1:]] return target, ".".join(target) if parts[0] == "profile": target = ["skills", skill_id, "profile", *parts[1:]] return target, ".".join(target) if parts[:3] == ["skills", skill_id, "records"]: return parts, ".".join(parts) if parts[:3] == ["skills", skill_id, "profile"]: return parts, ".".join(parts) if parts[0] in SAFE_ROOTS: return parts, ".".join(parts) expected = f"records.<field> or one of: {', '.join(sorted(SAFE_ROOTS))}.<field>" raise ValueError(f"path must use {expected}") def parse_value(raw: str) -> Any: try: return json.loads(raw) except json.JSONDecodeError: return raw def read_command(args: argparse.Namespace) -> dict[str, Any]: path = profile_path(args.profile) profile = read_profile(path) skills = profile.get("skills", {}) if not isinstance(skills, dict): skills = {} skill = skills.get(args.skill_id, {}) if not isinstance(skill, dict): skill = {} user = profile.get("user") if not isinstance(user, dict) or not user: user = profile.get("identity", {}) if not isinstance(user, dict): user = {} brand = profile.get("brand", {}) if not isinstance(brand, dict): brand = {} workspace = profile.get("workspace", {}) if not isinstance(workspace, dict): workspace = {} preferences = profile.get("preferences", {}) if not isinstance(preferences, dict): preferences = {} records = skill.get("records", {}) if not isinstance(records, dict): records = {} return { "status": "ready", "schema": USER_PROFILE_SCHEMA, "skill_id": args.skill_id, "profile_path": str(path), "user": user, "brand": brand, "workspace": workspace, "preferences": preferences, "skill": skill, "records": records, } def record_command(args: argparse.Namespace) -> dict[str, Any]: if not args.confirm: raise PermissionError("record requires --confirm after the user has stated the value") parts, canonical_path = target_parts(args.skill_id, args.path) path = profile_path(args.profile) profile = read_profile(path) if not profile: profile = profile_skeleton(args.skill_id) set_path(profile, parts, parse_value(args.value)) revision = profile.get("revision") if isinstance(revision, int) and not isinstance(revision, bool): profile["revision"] = revision + 1 atomic_write(path, profile) return { "status": "saved", "schema": USER_PROFILE_SCHEMA, "skill_id": args.skill_id, "profile_path": str(path), "path": canonical_path, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--profile", type=Path, default=None, help="Shared profile JSON path") subparsers = parser.add_subparsers(dest="command", required=True) read_parser = subparsers.add_parser("read", help="Read shared and Skill-specific profile context") read_parser.add_argument("--skill-id", required=True) read_parser.add_argument("--pretty", action="store_true") record_parser = subparsers.add_parser("record", help="Persist a user-stated profile value") record_parser.add_argument("--skill-id", required=True) record_parser.add_argument("--path", required=True, help="records.<field>, brand.<field>, or user.<field>") record_parser.add_argument("--value", required=True) record_parser.add_argument("--confirm", action="store_true") args = parser.parse_args() try: result = read_command(args) if args.command == "read" else record_command(args) except (OSError, PermissionError, ValueError) as exc: print(json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False), file=sys.stderr) return 2 indent = 2 if getattr(args, "pretty", False) else None print(json.dumps(result, ensure_ascii=False, indent=indent, sort_keys=bool(indent))) return 0 if __name__ == "__main__": raise SystemExit(main()) -
validate_skill.py 26.2 KB
#!/usr/bin/env python3 """Validate a portable local Skill Publisher Skill source directory.""" from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any, Iterable try: import yaml except ImportError: print( "ERROR: PyYAML is required. Install it with: python3 -m pip install PyYAML", file=sys.stderr, ) raise SystemExit(2) FRONTMATTER_KEYS = { "name", "description", "license", "compatibility", "allowed-tools", "depends_on", "metadata", } TEXT_SUFFIXES = {".md", ".json", ".yaml", ".yml", ".txt", ".svg", ".py"} JUNK_NAMES = {"__pycache__", ".DS_Store"} JUNK_SUFFIXES = {".pyc", ".pyo"} SKIP_DIRS = {".git", "dist", ".venv", "venv", "node_modules"} SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$") NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*]\(([^)]+)\)") SKILL_PATH_RE = re.compile(r"\$(SKILL_DIR|KIT_DIR)/([A-Za-z0-9_./-]+)") CARD_STANDARD = "lovstudio/skill-card/v1" PRICING_CARD_SCHEMA = "lovstudio/pricing-card/v1" MANIFEST_SCHEMA = "skill-manifest/v1" RUNTIME_VERSION = "skill-runtime/v1" PROFILE_SCHEMA = "user-profile/v1" CONTENT_CLASSES = { "authored-prose", "microcopy", "verbatim", "deterministic-output", } class ValidationFailure(Exception): """Raised when source metadata cannot be parsed.""" def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def compact_text(value: Any) -> str: return re.sub(r"\s+", " ", value).strip() if isinstance(value, str) else "" def split_frontmatter(path: Path) -> tuple[dict[str, Any], str]: text = read_text(path) if not text.startswith("---\n"): raise ValidationFailure(f"{path}: missing YAML frontmatter") marker = text.find("\n---\n", 4) if marker < 0: raise ValidationFailure(f"{path}: frontmatter is not closed") try: data = yaml.safe_load(text[4:marker]) except yaml.YAMLError as exc: raise ValidationFailure( f"{path}: standard YAML parser rejected frontmatter: {exc}" ) from exc if not isinstance(data, dict): raise ValidationFailure(f"{path}: frontmatter must be a mapping") return data, text[marker + 5 :] def iter_files(root: Path) -> Iterable[Path]: for path in root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(root).parts): continue if path.is_file(): yield path def is_relative_to(path: Path, parent: Path) -> bool: try: path.relative_to(parent) return True except ValueError: return False def validate_skill_file(path: Path, errors: list[str]) -> dict[str, Any] | None: try: data, body = split_frontmatter(path) except ValidationFailure as exc: errors.append(str(exc)) return None unexpected = sorted(set(data) - FRONTMATTER_KEYS) if unexpected: errors.append(f"{path}: unsupported frontmatter keys: {', '.join(unexpected)}") name = compact_text(data.get("name")) if not NAME_RE.fullmatch(name) or len(name) > 64: errors.append(f"{path}: name must be kebab-case and at most 64 characters") description = compact_text(data.get("description")) if not 50 <= len(description) <= 200: errors.append( f"{path}: description must contain 50-200 characters " f"(found {len(description)})" ) compatibility = compact_text(data.get("compatibility")) if not compatibility: errors.append(f"{path}: compatibility is required at the top level") depends_on = data.get("depends_on", []) if isinstance(depends_on, str): depends_on = [depends_on] if not isinstance(depends_on, list) or not all( isinstance(item, str) and NAME_RE.fullmatch(item) for item in depends_on ): errors.append(f"{path}: depends_on must be a list of exact Skill names") metadata = data.get("metadata") if not isinstance(metadata, dict): errors.append(f"{path}: metadata must be a mapping") else: if not compact_text(metadata.get("author")): errors.append(f"{path}: metadata.author is required") if not SEMVER_RE.fullmatch(compact_text(metadata.get("version"))): errors.append(f"{path}: metadata.version must use SemVer") tags = metadata.get("tags") if not isinstance(tags, list) or not tags or not all( isinstance(tag, str) and tag.strip() for tag in tags ): errors.append(f"{path}: metadata.tags must be a non-empty list") if "card_standard" in metadata and metadata.get("card_standard") != CARD_STANDARD: errors.append(f"{path}: metadata.card_standard must be {CARD_STANDARD}") content_class = compact_text(metadata.get("content_class")) if content_class and content_class not in CONTENT_CLASSES: errors.append( f"{path}: metadata.content_class must be one of " f"{', '.join(sorted(CONTENT_CLASSES))}" ) if content_class in {"authored-prose", "microcopy"} and ( not isinstance(depends_on, list) or "lov-branding-consistency" not in depends_on ): errors.append( f"{path}: {content_class} requires lov-branding-consistency" ) if content_class == "authored-prose": authorship_reference = path.parent / "references" / "authorship-integrity.md" if not authorship_reference.is_file(): errors.append( f"{authorship_reference}: authored-prose contract is required" ) if "references/authorship-integrity.md" not in body: errors.append( f"{path}: authored-prose must route to references/authorship-integrity.md" ) trigger_block = re.search( r"(?ms)^##\s+Triggers\s*$([\s\S]*?)(?=^##\s+|\Z)", body ) if not trigger_block: errors.append(f"{path}: add an explicit '## Triggers' section") else: block = trigger_block.group(1) if len(re.findall(r"(?m)^\s*-\s+\S", block)) < 3: errors.append(f"{path}: add two activation examples and one non-trigger") if not re.search(r"[\u3400-\u9fff]", block): errors.append(f"{path}: add a concrete Chinese trigger phrase") if not re.search(r"(?i)\b(?:the|a|an|create|build|help|publish|review|use)\b", block): errors.append(f"{path}: add a concrete English trigger phrase") if not re.search( r"(?mi)^###\s+(?:Do not activate when|Non-triggers?|不应触发|不要触发)\s*$", body, ): errors.append(f"{path}: add explicit non-trigger conditions") if len(read_text(path).splitlines()) >= 500: errors.append(f"{path}: keep SKILL.md below 500 lines") if not body.strip(): errors.append(f"{path}: body is empty") return data def load_yaml(path: Path, errors: list[str]) -> dict[str, Any] | None: try: data = yaml.safe_load(read_text(path)) except yaml.YAMLError as exc: errors.append(f"{path}: standard YAML parser rejected file: {exc}") return None if not isinstance(data, dict): errors.append(f"{path}: expected a YAML mapping") return None return data def validate_runtime_manifest( skill_root: Path, expected_skill_id: str, errors: list[str] ) -> None: manifest_path = skill_root / "skill.yaml" if not manifest_path.is_file(): errors.append(f"{manifest_path}: user-profile runtime manifest is required") return data = load_yaml(manifest_path, errors) if data is None: return if data.get("schema") != MANIFEST_SCHEMA: errors.append(f"{manifest_path}: schema must be {MANIFEST_SCHEMA}") if data.get("id") != expected_skill_id: errors.append(f"{manifest_path}: id must match {expected_skill_id}") version = compact_text(data.get("version")) if not SEMVER_RE.fullmatch(version): errors.append(f"{manifest_path}: version must use SemVer") if data.get("runtime") != RUNTIME_VERSION: errors.append(f"{manifest_path}: runtime must be {RUNTIME_VERSION}") context = data.get("context") if not isinstance(context, dict): errors.append(f"{manifest_path}: context must be a mapping") return profile = context.get("profile") preferences = context.get("preferences") interaction = context.get("interaction") if not isinstance(profile, dict): errors.append(f"{manifest_path}: context.profile must be a mapping") else: if profile.get("schema") != PROFILE_SCHEMA: errors.append(f"{manifest_path}: context.profile.schema must be {PROFILE_SCHEMA}") if profile.get("source") != "shared-profile": errors.append(f"{manifest_path}: context.profile.source must be shared-profile") reads = profile.get("read") if not isinstance(reads, list) or not reads or not all( isinstance(item, str) and item.strip() for item in reads ): errors.append(f"{manifest_path}: context.profile.read must be a non-empty list") persist = profile.get("persist") if not isinstance(persist, dict): errors.append(f"{manifest_path}: context.profile.persist must be a mapping") else: if persist.get("enabled") is not True: errors.append(f"{manifest_path}: profile persistence must be enabled") if persist.get("namespace") != f"skills.{expected_skill_id}": errors.append(f"{manifest_path}: persist.namespace must target skills.{expected_skill_id}") if persist.get("records_path") != f"skills.{expected_skill_id}.records": errors.append(f"{manifest_path}: persist.records_path must target Skill records") if persist.get("write_policy") != "direct-user-statement": errors.append(f"{manifest_path}: persist.write_policy must be direct-user-statement") if persist.get("atomic") is not True: errors.append(f"{manifest_path}: profile persistence must be atomic") fields = profile.get("fields") if not isinstance(fields, list) or not fields: errors.append(f"{manifest_path}: context.profile.fields must be a non-empty list") else: validate_manifest_fields(manifest_path, "profile", fields, errors) if not isinstance(preferences, dict): errors.append(f"{manifest_path}: context.preferences must be a mapping") else: fields = preferences.get("fields") if not isinstance(fields, list): errors.append(f"{manifest_path}: context.preferences.fields must be a list") else: validate_manifest_fields(manifest_path, "preferences", fields, errors) if not isinstance(interaction, dict): errors.append(f"{manifest_path}: context.interaction must be a mapping") else: if not isinstance(interaction.get("ask_missing"), bool): errors.append(f"{manifest_path}: interaction.ask_missing must be boolean") max_questions = interaction.get("max_questions") if not isinstance(max_questions, int) or isinstance(max_questions, bool) or not 1 <= max_questions <= 3: errors.append(f"{manifest_path}: interaction.max_questions must be 1-3") def validate_manifest_fields( manifest_path: Path, source_name: str, fields: list[Any], errors: list[str] ) -> None: for index, field in enumerate(fields): label = f"{manifest_path}: context.{source_name}.fields[{index}]" if not isinstance(field, dict): errors.append(f"{label} must be a mapping") continue if not compact_text(field.get("path")): errors.append(f"{label}.path is required") if not isinstance(field.get("required"), bool): errors.append(f"{label}.required must be boolean") if not compact_text(field.get("question")): errors.append(f"{label}.question is required") aliases = field.get("aliases", []) if isinstance(aliases, str): aliases = [aliases] if not isinstance(aliases, list) or not all( isinstance(alias, str) and alias.strip() for alias in aliases ): errors.append(f"{label}.aliases must be a list of paths") def has_content(value: Any) -> bool: if isinstance(value, str): return bool(value.strip()) if isinstance(value, list): return any(has_content(item) for item in value) if isinstance(value, dict): return any(has_content(item) for item in value.values()) return value is not None def contains_placeholder(value: Any) -> bool: if isinstance(value, str): return bool(re.search(r"\bTODO\b|\{[^}]+\}", value, re.I)) if isinstance(value, list): return any(contains_placeholder(item) for item in value) if isinstance(value, dict): return any(contains_placeholder(item) for item in value.values()) return False def validate_card_bundle(skill_root: Path, errors: list[str]) -> None: card_path = skill_root / "skill-card.yaml" card_doc_path = skill_root / "skill-card.md" cases_path = skill_root / "cases" / "cases.json" pricing_path = skill_root / "pricing-card.yaml" for path in (card_path, card_doc_path, cases_path, pricing_path): if not path.is_file(): errors.append(f"{path}: required Skill trust-bundle file is missing") card = load_yaml(card_path, errors) if card_path.is_file() else None if card is not None: if card.get("schema") != CARD_STANDARD: errors.append(f"{card_path}: schema must be {CARD_STANDARD}") required = ( "description", "owner", "license", "use_case", "deployment", "requirements", "risks", "references", "output", "version", "ethical_considerations", "dimensions", "pricing", "distribution", ) for key in required: if key not in card or not has_content(card.get(key)): errors.append(f"{card_path}: required field '{key}' is missing or empty") dimensions = card.get("dimensions") if not isinstance(dimensions, list) or len(dimensions) < 3: errors.append(f"{card_path}: dimensions must contain at least three named dimensions") else: ids: set[str] = set() for index, dimension in enumerate(dimensions): label = f"{card_path}: dimensions[{index}]" if not isinstance(dimension, dict): errors.append(f"{label}: expected a mapping") continue dimension_id = compact_text(dimension.get("id")) if not dimension_id or dimension_id in ids: errors.append(f"{label}: id is required and must be unique") ids.add(dimension_id) for key in ("label", "description", "evidence"): if not compact_text(dimension.get(key)): errors.append(f"{label}: '{key}' is required") risks = card.get("risks") if not isinstance(risks, list) or not risks: errors.append(f"{card_path}: risks must contain at least one risk and mitigation") else: for index, risk in enumerate(risks): if not isinstance(risk, dict) or not compact_text(risk.get("risk")) or not compact_text(risk.get("mitigation")): errors.append(f"{card_path}: risks[{index}] needs risk and mitigation") distribution = card.get("distribution") if not isinstance(distribution, dict) or not isinstance(distribution.get("paid"), list) or not isinstance(distribution.get("free"), list): errors.append(f"{card_path}: distribution must declare paid and free lists") if contains_placeholder(card): errors.append(f"{card_path}: replace unresolved TODO or template placeholders") if card_doc_path.is_file(): card_doc = read_text(card_doc_path) required_headings = ( "Description", "Owner", "License", "Use Case", "Deployment Geography", "Requirements", "Known Risks", "References", "Skill Output", "Skill Version", "Ethical Considerations", "User Cases", "Dimension Map", "Pricing Basis", "Distribution", ) for heading in required_headings: if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", card_doc): errors.append(f"{card_doc_path}: add the '{heading}' section") if re.search(r"\bTODO\b|\{[^}]+\}", card_doc, re.I): errors.append(f"{card_doc_path}: replace unresolved TODO or template placeholders") if cases_path.is_file(): try: cases = json.loads(read_text(cases_path)) except json.JSONDecodeError as exc: errors.append(f"{cases_path}: invalid JSON: {exc}") cases = [] if not isinstance(cases, list) or not cases: errors.append(f"{cases_path}: include at least one real user case") else: for index, case in enumerate(cases): label = f"{cases_path}: cases[{index}]" if not isinstance(case, dict): errors.append(f"{label}: expected a mapping") continue for key in ("title", "description", "input", "prompt", "output"): if not has_content(case.get(key)): errors.append(f"{label}: '{key}' is required") if contains_placeholder(case): errors.append(f"{label}: replace unresolved TODO or template placeholders") for image_key in ("cover", "gallery"): image_values = case.get(image_key, []) if isinstance(image_values, str): image_values = [image_values] if not isinstance(image_values, list): errors.append(f"{label}: '{image_key}' must be a string or list") continue for image in image_values: if not isinstance(image, str) or not image.strip(): errors.append(f"{label}: '{image_key}' contains an empty path") elif not re.match(r"^(?:https?:|/|data:)", image) and not (skill_root / image).is_file(): errors.append(f"{label}: case asset does not exist: {image}") pricing = load_yaml(pricing_path, errors) if pricing_path.is_file() else None if pricing is not None: if pricing.get("schema") != PRICING_CARD_SCHEMA: errors.append(f"{pricing_path}: schema must be {PRICING_CARD_SCHEMA}") for key in ("model", "currency", "list_price_cny", "basis", "boundary", "review_trigger", "confidence"): if key not in pricing or (key != "list_price_cny" and not has_content(pricing.get(key))): errors.append(f"{pricing_path}: required field '{key}' is missing or empty") if contains_placeholder(pricing): errors.append(f"{pricing_path}: replace unresolved TODO or template placeholders") def validate_composition_reference(skill_root: Path, errors: list[str]) -> None: path = skill_root / "references" / "skill-composition.md" if not path.is_file(): errors.append(f"{path}: required Skill group composition record is missing") return text = read_text(path) required_headings = ( "Nearby Skills Inspected", "Atomic Handoffs", "Overlap Decisions", "Composition Decision", ) for heading in required_headings: if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", text): errors.append(f"{path}: add the '{heading}' section") if re.search(r"\bTODO\b|\{[^}]+\}", text, re.I): errors.append(f"{path}: replace unresolved TODO or template placeholders") def validate_kit(root: Path, skill_names: set[str], errors: list[str]) -> None: manifest = root / "kit.yaml" if not manifest.exists(): return data = load_yaml(manifest, errors) if data is None: return modules = data.get("modules") if not isinstance(modules, list) or not modules: errors.append(f"{manifest}: modules must be a non-empty list") return module_ids: set[str] = set() for index, module in enumerate(modules): label = f"{manifest}: modules[{index}]" if not isinstance(module, dict): errors.append(f"{label}: expected a mapping") continue module_id = compact_text(module.get("id")) skill_name = compact_text(module.get("skill")) relative = compact_text(module.get("path")) if not module_id or module_id in module_ids: errors.append(f"{label}: id is required and must be unique") module_ids.add(module_id) module_path = (root / relative).resolve() if ( not relative or not is_relative_to(module_path, root.resolve()) or not (module_path / "SKILL.md").is_file() ): errors.append(f"{label}: missing module at '{relative}/SKILL.md'") if skill_name not in skill_names: errors.append(f"{label}: unresolved child skill '{skill_name}'") pipelines = data.get("pipelines") if not isinstance(pipelines, dict) or not pipelines: errors.append(f"{manifest}: pipelines must be a non-empty mapping") return for pipeline, sequence in pipelines.items(): if not isinstance(sequence, list) or not sequence: errors.append(f"{manifest}: pipeline '{pipeline}' must be a non-empty list") continue missing = [str(item) for item in sequence if item not in module_ids] if missing: errors.append( f"{manifest}: pipeline '{pipeline}' has unknown modules: " + ", ".join(missing) ) def validate_local_references(root: Path, errors: list[str]) -> None: for path in iter_files(root): if path.suffix.lower() != ".md": continue text = read_text(path) for raw in MARKDOWN_LINK_RE.findall(text): target = raw.strip().split(maxsplit=1)[0].strip("<>").split("#", 1)[0] if ( not target or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I) or any(token in target for token in ("TODO", "{", "}")) ): continue if not (path.parent / target).resolve().exists(): errors.append(f"{path}: broken local link '{target}'") skill_root = path.parent if path.name == "SKILL.md" else root for variable, target in SKILL_PATH_RE.findall(text): if "TODO" in target: continue base = skill_root if variable == "SKILL_DIR" else root resolved = (base / target.rstrip(".,;:)")).resolve() if not is_relative_to(resolved, root.resolve()) or not resolved.exists(): errors.append(f"{path}: missing required resource '${variable}/{target}'") def validate_hygiene(root: Path, errors: list[str]) -> None: private_path = re.compile(r"(?:/Users/[^/\s]+/|[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\)") for path in root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(root).parts): continue if path.name in JUNK_NAMES or path.suffix.lower() in JUNK_SUFFIXES: errors.append(f"{path}: generated/cache artifact must not ship") for path in iter_files(root): if path.suffix.lower() not in TEXT_SUFFIXES or path.name == "validate_skill.py": continue text = read_text(path) if private_path.search(text): errors.append(f"{path}: contains a private absolute user path") if path.name != "init_skill.py" and re.search(r"\bTODO\s*[::]", text): errors.append(f"{path}: unresolved TODO placeholder") for relative in ("workbuddy", "scripts/build_workbuddy.py"): if (root / relative).exists(): errors.append( f"{root / relative}: platform distribution artifacts belong to skill-publish" ) def validate_source(root: Path, errors: list[str]) -> None: root_skill = root / "SKILL.md" skill_files = [root_skill, *sorted((root / "skills").glob("*/SKILL.md"))] if not root_skill.is_file(): errors.append(f"{root_skill}: file is required") return parsed: list[tuple[Path, dict[str, Any]]] = [] for path in skill_files: data = validate_skill_file(path, errors) if data: parsed.append((path, data)) names = {compact_text(data.get("name")) for _, data in parsed} if len(names) != len(parsed): errors.append(f"{root}: every embedded Skill must have a unique name") for path, data in parsed: metadata = data.get("metadata") if isinstance(metadata, dict) and metadata.get("card_standard") == CARD_STANDARD: validate_card_bundle(path.parent, errors) validate_composition_reference(path.parent, errors) validate_runtime_manifest(path.parent, compact_text(data.get("name")), errors) validate_kit(root, names, errors) readme = root / "README.md" if not readme.is_file(): errors.append(f"{readme}: file is required") elif parsed: metadata = parsed[0][1].get("metadata") version = compact_text(metadata.get("version")) if isinstance(metadata, dict) else "" if version and f"version-{version}-" not in read_text(readme): errors.append(f"{readme}: version badge must match {version}") validate_hygiene(root, errors) validate_local_references(root, errors) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("path", type=Path, help="Local Skill source directory") args = parser.parse_args() root = args.path.expanduser().resolve() if not root.is_dir(): print(f"ERROR: directory does not exist: {root}", file=sys.stderr) return 2 errors: list[str] = [] validate_source(root, errors) if errors: print(f"FAILED: {len(errors)} issue(s)") for error in errors: print(f"- {error}") return 1 print(f"PASSED: source validation ({root})") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
.gitignore 84 B · in bundle
-
CHANGELOG.md 592 B
# Changelog ## 0.3.0 — 2026-09-06 首次公开发行准备:2026-09-07。补充官网安装入口,渠道状态移至 Publisher 发布记录。 - 将本地 frontend-design 0.2.0 的工作室改进整理为独立 lov-frontend-design,保留旧版安装。 - 独立编写产品交互、内容适配、GSAP、多媒体与真实验收工作流,允许作品按内容选择风格。 - 补齐 Profile、相邻能力边界、来源说明、历史案例、新版演练、维度地图与定价分发记录。 - 使用共享 canonical 安装与宿主相对链接,完成本地结构校验。 -
LICENSE 1.1 KB · in bundle
-
pricing-card.yaml 554 B
schema: lovstudio/pricing-card/v1 version: 0.3.0 model: free currency: CNY list_price_cny: 0 basis: 当前提供可移植设计指令、参考方法与真实案例,没有托管服务或专有数据,当前本地使用免费。 boundary: 包含界面设计、实现与相称验收;不包含模型调用、媒体生成、托管、商业品牌咨询或远程发布费用。 review_trigger: 增加独立自动化设计工具、托管验收或持续维护服务时重新评估;不因新增案例直接推断涨价。 confidence: documented-local-scope -
README.md 3.9 KB
# 界面设计师 · Interface Designer  手工川工作室维护的前端设计 Skill:把需求、真实内容和现有代码变成有辨识度、 交互一致、支持图片音视频的可用界面。 ## 适用场景 - 页面、组件、工具栏、设置与导航设计。 - 修复图标、按钮、状态和响应式之间的交互不一致。 - 制作 GSAP 交互地图、教学展示与多媒体案例集。 - 只读审查现有界面的内容、操作与真实访问路径。 完整品牌定位与营销落地页优先使用 `lov-oh-my-landingpage`;性能架构、API 请求封装、 纯 CSS 清理和独立剪辑不由本 Skill 接管。 ## 工作室版本增加了什么 | 关注点 | 具体约定 | | --- | --- | | 产品语义 | 链接、动作、状态、切换各用合适控件,同级操作保持一致 | | 内容适配 | 根据受众、任务与素材决定风格,保留既有设计系统 | | 图片与音视频 | 来源、加载、错误、实际播放和替代内容分别验收 | | GSAP | 关系重排与演示,包含减少动效、清理和失败时可读路径 | | 站点集成 | 桌面、手机、语言和身份分支一起核查,实际点击目标路由 | | 可信证据 | 历史案例、新版本演练、静态检查与线上结果分别记录 | 工作室是维护者,不是所有作品的默认品牌。用户可以明确要求专题自由选风格。 这是对本地 `frontend-design` 0.2.0 方法的独立整理,来源说明见 [Provenance](references/provenance.md)。旧版安装保留,新版使用独立 ID。 ## 安装 官网安装命令: ```sh npx lovstudio skills add frontend-design ``` 需要手动管理源码时,将源码保存在自己的 Skill 源目录,设置 `SKILL_SOURCE` 为该目录、`SKILL_INSTALL_DIR` 为共享 Skill 安装目录后执行: ```sh mkdir -p "$SKILL_INSTALL_DIR" ln -s "$SKILL_SOURCE" "$SKILL_INSTALL_DIR/lov-frontend-design" ``` 安装目标必须空闲,不能覆盖旧文件。宿主适配入口使用相对链接指向共享安装位。 也可以通过统一目录指定 Skill ID 安装: ```sh npx skills add lovstudio/skills --skill lov-frontend-design -g -y ``` ## 使用 “用 lov-frontend-design 把这些真实案例做成适合课堂展示的交互页面,支持图片、音频和 视频,并用 GSAP 解释能力分布。风格服从内容。” “用 lov-frontend-design 只读检查官网资源入口,验证电脑和手机上的标签、键盘与跳转。” “Design this settings toolbar with consistent control semantics and accessible states.” ## Profile 与依赖 每次读取 [skill.yaml](skill.yaml) 的 `user-profile/v1`。品牌、语言、项目路径与偏好从 当前请求、项目和共享 Profile 解析;不写死维护者环境。只有明确长期偏好才持久保存。 ```sh python3 scripts/profile_store.py read --skill-id lov-frontend-design --pretty ``` 可见文案依赖 `lov-branding-consistency`。主体为 instruction-first,不绑定前端框架。 使用项目已有工具链;运行验收需浏览器工具,Profile/Skill 校验需 Python 3.8+ 与 PyYAML。 GSAP 只在任务需要时使用;安装依赖不是此 Skill 固定副作用。 ## 验证与案例 ```sh python3 scripts/validate_skill.py . ``` 完整验收见 [Acceptance](references/acceptance.md)。 [真实案例](cases/cases.json) 收录能力地图与官网入口,并区分旧任务证据和新版本复核。 [Skill Card](skill-card.md) 提供能力维度与限制;[定价卡](pricing-card.yaml) 记录免费依据。 本 Skill 免费提供源码与方法,媒体生成和运行工具的成本另计。 版本与安装入口以 [官网目录](https://lovstudio.ai/skills/frontend-design) 和 [源码 Release](https://github.com/lovstudio/frontend-design-skill/releases) 为准。 渠道发布记录与发行定价卡由 Publisher 在源码之外管理。 -
skill-card.md 3.9 KB
# 界面设计师 · Interface Designer · Skill Card ## Description 手工川工作室维护的通用前端设计 Skill。把需求、真实内容和现有代码落实为有辨识度、 交互一致、支持图片音视频的界面;风格由目标内容与项目约束决定。 ## Owner LovStudio contributors;维护入口为 Skill 真源仓库。 ## License / Terms [MIT](LICENSE) 适用于本包指令与配套工具,保留来源归属。案例作品和第三方素材不随 本包重新授权。旧版来源说明见 [Provenance](references/provenance.md)。 ## Use Case 面向产品开发者、设计者和代码 Agent。输入为需求、真实素材、现有页面或项目,输出 为可运行界面或只读审查。适用页面、工具栏、设置、导航与多媒体教学展示。 ## Deployment Geography 全球本地 Agent 与项目环境;作品可按任务部署在用户自己的环境。 ## Requirements / Dependencies 文案门禁依赖 `lov-branding-consistency`。复用项目工具链,运行验收需要浏览器能力; Profile 与 Skill 校验需要 Python 3.8+ 和 PyYAML。核心方法不需要凭据,发布另按任务授权。 ## Known Risks and Mitigations - 新视觉破坏旧习惯:先读取相邻控件、token 和用户路径。 - 资源存在被当成媒体可用:实际播放、拖动、点击和触控分别记录。 - 私人素材泄露:区分公开范围,记录来源,不携带聊天原件、秘密或私人路径。 - 历史成果夸大新能力:历史案例与新版演练分别归档,不使用无依据评分。 ## References - [主入口](SKILL.md) - [验收方法](references/acceptance.md) - [机器卡片](skill-card.yaml) - [来源说明](references/provenance.md) ## Skill Output 项目原生前端源码、明确的内容层级与控件状态、真实素材引用和可追溯验收。 只读审查输出核实的问题、影响和检查边界。发布不作为默认副作用。 ## Skill Version 0.3.0。由本地通用名 frontend-design 0.2.0 的工作室方法整理而来,新包独立命名和安装。 ## Ethical Considerations 不虚构案例、来源、用户评价或量化评分。品牌来自当前任务和 Profile,不以维护者 品牌覆盖其他用户。保留真实或合成媒体标识,不把构建通过声称为全部体验已验证。 ## User Cases [cases/cases.json](cases/cases.json) 保存真实 Input → Prompt → Output: 能力地图多媒体展示、官网资源入口,以及单独标识的新版本只读演练。 前两项早于本 Skill 创建,归档运行证据不代表 0.3.0 重新创造了这些作品。 ## Dimension Map | 维度 | 证据 | 当前状态 | | --- | --- | --- | | 产品交互 | 桌面、手机菜单到达真实地图 | 新演练入口已验;窄屏菜单另有待修问题 | | 内容表达 | 244 节点、6 案例与可检索索引 | 历史案例已验 | | 多媒体 | 图片、两段声音、视频播放与跳转 | 历史案例已验 | | 可移植性 | Profile、相邻能力边界和独立 ID | 契约审查 | | 动效与可访问性 | 减少动效、清理、键盘与文本路径规则 | 规范已写,完整运行覆盖未完成 | 这些状态表示证据成熟程度,不是能力百分比或综合分数。 只读演练发现 320×740 视口下,长菜单底部的登录入口未完整露出,菜单滚轮也未使它 完整可见。能力地图入口本身可触控跳转;本次没有修改网站。见 [窄屏问题记录](cases/evidence/narrow-menu-finding.json)。 ## Pricing Basis 免费提供本地指令与案例,模型、媒体制作和部署费用另计。 [pricing-card.yaml](pricing-card.yaml) 保留源码开放使用依据;官网发行定价卡在 Publisher 输出与目录中维护,产品展示使用 Credits。 ## Distribution 渠道状态由 Publisher 在源码之外维护,以 [官网目录](https://lovstudio.ai/skills/frontend-design) 和对应 Release 为准。 案例网页上线与 Skill 上架是两个状态。 -
skill-card.yaml 5.1 KB
schema: lovstudio/skill-card/v1 version: 0.3.0 description: 手工川工作室维护的前端设计方法,统一内容、视觉、产品交互、GSAP 与真实多媒体,并输出可运行界面和可追溯验收。 owner: team: LovStudio contributors contact: local Skill source maintainers license: name: MIT terms: 新编写指令与配套工具保留许可归属;示例站点及第三方素材不随本包重新授权。 url: LICENSE use_case: audience: 维护产品前端、教学展示与案例集的开发者、设计者及代码 Agent scenario: 已有页面要统一交互,或真实内容需要形成清楚、有辨识度的展示。 tasks: - 理解受众与内容,选择适配风格 - 实现一致的控件、导航与响应式界面 - 用 GSAP 和真实图片音视频解释内容 - 记录构建、交互和最终路径的验收证据 deployment: geography: global environments: - local repository-aware agent - existing frontend project or standalone exhibit requirements: credentials: 核心本地方法不需要凭据;项目私有数据和发布渠道按任务单独提供。 dependencies: - lov-branding-consistency runtime: - target project frontend toolchain - browser tooling for runtime acceptance - Python 3.8+ and PyYAML for local Skill utilities risks: - risk: 过度追求新视觉破坏已有产品操作习惯。 mitigation: 先读取相邻控件与 token,语义和状态优先。 - risk: 把静态资源存在或构建成功误写成媒体、交互已验证。 mitigation: 实际点击、触控、播放和拖动分别记录;不可测项明确标记。 - risk: 从历史聊天和素材泄露私人内容或伪造来源。 mitigation: 明确公开范围,保留来源与真实或合成属性,不打包聊天原件或凭据。 - risk: 新版本借用历史成果夸大能力。 mitigation: 历史案例和新版本复核分别存证,不给无依据评分。 references: - title: Primary instructions path: SKILL.md - title: Acceptance path: references/acceptance.md - title: Provenance path: references/provenance.md - title: Real cases path: cases/cases.json output: types: - frontend source or scoped patch - media references and provenance - verification evidence formats: - project-native HTML/CSS/JS or components - JSON evidence and concise Markdown parameters: - audience and user task - project design system and style scope - media sources and access boundary - supported viewports and verification scope validation: - real navigation reaches expected content - labels and actions retain meaning at narrow widths - relevant media actually plays - project-native checks pass or gaps are explicit description: 可用的页面或组件、真实素材与可核查的验收范围。 ethical_considerations: 不制造案例、评价或量化能力分数。读者文案不混入私有过程,作品来源与合成声音明确标识;不默认发布。 dimensions: - id: product-semantics label: 产品交互 description: 导航、动作、状态与切换使用稳定语义。 evidence: cases/evidence/navigation-replay.json:四种语言、桌面键盘和两种手机宽度的触控均实际到达 244 节点地图。 同轮另发现窄屏长菜单底部溢出,见 cases/evidence/narrow-menu-finding.json。 score: verified-entry-with-open-finding - id: content-structure label: 内容与能力表达 description: 大量能力以地图、索引和案例逐层展示。 evidence: cases/evidence/atlas-historical.json 记录 244 个节点、6 个案例、布局切换与索引检查。 score: verified-historical-case - id: multimedia label: 真实多媒体 description: 验收实际图像浏览、声音播放、视频与拖动。 evidence: cases/evidence/atlas-historical.json 保存音频时间增长、视频 1280×720 与跳转记录。 score: verified-historical-case - id: portable-method label: 方法可移植性 description: 维护品牌与作品品牌分离,复用项目技术和共享 Profile。 evidence: skill.yaml、references/skill-composition.md 与 references/provenance.md 明确上下文和边界。 score: reviewed-contract - id: motion-accessibility label: 动效与可访问性 description: GSAP 作用域、减少动效、文本替代路径与键盘行为纳入验收。 evidence: references/motion-and-media.md 和 references/acceptance.md 声明要求;完整跨浏览器和辅助技术测试仍未完成。 score: specified-not-fully-verified pricing: model: free currency: CNY list_price_cny: 0 basis: 当前提供可移植设计指令、参考方法与真实案例,没有托管服务或专有数据,当前本地使用免费。 boundary: 包含界面设计、实现与相称验收;不包含模型调用、媒体生成、托管、商业品牌咨询或远程发布费用。 review_trigger: 增加独立自动化设计工具、托管验收或持续维护服务时重新评估;不因新增案例直接推断涨价。 confidence: documented-local-scope distribution: policy: Release-specific channel states are maintained outside canonical source by the publisher. paid: [] free: [] -
SKILL.md 7.2 KB
--- name: lov-frontend-design description: > 把需求、现有界面与真实素材落实为有辨识度、交互一致的前端,覆盖页面、组件、导航、GSAP 展示及图片音视频。触发:设计前端、界面不够专业、制作交互案例集;design or refine a frontend UI. license: MIT compatibility: "Repository-aware agent; HTML/CSS/JS, React, Vue or existing stack. Browser inspection for runtime acceptance; Python 3.8+ and PyYAML for local Skill utilities." depends_on: - lov-branding-consistency metadata: author: LovStudio version: "0.3.0" content_class: microcopy card_standard: lovstudio/skill-card/v1 tags: [frontend-design, product-interaction, art-direction, gsap, multimedia, accessibility] --- # 界面设计师 · Interface Designer 把内容、视觉和交互落实为可用的界面。目标受众应能看懂、能操作、能完成任务,同时 获得与内容相称的审美体验。直接实现修改请求;用户明确要求审查时保持只读。 这是手工川工作室维护的通用前端设计方法。维护者身份不决定作品品牌:优先遵守当前 请求与项目设计系统;独立专题可以采用适合内容的视觉方向,不把工作室配色、Logo、 字体或个人路径写成所有用户的默认值。 ## Triggers ### Activate when - “用手工川工作室版本设计这个前端,直接实现。” - “这个工具栏能用,但按钮、图标和状态不像一个专业产品。” - “把真实案例做成支持图片、音频和视频的交互展示,使用 GSAP。” - “为现有官网增加资源入口,并验证桌面和手机的访问路径。” - “Design or refine this frontend UI with the existing product conventions.” - “Audit this interactive case collection without changing the site.” ### Do not activate when - 只创建或安装 Skill,或只发布已完成页面:使用相应创建、发布能力。 - 核心任务是品牌定位、商业叙事和营销转化:优先已有 Landing Page 能力。 - 只调后端并发、启动架构、请求封装、CSS 清理或独立媒体剪辑:交给对应能力。 ## Runtime context 每次读取 `skill.yaml` 和 [Profile contract](references/user-profile.md)。从当前请求、 项目现状、Skill records、共享 preferences、brand/user Profile、安全默认值解析上下文。 环境变量可指定 Profile 来源;显式请求始终优先。仅消费 Manifest 声明的字段。 python3 scripts/profile_store.py read --skill-id lov-frontend-design --pretty 只有用户明确表达跨任务偏好时才用 `profile_store.py record --confirm` 保存,随后报告 保存路径。单次“无需遵循品牌限制”留在当次上下文,不推断成永久品牌偏好。 秘密、素材原件和个人绝对路径不得写入可分发 Skill。 ## References by need - 所有任务:[Interaction contract](references/interaction.md)。 - 新视觉方向或整体重设计:[Art direction](references/art-direction.md)。 - 动态图表、GSAP、图片、音频、视频或演示模式:[Motion and media](references/motion-and-media.md)。 - 交付前:[Acceptance](references/acceptance.md)。 - 选择相邻能力时:[Skill composition](references/skill-composition.md)。 - 来源与历史案例:[Provenance](references/provenance.md)。 ## Workflow ### 1. 找到真实界面与验收路径 读取项目规则、Git 状态、目标路由、相邻页面、共享组件和已有数据来源,保留其他工作。 区分产品小改、完整界面设计和独立展示。新增导航不必重做品牌策略;全新展示需要先 理解受众和内容结构。工作量随范围变化。 内部明确受众、打开时已知信息、主要任务、核心内容、输入输出、技术限制、风格约束、 资产真伪与最终访问路径。从上下文能确认的直接决定,只问会改变结果的缺口。 使用宿主真实工具,不假定拥有浏览器、媒体生成或部署能力。 ### 2. 设计理解顺序与操作语义 确定第一屏让人理解什么,主体如何展开证据,最后如何继续行动。长内容采用逐层 展开、检索、筛选、索引或具体案例,不把章节机械排成同尺寸卡片。 按 [Interaction contract](references/interaction.md) 区分导航、动作、状态和切换。 主要控件有明确输入、状态、结果和失败后的下一步,复用现有组件与状态模型。 调用 `lov-branding-consistency` 审校标题、按钮、说明和 Caption,保留来源文本与数据。 需要长篇正文时接受经确认的稿件,不把前端设计自动扩展成研究或文章创作。 ### 3. 按情境确定视觉方向 已有产品沿用 token、组件、字体和布局习惯,把新意放在内容组织与细节上。 新作品用一句话说明这个方向为什么适合内容与受众,再确定排版、密度、色彩、素材 处理和一项可记忆的视觉特征。装饰不能争夺图表、证据与主要动作的注意力。 标题、正文、数据、注释承担不同排版角色;检查中文断行、数字、长英文和真实宽度。 字体选择服从可读性、授权、加载与项目现状,不因反模板化而全局替换字体。 ### 4. 实现完整用户路径 使用现有技术栈和依赖。独立静态展示可采用 HTML/CSS/JS,不为样式增加应用框架。 相同动作组重复出现时复用公共实现。数据来源真实,快照标注范围与日期,不把历史 样本数量冒充当前全部能力。 涉及媒体时完整读取 [Motion and media](references/motion-and-media.md)。GSAP 服务于 关系变化、叙事推进与演示,不作为基本内容可见性的唯一开关。主要操作都有真实结果。 集成站点同时检查桌面导航、移动端、身份分支、语言与目标路由。独立 HTML、下载和 浏览器目的地采用合适的原生链接,避免客户端路由误接管。仅在当前授权范围内部署, 设计完成和线上发布分别验收。 ### 5. 在真实界面检查并修复 按 [Acceptance](references/acceptance.md) 覆盖本次涉及的状态与操作,以真实输入走完 主要路径。先解决内容、交互和可访问性,再调整动效与细节;检查最窄支持宽度和常用 桌面。浏览器不可用时报告静态结果与运行待验项,不声称点击、播放或辅助技术已通过。 保存必要截图、测量、检查命令或可回读产物。只读审查列出核实的问题及影响;修改 请求落实修复并复验。证据不完整时标记具体缺口,不写虚构评分。 ### 6. 简洁交付 给出实际页面或文件、主要变化、已验证路径与剩余限制。页面不展示内部 brief、设计 自评、Agent 操作记录或“专业制作”等自我说明。可复用经验更新最窄参考与真实案例, 不把单个作品的风格升级成普遍规则。 ## Output contract - 页面或组件的项目原生代码,以及必要的真实素材引用。 - 明确的内容层级、控件文案和一致的操作状态。 - 媒体文件或经核实的地址、用途与来源记录。 - 相称的构建检查、浏览器验证与未验证项。 - 准确区分本地与线上状态;Skill 公开分发交给发布能力。 -
skill.yaml 2.6 KB
schema: skill-manifest/v1 id: lov-frontend-design version: 0.3.0 runtime: skill-runtime/v1 context: profile: schema: user-profile/v1 source: shared-profile read: - user - brand - workspace - preferences - skills.lov-frontend-design persist: enabled: true namespace: skills.lov-frontend-design records_path: skills.lov-frontend-design.records write_policy: direct-user-statement atomic: true fields: - path: user.name aliases: - identity.name required: false question: 如果本次输出需要用户身份,请提供名称。 - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? - path: brand.name aliases: - identity.name required: false question: 如果本次输出需要品牌身份,请提供品牌名称。 - path: brand.site required: false question: 如果需要品牌官网,请提供地址。 - path: brand.tone required: false question: 如果已有品牌语气或审美关键词,请提供它们。 - path: brand.logo required: false question: 已有确认过的品牌标识时,请提供它的位置。 - path: brand.design_guide required: false question: 已有设计规范时,请提供其内容或位置。 - path: workspace.project_root required: false question: 无法从当前项目判断时,请提供目标项目目录。 - path: workspace.output_root required: false question: 有指定交付目录时,请提供它的位置。 - path: skills.lov-frontend-design.records.style_mode required: false question: 是否有明确需要长期沿用的风格模式? - path: skills.lov-frontend-design.records.default_audience required: false question: 是否有明确需要长期沿用的受众? - path: skills.lov-frontend-design.records.min_viewport required: false question: 是否有明确需要长期沿用的最窄支持宽度? - path: skills.lov-frontend-design.records.motion_preference required: false question: 是否有明确需要长期沿用的动效偏好? preferences: namespace: lov_frontend_design fields: - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? interaction: ask_missing: true max_questions: 1
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.