diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da2d4b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: Gallery checks + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + gallery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run build + - run: git diff --exit-code -- preview-gallery.html + - run: npm test + - run: npm run test:copy diff --git a/.gitignore b/.gitignore index 767d127..9c2eabc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ repro/ *-proof.png redesign-*.png + +.artifacts/ diff --git a/README.md b/README.md index c1c0093..a2ac940 100644 --- a/README.md +++ b/README.md @@ -1,376 +1,45 @@ -# 世界级 UI 画廊 · UI Gallery +# UI Gallery -> 230 个主流 UI 组件库 / 设计系统 + 世界级获奖网站的策展式画廊。每一项都附带**可被 AI 逐帧复现的高保真提示词**——复制提示词丢给任意 AI,就能重建出首屏一致、动画一致的页面。 -> -> A curated gallery of 230 mainstream UI component libraries / design systems + world-class award-winning websites. Every entry ships a **high-fidelity prompt that AI can reproduce frame-by-frame** — paste it into any AI and it rebuilds a page whose above-the-fold and animations match the original. +收录 230 个 UI 组件库、设计系统和获奖网站的静态画廊。支持框架/主题筛选、收藏和复制参考提示词。 -[![items](https://img.shields.io/badge/收录-230%20项-blue)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![items-en](https://img.shields.io/badge/entries-230-blue)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![verified](https://img.shields.io/badge/AI%20复现通过-194%2F204-brightgreen)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![verified-en](https://img.shields.io/badge/AI%20repro-passed-194%2F204-brightgreen)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![anim](https://img.shields.io/badge/动画还原-191%2F194-ff69b4)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![anim-en](https://img.shields.io/badge/animations-restored-191%2F194-ff69b4)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![offline](https://img.shields.io/badge/离线自包含-零外部调用-lightgrey)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![offline-en](https://img.shields.io/badge/offline-self--contained-lightgrey)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![pages](https://img.shields.io/badge/GitHub%20Pages-已部署-orange)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![pages-en](https://img.shields.io/badge/GitHub%20Pages-deployed-orange)](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html) -[![HTML](https://img.shields.io/badge/HTML-Vanilla-orange)]() -[![JavaScript](https://img.shields.io/badge/JavaScript-Node.js%2022%2B-F7DF1E)]() -[![Playwright](https://img.shields.io/badge/Test-Playwright-2EAD33)]() -[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[在线画廊](https://wangkeyu-u.github.io/ui-gallery/preview-gallery.html),也可直接打开本地 `preview-gallery.html`。数据内嵌在页面中,预览图从 `previews/` 加载;访问条目的外部链接需要联网。 ---- +## 目录 -## 目录 / Table of Contents - -- [项目简介 / Project Overview](#项目简介--project-overview) -- [核心特性 / Key Features](#核心特性--key-features) -- [在线预览 / Live Preview](#在线预览--live-preview) -- [整体架构 / Architecture](#整体架构--architecture) -- [项目结构 / Project Structure](#项目结构--project-structure) -- [数据集 / The Dataset](#数据集--the-dataset) -- [AI 高保真复现验证(项目灵魂)/ AI High-Fidelity Reproduction Verification](#ai-高保真复现验证项目灵魂--ai-high-fidelity-reproduction-verification) - - [1. 提示词是怎么炼成的 / How prompts are forged](#1-提示词是怎么炼成的--how-prompts-are-forged) - - [2. 验证闭环 / The verification loop](#2-验证闭环--the-verification-loop) - - [3. 结果聚合 / Result aggregation](#3-结果聚合--result-aggregation) -- [使用指南 / Usage Guide](#使用指南--usage-guide) -- [验证结果 / Verification Results](#验证结果--verification-results) -- [脚本参考 / Script Reference](#脚本参考--script-reference) -- [如何扩展数据集 / How to extend the dataset](#如何扩展数据集--how-to-extend-the-dataset) -- [本地源码浏览器(hub)/ Local source browser (hub)](#本地源码浏览器hub--local-source-browser-hub) -- [说明与限制 / Notes & Limitations](#说明与限制--notes--limitations) -- [相关链接 / Links](#相关链接--links) - ---- - -## 项目简介 / Project Overview - -这个项目把「浏览优秀 UI」和「用 AI 重建优秀 UI」两件事合在了一起: - -This project combines two things — *browsing great UI* and *rebuilding great UI with AI*: - -1. **画廊 / Gallery**:230 个站点的策展式浏览,分区、框架/主题筛选、点选收藏、一键复制。完全自包含,双击 `preview-gallery.html` 即用,运行时零外部 CDN / API 调用。 - A curated browse of 230 sites with sections, framework/theme filters, selection, and one-click copy. Fully self-contained — double-click `preview-gallery.html` to use; zero external CDN / API calls at runtime. -2. **高保真提示词 / High-fidelity prompts**:每个站点都生成了一份**混合提示词**——自然语言首屏描述 + 站点**真实的动画代码**(`@keyframes`、transition 缓动/时长、动画库名)。提示词里还叠加了从社区扒来的「高质量 UI 生成提示词」技巧。 - Each site gets a **hybrid prompt** — a natural-language above-the-fold description plus the site's **real animation code** (`@keyframes`, transition easing/duration, animation-library names). Community "high-quality UI prompt" techniques are layered in as well. -3. **AI 实机验证 / AI verification**:用 AI agent 把提示词重新生成单文件 HTML,与原始预览图逐帧对比,最多迭代 3 次,确认「首屏一致 + 动画一致」才标为通过。 - An AI agent regenerates the prompt into a single-file HTML, compares it frame-by-frame against the original preview, and iterates up to 3 times until "above-the-fold matches + animations match" before marking it passed. - -一句话:**这是一个让 AI 能学会「抄」顶级 UI 的提示词库 + 验证体系。** -In one line: **it's a prompt library + verification system that teaches AI to "copy" top-tier UI.** - ---- - -## 核心特性 / Key Features - -- **离线自包含 / Offline & self-contained** — 画廊 HTML 内嵌数据 JSON,预览图放本地 `previews/`,无需服务器、无需联网。The gallery HTML embeds the data JSON; preview images live in local `previews/`; no server, no network needed. -- **暗色高级风 UI / Dark premium UI** — 近黑背景 `#1b1b1f` + 径向渐变、金色 `#D4AF37` 强调、SF Pro 字体;卡片画框、毛玻璃 sticky 控制栏、滚动淡入上滑动效(缓动 `cubic-bezier(0.4,0,0.6,1)`)。Near-black `#1b1b1f` radial-gradient background, gold `#D4AF37` accent, SF Pro type; framed cards, frosted sticky controls, fade-and-rise scroll reveals (Apple easing). -- **画廊自身即用生成提示词改造 / The gallery eats its own dog food** — 本画廊的 UI 正是用其中一份获奖站高保真提示词(`awd-apple`)驱动重设计的。The gallery's own UI was restyled using one of its generated high-fidelity prompts (`awd-apple`). -- **分区浏览 / Sectioned browsing** — 组件库(177)与获奖项目(53)分块独立区域,默认全量摊开,纯滚动查看。Component libraries (177) and award projects (53) in separate areas, fully expanded by default, pure scroll. -- **多维筛选 / Multi-dimensional filters** — 类型切换(全部 / 组件库 / 获奖项目)、框架下拉、主题下拉,均显示各组卡片数量。Type switch (all / libraries / awards), framework dropdown, theme dropdown — each shows per-group card counts. -- **点选 + 持久化 / Select & persist** — 点击卡片选中(高亮 + ✓),选择存 localStorage,刷新不丢;右下角浮标显示已选数量。Click to select (highlight + ✓); selection persists in localStorage; bottom-right badge shows count. -- **复制提示词 / Copy prompt** — 每张卡片一键复制该站点高保真提示词,可直接丢给任意 AI UI 生成器。One click copies that site's high-fidelity prompt, ready to hand to any AI UI generator. -- **验证徽章 / Verification badges** — 每张卡片显示 AI 复现状态——`✓ 复现通过` / `✦ 动画还原` / `✕ 未复现` / `· 待验证`。Each card shows AI reproduction status — `✓ reproduced` / `✦ animation restored` / `✕ failed` / `· pending`. -- **动画感知 / Animation-aware** — 提示词直接包含站点真实动画规格,AI 生成结果经视觉对比核验动画一致。Prompts embed the site's real animation spec; generated results are visually checked for animation parity. - ---- - -## 在线预览 / Live Preview - -> **GitHub Pages**: -> -> 也可直接克隆后用浏览器打开本地 `preview-gallery.html`(推荐 Chrome / Edge)。 -> Or clone and open local `preview-gallery.html` directly (Chrome / Edge recommended). - ---- - -## 整体架构 / Architecture - -``` - ┌─────────────────────────────────────────┐ - 真实站点 / 获奖源码 │ 数据集与提示词管线 │ - (url / source/) │ Dataset & prompt pipeline │ - │ │ │ - ▼ │ build-dataset.js │ - ┌────────────┐ │ └─ 66 旧库 + 新增库 + 获奖项目 │ - │ 读取器 │ │ └─► preview-data.json (230 项) │ - │ repro-read │────────►│ repro-read2.js (动画感知) │ - │ repro-read2│ anim.json └─► 提取真实 @keyframes/transition│ - └────────────┘ │ │ - │ │ assemble-hifi.js │ - │ │ UNIVERSAL + 站点NL + 动画附录 │ - │ │ + web-prompts.md(WEB_EXTRA) │ - │ │ └─► repro//prompt.hifi.md │ - │ │ │ - │ │ merge-hifi.js ─► 写回 preview-data.json │ - ▼ └─────────────────────────────────────────┘ - previews/.png │ - ▲ gen-gallery.js - │ │ - ┌────────────┐ ┌─────────────────────▼──────────────────┐ - │ repro-shot │◄────────│ AI 验证闭环 (agent × N) │ - │ (本地截图) │ 对比 │ prompt.hifi.md → 单文件HTML → 截图 │ - └────────────┘ │ → 与 previews/.png 视觉对比 │ - ▲ │ → ≤3 次迭代 → result.hifi.json │ - │ └─────────────────────┬──────────────────┘ - │ │ - └─────────────────────────────────────┘ - agg-hifi.js ─► 写回 hifiPassed/animOk - └─► gen-gallery.js 重建 + 徽章 -``` - ---- - -## 项目结构 / Project Structure - -``` -ui-gallery/ -├── preview-gallery.html # 主画廊(自包含,数据内嵌 JSON + 本地预览图) -│ # Main gallery (self-contained, embedded JSON + local previews) -├── preview-data.json # 统一数据源(230 项,含 hifi 提示词与验证结果) -│ # Unified data source (230 entries, with hifi prompts & results) -├── gallery.template.html # 画廊模板,含 /*ITEMS*/ 占位符(卡片/徽章逻辑在此) -│ # Gallery template with /*ITEMS*/ placeholder (card/badge logic) -├── build-dataset.js # 数据集构建:旧库 + 新增库 + 获奖项目 → preview-data.json -│ # Dataset build: legacy + new libs + awards → preview-data.json -├── gen-gallery.js # 模板 + preview-data.json → 重建 preview-gallery.html -│ # Template + data → rebuild preview-gallery.html -│ -├── # —— AI 高保真复现管线 / AI high-fidelity reproduction pipeline —— -├── assemble-hifi.js # 组装混合高保真提示词(UNIVERSAL + NL + 动画附录 + WEB_EXTRA) -│ # Assemble hybrid hifi prompt (UNIVERSAL + NL + anim appendix + WEB_EXTRA) -├── merge-hifi.js # 把 prompt.hifi.md 合并写回 preview-data.json 的 prompt 字段 -│ # Merge prompt.hifi.md back into preview-data.json's prompt field -├── gen-hifi-batches.js # 把高保真站点分 12 个/批,供并行验证 -│ # Split hifi sites into batches of 12 for parallel verification -├── agg-hifi.js # 聚合所有 result.hifi.json 写回 preview-data.json(hifiPassed/animOk) -│ # Aggregate all result.hifi.json back into preview-data.json -├── repro-read2.js # 动画感知读取器:抓真实 @keyframes / 首屏动画计算样式 / 动画库 -│ # Animation-aware reader: real @keyframes / computed styles / libs -├── repro-shot.js # 把本地 HTML 按 1280×820 视口截图成 PNG(与参考图同视口) -│ # Screenshot local HTML at 1280×820 (same viewport as reference) -├── web-prompts.md # 社区优质 UI 生成提示词技巧(作为 WEB_EXTRA 注入每个提示词) -│ # Community UI-prompt techniques (injected as WEB_EXTRA) -├── hifi-agent-prompt.md # 验证 agent 的执行指令模板 -│ # Verification agent instruction template -│ -├── # —— 辅助 / 报告 / Aux & reports —— -├── test-functional.js # 端到端功能测试(Playwright + 系统 Chrome) -│ # End-to-end functional test (Playwright + system Chrome) -├── verify-gallery.js # 画廊验证脚本 / Gallery verification script -├── ui-components-hub.html # 66 库组件交叉索引(单文件)/ 66-lib cross-index (single file) -├── ui-awards-report.html # 获奖作品调研报告 + 提示词 / Awards research report + prompts -├── advanced-demo.html # Three.js + GSAP 高级 Demo / Advanced Three.js + GSAP demo -├── awards-source-index.html # 各届获奖源码索引 / Awards source index -├── repro-report.html # 早期「读→写提示词→重建→对比」闭环报告 / Early read→prompt→rebuild→compare report -│ -├── previews/ # 230 张 PNG 预览图(与 HTML 同级)/ 230 PNG previews -├── repro/ # 每站一个目录(细节见下)/ Per-site directory (details below) -│ ├── / -│ │ ├── read.json # 结构化首屏读取(230 站全有)/ Structured above-the-fold read -│ │ ├── anim.json # 动画感知数据(206 站)/ Animation data -│ │ ├── prompt.hifi.md # 混合高保真提示词(206 站)/ Hybrid hifi prompt -│ │ ├── build.vN.html # AI 生成的单文件重建版(验证产物)/ AI-built single-file rebuild -│ │ ├── build.vN.png # 重建版截图(与参考图对比用)/ Rebuild screenshot -│ │ └── result.hifi.json # 验证结果 {passed, animOk, attempts, notes} -│ └── hifi-batches.json # 17 批并行验证批次 / 17 parallel verification batches -├── hub/ # 零依赖 Node 服务(自动收录 source/ 获奖源码)/ Zero-dep Node service -│ ├── server.js # 静态服务 + 源码浏览器 API / Static server + source-browser API -│ ├── index.html -│ └── README.md -├── source/ # 6 个可运行获奖项目克隆(~632MB,未纳入仓库)/ 6 cloned award projects (~632MB, not in repo) -└── node_modules/ # 仅 playwright(运行截图/读取器需要)/ playwright only -``` - ---- - -## 数据集 / The Dataset - -`preview-data.json`(230 项)由 `build-dataset.js` 汇总生成: -`preview-data.json` (230 entries) is assembled by `build-dataset.js`: - -| 分区 / Section | 数量 / Count | 说明 / Description | -|---|---|---| -| 📦 UI 组件库与设计系统 / UI component libraries & design systems | 177 | React / Vue / Angular / Svelte / Solid / Qwik / Web Components / CSS / 大厂设计系统 / major-vendor design systems | -| 🏆 世界级获奖项目 / World-class award projects | 53 | Awwwards / FWA / CSSDA 等获奖网站,含可运行源码与真实预览 / award sites with runnable source & real previews | - -每条记录含:`id`、`name`、`vendor`、`fw`(框架 / framework)、`theme`(主题 / theme)、`link`、`repo`、`kind`(`item` / `proj`)、`img`(预览图路径 / preview path)、`chips`(组件标签 / component tags)、`prompt`(高保真提示词,207 项非空 / hifi prompt, 207 non-empty)、`hifiPassed` / `animOk`(验证结果 / verification results)。 - ---- - -## AI 高保真复现验证(项目灵魂)/ AI High-Fidelity Reproduction Verification - -普通「提示词库」给出的是自然语言描述,AI 生成结果往往「神似形不似」,动画更是对不上。本项目的关键差异在于:**提示词里直接塞了站点真实的动画代码,并且每一份提示词都经过 AI 实机复现验证。** -A typical "prompt library" gives natural-language descriptions; AI output is often "similar in spirit, not in form," and animations rarely match. The key difference here: **prompts embed the site's real animation code, and every prompt is verified by an AI reproducing it for real.** - -### 1. 提示词是怎么炼成的 / How prompts are forged - -`assemble-hifi.js` 把四部分拼成 `repro//prompt.hifi.md`: -`assemble-hifi.js` combines four parts into `repro//prompt.hifi.md`: - -1. **UNIVERSAL 指令(始终生效)/ UNIVERSAL directives (always on)**:单文件 HTML、真实 hex 颜色、真实文案禁占位、逐字抄动画、1280px 桌面优先、生产级质量等硬约束。Single-file HTML, exact hex colors, real copy (no placeholders), verbatim animation copying, 1280px desktop-first, production quality — hard constraints. -2. **站点自然语言提示词(bestNL)/ Site natural-language prompt**:首屏布局/配色/文案的 NL 描述。NL description of layout/color/copy. -3. **精确动画规格附录(`buildAnimAppendix`)/ Precise animation spec appendix**:来自 `repro-read2.js` 抓取的**真实** `@keyframes` 规则、首屏元素的 `animation` / `transition` 计算样式、检测到的动画库(GSAP / Framer / AOS / Lottie / Swiper / Canvas)。噪声 keyframe(加载圈、nprogress 等)会被 `isNoise()` 过滤。The **real** `@keyframes`, computed `animation`/`transition` styles, and detected libs (GSAP / Framer / AOS / Lottie / Swiper / Canvas) pulled by `repro-read2.js`. Noise keyframes (spinners, nprogress, …) are filtered by `isNoise()`. -4. **WEB_EXTRA**:从 `web-prompts.md` 注入——社区「AI 生成高保真 UI」提示词技巧(数值化约束、色彩系统、排版、间距、阴影、组件细节、动效、风格关键词、反模式)。Injected from `web-prompts.md` — community techniques for high-fidelity AI UI generation (numeric constraints, color systems, typography, spacing, shadows, component detail, motion, style keywords, anti-patterns). - -### 2. 验证闭环 / The verification loop - -每一份 `prompt.hifi.md` 交给 AI agent 执行: -Each `prompt.hifi.md` is handed to an AI agent: - -``` -读 prompt.hifi.md - → 写单文件 build.vN.html(CSS/JS 全内联)/ write single-file build.vN.html (inline CSS/JS) - → repro-shot.js 按 1280×820 截图 / screenshot at 1280×820 via repro-shot.js - → 与 previews/.png 视觉对比(首屏 + 动画)/ compare vs previews/.png (above-the-fold + animation) - → 不一致则修改 build.vN.html,最多迭代 3 次 / fix & re-iterate up to 3 times - → 写 repro//result.hifi.json { passed, animOk, attempts, lastBuild, notes } +```text +preview-data.json 画廊条目、来源链接、提示词及历史状态字段 +gallery.template.html 页面模板 +preview-gallery.html GitHub Pages 使用的生成页面 +previews/ 条目预览图 +scripts/ 构建、提取和数据维护工具 +tests/ Playwright 功能检查 +reports/ 早期目录与复现报告 +examples/ 独立页面示例 +hub/ 可选的本地源码浏览器 +docs/ 维护说明和验证范围 ``` -`gen-hifi-batches.js` 把带真实动画规格且本地有参考图的站点分成 12 个/批(共 17 批),供多个 agent 并行跑,验证幂等(已有 `result.hifi.json` 则跳过)。 -`gen-hifi-batches.js` splits sites with real animation specs and local references into batches of 12 (17 batches total) for parallel agents; verification is idempotent (skips if `result.hifi.json` exists). +## 构建与测试 -### 3. 结果聚合 / Result aggregation - -`agg-hifi.js` 读取全部 `result.hifi.json`,把 `hifiPassed` / `animOk` 写回 `preview-data.json`,随后 `gen-gallery.js` 重建画廊,卡片上即显示验证徽章。未参与验证的站点字段留空,画廊显示「待验证」,**不会误标失败**。 -`agg-hifi.js` reads all `result.hifi.json` and writes `hifiPassed` / `animOk` back into `preview-data.json`; `gen-gallery.js` then rebuilds the gallery with verification badges. Sites not yet verified leave the field empty and show "pending" — **never falsely marked failed.** - ---- - -## 使用指南 / Usage Guide - -### 浏览画廊 / Browse the gallery - -直接用浏览器打开 `preview-gallery.html`(推荐 Chrome / Edge)。预览图位于 `previews/`,与 HTML 同级。 -Open `preview-gallery.html` directly in a browser (Chrome / Edge recommended). Previews live in `previews/`, alongside the HTML. - -### 用提示词让 AI 重建某个 UI / Rebuild a UI from its prompt with AI - -1. 在画廊里找到目标卡片,点「复制提示词」。/ Find the target card and click "Copy prompt". -2. 把提示词丢给任意 AI UI 生成器(要求输出单文件 HTML)。/ Paste it into any AI UI generator (ask for a single-file HTML). -3. 生成结果的首屏与动画应与原站一致(动画规格是逐字抄的真实代码)。/ The result's above-the-fold and animations should match the original (animation spec is verbatim real code). - -### 运行管线脚本 / Run the pipeline - -环境要求:Node.js 22+,且安装 `playwright` + 系统 Chrome(读取器/截图器使用系统 Chrome 路径)。 -Requirements: Node.js 22+, `playwright` installed, and system Chrome (reader/screenshotter use the system Chrome path). +Node.js 22+。从仓库根目录执行: ```bash -# 1) 重新构建数据集(改完 build-dataset.js 后) -# 1) Rebuild the dataset (after editing build-dataset.js) -node build-dataset.js - -# 2) 组装高保真提示词(依赖 repro//anim.json 与 web-prompts.md) -# 2) Assemble hifi prompts (needs repro//anim.json and web-prompts.md) -node assemble-hifi.js -node merge-hifi.js # 合并写回 preview-data.json / merge back into preview-data.json - -# 3) 重建画廊 / Rebuild the gallery -node gen-gallery.js - -# 4) 分批 + 跑 AI 验证闭环(每批交给 agent 执行 hifi-agent-prompt.md) -# 4) Batch + run AI verification loop (each batch run by an agent per hifi-agent-prompt.md) -node gen-hifi-batches.js # 生成 repro/hifi-batches.json / produces repro/hifi-batches.json -# → 由 agent 读取 repro//prompt.hifi.md 执行闭环(见上文)/ agent reads prompt.hifi.md and runs the loop - -# 5) 聚合验证结果并重建带徽章的画廊 -# 5) Aggregate results and rebuild the badged gallery -node agg-hifi.js -node gen-gallery.js +npm ci +npm run build +npx playwright install chromium +npm test +npm run test:copy ``` ---- - -## 验证结果 / Verification Results - -| 指标 / Metric | 数值 / Value | -|---|---| -| 进入高保真复现验证的站点 / Sites entering hifi verification | 204 | -| AI 实机复现通过(`hifiPassed`)/ AI reproduction passed | **194 / 204(100%)** | -| 动画逐帧还原(`animOk`)/ Animation frame-restored | **191 / 194(98.5%)** | -| 复现失败 / Reproduction failures | **0** | -| 带高保真提示词的站点 / Sites with hifi prompt | 207 / 230 | -| 标注「待验证」/ Marked "pending" | 36(含尚未复现的站点,未误标失败 / includes not-yet-reproduced, not falsely failed) | - -画廊卡片徽章含义 / Badge meanings:`✓ 复现通过`(绿 / green)、`✦ 动画还原`(金 / gold)、`✕ 未复现`(红 / red)、`· 待验证`(灰 / grey)。 - -> 注:验证由 AI agent 在本地以系统 Chrome 实机截图对比完成,迭代上限 3 次。动画还原判定以首屏 `@keyframes` / transition 一致性为准。 -> Note: verification is done by AI agents locally with system Chrome, screenshot-compared, capped at 3 iterations. Animation parity is judged by above-the-fold `@keyframes` / transition consistency. - ---- - -## 脚本参考 / Script Reference - -| 脚本 / Script | 用途 / Purpose | -|---|---| -| `build-dataset.js` | 汇总组件库 + 获奖项目 → `preview-data.json` / Aggregate libs + awards → `preview-data.json` | -| `gen-gallery.js` | 模板 + 数据 → 重建自包含 `preview-gallery.html` / Template + data → rebuild self-contained gallery | -| `assemble-hifi.js` | 组装混合高保真提示词 → `repro//prompt.hifi.md` / Assemble hybrid hifi prompt | -| `merge-hifi.js` | 把 `prompt.hifi.md` 合并回 `preview-data.json` 的 `prompt` 字段 / Merge prompt back into data | -| `gen-hifi-batches.js` | 把高保真站点分 12/批供并行验证 → `repro/hifi-batches.json` / Batch hifi sites for parallel verify | -| `agg-hifi.js` | 聚合 `result.hifi.json` 写回 `hifiPassed` / `animOk` / Aggregate results back | -| `repro-read2.js` | 动画感知读取器:抓真实 `@keyframes` / 首屏动画 / 动画库 → `anim.json` / Animation-aware reader | -| `repro-shot.js` | 本地 HTML 按 1280×820 截图 → PNG / Screenshot local HTML at 1280×820 | -| `web-prompts.md` | 社区优质 UI 生成提示词技巧(WEB_EXTRA 注入位)/ Community UI-prompt techniques (WEB_EXTRA) | -| `hifi-agent-prompt.md` | 验证 agent 执行指令模板 / Verification agent instruction template | -| `test-functional.js` | Playwright 端到端功能测试 / Playwright e2e functional test | -| `verify-gallery.js` | 画廊渲染验证 / Gallery render verification | - ---- - -## 如何扩展数据集 / How to extend the dataset - -1. 在 `build-dataset.js` 的 `NEW` 数组(或获奖项目数组)追加条目,元组格式: - Append an entry to the `NEW` array (or awards array) in `build-dataset.js`, tuple format: - `[id, name, vendor, fw, theme, link, repo, license, chips]` -2. 运行 `node build-dataset.js` 重建 `preview-data.json`。/ Run `node build-dataset.js` to rebuild the data. -3. 生成该站预览图(`previews/.png`),并确保本地有可访问 URL 供读取器抓取动画。/ Generate its preview (`previews/.png`) and ensure a reachable URL for the animation reader. -4. 跑 `assemble-hifi.js` → `merge-hifi.js` → 把新站纳入 `gen-hifi-batches.js` 批次 → 交 agent 验证 → `agg-hifi.js` → `gen-gallery.js`。/ Run assemble → merge → include in batches → agent verify → aggregate → rebuild. - ---- - -## 本地源码浏览器(hub)/ Local source browser (hub) - -`hub/` 是一个零依赖 Node 服务,自动扫描 `source/` 里克隆的获奖源码,提供**真实代码预览**与**源码浏览器**(内容逐字节一致,不做改写)。 -`hub/` is a zero-dependency Node service that auto-scans cloned award source in `source/`, offering **real-code preview** and a **source browser** (byte-identical, unmodified). - -```bash -cd hub -node server.js # 默认端口 8200,可用 PORT=xxxx 覆盖 / default port 8200, override with PORT=xxxx -# 打开 http://127.0.0.1:8200 / open http://127.0.0.1:8200 -``` - -把任意获奖仓库浅克隆进 `../source/`,刷新页面即自动出现。接口与说明见 `hub/README.md`。 -Shallow-clone any award repo into `../source/` and it appears on refresh. API & details in `hub/README.md`. - ---- - -## 说明与限制 / Notes & Limitations - -- **`source/` 目录(约 632MB,含 6 个可运行获奖项目克隆)未纳入本仓库**(`.gitignore` 已排除)。如需本地源码浏览,按 `awards-source-index.html` 中的链接自行克隆。 - The `source/` directory (~632MB, 6 cloned award projects) is **not in this repo** (excluded by `.gitignore`). To browse source locally, clone per links in `awards-source-index.html`. -- 画廊运行时**完全离线**:数据内嵌、预览图本地、零 CDN / 外部 API 调用。 - The gallery is **fully offline at runtime**: embedded data, local previews, zero CDN / external API calls. -- 动画还原验证以首屏可见动画为准;部分站点需交互(悬停 / 滚动)才触发的动画,验证以「触发条件与表现一致」判定。 - Animation parity covers above-the-fold visible motion; interaction-triggered (hover/scroll) animations are judged by "same trigger + same behavior." -- 验证结果由 AI 在本地以系统 Chrome 实机生成对比,属尽力而为(best-effort),非像素级强制相等。 - Verification is AI-driven, locally screenshot-compared, best-effort — not pixel-enforced equality. - ---- - -## 相关链接 / Links - -- 🌐 在线画廊 / Live gallery: -- 📦 仓库 / Repo:`wangkeyu-u/ui-gallery`(GitHub Pages 自动部署 `main` 分支 / auto-deploys `main` via GitHub Pages) - ---- +也可设置 `CHROME_PATH` 使用已安装的 Chrome。构建只读取模板与已提交数据;不需要访问第三方站点。修改条目后,检查数据差异并重新生成页面。 ---- +功能检查覆盖卡片、图片、筛选、选择、持久化和复制。测试截图写入被忽略的 `.artifacts/`。它不评估提示词生成页面与原网站的视觉或动画一致程度。 -## License / 许可证 +## 维护与记录 -MIT +- [维护工具](docs/maintenance.md):各脚本的输入、输出和运行范围。 +- [历史验证范围](docs/validation.md):现有状态字段与材料限制。 +- [本地源码浏览器](hub/README.md):需自行准备未入库的 `source/` 目录。 +- [早期复现报告](reports/repro-report.html)和[获奖项目目录](reports/ui-awards-report.html)。 -

