@chinhae/atw-cli 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +2 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.2.10.json +9 -0
- package/dist/migrations/manifests/0.2.9.json +9 -0
- package/dist/templates/atw/workflow.md +8 -6
- package/dist/templates/common/bundled-skills/atw-code-review/SKILL.md +22 -5
- package/dist/templates/common/bundled-skills/atw-implement/SKILL.md +3 -1
- package/dist/templates/common/bundled-skills/atw-prototype/LOGIC.md +41 -41
- package/dist/templates/common/bundled-skills/atw-prototype/SKILL.md +16 -14
- package/dist/templates/common/bundled-skills/atw-prototype/UI.md +120 -58
- package/dist/templates/common/bundled-skills/atw-prototype/assets/style-explorer.html +333 -0
- package/dist/templates/common/bundled-skills/atw-prototype/references/design-direction.md +170 -0
- package/dist/templates/common/bundled-skills/atw-prototype/references/style-explorer.md +92 -0
- package/dist/templates/common/bundled-skills/atw-prototype/references/visual-language.md +88 -0
- package/dist/templates/common/bundled-skills/atw-prototype/references/visual-review.md +63 -0
- package/dist/templates/common/bundled-skills/atw-prototype/scripts/build_explorer.py +334 -0
- package/dist/templates/common/bundled-skills/atw-spec/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/atw-spec-bootstrap/references/spec-writing.md +4 -0
- package/dist/templates/common/bundled-skills/atw-tickets/SKILL.md +6 -0
- package/dist/templates/common/bundled-skills/atw-ui/SKILL.md +126 -0
- package/dist/templates/common/bundled-skills/atw-ui/agents/openai.yaml +3 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/imagery.md +69 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/layout-and-viewport.md +15 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/media.md +45 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/motion.md +101 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/visual-language.md +88 -0
- package/dist/templates/common/bundled-skills/atw-ui/references/visual-review.md +94 -0
- package/dist/templates/common/bundled-skills/oxy-learning-hub/scripts/validate_html.py +2 -0
- package/dist/templates/common/skills/update-spec.md +1 -0
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +1 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/frontend/design-system.md.txt +68 -0
- package/dist/templates/markdown/spec/frontend/index.md.txt +1 -0
- package/package.json +2 -2
|
@@ -1,64 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 界面原型
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
界面原型分两段:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **第一段:选方向。** 做几个风格和结构都明显不同的方向,放进同一个对比页,由用户挑选,再说出具体喜欢和不喜欢哪里。
|
|
6
|
+
- **第二段:深化成可交互原型。** 只在问题包含“用起来对不对”时才做:把选定方向做成完整页面,接上按 [LOGIC.md](LOGIC.md) 写的纯逻辑模块,让人能把真实业务流程完整点一遍。
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
两段的产物都是用来做决定的,不是正式代码。选定之后的正式界面由 atw-ui 按设计说明重新实现。
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- "I want to see a few options for this dashboard before committing."
|
|
11
|
-
- "Try a different layout for the settings screen."
|
|
12
|
-
- Any time the user would otherwise spend a day picking between three vague mockups in their head.
|
|
10
|
+
如果问题只关于逻辑或状态、与外观无关,分支选错了,改用 [LOGIC.md](LOGIC.md)。
|
|
13
11
|
|
|
14
|
-
##
|
|
12
|
+
## 什么时候用
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
- “这个页面应该长什么样?”
|
|
15
|
+
- “正式做之前,我想先看看这个后台有几种可能。”
|
|
16
|
+
- “给设置页换个布局试试。”
|
|
17
|
+
- “这个下单流程连同规则,用起来顺不顺?”(两段都做)
|
|
18
|
+
- 任何用户本来要在脑子里对着三张模糊草图纠结一整天的时候。
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## 承载方式:优先挂在已有页面上
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
界面原型**贴着应用的其他部分**才好判断:真实的顶栏、侧栏、数据和密度。一个孤零零的新页面是真空,每个方案单独看都还行。所以只要有合理的宿主页面,就优先方式 A。
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
### 方式 A:在已有页面上调整(优先)
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
路由已经存在。各方案**在同一路由上**渲染,用 URL 参数 `?variant=` 切换。现有的数据获取、参数和权限都保留,只换渲染部分。
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
还没有页面、但*自然属于某个页面*的东西(后台里的新区块、设置页里的新卡片、现有流程中的新一步)也算方式 A:把方案挂在宿主页面里。
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
这些方案同时放进对比页作为本机地址候选(`"kind": "url"`),并把当前页面作为基线(`"baseline": true`),每个方向都和现状对照。见 [风格对比页](references/style-explorer.md)。
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
### 方式 B:独立小样(没有宿主页面时)
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
只有确实没有可挂靠的页面时才用,例如全新的顶层界面、或嵌不进任何现有位置的流程。先确认:真的没有现有页面能挂吗?空页面会掩盖有数据时才暴露的设计问题。
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
方式 B 默认做成自包含的单个 HTML 小样,直接放进对比页,双击就能打开,不需要服务器。小样必须用到项目里的组件或框架时,按项目已有的路由约定建一个一次性路由,名字里带 `prototype`,同样用 `?variant=` 切换,并作为本机地址候选放进对比页。
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
## 第一段:选方向
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
### 1. 写下问题,定方案数量
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
默认 **3 个方向**。超过 5 个就不再是“明显不同”而是噪音,上限 5。窄小的任务不强求多方案。
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
把计划写成一句话,放在原型所在位置或文件顶部注释里:
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
> “设置页的三个方向,用 `?variant=` 在现有 `/settings` 路由上切换,并汇总到对比页。”
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
用户在不在场都适用。
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
### 2. 定调性,写方向卡,做差异检验
|
|
49
51
|
|
|
50
|
-
-
|
|
51
|
-
- The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever).
|
|
52
|
-
- A clear exported component name, e.g. `VariantA`, `VariantB`, `VariantC`.
|
|
52
|
+
方向未定时按 [设计方向](references/design-direction.md) 的顺序做:先从产品题材、受众和使用场合推出调性;每个方向从一个具体的生成引擎出发;写文案之前先定首屏骨架;为每个方向填方向卡,写下它的记忆点;最后逐条做差异检验。
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
方向**必须在结构上不同**:布局、信息层级、主要操作方式都不一样,不只是换颜色。三个微调过的卡片网格不是界面原型,是墙纸。两个方向做出来太像时,回到骨架表重选其中一个,不靠换装饰补救。
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
方向由用户的品味来定,不直接采用模型自己想出的方向。用户已经给定方向时跳过这一步,直接进入第二段或交给 atw-ui。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
### 3. 做小样
|
|
59
|
+
|
|
60
|
+
每个方向只做首屏和最能体现方向的一两个区块;用户明确要完整页面时照做。挑能暴露方向优缺点的内容:含复杂表单的产品不能只做封面。
|
|
61
|
+
|
|
62
|
+
每个方向要守住:
|
|
63
|
+
|
|
64
|
+
- 页面的用途和它能拿到的数据;所有方向共享同样的核心内容和主要操作。
|
|
65
|
+
- 方式 A 用项目已有的组件库和样式体系(TailwindCSS、shadcn、MUI、纯 CSS 等);方式 B 的独立小样不受此限,但图标只内联用到的 SVG,不引用外部资源。
|
|
66
|
+
- 方式 A 中每个方向导出清楚的组件名,例如 `VariantA`、`VariantB`、`VariantC`。
|
|
67
|
+
|
|
68
|
+
示例内容按真实产品的样子写,避免整数指标和“标题”“描述”这类占位字;画面上不出现“示例”“示意图”“按钮未接入”这类制作说明,这些写进交付说明。视觉层级、字体、色彩和留白的判断见 [视觉语言](references/visual-language.md)。
|
|
69
|
+
|
|
70
|
+
### 4. 生成对比页
|
|
71
|
+
|
|
72
|
+
按 [风格对比页](references/style-explorer.md) 准备 `manifest.json`,把方向卡里的北极星、字体、色板和特征写进去,用内置生成器组装成一个离线 HTML。不要每次自己重做对比工具。对比页和 manifest 放在 ATW 任务目录(没有任务时放在原型旁边),不写进技能安装目录。
|
|
73
|
+
|
|
74
|
+
### 5. 交给用户选
|
|
75
|
+
|
|
76
|
+
给出简短的方向清单和对比页,附推荐理由,请用户选定,并说出具体喜欢和不喜欢哪里。用户只说“再高级一点”这类模糊词时,用设计方向里的翻译表追问,或拿两张小样让用户比较。
|
|
77
|
+
|
|
78
|
+
交付对比页时,把一轮独立评审作为**推荐的下一步**告诉用户:说明它能改善什么(打破几个方向共用的模板骨架)、大约需要多少时间和调用,由用户决定是否执行,不默认派发。用户同意时按 [视觉评审协议](references/visual-review.md) 给每个方向各评审、修改一轮;用户直接选定方向时跳过。
|
|
79
|
+
|
|
80
|
+
用户明确让你自行决定、或当前无法交互时,仍然先做小样、生成对比页留档,再按任务适配、辨识度和可执行性选定,并说明理由。
|
|
81
|
+
|
|
82
|
+
选定后写一份简短的**设计说明**:用户任务、内容与主动作、选定方向(附方向卡)、用户的喜欢与不喜欢、视觉关系、关键状态、设备约束和验收重点。放在 ATW 任务目录的 `ui-direction.md`;已有说明直接更新。这份说明是第二段和 atw-ui 的依据。
|
|
83
|
+
|
|
84
|
+
## 第二段:深化成可交互原型
|
|
85
|
+
|
|
86
|
+
只有问题包含业务规则或完整操作流程时才做;只问外观时,第一段选定后就收尾。
|
|
87
|
+
|
|
88
|
+
### 1. 先写逻辑模块
|
|
89
|
+
|
|
90
|
+
按 [LOGIC.md](LOGIC.md) 第 2 步,把业务规则写成一个纯模块(reducer、状态机或纯函数),不碰 DOM。这部分以后能整体搬进正式代码,是第二段最值得保留的产物。
|
|
91
|
+
|
|
92
|
+
### 2. 选定方向做成完整页面
|
|
93
|
+
|
|
94
|
+
把选定方向从小样扩展成完整页面,作为逻辑模块外面的一层外壳:页面只调用模块、渲染模块返回的状态。关键状态都要在界面上有样子——空、加载、成功、失败、被拒绝的非法操作——不只做漂亮的默认状态。
|
|
95
|
+
|
|
96
|
+
方向卡里的记忆点(某个动作的反馈、完成的那一刻)在这里做出来;动效遵循方向的调性。修改数据的动作一律经过逻辑模块,接口用桩代替,不接真实后端。
|
|
97
|
+
|
|
98
|
+
默认只深化**一个**选定方向。用户要求同时比较两三个方向的完整体验时,多个外壳共用同一个逻辑模块,逻辑只写一次。
|
|
99
|
+
|
|
100
|
+
### 3. 加一个状态抽屉
|
|
101
|
+
|
|
102
|
+
LOGIC.md 里的“当前状态面板”和“引导演练”放进一个可开关的抽屉,供不写代码的人逐步走查业务规则:
|
|
103
|
+
|
|
104
|
+
- 抽屉视觉上明显不属于设计本身(例如高对比的面板),和浮动切换条一样,不参与设计评判。
|
|
105
|
+
- 抽屉里保持朴素:标签式的状态字段、标出刚变化的部分、按场景划分的演练按钮;不加动画。
|
|
106
|
+
- 开始演练时重置到已知初始状态。
|
|
107
|
+
- 生产构建中隐藏。
|
|
108
|
+
|
|
109
|
+
### 4. 承载
|
|
110
|
+
|
|
111
|
+
- **没有现成项目**:单个 HTML,脚本内联,双击即可打开;放进对比页时加 `"interactive": true`,单张查看时就能直接操作。
|
|
112
|
+
- **有现成项目**:挂在已有页面上,用 `?variant=` 切换;作为本机地址候选放进对比页。
|
|
113
|
+
|
|
114
|
+
## 接线(方式 A)
|
|
115
|
+
|
|
116
|
+
在路由上建一个切换组件:
|
|
59
117
|
|
|
60
118
|
```tsx
|
|
61
|
-
//
|
|
119
|
+
// 伪代码——按项目框架改写
|
|
62
120
|
const variant = searchParams.get('variant') ?? 'A';
|
|
63
121
|
return (
|
|
64
122
|
<>
|
|
@@ -70,43 +128,47 @@ return (
|
|
|
70
128
|
);
|
|
71
129
|
```
|
|
72
130
|
|
|
73
|
-
|
|
131
|
+
已有页面:现有数据获取都放在切换组件之上,各方向只替换渲染的子树。一次性路由:同一个切换组件挂在 `prototype` 路由下。
|
|
74
132
|
|
|
75
|
-
|
|
133
|
+
## 浮动切换条(方式 A)
|
|
76
134
|
|
|
77
|
-
|
|
135
|
+
屏幕底部居中、固定位置的小条,三部分:
|
|
78
136
|
|
|
79
|
-
|
|
137
|
+
- **左箭头**:切到上一个方向(循环)。
|
|
138
|
+
- **方向标签**:当前方向的编号,方向有名字时一并显示,例如 `B — 侧栏布局`。
|
|
139
|
+
- **右箭头**:切到下一个方向(循环)。
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
- **Variant label** — shows the current variant key and, if the variant exports a name, that name too. e.g. `B — Sidebar layout`.
|
|
83
|
-
- **Right arrow** — cycles forward (wraps around).
|
|
141
|
+
行为:
|
|
84
142
|
|
|
85
|
-
|
|
143
|
+
- 点箭头时用框架的路由更新 URL 参数(Next 用 `router.replace`,React Router 用 `navigate` 等),保证方向可分享、刷新不丢。
|
|
144
|
+
- 键盘 `←` `→` 也能切换。焦点在 `<input>`、`<textarea>` 或 `[contenteditable]` 上时不拦截方向键。
|
|
145
|
+
- 视觉上明显区别于页面(例如高对比胶囊、轻阴影),一看就不属于被评判的设计。
|
|
146
|
+
- 生产构建中隐藏——用 `process.env.NODE_ENV !== 'production'` 或等价判断,避免误合并后把切换条发给用户。
|
|
86
147
|
|
|
87
|
-
|
|
88
|
-
- Keyboard: `←` and `→` arrow keys also cycle. Don't intercept arrow keys when an `<input>`, `<textarea>`, or `[contenteditable]` is focused.
|
|
89
|
-
- Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated.
|
|
90
|
-
- Hidden in production builds — gate on `process.env.NODE_ENV !== 'production'` or an equivalent check, so a stray prototype merge can't ship the bar to users.
|
|
148
|
+
切换条做成一个共享组件,放在项目存放共享界面组件的位置。对比页适合并排比较;切换条适合在真实页面里来回看,开发服务器禁止被嵌入时也只能靠它。
|
|
91
149
|
|
|
92
|
-
|
|
150
|
+
## 交给用户
|
|
93
151
|
|
|
94
|
-
|
|
152
|
+
给出对比页路径、页面地址和 `?variant=` 的取值;第二段还要说明状态抽屉怎么打开、有哪些演练场景。用户有空时会翻看。最有价值的反馈常常是 **“我要 B 的顶栏配 C 的侧栏”**——那才是他们真正想要的设计。
|
|
95
153
|
|
|
96
|
-
|
|
154
|
+
交付说明里写清:哪些是模拟数据、生成图片、未接入的动作;实际查看过哪些视口;哪一步没做以及原因。
|
|
97
155
|
|
|
98
|
-
|
|
156
|
+
## 记录结论,收起原型
|
|
99
157
|
|
|
100
|
-
|
|
158
|
+
用户选定后,先记录结论——选了哪个方向、为什么、用户的喜欢与不喜欢——更新到 `ui-direction.md`,并把影响需求的部分写进任务的需求文档。然后按 [SKILL](SKILL.md) 的规则收起原型。所有方向、切换组件、对比页和 manifest 进入一次性分支,作为一手资料保存,不进主分支:
|
|
101
159
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
160
|
+
- **方式 A**:主分支上去掉所有方向和切换条,现有页面保持原样。
|
|
161
|
+
- **方式 B**:主分支上去掉一次性路由和切换条。
|
|
162
|
+
- **第二段**:验证过的逻辑模块搬进正式代码;页面外壳留在一次性分支。
|
|
104
163
|
|
|
105
|
-
|
|
164
|
+
正式界面交给 atw-ui,按 `ui-direction.md` 在正式代码里重新实现。留在主分支的方向组件和切换条很快会腐烂,还会误导下一个读代码的人。
|
|
106
165
|
|
|
107
|
-
##
|
|
166
|
+
## 反模式
|
|
108
167
|
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
168
|
+
- **方向之间只差颜色或文案。** 那是微调,不是原型。真正的方向在结构上互不相同。
|
|
169
|
+
- **按明暗分方向。** “一个亮、一个暗、一个暖”是最常见的失败;每个方向要有自己的色彩身份。
|
|
170
|
+
- **默认左右分栏。** 一轮里最多一个方向用左右分栏,并写出为什么非它不可。
|
|
171
|
+
- **方向之间共享太多代码。** 共享 `<Header>` 可以;共享 `<Layout>` 就失去意义了。每个方向都应该能推翻布局。
|
|
172
|
+
- **把方向接到真实的写操作上。** 只读原型没问题;需要修改数据时经过逻辑模块、指向桩。问题是“它应该长什么样、用起来对不对”,不是“后端通不通”。
|
|
173
|
+
- **把业务逻辑写进页面事件里。** 第二段的规则必须在纯模块里,否则既不能搬走,也没法在状态抽屉里走查。
|
|
174
|
+
- **把原型直接提升为正式代码。** 原型代码是在原型约束下写的(没有测试、错误处理很少)。正式实现交给 atw-ui 和实现阶段重新写。
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>风格对比</title><style>
|
|
3
|
+
:root{--canvas:#f4f4f4;--surface:#fff;--well:#e8e8e8;--raised:#fff;--line:#e2e2e2;--line-2:#d0d0d0;--text:#161616;--soft:#5c5c5c;--faint:#8c8c8c;--ink:#161616;--hl:#ffe28a;--bar-h:48px;--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;--brand:"Helvetica Neue",Helvetica,Arial,sans-serif;color-scheme:light;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--canvas);font-synthesis:none;-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums;
|
|
4
|
+
/* 动效:咔嗒用于控件,滑行用于画面与切换,弹出用于确认;弹簧曲线由刚度/阻尼采样 */
|
|
5
|
+
--snap:cubic-bezier(.2,.9,.25,1.04);--snap-d:330ms;--glide:cubic-bezier(.2,.8,.2,1);--glide-d:440ms;--pop:cubic-bezier(.3,1.5,.5,1);--exit:cubic-bezier(.4,0,1,1)}
|
|
6
|
+
@supports (transition-timing-function:linear(0,1)){:root{
|
|
7
|
+
--snap:linear(0,.058,.186,.339,.491,.626,.739,.827,.893,.941,.973,.994,1.005,1.011,1.013,1.013,1.012,1.01,1.008,1.006,1);
|
|
8
|
+
--glide:linear(0,.028,.097,.186,.286,.384,.48,.565,.644,.71,.768,.816,.856,.889,.915,.936,.953,.966,.976,.983,.989,.993,1);
|
|
9
|
+
--pop:linear(0,.087,.289,.533,.765,.954,1.084,1.156,1.176,1.161,1.126,1.083,1.042,1.009,.986,.973,.969,.971,.977,.985,.992,.998,1.002,1.005,1.005,1.005,1)}}
|
|
10
|
+
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--canvas)}h1,h2,p,dl,dd{margin:0}button,textarea,input{font:inherit;color:inherit}button{background:none;border:0;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent}:focus-visible{outline:2px solid var(--ink);outline-offset:3px}[hidden]{display:none!important}
|
|
11
|
+
|
|
12
|
+
.bar{position:sticky;top:0;z-index:10;min-height:48px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:14px;padding:6px 20px;background:#f4f4f4e6;backdrop-filter:saturate(1.4) blur(14px);border-bottom:1px solid var(--line);view-transition-name:bar}
|
|
13
|
+
.bar-l{display:flex;align-items:center;gap:12px;min-width:0}
|
|
14
|
+
.brand{display:flex;align-items:center;gap:8px;flex:none;color:var(--ink);text-decoration:none}
|
|
15
|
+
.wm{font-family:var(--brand);font-size:19px;line-height:1;font-weight:700;letter-spacing:-.045em;display:flex;align-items:flex-start;gap:3px}
|
|
16
|
+
.sep{width:1px;height:18px;background:var(--line-2);flex:none}.proj{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.round{font-size:13px;color:var(--faint);white-space:nowrap}
|
|
17
|
+
.bar-c{display:flex;gap:8px}.bar-r{display:flex;justify-content:flex-end;align-items:center;gap:12px}
|
|
18
|
+
.seg{position:relative;display:flex;padding:2px;border-radius:8px;background:var(--well)}
|
|
19
|
+
.seg button{position:relative;z-index:1;font-size:13px;line-height:1.5;padding:4px 11px;border-radius:6px;color:var(--soft);white-space:nowrap;transition:color .2s}.seg button:hover{color:var(--text)}.seg button[aria-pressed=true]{color:var(--text)}
|
|
20
|
+
.thumb{position:absolute;left:0;top:0;width:0;border-radius:6px;background:var(--raised);box-shadow:0 1px 2px #0000001f,0 2px 8px -2px #00000014,inset 0 0 0 .5px #0000000f;transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap)}
|
|
21
|
+
.toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--soft);cursor:pointer;white-space:nowrap}.toggle input{appearance:none;width:28px;height:16px;margin:0;border-radius:99px;background:var(--line-2);position:relative;cursor:pointer;transition:background .2s}.toggle input::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:99px;background:#fff;box-shadow:0 1px 2px #0003;transition:transform var(--snap-d) var(--snap),width .15s}.toggle input:checked{background:var(--ink)}.toggle input:checked::after{transform:translateX(12px)}.toggle:active input::after{width:14px}.toggle:active input:checked::after{transform:translateX(10px)}
|
|
22
|
+
.filter{position:relative;font-size:13px}.filter summary{list-style:none;cursor:pointer;color:var(--soft);padding:4px 10px;border-radius:6px;box-shadow:inset 0 0 0 1px var(--line);white-space:nowrap;transition:color .15s,box-shadow .15s}.filter summary::-webkit-details-marker{display:none}.filter summary:hover,.filter[open] summary{color:var(--text);box-shadow:inset 0 0 0 1px var(--line-2)}
|
|
23
|
+
.filter-list{position:absolute;right:0;top:calc(100% + 8px);min-width:220px;display:grid;gap:2px;padding:6px;border-radius:10px;background:var(--raised);box-shadow:0 18px 44px -8px #00000026,0 0 0 1px #00000012;transform-origin:top right}.filter[open]>.filter-list{animation:pop-in var(--snap-d) var(--snap) both}
|
|
24
|
+
.filter-list label{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:6px;cursor:pointer;font-size:14px;transition:background .12s}.filter-list label:hover{background:#0000000a}.filter-list input{accent-color:var(--ink);width:15px;height:15px;margin:0}.filter-list .n{font-family:var(--mono);color:var(--faint);font-size:13px}
|
|
25
|
+
|
|
26
|
+
.intro{max-width:1840px;margin:0 auto;padding:28px 24px 0}.intro p{font-size:14px;line-height:1.75;color:var(--soft);max-width:68ch}
|
|
27
|
+
|
|
28
|
+
.grid{max-width:1840px;margin:0 auto;padding:24px 24px 64px;display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:48px 28px;align-items:start}
|
|
29
|
+
.grid.first .card,.intro.first{animation:rise .7s var(--glide) both;animation-delay:calc(var(--i,0) * 70ms + 60ms)}
|
|
30
|
+
.card{min-width:0;display:flex;flex-direction:column}
|
|
31
|
+
.cap{display:flex;align-items:center;gap:12px;padding-top:14px}.cap .no{font-family:var(--mono);font-size:13px;color:var(--faint);transition:color .2s}.cap .name{font-size:16px;font-weight:600;line-height:1.4;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cap .strip-mini{margin:0 auto 0 4px;width:40px;flex:none}.cap .no.base{font-family:inherit;font-size:12px;font-weight:600;line-height:1.6;padding:0 8px;border-radius:99px;background:var(--well);color:var(--soft)}
|
|
32
|
+
.lede{margin-top:8px;font-size:13.5px;line-height:1.7;color:var(--soft);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.grid[data-notes=off] .lede{display:none}
|
|
33
|
+
.name{font-size:19px;line-height:1.35;font-weight:600;overflow-wrap:anywhere}.strip-mini{display:flex;height:4px;width:96px;margin-top:9px;border-radius:2px;overflow:hidden;box-shadow:0 0 0 1px #0000001f}.strip-mini i{flex:1}
|
|
34
|
+
.frame{position:relative;background:var(--surface);border-radius:4px;box-shadow:0 0 0 1px #00000014,0 24px 48px -30px #00000040;transition:box-shadow .25s,transform .45s var(--glide)}.frame:hover{box-shadow:0 0 0 1px #0000002e,0 36px 64px -30px #00000052;transform:translateY(-3px)}.frame:active{transform:translateY(-1px) scale(.995);transition-duration:.25s,.12s}.frame[data-viewport=mobile]{padding:20px 0;background:var(--well)}
|
|
35
|
+
.peek{position:absolute;inset:auto 0 0 0;z-index:2;padding:10px 12px;font-size:12.5px;color:#fff;background:linear-gradient(transparent,#000b);opacity:0;transform:translateY(6px);transition:opacity .18s,transform .3s var(--glide);pointer-events:none;border-radius:0 0 4px 4px}.frame:hover .peek,.hit:focus-visible~.peek{opacity:1;transform:none}
|
|
36
|
+
.stage{position:relative;overflow:hidden;margin:0 auto;border-radius:3px;background:#ebebeb}.stage iframe,.stage img{position:absolute;left:0;top:0;border:0;display:block;transform-origin:top left;background:#fff;opacity:0;transition:opacity .4s ease-out}.stage.ready iframe,.stage.ready img{opacity:1}.stage iframe{pointer-events:none}.stage img{object-fit:contain;background:#ebebeb}
|
|
37
|
+
.hit{position:absolute;inset:0;width:100%;height:100%;z-index:1;border-radius:4px;cursor:zoom-in}.hit:focus-visible{outline-offset:-2px}
|
|
38
|
+
.slide .static{display:none}.static{position:absolute;left:10px;bottom:10px;z-index:2;font-size:12.5px;line-height:1.4;padding:3px 8px;border-radius:4px;background:#000c;color:#ddd;pointer-events:none}
|
|
39
|
+
.pick-mark{position:absolute;top:10px;right:10px;z-index:3;display:none;align-items:center;gap:6px;padding:4px 10px 4px 8px;border-radius:99px;background:var(--ink);color:#fff;font-size:13px;font-weight:600;line-height:1.4;box-shadow:0 6px 16px #0000003d;pointer-events:none;transform-origin:85% 50%}.pick-mark::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--hl)}
|
|
40
|
+
.is-chosen .pick-mark{display:inline-flex}.is-chosen .frame{box-shadow:0 0 0 2px var(--ink),0 30px 60px -30px #00000052}.is-chosen .cap .no{color:var(--text)}
|
|
41
|
+
|
|
42
|
+
.spec{display:grid;grid-template-columns:32px minmax(0,1fr);gap:10px 12px;font-size:13px;line-height:1.6}.spec dt{color:var(--faint)}.spec dd{color:var(--soft);min-width:0}
|
|
43
|
+
.pick{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-size:14px;line-height:1.5;padding:8px 16px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--text);white-space:nowrap;transition:background .2s,color .2s,box-shadow .2s,transform .35s var(--snap)}.pick:hover{background:#0000000a}.pick:active{transform:scale(.96);transition-duration:.2s,.2s,.2s,.08s}
|
|
44
|
+
.pick kbd{font-family:var(--mono);font-size:12px;color:var(--faint);padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px var(--line-2);transition:color .2s,box-shadow .2s}
|
|
45
|
+
.pick[data-on]{background:var(--ink);color:#fff;box-shadow:none}.pick[data-on]:hover{background:#333}.pick[data-on] kbd{color:#bdbdbd;box-shadow:inset 0 0 0 1px #ffffff40}
|
|
46
|
+
.pick[data-on]::before{content:"";width:9px;height:5px;margin:-3px 1px 0 0;border-left:1.6px solid;border-bottom:1.6px solid;transform:rotate(-45deg)}
|
|
47
|
+
.cap .pick{flex:none;font-size:13px;padding:4px 12px;border-radius:6px}
|
|
48
|
+
|
|
49
|
+
.loupe{position:fixed;top:var(--bar-h);left:0;right:0;bottom:0;display:grid;grid-template-columns:minmax(0,1fr) 300px;grid-template-rows:minmax(0,1fr)}
|
|
50
|
+
.view{position:relative;min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
51
|
+
.surface{position:relative;flex:1;min-height:0;overflow:hidden}
|
|
52
|
+
.track{display:flex;height:100%;transition:transform var(--glide-d) var(--glide)}
|
|
53
|
+
.slide{flex:0 0 100%;min-width:0;height:100%;overflow:auto;padding:20px 72px;display:flex;justify-content:center;align-items:center}.slide[data-zoom=actual]{display:block}
|
|
54
|
+
.slide .stage{flex:none;box-shadow:0 0 0 1px #00000014,0 40px 80px -40px #00000059;transition:transform var(--glide-d) var(--glide),opacity var(--glide-d) ease-out}.slide:not(.is-current) .stage{transform:scale(.92);opacity:.4}.slide.is-current .stage iframe{pointer-events:auto}
|
|
55
|
+
.nav{position:absolute;top:50%;z-index:2;width:44px;height:44px;margin-top:-22px;border-radius:50%;display:grid;place-items:center;font-size:18px;background:#ffffffd9;color:var(--ink);box-shadow:0 0 0 1px #00000014,0 8px 24px -6px #00000033;backdrop-filter:blur(8px);opacity:.85;transition:opacity .2s,transform .35s var(--snap)}.nav:hover{opacity:1}.nav:active{transform:scale(.9);transition-duration:.2s,.08s}.nav.prev{left:14px}.nav.next{right:14px}.nav:disabled{opacity:0;pointer-events:none;transform:scale(.8)}
|
|
56
|
+
.info{grid-row:1;grid-column:2;overflow:auto;border-left:1px solid var(--line);background:var(--surface);display:flex;flex-direction:column;view-transition-name:info}
|
|
57
|
+
.film{position:relative;display:grid;gap:2px;padding:10px;border-bottom:1px solid var(--line)}
|
|
58
|
+
.film-ind{position:absolute;left:0;top:0;width:0;height:0;border-radius:8px;background:#f0f0f0;box-shadow:inset 0 0 0 1px var(--line);transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap),height var(--snap-d) var(--snap)}
|
|
59
|
+
.film button{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:3px 10px;padding:7px 10px;border-radius:8px;color:var(--soft);text-align:left;transition:color .2s,background .15s}.film button:hover{color:var(--text)}.film button:not([aria-current=true]):hover{background:#00000006}.film button[aria-current=true]{color:var(--text)}
|
|
60
|
+
.film .fn{font-family:var(--mono);font-size:12.5px;color:var(--faint);grid-row:span 2}.film .ft{font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.film .strip-mini{margin:0;width:48px;height:3px}
|
|
61
|
+
.film .ft.chosen::after{content:"";width:6px;height:6px;border-radius:50%;background:var(--ink);display:inline-block;margin-left:8px;vertical-align:middle}
|
|
62
|
+
.info-body{padding:20px 20px 24px;display:flex;flex-direction:column;gap:16px}
|
|
63
|
+
.idx{font-size:48px;line-height:.9;font-weight:200;letter-spacing:-.02em;color:var(--faint)}.info .name{font-size:20px;line-height:1.35;margin-top:-6px}.north{font-size:14px;line-height:1.7;color:#2b2b2b}
|
|
64
|
+
.big-sw{display:grid;gap:6px}.big-sw div{display:grid;grid-template-columns:32px 1fr;align-items:center;gap:10px;font-family:var(--mono);font-size:12.5px;color:var(--soft)}.big-sw i{height:20px;border-radius:4px;box-shadow:inset 0 0 0 1px #00000017}
|
|
65
|
+
.info .pick{align-self:stretch;padding:8px 14px;font-size:13.5px}
|
|
66
|
+
.tools{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.tool{font-size:13px;padding:5px 12px;border-radius:6px;color:var(--soft);box-shadow:inset 0 0 0 1px var(--line-2);transition:color .15s,background .2s}.tool:hover{color:var(--text)}.tool[aria-pressed=true]{background:var(--ink);color:#fff;box-shadow:none}
|
|
67
|
+
.hint{font-size:12px;line-height:1.9;color:var(--faint)}.hint kbd{font-family:var(--mono);font-size:12px;padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--soft)}
|
|
68
|
+
.src{font-size:12px;line-height:1.6;color:var(--faint);overflow-wrap:anywhere}
|
|
69
|
+
|
|
70
|
+
.picked{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--soft);white-space:nowrap;max-width:220px;padding-left:12px;border-left:1px solid var(--line);overflow:hidden}.picked #decide-name{display:block;overflow:hidden;text-overflow:ellipsis}.picked.on #decide-name{color:var(--text);font-weight:600}
|
|
71
|
+
.picked .strip-mini,.choice-empty{flex:none;margin:0;width:22px;height:12px;border-radius:3px}.choice-empty{box-shadow:inset 0 0 0 1px var(--line-2)}
|
|
72
|
+
.notes-panel{width:340px;padding:10px}.notes-panel textarea{width:100%;min-height:110px;resize:vertical;padding:10px 12px;border:0;border-radius:6px;background:#f6f6f6;box-shadow:inset 0 0 0 1px var(--line);font-size:14px;line-height:1.7;transition:box-shadow .15s,background .15s}.notes-panel textarea:focus{outline:0;background:#fff;box-shadow:inset 0 0 0 1px var(--faint)}.notes-panel textarea::placeholder{color:var(--faint)}
|
|
73
|
+
.state{font-size:12px;line-height:1.6;color:var(--faint);padding:8px 2px 0}.state:empty{display:none}.state[data-warn]{color:#b3401f}
|
|
74
|
+
|
|
75
|
+
.toast{position:fixed;left:50%;bottom:24px;z-index:40;display:flex;align-items:center;gap:12px;max-width:min(560px,calc(100vw - 32px));padding:10px 16px 10px 12px;border-radius:12px;background:var(--ink);color:#fff;box-shadow:0 18px 44px -10px #00000066;font-size:13.5px;line-height:1.5;opacity:0;transform:translate(-50%,14px) scale(.96);pointer-events:none;transition:opacity .16s var(--exit),transform .2s var(--exit)}
|
|
76
|
+
.toast.show{opacity:1;transform:translate(-50%,0);pointer-events:auto;transition:opacity .18s ease-out,transform var(--snap-d) var(--snap)}
|
|
77
|
+
.toast .tick{flex:none;width:22px;height:22px;border-radius:50%;background:var(--hl);display:grid;place-items:center}.toast .tick::before{content:"";width:8px;height:4px;margin-top:-2px;border-left:1.6px solid var(--ink);border-bottom:1.6px solid var(--ink);transform:rotate(-45deg)}.toast[data-fail] .tick{background:#c2462e}.toast[data-fail] .tick::before{width:2px;height:9px;margin:0;border:0;background:#fff;transform:none}
|
|
78
|
+
.toast b{font-weight:600;white-space:nowrap}.toast code{font-family:var(--mono);font-size:12.5px;color:#a8a8a8;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;user-select:all}.toast[data-fail] code{white-space:pre-wrap}
|
|
79
|
+
|
|
80
|
+
.none,.empty{max-width:560px;margin:18vh auto;padding:24px;text-align:center;font-size:15px;line-height:1.8;color:var(--soft)}.none button{margin-top:12px;color:var(--text);text-decoration:underline;text-underline-offset:4px}
|
|
81
|
+
|
|
82
|
+
@keyframes rise{from{opacity:0;transform:translateY(16px)}}
|
|
83
|
+
@keyframes pop-in{from{opacity:0;transform:translateY(-4px) scale(.96)}}
|
|
84
|
+
@keyframes vt-out{to{opacity:0;transform:scale(.97)}}
|
|
85
|
+
@keyframes vt-in{from{opacity:0;transform:scale(.98)}}
|
|
86
|
+
@keyframes info-in{from{transform:translateX(100%)}}
|
|
87
|
+
@keyframes info-out{to{transform:translateX(100%)}}
|
|
88
|
+
/* 并排与单张之间、视口与筛选变化用页面过渡:同一方向的画面从卡片放大到单张,其他卡片淡出,侧栏从右侧滑入 */
|
|
89
|
+
::view-transition-group(*){animation-duration:var(--glide-d);animation-timing-function:var(--glide)}
|
|
90
|
+
::view-transition-old(*),::view-transition-new(*){height:100%;object-fit:cover;object-position:50% 0}
|
|
91
|
+
::view-transition-old(root),::view-transition-new(root){animation-duration:.2s}
|
|
92
|
+
::view-transition-old(*):only-child{animation:vt-out .18s var(--exit) both}
|
|
93
|
+
::view-transition-new(*):only-child{animation:vt-in .3s .08s ease-out both}
|
|
94
|
+
::view-transition-new(info):only-child{animation:info-in var(--glide-d) var(--glide) both}
|
|
95
|
+
::view-transition-old(info):only-child{animation:info-out .22s var(--exit) both}
|
|
96
|
+
::view-transition-old(bar){display:none}::view-transition-new(bar){animation:none}
|
|
97
|
+
/* 视口、筛选、说明开关和缩放只改变形状与位置:外框直接变形,内容不做叠影 */
|
|
98
|
+
html[data-vt=reshape]::view-transition-old(*){animation:none;opacity:0}
|
|
99
|
+
html[data-vt=reshape]::view-transition-new(*){animation:none}
|
|
100
|
+
html[data-vt=reshape]::view-transition-old(*):only-child{animation:vt-out .18s var(--exit) both;opacity:1}
|
|
101
|
+
html[data-vt=reshape]::view-transition-new(*):only-child{animation:vt-in .3s .08s ease-out both}
|
|
102
|
+
|
|
103
|
+
@media(max-width:1180px){.grid{--cols:2!important}.bar{grid-template-columns:minmax(0,1fr) auto;row-gap:10px}.bar-r{grid-column:1/-1;justify-content:flex-start;flex-wrap:wrap}.loupe{grid-template-columns:minmax(0,1fr) 280px}}
|
|
104
|
+
@media(max-width:760px){.bar{grid-template-columns:1fr;padding:10px 16px}.bar-c{flex-wrap:wrap}.intro{padding:24px 16px 0}.grid{--cols:1!important;padding:24px 16px 48px;gap:48px}.loupe{position:static;display:flex;flex-direction:column}.surface{flex:none}.track{height:auto;align-items:flex-start}.slide{height:auto;padding:16px 12px}.info{border-left:0;border-top:1px solid var(--line)}.film{display:flex;overflow-x:auto}.film button{flex:none}.info-body{padding:20px 16px}.nav{display:none}.picked{border-left:0;padding-left:0}.notes-panel{width:min(340px,calc(100vw - 32px))}.filter-list{right:auto;left:0;transform-origin:top left}}
|
|
105
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:0s!important;animation-duration:0s!important;animation-delay:0s!important}.frame:hover{transform:none}}
|
|
106
|
+
</style></head><body>
|
|
107
|
+
<header class="bar" id="bar" hidden>
|
|
108
|
+
<div class="bar-l"><span class="brand" title="风格对比"><span class="wm">风格对比</span></span><span class="sep" aria-hidden="true"></span><span class="proj" id="project"></span><span class="round" id="round"></span></div>
|
|
109
|
+
<div class="bar-c">
|
|
110
|
+
<div class="seg" role="group" aria-label="查看方式"><span class="thumb" aria-hidden="true"></span><button type="button" data-layout="compare" aria-pressed="true">并排</button><button type="button" data-layout="loupe" aria-pressed="false">单张</button></div>
|
|
111
|
+
<div class="seg" role="group" aria-label="预览视口"><span class="thumb" aria-hidden="true"></span><button type="button" data-viewport="desktop" aria-pressed="true">桌面 1280</button><button type="button" data-viewport="mobile" aria-pressed="false">手机 390</button></div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="bar-r">
|
|
114
|
+
<label class="toggle"><input id="notes-toggle" type="checkbox" checked>设计说明</label>
|
|
115
|
+
<details class="filter"><summary id="filter-summary"></summary><div id="filter-list" class="filter-list"></div></details>
|
|
116
|
+
<span class="picked" id="picked" title="当前选择的方向"><span id="choice-chip" class="choice-empty"></span><span id="decide-name">未选择方向</span></span>
|
|
117
|
+
<details class="filter"><summary id="notes-open">备注</summary><div class="filter-list notes-panel"><textarea id="notes" aria-label="备注" placeholder="喜欢哪里、要改哪里,可以用编号指明方向。"></textarea><p id="state" class="state"></p></div></details>
|
|
118
|
+
</div>
|
|
119
|
+
</header>
|
|
120
|
+
<div id="compare-view" hidden>
|
|
121
|
+
<section class="intro"><p id="brief"></p></section>
|
|
122
|
+
<main id="grid" class="grid" aria-label="候选方向"></main>
|
|
123
|
+
</div>
|
|
124
|
+
<main id="loupe" class="loupe" hidden aria-label="单张查看">
|
|
125
|
+
<div class="view">
|
|
126
|
+
<button type="button" class="nav prev" id="prev" aria-label="上一个方向">←</button>
|
|
127
|
+
<div id="surface" class="surface"><div id="track" class="track"></div></div>
|
|
128
|
+
<button type="button" class="nav next" id="next" aria-label="下一个方向">→</button>
|
|
129
|
+
</div>
|
|
130
|
+
<aside id="info" class="info"><nav id="film" class="film" aria-label="切换方向"></nav><div id="info-body" class="info-body" aria-live="polite"></div></aside>
|
|
131
|
+
</main>
|
|
132
|
+
<div id="none" class="none" hidden>当前没有要比较的方向。<br><button type="button" id="show-all">显示全部</button></div>
|
|
133
|
+
<div id="empty" class="empty" hidden>尚未加入设计小样。按 references/style-explorer.md 准备 manifest 后用生成器组装。</div>
|
|
134
|
+
<div id="toast" class="toast" role="status" aria-live="polite"><span class="tick"></span><b></b><code></code></div>
|
|
135
|
+
<script>
|
|
136
|
+
const DATA = /*__ATW_EXPLORER_DATA__*/ null;
|
|
137
|
+
(()=>{
|
|
138
|
+
const $=s=>document.querySelector(s),$$=s=>document.querySelectorAll(s);
|
|
139
|
+
if(!DATA?.candidates?.length){$('#empty').hidden=false;return;}
|
|
140
|
+
const C=DATA.candidates,SIZES={desktop:{w:1280,h:900},mobile:{w:390,h:844}},KEY=`atw-prototype:${DATA.fingerprint||DATA.project}`;
|
|
141
|
+
const byId=Object.fromEntries(C.map(c=>[c.id,c])),DIRS=C.filter(c=>!c.baseline),no=c=>c.baseline?'现状':String(DIRS.indexOf(c)+1).padStart(2,'0'),vtName=c=>`stage-${C.indexOf(c)}`;
|
|
142
|
+
const root=document.documentElement,calm=matchMedia('(prefers-reduced-motion: reduce)'),narrow=()=>matchMedia('(max-width:760px)').matches;
|
|
143
|
+
const ease=n=>getComputedStyle(root).getPropertyValue(`--${n}`).trim()||'ease-out';
|
|
144
|
+
let st={layout:'compare',viewport:'desktop',notesOn:true,visible:C.map(c=>c.id),current:C[0].id,chosen:null,notes:''},stored=true,zoomActual=false,compareScroll=0,gridKey='',loupeKey='',toastTimer=0;
|
|
145
|
+
try{const saved=JSON.parse(localStorage.getItem(KEY)||'null');if(saved&&typeof saved==='object'){Object.assign(st,saved);st.visible=(Array.isArray(st.visible)?st.visible:[]).filter(id=>byId[id]);if(!byId[st.chosen])st.chosen=null;if(!byId[st.current])st.current=C[0].id;if(!['compare','loupe'].includes(st.layout))st.layout='compare';if(!SIZES[st.viewport])st.viewport='desktop';}}catch{stored=false;}
|
|
146
|
+
function save(){if(!stored)return;try{localStorage.setItem(KEY,JSON.stringify(st));}catch{stored=false;status();}}
|
|
147
|
+
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!=null)n.textContent=text;return n;};
|
|
148
|
+
const shown=()=>C.filter(c=>st.visible.includes(c.id));
|
|
149
|
+
|
|
150
|
+
// 动效工具:尊重“减少动态效果”,浏览器不支持时直接落到最终状态
|
|
151
|
+
function animate(node,frames,opts){if(calm.matches||!node?.animate)return;try{node.animate(frames,opts);}catch{node.animate(frames,{...opts,easing:'ease-out'});}}
|
|
152
|
+
let vtSeq=0;
|
|
153
|
+
function morph(update,kind){
|
|
154
|
+
if(calm.matches||!document.startViewTransition||document.hidden){update();return;}
|
|
155
|
+
const n=++vtSeq;root.dataset.vt=kind||'';
|
|
156
|
+
try{document.startViewTransition(update).finished.finally(()=>{if(n===vtSeq)delete root.dataset.vt;});}catch{update();}
|
|
157
|
+
}
|
|
158
|
+
function slideTo(ind,target,instant){
|
|
159
|
+
if(!ind||!target||!target.offsetWidth)return;
|
|
160
|
+
if(instant)ind.style.transition='none';
|
|
161
|
+
ind.style.width=`${target.offsetWidth}px`;ind.style.height=`${target.offsetHeight}px`;ind.style.transform=`translate(${target.offsetLeft}px,${target.offsetTop}px)`;
|
|
162
|
+
if(instant){ind.offsetWidth;ind.style.transition='';}
|
|
163
|
+
}
|
|
164
|
+
const placeThumbs=instant=>$$('.seg').forEach(s=>slideTo(s.querySelector('.thumb'),s.querySelector('[aria-pressed=true]'),instant));
|
|
165
|
+
const placeFilm=instant=>{const f=$('#film');slideTo(f.querySelector('.film-ind'),f.querySelector('[aria-current=true]'),instant);};
|
|
166
|
+
|
|
167
|
+
document.title=`${DATA.project} · 风格对比`;
|
|
168
|
+
$('#project').textContent=DATA.project;$('#round').textContent=DATA.round||'';
|
|
169
|
+
$('#brief').textContent=DATA.brief||'';
|
|
170
|
+
|
|
171
|
+
function stripMini(c){const s=el('span','strip-mini');(c.palette||[]).forEach(h=>{const i=el('i');i.style.background=h;s.append(i);});return s;}
|
|
172
|
+
function stage(c,interactive){
|
|
173
|
+
const s=el('div','stage');s.dataset.id=c.id;
|
|
174
|
+
const m=document.createElement(c.kind==='image'?'img':'iframe');
|
|
175
|
+
if(c.kind==='image'){m.alt=c.name;m.draggable=false;}
|
|
176
|
+
else{m.title=c.name;m.setAttribute('sandbox',c.kind==='url'?'allow-scripts allow-same-origin allow-forms':c.interactive?'allow-scripts allow-forms':'');m.referrerPolicy='no-referrer';if(!interactive)m.tabIndex=-1;}
|
|
177
|
+
m.addEventListener('load',()=>{s.classList.add('ready');fit(s);});
|
|
178
|
+
if(c.kind==='image')m.src=c.content;else if(c.kind==='url')m.src=c.content;else m.srcdoc=c.content;
|
|
179
|
+
s.append(m);
|
|
180
|
+
if(c.kind!=='html'||c.interactive)s.append(el('span','static',c.kind==='url'?'本地运行':c.kind==='image'?'静态图':'可操作'));
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
183
|
+
function fit(s){
|
|
184
|
+
if(!s.isConnected)return;
|
|
185
|
+
const size=SIZES[st.viewport],m=s.querySelector('iframe,img'),box=s.parentElement;let r;
|
|
186
|
+
if(box.classList.contains('slide')){const cs=getComputedStyle(box),w=box.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight),h=narrow()?Infinity:box.clientHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom);r=box.dataset.zoom==='actual'?1:Math.min(w/size.w,h/size.h,1);}
|
|
187
|
+
else r=Math.min(box.clientWidth/size.w,1);
|
|
188
|
+
if(!(r>0))return;
|
|
189
|
+
s.style.width=`${size.w*r}px`;s.style.height=`${size.h*r}px`;m.style.width=`${size.w}px`;m.style.height=`${size.h}px`;m.style.transform=`scale(${r})`;
|
|
190
|
+
}
|
|
191
|
+
const fitAll=()=>$$('.stage').forEach(fit),refit=()=>requestAnimationFrame(fitAll);
|
|
192
|
+
|
|
193
|
+
function specList(c){
|
|
194
|
+
const dl=el('dl','spec');const row=(k,v)=>{dl.append(el('dt',null,k),el('dd',null,v));};
|
|
195
|
+
row('字体',c.typography);row('特征',(c.traits||[]).join(' · '));return dl;
|
|
196
|
+
}
|
|
197
|
+
function pickButton(c){const b=el('button','pick');b.type='button';b.dataset.pick=c.id;b.addEventListener('click',()=>choose(c.id));return b;}
|
|
198
|
+
function card(c,i){
|
|
199
|
+
const a=el('article','card');a.dataset.id=c.id;a.style.setProperty('--i',i);a.setAttribute('aria-label',c.name);
|
|
200
|
+
const frame=el('div','frame');frame.style.viewTransitionName=vtName(c);const s=stage(c,false);
|
|
201
|
+
const hit=el('button','hit');hit.type='button';hit.setAttribute('aria-label',`单张查看 ${c.name}`);hit.addEventListener('click',()=>setLayout('loupe',c.id));
|
|
202
|
+
s.append(hit);frame.append(s,el('span','pick-mark','已选择'),el('span','peek','点击单张查看 · 实际尺寸'));
|
|
203
|
+
const cap=el('div','cap');cap.style.viewTransitionName=`cap-${C.indexOf(c)}`;cap.append(el('span',c.baseline?'no base':'no',no(c)),el('h2','name',c.name),stripMini(c),pickButton(c));
|
|
204
|
+
const lede=el('p','lede',c.concept);lede.title=c.concept;lede.style.viewTransitionName=`lede-${C.indexOf(c)}`;
|
|
205
|
+
a.append(frame,cap,lede);return a;
|
|
206
|
+
}
|
|
207
|
+
function syncCompare(){
|
|
208
|
+
const list=shown(),g=$('#grid'),key=list.map(c=>c.id).join();
|
|
209
|
+
if(key!==gridKey){gridKey=key;g.replaceChildren(...list.map(card));}
|
|
210
|
+
g.dataset.notes=st.notesOn?'on':'off';g.style.setProperty('--cols',Math.min(Math.max(list.length,1),st.viewport==='mobile'?4:3));
|
|
211
|
+
$$('.card .frame').forEach(f=>f.dataset.viewport=st.viewport);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 单张查看:所有方向预先排成一条横向轨道,切换时整条轨道滑动,不重新加载画面
|
|
215
|
+
function syncLoupe(dir){
|
|
216
|
+
const list=shown(),key=list.map(c=>c.id).join();if(!list.length)return;
|
|
217
|
+
if(key!==loupeKey){
|
|
218
|
+
loupeKey=key;dir=0;
|
|
219
|
+
$('#track').replaceChildren(...list.map(c=>{const sl=el('div','slide');sl.dataset.id=c.id;sl.append(stage(c,true));return sl;}));
|
|
220
|
+
$('#film').replaceChildren(el('span','film-ind'),...list.map(c=>{const b=el('button');b.type='button';b.dataset.id=c.id;b.append(el('span','fn',no(c)),el('span','ft',c.name),stripMini(c));b.addEventListener('click',()=>go(c.id));return b;}));
|
|
221
|
+
}
|
|
222
|
+
if(!list.some(c=>c.id===st.current))st.current=list[0].id;
|
|
223
|
+
const i=list.findIndex(c=>c.id===st.current),track=$('#track');
|
|
224
|
+
if(!dir)track.style.transition='none';
|
|
225
|
+
track.style.transform=`translateX(${-i*100}%)`;
|
|
226
|
+
if(!dir){track.offsetWidth;track.style.transition='';}
|
|
227
|
+
$$('.slide').forEach(sl=>{const on=sl.dataset.id===st.current;sl.classList.toggle('is-current',on);sl.inert=!on;sl.dataset.zoom=on&&zoomActual?'actual':'fit';sl.firstElementChild.style.viewTransitionName=on?vtName(byId[sl.dataset.id]):'none';});
|
|
228
|
+
$('#prev').disabled=i<=0;$('#next').disabled=i>=list.length-1;
|
|
229
|
+
$$('#film button').forEach(b=>b.setAttribute('aria-current',String(b.dataset.id===st.current)));
|
|
230
|
+
renderInfo(dir);placeFilm(!dir);
|
|
231
|
+
const cur=$('#film [aria-current=true]'),f=$('#film');
|
|
232
|
+
if(cur&&f.scrollWidth>f.clientWidth)f.scrollTo({left:cur.offsetLeft-16,behavior:calm.matches||!dir?'auto':'smooth'});
|
|
233
|
+
}
|
|
234
|
+
function renderInfo(dir){
|
|
235
|
+
const c=byId[st.current],body=$('#info-body');
|
|
236
|
+
const sw=el('div','big-sw');(c.palette||[]).forEach(h=>{const d=el('div');const x=el('i');x.style.background=h;d.append(x,h.toUpperCase());sw.append(d);});
|
|
237
|
+
const tools=el('div','tools');const z=el('button','tool',zoomActual?'适应窗口':'实际尺寸 100%');z.id='zoom';z.type='button';z.setAttribute('aria-pressed',String(zoomActual));z.addEventListener('click',toggleZoom);tools.append(z);
|
|
238
|
+
const hint=el('p','hint');hint.innerHTML='<kbd>←</kbd> <kbd>→</kbd> 切换方向 <kbd>P</kbd> 选择并复制<br><kbd>Esc</kbd> 回到并排';
|
|
239
|
+
const blocks=[el('span','idx',no(c)),el('h2','name',c.name)];
|
|
240
|
+
if(st.notesOn)blocks.push(el('p','north',c.concept),sw,specList(c));
|
|
241
|
+
blocks.push(pickButton(c),tools,hint,el('p','src',c.kind==='image'?`静态图:${c.sourceLabel}。缩放不代表响应式重排。`:c.kind==='url'?`实时运行 ${c.sourceLabel}。空白时确认开发服务器已启动,且允许被嵌入。`:c.interactive?`单张查看时可以直接操作 ${c.sourceLabel};无法联网,也不保留浏览器存储。`:`预览禁用脚本与提交;交互请直接打开 ${c.sourceLabel}。`));
|
|
242
|
+
body.replaceChildren(...blocks);paintChoice();
|
|
243
|
+
if(dir)animate(body,[{opacity:0,transform:`translateX(${dir*18}px)`},{opacity:1,transform:'none'}],{duration:420,easing:ease('glide')});
|
|
244
|
+
}
|
|
245
|
+
function toggleZoom(){
|
|
246
|
+
morph(()=>{zoomActual=!zoomActual;const sl=$('.slide.is-current');sl.dataset.zoom=zoomActual?'actual':'fit';fitAll();sl.scrollTo(0,0);const z=$('#zoom');z.textContent=zoomActual?'适应窗口':'实际尺寸 100%';z.setAttribute('aria-pressed',String(zoomActual));},'reshape');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function render(){
|
|
250
|
+
const list=shown(),loupe=st.layout==='loupe'&&list.length>0;
|
|
251
|
+
$('#compare-view').hidden=st.layout!=='compare'||!list.length;$('#loupe').hidden=!loupe;$('#none').hidden=list.length>0;
|
|
252
|
+
syncCompare();syncLoupe(0);
|
|
253
|
+
$$('[data-layout]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.layout===st.layout)));
|
|
254
|
+
$$('[data-viewport]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.viewport===st.viewport)));
|
|
255
|
+
placeThumbs(false);placeFilm(true);
|
|
256
|
+
$('#notes-toggle').checked=st.notesOn;
|
|
257
|
+
$('#filter-summary').textContent=`比较范围 ${list.length}/${C.length}`;
|
|
258
|
+
$$('#filter-list input').forEach(i=>i.checked=st.visible.includes(i.value));
|
|
259
|
+
paintChoice();fitAll();
|
|
260
|
+
}
|
|
261
|
+
function setLayout(layout,id){
|
|
262
|
+
if(layout===st.layout&&!(id&&id!==st.current))return;
|
|
263
|
+
if(layout==='loupe'&&st.layout==='compare')compareScroll=scrollY;
|
|
264
|
+
if(id)st.current=id;st.layout=layout;zoomActual=false;save();
|
|
265
|
+
morph(()=>{
|
|
266
|
+
render();
|
|
267
|
+
if(layout==='loupe'){window.scrollTo(0,0);return;}
|
|
268
|
+
window.scrollTo(0,compareScroll);
|
|
269
|
+
const a=$(`.card[data-id="${CSS.escape(st.current)}"]`),r=a?.getBoundingClientRect();
|
|
270
|
+
if(r&&(r.top<60||r.bottom>innerHeight))a.scrollIntoView({block:'center'});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function go(id){
|
|
274
|
+
const list=shown(),from=list.findIndex(c=>c.id===st.current),to=list.findIndex(c=>c.id===id);
|
|
275
|
+
if(to<0||to===from)return;
|
|
276
|
+
st.current=id;zoomActual=false;save();syncLoupe(Math.sign(to-from));fitAll();
|
|
277
|
+
}
|
|
278
|
+
function step(d){const list=shown(),i=list.findIndex(c=>c.id===st.current),n=list[i+d];if(n)go(n.id);else nudge(d);}
|
|
279
|
+
function nudge(d){animate($('.slide.is-current .stage'),[{transform:'none'},{transform:`translateX(${d*14}px)`},{transform:'none'}],{duration:320,easing:'ease-out'});}
|
|
280
|
+
|
|
281
|
+
// 选择即复制:再次点击“已选择”会带上最新备注重新复制,不再有单独的复制按钮
|
|
282
|
+
function choose(id){
|
|
283
|
+
const fresh=st.chosen!==id;st.chosen=id;save();paintChoice();
|
|
284
|
+
if(fresh){
|
|
285
|
+
animate($(`.card[data-id="${CSS.escape(id)}"] .pick-mark`),[{opacity:0,transform:'scale(.4)'},{opacity:1,transform:'scale(1)'}],{duration:500,easing:ease('pop')});
|
|
286
|
+
animate($('#decide-name'),[{opacity:0,transform:'translateY(10px)'},{opacity:1,transform:'none'}],{duration:380,easing:ease('glide')});
|
|
287
|
+
animate($('#choice-chip'),[{transform:'scale(.3)'},{transform:'scale(1)'}],{duration:500,easing:ease('pop')});
|
|
288
|
+
}
|
|
289
|
+
copy();
|
|
290
|
+
}
|
|
291
|
+
function paintChoice(){
|
|
292
|
+
$$('.card').forEach(a=>a.classList.toggle('is-chosen',a.dataset.id===st.chosen));
|
|
293
|
+
$$('[data-pick]').forEach(b=>{const on=b.dataset.pick===st.chosen,short=!!b.closest('.cap');b.replaceChildren(document.createTextNode(on?(short?'已选择':'已选择 · 复制'):(short?'选择':'选这个方向')));if(!short)b.append(el('kbd',null,'P'));b.toggleAttribute('data-on',on);b.title=on?'再点一次,带上最新备注重新复制':'选择这个方向,并复制给 Agent';});
|
|
294
|
+
$$('#film button').forEach(b=>b.querySelector('.ft').classList.toggle('chosen',b.dataset.id===st.chosen));
|
|
295
|
+
const c=byId[st.chosen];$('#decide-name').textContent=c?`${no(c)} ${c.name}`:'未选择方向';$('#picked').classList.toggle('on',!!c);
|
|
296
|
+
const chip=$('#choice-chip'),next=c?stripMini(c):el('span','choice-empty');next.id='choice-chip';chip.replaceWith(next);
|
|
297
|
+
status();
|
|
298
|
+
}
|
|
299
|
+
function status(){$('#notes-open').textContent=st.notes?'备注 ·':'备注';const s=$('#state');if(!stored){s.textContent='浏览器存储不可用:关闭页面后选择和备注不会保留。';s.dataset.warn='';}else{s.textContent='备注会和选择一起复制。改完备注,再点一次“已选择”重新复制。';delete s.dataset.warn;}}
|
|
300
|
+
function copyText(){const c=byId[st.chosen];let t=`${DATA.round?DATA.round+':':''}选 ${no(c)} ${c.name}`;if(st.notes.trim())t+=`\n备注:${st.notes.trim()}`;return t;}
|
|
301
|
+
async function copy(){
|
|
302
|
+
const t=copyText();let ok=false;
|
|
303
|
+
try{await navigator.clipboard.writeText(t);ok=true;}catch{const a=el('textarea');a.value=t;a.setAttribute('readonly','');a.style.cssText='position:fixed;opacity:0';document.body.append(a);a.select();try{ok=document.execCommand('copy');}catch{}a.remove();}
|
|
304
|
+
toast(ok,t);
|
|
305
|
+
}
|
|
306
|
+
function toast(ok,t){
|
|
307
|
+
const n=$('#toast');n.toggleAttribute('data-fail',!ok);n.querySelector('b').textContent=ok?'已复制,粘贴给 Agent':'复制失败,请手动复制';n.querySelector('code').textContent=ok?t.replace(/\n/g,' · '):t;
|
|
308
|
+
n.classList.remove('show');n.offsetWidth;n.classList.add('show');
|
|
309
|
+
clearTimeout(toastTimer);toastTimer=setTimeout(()=>n.classList.remove('show'),ok?2400:8000);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const fl=$('#filter-list');
|
|
313
|
+
C.forEach(c=>{const l=el('label'),i=el('input');i.type='checkbox';i.value=c.id;i.addEventListener('change',()=>{st.visible=C.map(x=>x.id).filter(id=>id===c.id?i.checked:st.visible.includes(id));save();morph(render,'reshape');});l.append(i,el('span','n',no(c)),c.name);fl.append(l);});
|
|
314
|
+
$('#show-all').addEventListener('click',()=>{st.visible=C.map(c=>c.id);save();morph(render,'reshape');});
|
|
315
|
+
$$('button[data-layout]').forEach(b=>b.addEventListener('click',()=>setLayout(b.dataset.layout)));
|
|
316
|
+
$$('button[data-viewport]').forEach(b=>b.addEventListener('click',()=>{if(st.viewport===b.dataset.viewport)return;st.viewport=b.dataset.viewport;save();morph(render,'reshape');}));
|
|
317
|
+
$('#notes-toggle').addEventListener('change',e=>{st.notesOn=e.target.checked;save();morph(render,'reshape');});
|
|
318
|
+
$('#prev').addEventListener('click',()=>step(-1));$('#next').addEventListener('click',()=>step(1));
|
|
319
|
+
document.addEventListener('keydown',e=>{
|
|
320
|
+
if(e.target.closest?.('textarea,input,summary')||e.metaKey||e.ctrlKey||e.altKey||st.layout!=='loupe')return;
|
|
321
|
+
if(e.key==='ArrowLeft')step(-1);else if(e.key==='ArrowRight')step(1);else if(e.key==='Escape')setLayout('compare');else if(e.key.toLowerCase()==='p')choose(st.current);
|
|
322
|
+
});
|
|
323
|
+
const notes=$('#notes');notes.value=st.notes||'';
|
|
324
|
+
notes.addEventListener('input',()=>{st.notes=notes.value;save();status();});
|
|
325
|
+
$('#notes-open').parentElement.addEventListener('toggle',e=>{if(e.target.open)notes.focus();});
|
|
326
|
+
function syncBars(){root.style.setProperty('--bar-h',`${$('#bar').offsetHeight}px`);placeThumbs(true);placeFilm(true);refit();}
|
|
327
|
+
window.addEventListener('resize',syncBars,{passive:true});
|
|
328
|
+
document.fonts?.ready.then(()=>{placeThumbs(true);placeFilm(true);});
|
|
329
|
+
$('#bar').hidden=false;
|
|
330
|
+
if(st.layout==='compare'){$('#grid').classList.add('first');$('.intro').classList.add('first');setTimeout(()=>{$('#grid').classList.remove('first');$('.intro').classList.remove('first');},1400);}
|
|
331
|
+
render();syncBars();
|
|
332
|
+
})();
|
|
333
|
+
</script></body></html>
|