- 策展式 UI 画廊 · 让 AI 学会「抄」顶级 UI / A curated UI gallery that teaches AI to "copy" top-tier UI -

+条目的原始项目和素材归各自作者所有;来源链接保存在数据中。提示词和 AI 辅助生成结果需要审查,不能保证复制后得到与原站一致的实现。 diff --git a/_gen_hifi_batch5.js b/_gen_hifi_batch5.js deleted file mode 100644 index 5a48a01..0000000 --- a/_gen_hifi_batch5.js +++ /dev/null @@ -1,1320 +0,0 @@ -// Batch 5 hifi generator — produces build.hifi.v1.html for 11 sites. -const fs = require('fs'); -const path = require('path'); -const ROOT = '/Users/wangkeyu/WorkBuddy/2026-07-12-02-47-36'; - -const SEMANTIC_KF = `@-webkit-keyframes labeled-button-icon-loading { - 0% { - transform: translateY(-50%) rotate(0deg); - } 100% { - transform: translateY(-50%) rotate(360deg); - } -} -@-webkit-keyframes icon-loading { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@-webkit-keyframes loader { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@-webkit-keyframes placeholderShimmer { - 0% { - background-position: -1200px 0px; - } 100% { - background-position: 1200px 0px; - } -} -@-webkit-keyframes progress-active { - 0% { - opacity: 0.3; - width: 0px; - } 100% { - opacity: 0; - width: 100%; - } -} -@-webkit-keyframes browseIn { - 0% { - transform: scale(0.8) translateZ(0px); - z-index: -1; - } 10% { - transform: scale(0.8) translateZ(0px); - z-index: -1; - opacity: 0.7; - } 80% { - transform: scale(1.05) translateZ(0px); - opacity: 1; - z-index: 999; - } 100% { - transform: scale(1) translateZ(0px); - z-index: 999; - } -} -@-webkit-keyframes browseOutLeft { - 0% { - z-index: 999; - transform: translateX(0px) rotateY(0deg) rotateX(0deg); - } 50% { - z-index: -1; - transform: translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px); - } 80% { - opacity: 1; - } 100% { - z-index: -1; - transform: translateX(0px) rotateY(0deg) rotateX(0deg) translateZ(-10px); - opacity: 0; - } -} -@-webkit-keyframes browseOutRight { - 0% { - z-index: 999; - transform: translateX(0px) rotateY(0deg) rotateX(0deg); - } 50% { - z-index: 1; - transform: translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px); - } 80% { - opacity: 1; - } 100% { - z-index: 1; - transform: translateX(0px) rotateY(0deg) rotateX(0deg) translateZ(-10px); - opacity: 0; - } -} -@-webkit-keyframes dropIn { - 0% { - opacity: 0; - transform: scale(0); - } 100% { - opacity: 1; - transform: scale(1); - } -} -@-webkit-keyframes dropOut { - 0% { - opacity: 1; - transform: scale(1); - } 100% { - opacity: 0; - transform: scale(0); - } -} -@-webkit-keyframes fadeIn { - 0% { - opacity: 0; - } 100% { - opacity: 1; - } -} -@-webkit-keyframes fadeInUp { - 0% { - opacity: 0; - transform: translateY(10%); - } 100% { - opacity: 1; - transform: translateY(0px); - } -} -@-webkit-keyframes fadeInDown { - 0% { - opacity: 0; - transform: translateY(-10%); - } 100% { - opacity: 1; - transform: translateY(0px); - } -} -@-webkit-keyframes fadeInLeft { - 0% { - opacity: 0; - transform: translateX(10%); - } 100% { - opacity: 1; - transform: translateX(0px); - } -}`; - -const SPECTRE_KF = `@keyframes loading { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@keyframes slide-down { - 0% { - opacity: 0; - transform: translateY(-1.6rem); - } 100% { - opacity: 1; - transform: translateY(0px); - } -} -@keyframes carousel-slidein { - 0% { - transform: translateX(100%); - } 100% { - transform: translateX(0px); - } -} -@keyframes carousel-slideout { - 0% { - opacity: 1; - transform: translateX(0px); - } 100% { - opacity: 1; - transform: translateX(-50%); - } -} -@keyframes first-run { - 0% { - width: 0px; - } 25% { - width: 2.4rem; - } 50% { - width: 0.8rem; - } 75% { - width: 1.2rem; - } 100% { - width: 0px; - } -} -@keyframes progress-indeterminate { - 0% { - background-position: 200% 0px; - } 100% { - background-position: -200% 0px; - } -} -@keyframes sbx-reset-in { - 0% { - opacity: 0; - transform: translate3d(-20%, 0px, 0px); - } 100% { - opacity: 1; - transform: none; - } -}`; - -const FLUENT_KF = `@keyframes rb7n1on { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@keyframes r1gx3jof { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(-360deg); - } -} -@keyframes r15mim6k { - 0% { - transform: rotate(-135deg); - } 50% { - transform: rotate(0deg); - } 100% { - transform: rotate(225deg); - } -} -@keyframes r18vhmn8 { - 0% { - transform: rotate(0deg); - } 50% { - transform: rotate(105deg); - } 100% { - transform: rotate(0deg); - } -} -@keyframes rkgrvoi { - 0% { - transform: rotate(0deg); - } 50% { - transform: rotate(225deg); - } 100% { - transform: rotate(0deg); - } -} -@keyframes r109gmi5 { - 0% { - transform: rotate(135deg); - } 50% { - transform: rotate(0deg); - } 100% { - transform: rotate(-225deg); - } -} -@keyframes r17whflh { - 0% { - transform: rotate(0deg); - } 50% { - transform: rotate(-105deg); - } 100% { - transform: rotate(0deg); - } -} -@keyframes re4odhl { - 0% { - transform: rotate(0deg); - } 50% { - transform: rotate(-225deg); - } 100% { - transform: rotate(0deg); - } -} -@keyframes f1efwx7q { - 100% { - transform: translate(100%); - } -} -@keyframes f1kkgpz1 { - 100% { - transform: translate(-100%); - } -} -@keyframes f12o7gg6 { - 0% { - opacity: 1; - } 50% { - opacity: 0.4; - } 100% { - opacity: 1; - } -}`; - -const ALIGNUI_KF = `@keyframes cm-blink { - 50% { - opacity: 0; - } -} -@keyframes cm-blink2 { - 50% { - opacity: 0; - } -} -@keyframes flash-code { - 0% { - background-color: rgba(46, 46, 15, 0.18); - } 100% { - background-color: transparent; - } -} -@keyframes heroBlock { - 100% { - transform: translateZ(0px); - opacity: 1; - } -} -@keyframes heroDynamicIsland { - 100% { - filter: blur(0px); - opacity: 1; - } -} -@keyframes heroDynamicIslandExit { - 100% { - filter: blur(4px); - transform: translate3d(-50%, 0px, 0px) scale(0.95); - opacity: 0; - } -} -@keyframes heroTemplates { - 0% { - transform: translateZ(0px) scale(0.98); - filter: blur(4px); - opacity: 0.8; - } 100% { - transform: translateZ(0px) scale(1); - filter: blur(0px); - opacity: 1; - } -} -@keyframes spark { - 0% { - transform: translateY(-100%); - } 100% { - transform: translateY(var(--to,1000px)); - } -} -@keyframes enter { - 0% { - opacity: var(--tw-enter-opacity,1); - transform: translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0) scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1)) rotate(var(--tw-enter-rotate,0)); - } -} -@keyframes exit { - 100% { - opacity: var(--tw-exit-opacity,1); - transform: translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0) scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1)) rotate(var(--tw-exit-rotate,0)); - } -} -@keyframes rapid-b-item { - 100% { - opacity: 1; - transform: translateZ(0px); - filter: blur(0px); - } -} -@keyframes mobile-menu-open { - 0% { - opacity: 0; - transform: translate3d(-100%, 0px, 0px); - } 100% { - opacity: 1; - transform: translateZ(0px); - } -} -@keyframes mobile-menu-close { - 0% { - opacity: 1; - transform: translateZ(0px); - } 100% { - opacity: 0; - transform: translate3d(-100%, 0px, 0px); - } -} -@keyframes mobile-menu-item-in { - 100% { - opacity: 1; - filter: blur(0px); - transform: translateZ(0px); - } -}`; - -const BASEUI_KF = `@keyframes spin { - 100% { - transform: rotate(1turn); - } -}`; - -const KIBOUI_KF = `@keyframes ping { - 75%, 100% { - opacity: 0; - transform: scale(2); - } -} -@keyframes pulse { - 50% { - opacity: 0.5; - } -} -@keyframes fd-sidebar-in { - 0% { - transform: translateX(var(--fd-sidebar-drawer-offset)); - } -} -@keyframes fd-sidebar-out { - 100% { - transform: translateX(var(--fd-sidebar-drawer-offset)); - } -} -@keyframes fd-dialog-in { - 0% { - opacity: 0; - transform: scale(1.06); - } 100% { - transform: scale(1); - } -} -@keyframes fd-dialog-out { - 0% { - transform: scale(1); - } 100% { - opacity: 0; - transform: scale(1.04); - } -} -@keyframes fd-popover-in { - 0% { - opacity: 0; - transform: scale(0.9); - } -} -@keyframes fd-popover-out { - 100% { - opacity: 0; - transform: scale(0.9); - } -} -@keyframes fd-fade-in { - 0% { - opacity: 0; - } 100% { - opacity: 1; - } -} -@keyframes fd-fade-out { - 0% { - opacity: 1; - } 100% { - opacity: 0; - } -} -@keyframes fd-enterFromRight { - 0% { - opacity: 0; - transform: translate(200px); - } 100% { - opacity: 1; - transform: translate(0px); - } -} -@keyframes fd-enterFromLeft { - 0% { - opacity: 0; - transform: translate(-200px); - } 100% { - opacity: 1; - transform: translate(0px); - } -} -@keyframes fd-exitToRight { - 0% { - opacity: 1; - transform: translate(0px); - } 100% { - opacity: 0; - transform: translate(200px); - } -} -@keyframes fd-exitToLeft { - 0% { - opacity: 1; - transform: translate(0px); - } 100% { - opacity: 0; - transform: translate(-200px); - } -}`; - -const RESHAPED_KF = ``; - -// ---- Gallery card generator ---- -function galleryCard({ id, title, subtitle, accent, tags, keyframes, decor }) { - const tagsHtml = tags.map(t => `${t}`).join(''); - const html = ` - - - -${title} - - - -
-
-
${title}
${subtitle}
-
- - - -
- Badge -
Select
- ${decor || ''} -
-

Card title

This is a preview of the component look.

-
${tagsHtml}
-
- -`; - return html; -} - -const gallerySites = [ - { - id: 'semantic-ui', title: 'Semantic UI', subtitle: 'CSS · 语义化', accent: '#F59E0B', - tags: ['Button','Form','Input','Select','Checkbox','Radio','Table','Card','Label'], - keyframes: SEMANTIC_KF, - decor: `
` - }, - { - id: 'spectre', title: 'Spectre.css', subtitle: 'CSS · 轻量 / 现代', accent: '#6366F1', - tags: ['Button','Form','Input','Select','Checkbox','Radio','Switch','Table','Card'], - keyframes: SPECTRE_KF, - decor: `
` - }, - { - id: 'fluent-ds', title: 'Fluent 2 Design', subtitle: 'Multi · Microsoft 全平台', accent: '#EC4899', - tags: ['Button','Input','Select','Checkbox','Radio','Switch','Slider','Form','Table'], - keyframes: FLUENT_KF, - decor: `
` - }, - { - id: 'reshaped', title: 'Reshaped', subtitle: 'React · 现代产品 UI / 设计系统', accent: '#38BDF8', - tags: ['Button','Input','Select','Modal','Toast'], - keyframes: RESHAPED_KF, - decor: '' - }, - { - id: 'alignui', title: 'AlignUI', subtitle: 'React · 设计系统组件 / Figma', accent: '#38BDF8', - tags: ['Button','Input','Card','Tabs','Dialog'], - keyframes: ALIGNUI_KF, - decor: `$` - }, - { - id: 'baseui-primitives', title: 'Base UI', subtitle: 'React · 无样式原语 / Headless', accent: '#38BDF8', - tags: ['Menu','Select','Dialog','Popover','Tooltip'], - keyframes: BASEUI_KF, - decor: `
` - }, - { - id: 'kiboui', title: 'Kibo UI', subtitle: 'React · shadcn 风格区块', accent: '#38BDF8', - tags: ['Accordion','Carousel','Command','Calendar'], - keyframes: KIBOUI_KF, - decor: `Live` - } -]; - -gallerySites.forEach(s => { - const html = galleryCard(s); - fs.writeFileSync(path.join(ROOT, 'repro', s.id, 'build.hifi.v1.html'), html); - console.log('wrote', s.id); -}); - -// ---- Unique pages ---- -const ATLASSIAN_KF = `@keyframes k3f15u2 { - 0% { - opacity: 0.3; - } 50% { - opacity: 1; - } 100% { - opacity: 0.3; - } -} -@keyframes kpgn03l { - 0% { - transform: scale(0); - } 5.26316% { - transform: scale(1); - } 78.9474% { - transform: scale(1); - } 84.2105% { - transform: scale(0); - } 100% { - transform: scale(0); - } -} -@keyframes k1h9ztfy { - 0% { - transform: scale(0); - } 10.5263% { - transform: scale(0); - } 15.7895% { - transform: scale(1); - } 68.4211% { - transform: scale(1); - } 73.6842% { - transform: scale(0); - } 100% { - transform: scale(0); - } -} -@keyframes kjt05w7 { - 0% { - stroke-width: 0; - } 21.0526% { - stroke-width: 0; - } 26.3158% { - stroke-width: 133.5; - } 57.8947% { - stroke-width: 133.5; - } 63.1579% { - stroke-width: 0; - } 100% { - stroke-width: 0; - } -} -@keyframes k1k6o5g7 { - 0% { - transform: rotate(0deg); - } 31.5789% { - transform: rotate(0deg); - } 36.8421% { - transform: rotate(90deg); - } 47.3684% { - transform: rotate(90deg); - } 52.6316% { - transform: rotate(0deg); - } 100% { - transform: rotate(0deg); - } -} -@keyframes kgnpaw5 { - 0% { - opacity: 0; - } 100% { - opacity: 1; - } -} -@keyframes SlideInTop8px { - 0% { - transform: translateY(8px); - } 100% { - transform: translateY(0px); - } -} -@keyframes SlideInBottom8px { - 0% { - transform: translateY(-8px); - } 100% { - transform: translateY(0px); - } -} -@keyframes SlideInLeft8px { - 0% { - transform: translateX(8px); - } 100% { - transform: translateX(0px); - } -} -@keyframes SlideInRight8px { - 0% { - transform: translateX(-8px); - } 100% { - transform: translateX(0px); - } -} -@keyframes SlideOutTop8px { - 0% { - transform: translateY(0px); - } 100% { - transform: translateY(4px); - } -} -@keyframes SlideOutBottom8px { - 0% { - transform: translateY(0px); - } 100% { - transform: translateY(-4px); - } -} -@keyframes SlideOutLeft8px { - 0% { - transform: translateX(0px); - } 100% { - transform: translateX(4px); - } -} -@keyframes SlideOutRight8px { - 0% { - transform: translateX(0px); - } 100% { - transform: translateX(-4px); - } -}`; - -const atlassian = ` - - - -Atlassian Design - - - -
-
-
- - Atlassian Design -
- -
-
-
Better teamwork
by design
-
Tony
-
Kyah
-
Rovo
-
- -`; -fs.writeFileSync(path.join(ROOT, 'repro', 'atlassian', 'build.hifi.v1.html'), atlassian); -console.log('wrote atlassian'); - -const LIGHTNING_KF = `@keyframes go2264125279 { - 0% { - transform: scale(0) rotate(45deg); - opacity: 0; - } 100% { - transform: scale(1) rotate(45deg); - opacity: 1; - } -} -@keyframes go3020080000 { - 0% { - transform: scale(0); - opacity: 0; - } 100% { - transform: scale(1); - opacity: 1; - } -} -@keyframes go463499852 { - 0% { - transform: scale(0) rotate(90deg); - opacity: 0; - } 100% { - transform: scale(1) rotate(90deg); - opacity: 1; - } -} -@keyframes go1268368563 { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@keyframes go1310225428 { - 0% { - transform: scale(0) rotate(45deg); - opacity: 0; - } 100% { - transform: scale(1) rotate(45deg); - opacity: 1; - } -} -@keyframes go651618207 { - 0% { - height: 0px; - width: 0px; - opacity: 0; - } 40% { - height: 0px; - width: 6px; - opacity: 1; - } 100% { - opacity: 1; - height: 10px; - } -} -@keyframes go901347462 { - 0% { - transform: scale(0.6); - opacity: 0.4; - } 100% { - transform: scale(1); - opacity: 1; - } -} -@-webkit-keyframes token-inline-search-highlight-fade-out { - 0% { - background-color: transparent; - } 70% { - background-color: unset; - } 100% { - background-color: unset; - } -} -@-webkit-keyframes zheditor-search-highlight-fade-out { - 0% { - padding: 8px; - margin: -8px; - background-color: var(--colors-blocks-content-background-search-highlight,#ffea6a); - } 5% { - padding: 0px; - margin: 0px; - } 70% { - background-color: var(--colors-blocks-content-background-search-highlight,#ffea6a); - } 100% { - background-color: unset; - } -} -@-webkit-keyframes fEWCgj { - 0% { - transform: rotate(0deg); - } 100% { - transform: rotate(360deg); - } -} -@keyframes appcues-beacon-pulse { - 0% { - stroke-width: 22px; - transform: scale(0.1); - } 25% { - stroke-width: 0.5px; - transform: scale(1); - } 30% { - stroke-width: 0; - } 100% { - stroke-width: 0; - } -} -@keyframes ProseMirror-cursor-blink { - 100% { - visibility: hidden; - } -}`; - -const lightning = ` - - - -Lightning Design System 2 - - - - -
-
-
-
-
-
-
-
Lightning Design System 2
-
Summer '26 v2.9
-
Styleguide updated 20 days ago
-
Bring your brand to life.
-
-
-
-

Welcome to SLDS 2

-
A team organising stock in a storage room
-
-
- -`; -fs.writeFileSync(path.join(ROOT, 'repro', 'lightning', 'build.hifi.v1.html'), lightning); -console.log('wrote lightning'); - -const PRIMEREACT_KF = `@keyframes topbar-cta-text-shimmer { - 0% { - background-position: 130% center; - } 100% { - background-position: -130% center; - } -} -@keyframes px-fadein { - 0% { - opacity: 0; - } 100% { - opacity: 1; - } -} -@keyframes px-fadeout { - 0% { - opacity: 1; - } 100% { - opacity: 0; - } -} -@keyframes px-scalein { - 0% { - opacity: 0; - transition: transform 0.12s cubic-bezier(0, 0, 0.2, 1), opacity 0.12s cubic-bezier(0, 0, 0.2, 1); - transform: scaleY(0.8); - } 100% { - opacity: 1; - transform: scaleY(1); - } -} -@keyframes px-slidedown { - 0% { - max-height: 0px; - } 100% { - } -} -@keyframes px-slideup { - 0% { - max-height: 1000px; - } 100% { - max-height: 0px; - } -} -@keyframes px-animate-overlay-enter { - 0% { - opacity: 0; - transform: scale(0.93); - } -} -@keyframes px-animate-overlay-leave { - 100% { - opacity: 0; - transform: scale(0.93); - } -} -@keyframes p-features-order-animation { - 0% { - clip-path: polygon(0% 100%, 15% 100%, 32% 100%, 54% 100%, 70% 100%, 84% 100%, 100% 100%, 100% 100%, 0% 100%); - } 20% { - clip-path: polygon(0% 45%, 15% 44%, 32% 50%, 54% 60%, 70% 61%, 84% 59%, 100% 52%, 100% 100%, 0% 100%); - } 40% { - clip-path: polygon(0% 60%, 16% 65%, 34% 66%, 51% 62%, 67% 50%, 84% 45%, 100% 46%, 100% 100%, 0% 100%); - } 60% { - clip-path: polygon(0px 51%, 14% 31%, 33% 42%, 49% 53%, 68% 55%, 85% 50%, 100% 60%, 100% 100%, 0px 100%); - } 100% { - clip-path: polygon(0px 0px, 18% 0px, 39% 0px, 53% 0px, 62% 0px, 87% 0px, 100% 0px, 100% 100%, 0px 100%); - } -} -@keyframes pulse { - 50% { - opacity: 0.5; - } -} -@keyframes bounce { - 0%, 100% { - animation-timing-function: cubic-bezier(0.8, 0, 1, 1); - transform: translateY(-25%); - } 50% { - animation-timing-function: cubic-bezier(0, 0, 0.2, 1); - transform: none; - } -} -@keyframes slideright { - 0% { - inset-inline: -35% 100%; - } 60% { - inset-inline: 100% -90%; - } 100% { - inset-inline: 100% -90%; - } -} -@keyframes star-rotate { - 0% { - transform: rotateY(0deg); - } 100% { - transform: rotateY(360deg); - } -} -@keyframes p-animate-collapsible-expand { - 0% { - grid-template-rows: 0fr; - } 100% { - grid-template-rows: 1fr; - } -} -@keyframes whouses-marquee { - from { - transform: translateX(0); - } to { - transform: translateX(-50%); - } -}`; - -const primereact = ` - - - -PrimeReact - - - - -
-
PRIMEREACT
- -
- - - - - -
-
-
-
primereact@11.0.0-rc.1
-
Premium UI Suite for React
-

Enhance your web applications with PrimeReact's comprehensive suite of customizable, feature-rich UI components.

-
- - -
-
-
- ✦ MercedesebayFord - ✦ MercedesebayFord -
-
-
-
-
Overview
Portfolio command
-
Live
-
-
- -`; -fs.writeFileSync(path.join(ROOT, 'repro', 'primereact', 'build.hifi.v1.html'), primereact); -console.log('wrote primereact'); - -const TAILARK_KF = `@keyframes enter { - 0% { - opacity: var(--tw-enter-opacity,1); - transform: translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0)); - } -} -@keyframes exit { - 100% { - opacity: var(--tw-exit-opacity,1); - transform: translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0)); - } -} -@keyframes pulse { - 50% { - opacity: 0.5; - } -} -@keyframes accordion-down { - 0% { - height: 0px; - } 100% { - height: var(--radix-accordion-content-height); - } -} -@keyframes accordion-up { - 0% { - height: var(--radix-accordion-content-height); - } 100% { - height: 0px; - } -} -@keyframes source-to-logo { - 0% { - stroke-dashoffset: 400px; - } 100% { - stroke-dashoffset: 0; - } -} -@keyframes logo-to-outputs { - 0% { - stroke-dashoffset: 550px; - } 10% { - stroke-dashoffset: 400px; - } 100% { - stroke-dashoffset: 0; - } -}`; - -const tailark = ` - - - -tailark - - - - -
- 13 new pages added to the Grid-2 style -

Build modern marketing
websites with Shadcn blocks.

-
- - -
-
QuartzDuskMistVeil
-
-
-
-
-

The Financial OS powering businesses on your platform

-

Unify payments, ledger and reporting into one composable surface.

- Get started -
-
-
-
-
-
-
Logo Cloud
-
10 blocks
-
-
-
Features
14 blocks
- -
-
Pricing
8 blocks
-
-
-
- -`; -fs.writeFileSync(path.join(ROOT, 'repro', 'tailark', 'build.hifi.v1.html'), tailark); -console.log('wrote tailark'); - -console.log('ALL DONE'); diff --git a/_results_batch5.js b/_results_batch5.js deleted file mode 100644 index 2a42d01..0000000 --- a/_results_batch5.js +++ /dev/null @@ -1,40 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const ROOT = '/Users/wangkeyu/WorkBuddy/2026-07-12-02-47-36'; - -const results = { - 'semantic-ui': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card rebuilt with exact hex colors, real copy, component showcase and tag pills; @keyframes pasted verbatim; layout/colors/components match preview.' }, - 'spectre': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with indigo accent (#6366F1), real tags and copy; @keyframes pasted verbatim; visual match.' }, - 'atlassian': { passed: true, animOk: true, attempts: 1, notes: 'Dot-grid landing page with header, headline and Tony/Kyah/Rovo lozenges; keyframes (kgnpaw5 etc.) verbatim.' }, - 'lightning': { passed: true, animOk: true, attempts: 1, notes: 'SLDS 2 docs layout with sidebar, gradient hero banner, coral orbs and Welcome photo placeholder; keyframes verbatim.' }, - 'fluent-ds': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with pink/magenta accent (#EC4899); spinner uses real rb7n1on keyframe; all tags and keyframes verbatim.' }, - 'primereact': { passed: true, animOk: true, attempts: 1, notes: 'PrimeReact homepage with banner, header, hero, marquee logo row and card preview; brand logos approximated via CSS/text; keyframes verbatim.' }, - 'reshaped': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with cyan accent (#38BDF8); no keyframes required by prompt; colors/copy/components match.' }, - 'alignui': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with cyan accent; blinking cursor uses real cm-blink keyframe; all keyframes pasted verbatim.' }, - 'baseui-primitives': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with cyan accent; spinner uses real spin keyframe; tags and colors match.' }, - 'kiboui': { passed: true, animOk: true, attempts: 1, notes: 'macOS gallery card with cyan accent; live dot uses real ping keyframe; all keyframes verbatim.' }, - 'tailark': { passed: true, animOk: true, attempts: 1, notes: 'Tailark marketing landing page with nav, hero badge, gradient headline, style tabs and bento grid; screenshot cards approximated with CSS shapes; keyframes verbatim.' }, - 'aceternity': { passed: true, animOk: true, attempts: 1, notes: 'skipped - result.hifi.json already exists.' } -}; - -for (const [id, r] of Object.entries(results)) { - const json = { - id, - url: '', - attempts: r.attempts, - passed: r.passed, - animOk: r.animOk, - lastBuild: `repro/${id}/build.hifi.v1.html`, - notes: r.notes - }; - fs.writeFileSync(path.join(ROOT, 'repro', id, 'result.hifi.json'), JSON.stringify(json, null, 2)); -} - -const lines = ['# Batch 5 hifi summary', '', `Generated: ${new Date().toISOString()}`, '', '| id | passed | animOk | attempts | notes |', '|----|--------|--------|----------|-------|']; -for (const [id, r] of Object.entries(results)) { - lines.push(`| ${id} | ${r.passed} | ${r.animOk} | ${r.attempts} | ${r.notes} |`); -} -lines.push(''); - -fs.writeFileSync(path.join(ROOT, 'repro', 'SUMMARY.hifi.5.md'), lines.join('\n')); -console.log('results and summary written'); diff --git a/awards-source-index.html b/awards-source-index.html deleted file mode 100644 index e788b13..0000000 --- a/awards-source-index.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - -Awwwards 各届前五名 · 可运行源码索引 - - - -
-
-

Awwwards 各届前五名 · 可运行源码索引

-

按年份(届)列出 Awwwards 年度大奖 Top 5 获奖设计,并为每一项标注「可被你直接使用的源码」状态:官方开源(获奖方亲自放出)、本地已克隆的可运行复刻(本仓库 source/ 内已下载,开箱即跑)、闭源(附同类可运行模板/教程链接)。

-
- ✅ 官方开源(直接用) - 🟢 本地已克隆可运行复刻 - 🔗 闭源·附同类模板 -
-
- - -

2025 年度大奖 Top 5

-

Awwwards Annual Awards 2025

- - - - - - - - -
奖项作品 / 工作室源码状态可用源码(点击用)
Site of the YearLando Norris — OFF+BRAND.PRO闭源原站/提名 · 同类:2025-adidas 克隆
Developer SOTYMessenger — abeto闭源提名页
E-commerce SOTYScout Motors闭源同类 3D 商品页:2025-adidas 克隆
Agency SOTYImmersive Garden闭源机构官网 · WebGL 拆解见报告
Studio SOTYMalvah闭源同类模板:作品集模板
🏆 官方开源彩蛋Bruno Simon Portfolio (SOTY 2025)官方开源github.com/brunosimon/folio-2025
- - -

2024 年度大奖 Top 5

-

Awwwards Annual Awards 2024 · Sites of the Year

- - - - - - - -
奖项作品 / 工作室源码状态可用源码
Site of the YearIgloo Inc — abeto闭源同类 3D:Atmos 克隆
Site of the YearDon't Board Me — The First The Last闭源同类模板:作品集模板
Site of the YearOpal Tadpole — Claudio Guglieri闭源同类:Gentlerain 克隆
🟢 已克隆Gentlerain.ai(2024 Winner)本地克隆source/2024-gentlerain/ (纯静态,开箱即跑)
notable 2024Wix Studio × Pantone / Mammut闭源同类:Obys 克隆
- - -

2023 年度大奖 Top 5

-

Awwwards Annual Awards 2023

- - - - - - - -
奖项作品 / 工作室源码状态可用源码
Site of the YearLusion v3 — Lusion闭源同类 R3F:Atmos 克隆
E-commerce SOTYNoomo Agency — Noomo闭源同类:Obys 克隆
Studio SOTYObys本地克隆source/2023-obys/ (GSAP+Lenis+Shery.js)
Agency SOTYLocomotive闭源同类:Two-Good-Co 克隆
🟢 已克隆 (R3F)Atmos(2023 SOTD 风格)本地克隆source/2023-atmos/
- - -

2022 年度大奖 Top 5

-

Awwwards Annual Awards 2022 · Sites of the Year

- - - - - - - -
奖项作品 / 工作室源码状态可用源码
Site of the YearKPR — Resn闭源同类:Atmos 克隆
Site of the YearThe Other Side of Truth — The First The Last闭源同类:Obys 克隆
Site of the YearPersepolis Reimagined — Monks闭源同类:Gentlerain 克隆
Studio SOTYSynchronized闭源同类:Two-Good-Co 克隆
🟢 已克隆Two-Good-Co(2022 精选风格)本地克隆source/2021-two-good-co/
- - -

2021 年度大奖 Top 5

-

Awwwards Annual Awards 2021

- - - - - - - -
奖项作品 / 工作室源码状态可用源码
Site of the YearPrometheus Fuels — Resn / Active Theory闭源同类:Atmos 克隆
Developer SOTYUmami Land — MediaMonks闭源同类:作品集模板
Studio SOTYGreen Chameleon闭源同类:Two-Good-Co 克隆
Agency SOTYLocomotive闭源同类:Obys 克隆
IndependentLuis Henrique Bizarro闭源同类:作品集模板
- - -

2019 年度大奖 Top 5(含官方开源标杆)

-

Awwwards Annual Awards 2019

- - - - - - - -
奖项作品 / 工作室源码状态可用源码
Site of the YearNomadic Tribe — Makemepulse闭源同类:Obys 克隆
🏆 SOTY + Dev SOTYBruno Simon Portfolio官方开源github.com/brunosimon/folio-2019
Studio SOTYAdoratorio闭源同类:Atmos 克隆
Agency SOTYLocomotive闭源同类:Two-Good-Co 克隆
E-commerce SOTYRetail 710 — MA闭源同类:Adidas 克隆
- - -

🟢 本地已克隆 · 可运行源码库

-

以下源码已下载到本项目的 source/ 目录,直接用对应命令即可跑起来。

-
-
- 20 MB -
2024-gentlerain
-
对应:Gentlerain.ai(Awwwards 2024 Winner)· 纯静态,零构建
-
cd source/2024-gentlerain
-python3 -m http.server 8081
-# 浏览器打开 http://localhost:8081
-
-
- 159 MB -
2021-two-good-co
-
对应:Two-Good-Co(极简平滑滚动风格)· 纯静态
-
cd source/2021-two-good-co
-python3 -m http.server 8082
-# 打开 http://localhost:8082
-
-
- 49 MB -
2023-obys
-
对应:Obys Agency(Studio SOTY 2023)· GSAP + Lenis + Shery.js
-
cd source/2023-obys
-python3 -m http.server 8083
-# 打开 http://localhost:8083
-
-
- 2.4 MB -
2023-atmos
-
对应:Atmos(R3F 3D 渐变)· React Three Fiber + Vite
-
cd source/2023-atmos
-npm install
-npm run dev      # 或 npm run build 后 preview
-
-
- 53 MB -
template-portfolio
-
通用 3D 作品集模板 · React + Vite + GSAP + R3F(已验证可 build)
-
cd source/template-portfolio
-npm install
-npm run dev      # ✓ 已实测 npm run build 通过
-
-
- 76 MB -
2025-adidas
-
对应:Adidas 3D 商品落地页(2025 风格)· Next.js + R3F
-
cd source/2025-adidas
-npm install
-npm run dev      # next dev --turbopack
-
-

说明:3 个「官方开源」获奖作品(Bruno Simon folio-2019 / folio-2025、以及各闭源站)未全部克隆,以节省体积——它们均提供 GitHub 链接,git clone 即可获得完整源码。本地 6 个仓库已做浅克隆(--depth 1),体积合计约 360 MB。

-
- -
索引生成于 2026-07-12 · 源码均为公开仓库(MIT/各自许可证)· 商用前请确认原作品授权
-
- - diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 0000000..b9a6429 --- /dev/null +++ b/docs/maintenance.md @@ -0,0 +1,32 @@ +# 数据维护工具 + +所有命令从仓库根目录运行。`npm run build` 是日常构建入口;不会重抓页面或更改条目。 + +| 工具 | 输入 | 输出/影响 | +| --- | --- | --- | +| `scripts/gen-gallery.js` | 模板、preview-data.json | preview-gallery.html | +| `scripts/build-dataset.js` | 早期 reports/ui-components-hub.html、脚本内条目、可选 repro 记录 | 重建 preview-data.json,可能覆盖后续编辑 | +| `scripts/merge-prompts.js` / `merge-hifi.js` | repro 下的提示词 | 更新数据中的 prompt | +| `scripts/agg-hifi.js` | repro 下的 result.hifi.json | 汇总历史结果字段,不自行执行视觉验收 | +| `scripts/assemble-hifi.js` | repro/accessible.json、anim.json、已有 NL 提示词、docs/prompt-guidelines.md | 组装 prompt.hifi.md、need-nl.json | +| `scripts/gen-hifi-batches.js` | 已提交提示词和预览图 | repro/hifi-batches.json,供人工安排评估 | +| `scripts/repro-read.js` / `repro-read2.js` | 单个 URL、输出文件路径 | 网页/动画/交互的观测记录,需要联网 | +| `scripts/read2-core.js` | 被读取器调用 | 共用提取模块,不是独立 CLI | +| `scripts/repro-shot.js` / `repro-shot-anim.js` | 本地 HTML、输出路径 | 静态或动画截图 | +| `scripts/run-anim-extract.js` | 并发数、可选清单路径 | 批量 anim.json,需要联网 | +| `scripts/rerun-read2.js` | 已有 anim.json 的条目 | 重新观测;失败时保留之前成功记录 | +| `scripts/gen-real-shots.js` | 条目 URL | 更新 previews/;空白时可能生成信息卡 | +| `scripts/gen-report.js` | repro/_rows.json | reports/repro-report.html | + +`repro/accessible.json` 是用户准备的 `[{"id":"…","url":"https://…"}]` 清单,`repro/`、`source/` 与测试产物不提交。批量观测只应使用有权限访问的站点;本次结构清理未执行网络观测或重抓预览图。 + +示例: + +```bash +mkdir -p repro/example +node scripts/repro-read2.js https://example.com repro/example/anim.json +node scripts/assemble-hifi.js repro/accessible.json +node scripts/run-anim-extract.js 4 repro/accessible.json +``` + +浏览器工具默认使用 Playwright Chromium,可用 `CHROME_PATH` 指定现有 Chrome。更新数据、历史字段或预览图后,应先审查差异,再重建画廊。状态字段来源于输入记录,不能代替人工核验。 diff --git a/web-prompts.md b/docs/prompt-guidelines.md similarity index 100% rename from web-prompts.md rename to docs/prompt-guidelines.md diff --git a/docs/validation.md b/docs/validation.md new file mode 100644 index 0000000..8ed75b2 --- /dev/null +++ b/docs/validation.md @@ -0,0 +1,13 @@ +# 验证范围 + +## 当前功能检查 + +`npm test` 检查本地画廊的卡片数量、图片加载、筛选、选择、localStorage、复制和控制台错误。`npm run test:copy` 检查单卡复制入口。这些检查不测量第三方网站或 AI 重建的动画质量。 + +## 历史复现记录 + +旧 README 报告 204 项进入评估、194 项标为复现通过、其中 191 项标为动画还原。`preview-data.json` 保留原有状态、失败记录、备注和提示词;本次清理没有重新判定这些状态。194/204 约为 95.1%,旧文档括号中的 100% 是错误表述。 + +完整逐站 `repro/` 原始输入、生成页面、迭代截图、独立评分标准和验收记录没有入库。删除的一次性批次脚本还包含硬编码的 passed 状态。因此历史字段不能证明所有条目都完成了可复核的逐帧视觉验收。页面徽章标明“历史”,仅展示已有数据。 + +[早期复现报告](../reports/repro-report.html)保留其原始结果。要发布新的质量结论,需要另外保存输入版本、生成页面、对比材料、评分方法和失败案例。 diff --git a/advanced-demo.html b/examples/advanced-demo.html similarity index 100% rename from advanced-demo.html rename to examples/advanced-demo.html diff --git a/gallery.template.html b/gallery.template.html index 76b750f..73309dc 100644 --- a/gallery.template.html +++ b/gallery.template.html @@ -265,7 +265,7 @@

UI Gallery

const chipsArr=isProj?(i.tech||[]):(i.chips||[]); const chipsHtml=chipsArr.slice(0,6).map(c=>''+c+'').join(''); const num=String(n).padStart(2,'0'); - const vbadge=(!i.prompt)?'':(i.hifiPassed===true?('✓ 复现通过'+(i.animOk===true?'✦ 动画还原':'')):(i.hifiPassed===false?'✕ 未复现':'· 待验证')); + const vbadge=(!i.prompt)?'':(i.hifiPassed===true?('✓ 历史:通过'+(i.animOk===true?'✦ 历史:动画':'')):(i.hifiPassed===false?'✕ 历史:失败':'· 待验证')); return '
'+ '
'+ ''+(isProj?'获奖':'库')+''+ diff --git a/gen-batch16.js b/gen-batch16.js deleted file mode 100644 index 124f1fb..0000000 --- a/gen-batch16.js +++ /dev/null @@ -1,286 +0,0 @@ -// gen-batch16.js — Generate all 12 batch-16 Awwwards gallery card HTML files -const fs = require('fs'); -const path = require('path'); - -// ── Per-site configuration (calibrated against real preview screenshots) ── -const sites = [ - // ── Family A: trophy top-left, title ~72px ── - { - id: 'awd-balenciaga', - family: 'A', // trophy top-left - bg: 'radial-gradient(140% 140% at 0% 0%, #0a0a0a 45%, #9ca3af 120%)', - title: 'Balenciaga', subtitle: 'Balenciaga · 2022', - pillBg: '#9ca3af', pillColor: '#fff', pillText: 'Awwwards', - theme: '奢侈', - tech: 'WebGL', techBg: 'rgba(0,0,0,0.45)', techBorder: 'none', - awardBorder: 'rgba(255,255,255,0.6)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-louisvuitton', - family: 'A', - bg: 'radial-gradient(140% 140% at 0% 0%, #0a0a0a 45%, #c9a227 120%)', - title: 'Louis Vuitton', subtitle: 'Louis Vuitton · 2023', - pillBg: '#c9a227', pillColor: '#fff', pillText: 'Awwwards', - theme: '奢侈', - tech: 'Three.js', techBg: 'rgba(0,0,0,0.45)', techBorder: 'none', - awardBorder: 'rgba(255,255,255,0.6)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-animade', - family: 'A', - bg: 'radial-gradient(130% 130% at 100% 0%, rgba(244,114,182,0.5) -10%, rgba(244,114,182,0) 55%), #0a0a0a', - title: 'Animade', subtitle: 'Animade · 2021', - pillBg: '#f472b6', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '创意', - tech: 'GSAP', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-unseen', - family: 'A', - bg: 'radial-gradient(130% 130% at 100% 0%, rgba(167,139,250,0.5) -10%, rgba(167,139,250,0) 55%), #0a0a0a', - title: 'Unseen', subtitle: 'Unseen · 2023', - pillBg: '#a78bfa', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '机构', - tech: 'WebGL', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-haus', - family: 'A', - bg: 'radial-gradient(140% 140% at 50% 0%, rgba(245,158,11,0.45) -20%, rgba(245,158,11,0) 60%), #0a0a0a', - title: 'Haus', subtitle: 'Haus · 2023', - pillBg: '#f59e0b', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '品牌', - tech: 'Three.js', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-buck', - family: 'A', - bg: 'radial-gradient(130% 130% at 100% 0%, rgba(96,165,250,0.5) -10%, rgba(96,165,250,0) 55%), #0a0a0a', - title: 'Buck', subtitle: 'Buck · 2022', - pillBg: '#60a5fa', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '创意', - tech: 'WebGL', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-raggededge', - family: 'A', - bg: 'radial-gradient(130% 130% at 100% 0%, rgba(52,211,153,0.5) -10%, rgba(52,211,153,0) 55%), #0a0a0a', - title: 'Ragged Edge', subtitle: 'Ragged Edge · 2022', - pillBg: '#34d399', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '品牌', - tech: 'Web', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - { - id: 'awd-studio-lumio', - family: 'A', - bg: 'radial-gradient(130% 130% at 100% 0%, rgba(52,211,153,0.5) -10%, rgba(52,211,153,0) 55%), #0a0a0a', - title: 'Studio Lumio', subtitle: 'Lumio · 2023', - pillBg: '#34d399', pillColor: '#1a1a1a', pillText: 'Awwwards', - theme: '工作室', - tech: 'Three.js', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.4)', awardText: '#fff', - contentLeft: 100, - }, - - // ── Family B: NO trophy, title ~46px ── - { - id: 'awd-apple', - family: 'B', - bg: '#1b1b1f', - title: 'Apple', subtitle: 'Apple · 2023', - pillBg: 'rgba(255,255,255,0.12)', pillColor: '#eee', pillText: 'Awwwards', - theme: '产品', - tech: 'WebGL', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.25)', awardText: '#ccc', - contentLeft: 64, awardSmall: true, - }, - { - id: 'awd-instrument', - family: 'B', - bg: 'linear-gradient(135deg, #1a1e2e 0%, #12151f 100%)', - title: 'Instrument', subtitle: 'Instrument · 2022', - pillBg: '#3b82f6', pillColor: '#fff', pillText: 'Awwwards', - theme: '数字代理', - tech: 'WebGL', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.22)', awardText: '#bbb', - contentLeft: 64, awardSmall: true, - }, - { - id: 'awd-field', - family: 'B', - bg: 'linear-gradient(135deg, #0f2027 0%, #1a3a3a 50%, #142a2a 100%)', - title: 'Field.io', subtitle: 'Field · 2022', - pillBg: '#06b6d4', pillColor: '#fff', pillText: 'Awwwards', - theme: '创意', - tech: 'Three.js', techBg: 'rgba(255,255,255,0.08)', techBorder: '1px solid rgba(255,255,255,0.15)', - awardBorder: 'rgba(255,255,255,0.22)', awardText: '#bbb', - contentLeft: 64, awardSmall: true, - }, - { - id: 'awd-verve', - family: 'B', - bg: '#1a1035', - title: 'Verve', subtitle: 'Verve · 2023', - pillBg: '#7b5cd0', pillColor: '#fff', pillText: 'Awwwards', - theme: '音乐', - tech: 'WebGL', techBg: '#251540', techBorder: '1px solid rgba(139,92,208,0.25)', - awardBorder: '#9080b0', awardText: '#9080b0', - contentLeft: 80, awardSmall: true, - }, -]; - -function extractKeyframes(id) { - const promptPath = `repro/${id}/prompt.hifi.md`; - if (!fs.existsSync(promptPath)) return ''; - const prompt = fs.readFileSync(promptPath, 'utf8'); - // Match @keyframes blocks (including -webkit- variants) in the precise animation spec - const re = /真实\s*@keyframes[^`]*```css\s*\n([\s\S]*?)```/g; - let cssBlocks = []; - let m; - while ((m = re.exec(prompt)) !== null) { - cssBlocks.push(m[1]); - } - return cssBlocks.join('\n'); -} - -function generateHTML(s, kfCSS) { - const isA = s.family === 'A'; - const isB = s.family === 'B'; - const smallAward = s.awardSmall; - const trophySize = isA ? 64 : 48; // B doesn't show trophy but we keep config - - return ` - - - - -${s.title} — Gallery Card - - - -
- -${isA ? `
- -
` : ''} - -
- -AWARD -
- -
-

${s.title}

-

${s.subtitle}

-${s.pillText} -

${s.theme}

-
- -
${s.tech}
- -
- -`; -} - -// ── Main: generate all sites ── -for (const s of sites) { - const dir = `repro/${s.id}`; - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - const kf = extractKeyframes(s.id); - const html = generateHTML(s, kf); - const outPath = `${dir}/build.hifi.v1.html`; - fs.writeFileSync(outPath, html, 'utf8'); - console.log(`✓ ${s.id} (${kf.length > 0 ? 'kf:' + (kf.match(/@keyframes/g)||[]).length + ' keyframes' : 'no keyframes'}) → ${outPath}`); -} -console.log('\nDone: all 12 HTML files generated.'); diff --git a/hifi-agent-prompt.md b/hifi-agent-prompt.md deleted file mode 100644 index f6f9b31..0000000 --- a/hifi-agent-prompt.md +++ /dev/null @@ -1,48 +0,0 @@ -# Hifi Repro Worker — 子代理工作指令 - -你是一个「提示词复现验证」工人。项目根目录: `/Users/wangkeyu/WorkBuddy/2026-07-12-02-47-36`。 -你的任务:对分配到的若干 UI 站点,**仅用其高保真提示词**让 AI(你自己)重建首屏 HTML,再与真实站点截图对比,判断是否「生成了一样的 UI(含动画)」。 - -## 运行环境(重要,务必照用) -- Node(托管版): `/Users/wangkeyu/.workbuddy/binaries/node/versions/22.22.2/bin/node` -- 系统 Chrome: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` -- Playwright 已装在项目 `node_modules/`(用系统 Chrome,不是下载的 chromium)。 -- 截图脚本(已存在): `node repro-shot.js "" ""` → 生成 1280×820 截图。 - -## 输入 -- 读取 `repro/hifi-batches.json`,只处理 `index === <你的批次号>` 的那一项里的 `sites`。 -- 每个 site: `{ id, url, target, promptFile }`。 - -## 对每个 site 执行(幂等:若 `repro//result.hifi.json` 已存在则跳过) -1. **读提示词**: `Read repro//prompt.hifi.md`(这是唯一输入——不得再读真实站点源码/ read.source.html,那是作弊)。 -2. **生成重建**:用 Write 写 `repro//build.hifi.v1.html`——**单个自包含 HTML 文件**(内联 CSS/JS,无构建步骤,浏览器直接打开): - - 严格照抄提示词里的真实 hex 颜色、字体栈、文案、布局结构; - - **逐字包含**「精确动画规格」段里的真实 `@keyframes` / `transition` / 动画库代码(时长/缓动/延迟不得改写); - - 1280px 桌面首屏优先;生产级质量(无报错、无横向溢出)。 -3. **截图**: `node repro-shot.js "绝对路径/repro//build.hifi.v1.html" "绝对路径/repro//build.hifi.v1.png"`。 -4. **视觉对比**:用 Read 同时打开 `repro//build.hifi.v1.png` 与 `target`(真实站点截图),判断: - - 布局/区块结构是否一致;真实 hex 颜色是否一致;文案是否一致;关键组件(导航/hero/按钮/卡片)是否都在。 - - 动画:检查 build.hifi.v1.html 里是否真的包含了提示词给的 `@keyframes` 名称与 transition 简写(用 Grep)。 -5. **迭代(≤3 次)**:若明显不一致,基于**同一提示词**自我修正 build(不要改提示词),生成 v2/v3 并重新截图对比。超过 3 次停止。 -6. **写结果**: `repro//result.hifi.json`: - ```json - { - "id": "", - "url": "", - "attempts": <实际生成次数 1..3>", - "passed": , - "animOk": , - "lastBuild": "repro//build.hifi.v<最佳版本>.html", - "notes": "<一行: 差异点 / 动画是否还原 / 不可达等>" - } - ``` - - 若提示词文件缺失或 target 缺失,写 `passed:false, attempts:0, notes:"missing input"`。 - -## 收尾 -- 处理完本批所有 site 后,写 `repro/SUMMARY.hifi.<批次号>.md`,列出每站 passed/animOk/attempts/notes。 -- 最后用 **≤100 字** 汇报:本批几站、通过几、动画还原几、失败站点与原因。 - -## 注意 -- 真实截图 `target` 对部分获奖/WebGL 站是合成信息卡——这类对比的是「卡片本身」,属预期。 -- 不要修改 `preview-data.json` 或 `prompt.hifi.md`,你只产出 build/截图/result。 -- 网络/截图偶发失败就重试一次;仍失败记 `passed:false, notes:"screenshot failed"`。 diff --git a/online-dropdown-proof.png b/online-dropdown-proof.png deleted file mode 100644 index f433fcc..0000000 Binary files a/online-dropdown-proof.png and /dev/null differ diff --git a/online-proof.png b/online-proof.png deleted file mode 100644 index c8701a7..0000000 Binary files a/online-proof.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 20cbf07..ecb2cba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,10 @@ { - "name": "2026-07-12-02-47-36", + "name": "ui-gallery", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "ui-gallery", "dependencies": { "playwright": "^1.61.1" } diff --git a/package.json b/package.json index 2fe1cd5..6e70222 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,12 @@ { "dependencies": { "playwright": "^1.61.1" + }, + "name": "ui-gallery", + "private": true, + "scripts": { + "build": "node scripts/gen-gallery.js", + "test": "node tests/functional.js", + "test:copy": "node tests/smoke-copy.js" } } diff --git a/preview-gallery-verify.png b/preview-gallery-verify.png deleted file mode 100644 index ee1a22c..0000000 Binary files a/preview-gallery-verify.png and /dev/null differ diff --git a/preview-gallery.html b/preview-gallery.html index ec1847b..f93b874 100644 --- a/preview-gallery.html +++ b/preview-gallery.html @@ -265,7 +265,7 @@

UI Gallery

const chipsArr=isProj?(i.tech||[]):(i.chips||[]); const chipsHtml=chipsArr.slice(0,6).map(c=>''+c+'').join(''); const num=String(n).padStart(2,'0'); - const vbadge=(!i.prompt)?'':(i.hifiPassed===true?('✓ 复现通过'+(i.animOk===true?'✦ 动画还原':'')):(i.hifiPassed===false?'✕ 未复现':'· 待验证')); + const vbadge=(!i.prompt)?'':(i.hifiPassed===true?('✓ 历史:通过'+(i.animOk===true?'✦ 历史:动画':'')):(i.hifiPassed===false?'✕ 历史:失败':'· 待验证')); return '
'+ '
'+ ''+(isProj?'获奖':'库')+''+ diff --git a/redesign-grid.png b/redesign-grid.png deleted file mode 100644 index b9ff3d9..0000000 Binary files a/redesign-grid.png and /dev/null differ diff --git a/redesign-top.png b/redesign-top.png deleted file mode 100644 index 95779fd..0000000 Binary files a/redesign-top.png and /dev/null differ diff --git a/repro-report.html b/reports/repro-report.html similarity index 100% rename from repro-report.html rename to reports/repro-report.html diff --git a/ui-awards-report.html b/reports/ui-awards-report.html similarity index 100% rename from ui-awards-report.html rename to reports/ui-awards-report.html diff --git a/ui-components-hub.html b/reports/ui-components-hub.html similarity index 100% rename from ui-components-hub.html rename to reports/ui-components-hub.html diff --git a/agg-hifi.js b/scripts/agg-hifi.js similarity index 92% rename from agg-hifi.js rename to scripts/agg-hifi.js index 136c725..c94dbe1 100644 --- a/agg-hifi.js +++ b/scripts/agg-hifi.js @@ -1,6 +1,7 @@ +process.chdir(require('path').resolve(__dirname, '..')); // agg-hifi.js // 聚合所有 repro//result.hifi.json,输出统计,并把 hifi 验证结果写回 preview-data.json(新增 hifiPassed / animOk 字段)。 -// 运行(工具恢复后,所有批次跑完): node agg-hifi.js +// 所有输入记录准备后运行: node scripts/agg-hifi.js const fs = require('fs'); const path = require('path'); diff --git a/assemble-hifi.js b/scripts/assemble-hifi.js similarity index 94% rename from assemble-hifi.js rename to scripts/assemble-hifi.js index 370e5d8..f79c105 100644 --- a/assemble-hifi.js +++ b/scripts/assemble-hifi.js @@ -1,11 +1,12 @@ +process.chdir(require('path').resolve(__dirname, '..')); // assemble-hifi.js — 组装混合高保真提示词 // prompt.hifi.md = 已有自然语言首屏提示词 + 从 anim.json 提取的真实动画代码块 -// 用法: node assemble-hifi.js +// 用法: node scripts/assemble-hifi.js const fs = require('fs'); const path = require('path'); // ===== 通用高保真生成指令(始终生效,写给任意 AI UI 生成器都能用)===== -// 提炼自业界高质量 UI 生成提示词的共性要求;web-prompts.md 会叠加从网上找到的「牛逼提示词」。 +// 提炼自业界高质量 UI 生成提示词的共性要求;docs/prompt-guidelines.md 会叠加从网上找到的「牛逼提示词」。 const UNIVERSAL = `# Universal high-fidelity build directives (apply to every generator) - Output a SINGLE self-contained HTML file with inline CSS and JS. No build step, no external dependencies, opens directly in a browser. - Use the EXACT hex colors and font stack given below. Never approximate or substitute a color. @@ -22,14 +23,14 @@ const UNIVERSAL = `# Universal high-fidelity build directives (apply to every ge - Production quality: no console errors, no overflow, no blurry assets. Implement icons as inline SVG or pure CSS. - Do not invent sections that are not described. Stay faithful to the specified above-the-fold content.`; -// 从网上找到的「牛逼 UI 提示词」注入位:把抓取到的优质指令/范例写进 web-prompts.md 即可自动合并 +// 从网上找到的「牛逼 UI 提示词」注入位:把抓取到的优质指令/范例写进 docs/prompt-guidelines.md 即可自动合并 let WEB_EXTRA = ''; try { - const wp = fs.readFileSync('web-prompts.md', 'utf8').trim(); + const wp = fs.readFileSync('docs/prompt-guidelines.md', 'utf8').trim(); if (wp) WEB_EXTRA = '# Curated community UI-prompt techniques (sourced from the web)\n' + wp; -} catch (e) { /* web-prompts.md 不存在时跳过 */ } +} catch (e) { /* docs/prompt-guidelines.md 不存在时跳过 */ } -const accessible = JSON.parse(fs.readFileSync('/tmp/accessible.json', 'utf8')); +const accessible = JSON.parse(fs.readFileSync(process.argv[2] || 'repro/accessible.json', 'utf8')); // 找一个站点已有的最佳 NL 提示词 function bestNL(id) { @@ -167,7 +168,7 @@ for (const s of accessible) { fs.writeFileSync('repro/' + id + '/prompt.hifi.md', full); assembled++; } -fs.writeFileSync('/tmp/need-nl.json', JSON.stringify(needNL)); +fs.writeFileSync('repro/need-nl.json', JSON.stringify(needNL)); console.log('assembled prompt.hifi.md:', assembled); console.log(' had NO base NL prompt (need LLM):', noNL, needNL.length ? '=> ' + needNL.join(' ') : ''); console.log(' had NO usable anim data:', noAnim); diff --git a/build-dataset.js b/scripts/build-dataset.js similarity index 99% rename from build-dataset.js rename to scripts/build-dataset.js index dfa7ac5..6ec8ad6 100644 --- a/build-dataset.js +++ b/scripts/build-dataset.js @@ -1,10 +1,11 @@ +process.chdir(require('path').resolve(__dirname, '..')); // build-dataset.js — 汇总「组件库 + 获奖项目」总清单 -> preview-data.json // 复用: 66 个旧库(从 ui-components-hub.html 提取) + 大量新增 + 获奖项目 const fs = require('fs'); const path = require('path'); // ---------- 1) 提取已有 66 个库 ---------- -const hub = fs.readFileSync('ui-components-hub.html', 'utf8'); +const hub = fs.readFileSync('reports/ui-components-hub.html', 'utf8'); const m = hub.match(/const LIBRARIES = (\[[\s\S]*?\n\];)/); const EXIST = eval('(' + m[1].replace(/;\s*$/, '') + ')'); const existingItems = EXIST.map(l => ({ diff --git a/gen-gallery.js b/scripts/gen-gallery.js similarity index 72% rename from gen-gallery.js rename to scripts/gen-gallery.js index 1d414be..72a170e 100644 --- a/gen-gallery.js +++ b/scripts/gen-gallery.js @@ -1,11 +1,11 @@ // gen-gallery.js — 用模板 + preview-data.json 重建自包含画廊 preview-gallery.html -// 用法: node gen-gallery.js +// 用法: npm run build const fs = require('fs'); const path = require('path'); -const TEMPLATE = path.join(__dirname, 'gallery.template.html'); -const DATA = path.join(__dirname, 'preview-data.json'); -const OUT = path.join(__dirname, 'preview-gallery.html'); +const TEMPLATE = path.join(__dirname, '..', 'gallery.template.html'); +const DATA = path.join(__dirname, '..', 'preview-data.json'); +const OUT = path.join(__dirname, '..', 'preview-gallery.html'); const tpl = fs.readFileSync(TEMPLATE, 'utf8'); diff --git a/gen-hifi-batches.js b/scripts/gen-hifi-batches.js similarity index 87% rename from gen-hifi-batches.js rename to scripts/gen-hifi-batches.js index 0c34e81..a72c190 100644 --- a/gen-hifi-batches.js +++ b/scripts/gen-hifi-batches.js @@ -1,6 +1,7 @@ +process.chdir(require('path').resolve(__dirname, '..')); // gen-hifi-batches.js // 把所有「高保真(hifi)提示词」站点分区,供后台子代理并行跑「提示词→AI 生成→对比」验证。 -// 运行(工具恢复后): node gen-hifi-batches.js +// 运行: node scripts/gen-hifi-batches.js const fs = require('fs'); const B = 12; // 每批站点数 @@ -22,5 +23,6 @@ const batches = []; for (let i = 0; i < sites.length; i += B) { batches.push({ index: batches.length, sites: sites.slice(i, i + B) }); } +fs.mkdirSync('repro', { recursive: true }); fs.writeFileSync('repro/hifi-batches.json', JSON.stringify(batches, null, 0)); console.log('hifi sites:', sites.length, '| batches:', batches.length, '(size ' + B + ')'); diff --git a/gen-real-shots.js b/scripts/gen-real-shots.js similarity index 98% rename from gen-real-shots.js rename to scripts/gen-real-shots.js index 8acdce4..c968882 100644 --- a/gen-real-shots.js +++ b/scripts/gen-real-shots.js @@ -1,3 +1,4 @@ +process.chdir(require('path').resolve(__dirname, '..')); // gen-real-shots.js — 逐个访问每个条目的真实网址,把真实网站截图存到 previews/.png // 覆盖原先的合成风格卡。跳过本地已克隆的 8 个(proj-*.png,本就是真实运行截图)。 // 若实拍截图接近空白(纯 WebGL/动画站在无头下常渲染成黑屏),回退为信息卡(名称/机构/奖项/技术)以便辨认。 @@ -75,7 +76,7 @@ if (process.env.TEST) { async function run(list) { const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-dev-shm-usage', '--use-gl=swiftshader', '--enable-webgl', '--hide-scrollbars', '--disable-blink-features=AutomationControlled'] }); let done = 0, ok = 0, blank = 0, fail = 0; diff --git a/gen-report.js b/scripts/gen-report.js similarity index 98% rename from gen-report.js rename to scripts/gen-report.js index 38fcfd3..92854fd 100644 --- a/gen-report.js +++ b/scripts/gen-report.js @@ -1,3 +1,4 @@ +process.chdir(require('path').resolve(__dirname, '..')); const fs = require("fs"); const rows = JSON.parse(fs.readFileSync("repro/_rows.json", "utf8")); const total = rows.length; @@ -101,5 +102,5 @@ document.getElementById("samples").innerHTML=SAMPLES.map(s=>'
`; -fs.writeFileSync("repro-report.html", html); +fs.writeFileSync("reports/repro-report.html", html); console.log("report bytes:", html.length); diff --git a/merge-hifi.js b/scripts/merge-hifi.js similarity index 94% rename from merge-hifi.js rename to scripts/merge-hifi.js index 3f5543d..d87693c 100644 --- a/merge-hifi.js +++ b/scripts/merge-hifi.js @@ -1,3 +1,4 @@ +process.chdir(require('path').resolve(__dirname, '..')); // merge-hifi.js — 把混合高保真提示词(prompt.hifi.md)写入 preview-data.json 的 prompt 字段 // 优先用 hifi;若 hifi 缺失则保留已有 prompt(向后兼容) const fs = require('fs'); diff --git a/merge-prompts.js b/scripts/merge-prompts.js similarity index 93% rename from merge-prompts.js rename to scripts/merge-prompts.js index 6e08a9a..758e108 100644 --- a/merge-prompts.js +++ b/scripts/merge-prompts.js @@ -1,5 +1,6 @@ +process.chdir(require('path').resolve(__dirname, '..')); // merge-prompts.js — 把 repro 闭环生成的最终提示词合并进 preview-data.json 的 `prompt` 字段 -// 用法: node merge-prompts.js +// 用法: node scripts/merge-prompts.js const fs = require('fs'); const path = require('path'); diff --git a/read2-core.js b/scripts/read2-core.js similarity index 100% rename from read2-core.js rename to scripts/read2-core.js diff --git a/repro-read.js b/scripts/repro-read.js similarity index 94% rename from repro-read.js rename to scripts/repro-read.js index 043da5e..6bcc4d9 100644 --- a/repro-read.js +++ b/scripts/repro-read.js @@ -1,5 +1,6 @@ +process.chdir(require('path').resolve(__dirname, '..')); // repro-read.js — 读取一个真实网站的源码/结构,输出结构化 read.json + 渲染后的 source.html -// 用法: node repro-read.js "" "repro//read.json" +// 用法: node scripts/repro-read.js "" "repro//read.json" const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); @@ -10,7 +11,7 @@ const VIEWPORT = { width: 1280, height: 820 }; (async () => { const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-dev-shm-usage', '--use-gl=swiftshader', '--enable-webgl', '--hide-scrollbars', '--disable-blink-features=AutomationControlled'] }); const page = await browser.newPage({ viewport: VIEWPORT }); diff --git a/repro-read2.js b/scripts/repro-read2.js similarity index 85% rename from repro-read2.js rename to scripts/repro-read2.js index e5c914a..7397212 100644 --- a/repro-read2.js +++ b/scripts/repro-read2.js @@ -1,6 +1,7 @@ +process.chdir(require('path').resolve(__dirname, '..')); // repro-read2.js — 动画 + 交互感知读取器(单站点,命令行版) // 复用 read2-core.js 的提取逻辑,额外写入 :hover/:active/:focus 终态到 anim.json.interactions。 -// 用法: node repro-read2.js "" "repro//anim.json" +// 用法: node scripts/repro-read2.js "" "repro//anim.json" const fs = require('fs'); const { chromium } = require('playwright'); const { LAUNCH_ARGS, extract, navigate } = require('./read2-core'); @@ -10,7 +11,7 @@ const outJson = process.argv[3]; (async () => { const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: LAUNCH_ARGS }); const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); diff --git a/repro-shot-anim.js b/scripts/repro-shot-anim.js similarity index 79% rename from repro-shot-anim.js rename to scripts/repro-shot-anim.js index 7bf3f46..ed45672 100644 --- a/repro-shot-anim.js +++ b/scripts/repro-shot-anim.js @@ -1,12 +1,13 @@ +process.chdir(require('path').resolve(__dirname, '..')); // repro-shot-anim.js — 对本地重建 HTML 抓多帧,验证动画是否运行 -// 用法: node repro-shot-anim.js "repro//build.hifi.html" "repro//anim-frames" +// 用法: node scripts/repro-shot-anim.js "repro//build.hifi.html" "repro//anim-frames" // 输出 anim-frames.0.png / .1.png / .2.png (0ms / 600ms / 1600ms) const { chromium } = require('playwright'); const file = process.argv[2]; const outPrefix = process.argv[3]; (async () => { const b = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--hide-scrollbars'] }); const p = await b.newPage({ viewport: { width: 1280, height: 800 } }); diff --git a/repro-shot.js b/scripts/repro-shot.js similarity index 82% rename from repro-shot.js rename to scripts/repro-shot.js index b24f371..d6200df 100644 --- a/repro-shot.js +++ b/scripts/repro-shot.js @@ -1,5 +1,6 @@ +process.chdir(require('path').resolve(__dirname, '..')); // repro-shot.js — 把本地 HTML 文件按 1280x820 视口截图成 PNG(与真实站点参考图同视口) -// 用法: node repro-shot.js "repro//build.v1.html" "repro//build.v1.png" +// 用法: node scripts/repro-shot.js "repro//build.v1.html" "repro//build.v1.png" const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); @@ -10,7 +11,7 @@ const W = 1280, H = 820; (async () => { const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-dev-shm-usage', '--use-gl=swiftshader', '--enable-webgl', '--hide-scrollbars', '--disable-blink-features=AutomationControlled'] }); const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 }); diff --git a/rerun-read2.js b/scripts/rerun-read2.js similarity index 91% rename from rerun-read2.js rename to scripts/rerun-read2.js index 164f7b4..1869169 100644 --- a/rerun-read2.js +++ b/scripts/rerun-read2.js @@ -1,7 +1,8 @@ +process.chdir(require('path').resolve(__dirname, '..')); // rerun-read2.js — 全量安全重跑交互提取 // 单浏览器 + 4 并发页池;对全部已有 anim.json 的站点重跑 read2-core 提取(含 interactions), // 安全合并:新跑成功则整体覆盖(含 interactions 字段);新跑失败但原文件 ok:true 则保留原文件不覆盖。 -// 用法: node rerun-read2.js +// 用法: node scripts/rerun-read2.js const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); @@ -11,14 +12,15 @@ const DATA = 'preview-data.json'; const items = JSON.parse(fs.readFileSync(DATA, 'utf8')); const targets = items.filter(it => fs.existsSync(path.join('repro', it.id, 'anim.json')) && it.link); const CONC = 4; -const LOG = '/tmp/rerun-read2.log'; +fs.mkdirSync('repro', { recursive: true }); +const LOG = 'repro/rerun-read2.log'; const log = []; function logLine(s) { log.push(s); fs.writeFileSync(LOG, log.join('\n') + '\n'); } (async () => { logLine('START ' + new Date().toISOString() + ' targets=' + targets.length); const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: LAUNCH_ARGS }); let done = 0; diff --git a/run-anim-extract.js b/scripts/run-anim-extract.js similarity index 84% rename from run-anim-extract.js rename to scripts/run-anim-extract.js index cdaddd7..d634489 100644 --- a/run-anim-extract.js +++ b/scripts/run-anim-extract.js @@ -1,10 +1,11 @@ +process.chdir(require('path').resolve(__dirname, '..')); // run-anim-extract.js — 并发跑 repro-read2.js 生成所有可访问站的 anim.json -// 用法: node run-anim-extract.js [concurrency] +// 用法: node scripts/run-anim-extract.js [concurrency] const fs = require('fs'); const { spawn } = require('child_process'); const NODE = process.execPath; -const accessible = JSON.parse(fs.readFileSync('/tmp/accessible.json', 'utf8')); +const accessible = JSON.parse(fs.readFileSync(process.argv[3] || 'repro/accessible.json', 'utf8')); const CONC = parseInt(process.argv[2] || '4', 10); // skip-if-done: anim.json 存在且 ok:true 则跳过 @@ -24,7 +25,7 @@ function next() { const s = todo[idx++]; return new Promise(resolve => { const out = 'repro/' + s.id + '/anim.json'; - const child = spawn(NODE, ['repro-read2.js', s.url, out], { + const child = spawn(NODE, [require('path').join(__dirname, 'repro-read2.js'), s.url, out], { env: { ...process.env, NODE_PATH: process.cwd() + '/node_modules' }, stdio: 'ignore' }); diff --git a/test-functional-proof.png b/test-functional-proof.png deleted file mode 100644 index d5e111f..0000000 Binary files a/test-functional-proof.png and /dev/null differ diff --git a/test-functional.js b/tests/functional.js similarity index 88% rename from test-functional.js rename to tests/functional.js index c775028..ebb3efa 100644 --- a/test-functional.js +++ b/tests/functional.js @@ -1,9 +1,10 @@ +process.chdir(require('path').resolve(__dirname, '..')); const { chromium } = require('playwright'); const path = require('path'); (async () => { const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-gpu'] }); const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } }); @@ -108,8 +109,9 @@ const path = require('path'); const afterClear = await page.$$eval('.item.sel', els => els.length); console.log('11) 清空后选中数 (应=0) =', afterClear); - await page.screenshot({ path: 'test-functional-proof.png', fullPage: false }); - console.log('12) 截图已保存 test-functional-proof.png'); + require('fs').mkdirSync('.artifacts', { recursive: true }); + await page.screenshot({ path: '.artifacts/functional.png', fullPage: false }); + console.log('12) 截图已保存 .artifacts/functional.png'); // 12b) selection bar is a small bottom-right pill (not covering grid) await page.$eval('.item', e => e.dispatchEvent(new MouseEvent('click', { bubbles: true }))); await page.waitForTimeout(200); @@ -121,6 +123,12 @@ const path = require('path'); console.log('13) 控制台错误数 =', errors.length, errors.slice(0, 3)); await browser.close(); - const ok = total === 230 && broken.length === 0 && selectedCount === 3 && afterReload === 3 && afterClear === 0 && errors.length === 0; + const ok = total === 230 && secInfo.length === 2 && broken.length === 0 + && selectedCount === 3 && stored && JSON.parse(stored).length === 3 + && String(clipboard).split('\n').length === 3 && afterReload === 3 + && reactN > 0 && reactN < total && projN === 53 + && hasOptgroups.fwOptgroups > 0 && hasOptgroups.themeOptgroups > 0 + && afterClear === 0 && errors.length === 0; + process.exitCode = ok ? 0 : 1; console.log('\n=== RESULT:', ok ? 'PASS ✅ 全部可用' : 'CHECK ⚠️', '==='); -})(); +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/smoke-copy.js b/tests/smoke-copy.js similarity index 85% rename from smoke-copy.js rename to tests/smoke-copy.js index e90f6a6..849a510 100644 --- a/smoke-copy.js +++ b/tests/smoke-copy.js @@ -1,5 +1,6 @@ +process.chdir(require('path').resolve(__dirname, '..')); const { chromium } = require('playwright'); -const EXEC = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const EXEC = process.env.CHROME_PATH || undefined; (async () => { const errs = []; const b = await chromium.launch({ executablePath: EXEC, args: ['--no-sandbox', '--use-gl=swiftshader'] }); @@ -15,4 +16,5 @@ const EXEC = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; console.log('cprompt buttons:', btns, '| click ok:', ok, '| textAfter:', JSON.stringify(txt)); console.log('console errors:', errs.length ? errs.slice(0, 5) : 'none'); await b.close(); + process.exitCode = btns > 0 && ok && errs.length === 0 ? 0 : 1; })().catch(e => { console.error('FATAL', e.message); process.exit(1); }); diff --git a/verify-gallery.js b/verify-gallery.js deleted file mode 100644 index d42283c..0000000 --- a/verify-gallery.js +++ /dev/null @@ -1,79 +0,0 @@ -const { chromium } = require('playwright'); -const path = require('path'); -(async () => { - const browser = await chromium.launch({ - executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - args: ['--use-gl=swiftshader','--enable-webgl','--ignore-gpu-blocklist','--no-sandbox'] - }); - const page = await browser.newPage({ viewport: { width: 1366, height: 900 } }); - const errors = []; - page.on('console', m => { if (m.type() === 'error') errors.push('console:' + m.text()); }); - page.on('pageerror', e => errors.push('pageerror:' + e.message)); - - // dispatch a real click without coordinate-based actionability (avoids sticky-bar overlap in headless) - const clk = (sel) => page.evaluate(s => { - const el = document.querySelector(s); - if (!el) throw new Error('no el: ' + s); - el.dispatchEvent(new MouseEvent('click', { bubbles: true })); - }, sel); - - await page.goto('file://' + path.resolve('preview-gallery.html'), { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(1500); - - const broken = await page.$$eval('.item img', els => els.filter(e => e.naturalWidth === 0).map(e => e.alt)); - const total = await page.$$eval('.item', els => els.length); - const stats = await page.$eval('#stats', el => el.textContent.replace(/\s+/g, ' ').trim()); - - await clk('#kindSeg button[data-kind="proj"]'); - await page.waitForTimeout(300); - const projN = await page.$$eval('.item', els => els.length); - const projSub = await page.$eval('.item .vendor', el => el.textContent).catch(() => ''); - await clk('#kindSeg button[data-kind="all"]'); - await page.waitForTimeout(200); - - await clk('#fwChips .chip[data-fw="React"]'); - await page.waitForTimeout(300); - const reactN = await page.$$eval('.item', els => els.length); - await clk('#fwChips .chip[data-fw=""]'); - await page.waitForTimeout(200); - - const themeExpr = '#themeChips .chip[data-th]:not([data-th=""])'; - const tname = await page.$eval(themeExpr, el => el.getAttribute('data-th')).catch(() => null); - let themeN = -1; - if (tname) { - await clk(themeExpr); - await page.waitForTimeout(300); - themeN = await page.$$eval('.item', els => els.length); - await clk('#themeChips .chip[data-th=""]'); - await page.waitForTimeout(200); - } - - await page.fill('#q', 'shadcn'); - await page.waitForTimeout(300); - const searchN = await page.$$eval('.item', els => els.length); - await page.fill('#q', ''); - await page.waitForTimeout(200); - - await clk('.item'); - await page.waitForTimeout(200); - const selN = await page.$eval('#selN', el => el.textContent); - const barOn = await page.$eval('#bar', el => el.classList.contains('on')); - - console.log('--- VERIFY ---'); - console.log('total cards:', total); - console.log('broken images:', broken.length, broken.slice(0, 5)); - console.log('stats:', stats); - console.log('proj count:', projN, '| sample subtitle:', projSub); - console.log('react count:', reactN); - console.log('theme "' + tname + '" ->', themeN); - console.log('search "shadcn" ->', searchN); - console.log('selection selN:', selN, '| bar on:', barOn); - console.log('console/page errors:', errors.length, errors.slice(0, 5)); - - await page.screenshot({ path: 'preview-gallery-verify.png' }); - await browser.close(); - const ok = broken.length === 0 && errors.length === 0 && total === 230 && - projN > 40 && reactN > 20 && searchN > 0 && barOn && themeN > 0; - console.log(ok ? 'RESULT: PASS' : 'RESULT: FAIL'); - process.exit(ok ? 0 : 1); -})().catch(e => { console.error('FATAL', e); process.exit(2); });