@chinhae/atw-cli 0.2.8 → 0.2.9
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.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 +328 -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/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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a portable, offline design comparison from a local manifest."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
from html.parser import HTMLParser
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from urllib.parse import urlsplit
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
TEMPLATE = SKILL_ROOT / "assets" / "style-explorer.html"
|
|
20
|
+
MARKER = "/*__ATW_EXPLORER_DATA__*/ null"
|
|
21
|
+
PREVIEW_CSP = (
|
|
22
|
+
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; "
|
|
23
|
+
"font-src data:; media-src data:; script-src 'none'; "
|
|
24
|
+
"form-action 'none'; base-uri 'none'; object-src 'none'"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def text_field(obj: dict, name: str, *, default: str | None = None) -> str:
|
|
29
|
+
value = obj.get(name, default)
|
|
30
|
+
if not isinstance(value, str) or not value.strip():
|
|
31
|
+
raise ValueError(f"{name} 必须是非空字符串")
|
|
32
|
+
return value.strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def text_list(obj: dict, name: str) -> list[str]:
|
|
36
|
+
values = obj.get(name)
|
|
37
|
+
if not isinstance(values, list) or not values:
|
|
38
|
+
raise ValueError(f"{name} 必须是非空字符串数组")
|
|
39
|
+
if any(not isinstance(v, str) or not v.strip() for v in values):
|
|
40
|
+
raise ValueError(f"{name} 的每项必须是非空字符串")
|
|
41
|
+
return [v.strip() for v in values]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AssetCheck(HTMLParser):
|
|
45
|
+
"""Reject resource dependencies; the browser sandbox disables behavior."""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.styles = []
|
|
50
|
+
self.in_style = False
|
|
51
|
+
self.head_position = None
|
|
52
|
+
|
|
53
|
+
def handle_starttag(self, tag: str, attrs: list) -> None:
|
|
54
|
+
attrs = dict(attrs)
|
|
55
|
+
if tag == "head" and self.head_position is None:
|
|
56
|
+
self.head_position = (self.getpos(), self.get_starttag_text())
|
|
57
|
+
if tag in ("iframe", "frame", "object", "embed"):
|
|
58
|
+
raise ValueError("候选 HTML 不支持嵌套文档;请提供静态内容或截图")
|
|
59
|
+
if tag == "style":
|
|
60
|
+
self.in_style = True
|
|
61
|
+
if attrs.get("style"):
|
|
62
|
+
self.styles.append(attrs["style"])
|
|
63
|
+
if tag == "base":
|
|
64
|
+
raise ValueError("候选 HTML 不应包含 base;资源需要内嵌")
|
|
65
|
+
if tag == "meta" and attrs.get("http-equiv", "").lower() == "refresh":
|
|
66
|
+
raise ValueError("候选 HTML 不应自动跳转")
|
|
67
|
+
for key in ("src", "poster", "data", "href", "xlink:href"):
|
|
68
|
+
value = attrs.get(key, "") or ""
|
|
69
|
+
if not value or value.startswith(("#", "data:")):
|
|
70
|
+
continue
|
|
71
|
+
raise ValueError(f"候选 HTML 含未内嵌资源 {tag}.{key}: {value}")
|
|
72
|
+
if attrs.get("srcset"):
|
|
73
|
+
raise ValueError("候选 HTML 请用内嵌 src 代替 srcset")
|
|
74
|
+
|
|
75
|
+
def handle_endtag(self, tag: str) -> None:
|
|
76
|
+
if tag == "style":
|
|
77
|
+
self.in_style = False
|
|
78
|
+
|
|
79
|
+
def handle_data(self, data: str) -> None:
|
|
80
|
+
if self.in_style:
|
|
81
|
+
self.styles.append(data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
CSS_TOKEN = re.compile(
|
|
85
|
+
r"(?P<comment>/\*.*?\*/)|(?P<string>\"(?:\\.|[^\"\\])*\"|'(?:\\.|[^'\\])*')"
|
|
86
|
+
r"|(?P<ident>(?:[-_a-zA-Z]|\\(?:[0-9a-fA-F]{1,6}\s?|[^\r\n]))"
|
|
87
|
+
r"(?:[-_a-zA-Z0-9]|\\(?:[0-9a-fA-F]{1,6}\s?|[^\r\n]))*)"
|
|
88
|
+
r"|(?P<space>\s+)|(?P<symbol>.)", re.S,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def css_unescape(value: str) -> str:
|
|
93
|
+
def replacement(match):
|
|
94
|
+
if match.group(1):
|
|
95
|
+
codepoint = int(match.group(1), 16)
|
|
96
|
+
return chr(codepoint) if 0 < codepoint <= 0x10FFFF else "\ufffd"
|
|
97
|
+
return match.group(2)
|
|
98
|
+
return re.sub(r"\\([0-9a-fA-F]{1,6})(?:\s)?|\\([^\r\n])", replacement, value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_css(css: str, label: str) -> None:
|
|
102
|
+
tokens = [(m.lastgroup, m.group()) for m in CSS_TOKEN.finditer(css)
|
|
103
|
+
if m.lastgroup not in ("comment", "space")]
|
|
104
|
+
|
|
105
|
+
def check_resource(value: str):
|
|
106
|
+
if not css_unescape(value).strip().lower().startswith(("data:", "#")):
|
|
107
|
+
raise ValueError(f"{label}: CSS 含未内嵌资源")
|
|
108
|
+
|
|
109
|
+
for index, (kind, value) in enumerate(tokens):
|
|
110
|
+
if value == "@" and index + 1 < len(tokens) and tokens[index + 1][0] == "ident" and css_unescape(tokens[index + 1][1]).lower() == "import":
|
|
111
|
+
raise ValueError(f"{label}: 请内嵌 CSS,不使用 @import")
|
|
112
|
+
name = css_unescape(value).lower() if kind == "ident" else ""
|
|
113
|
+
if name not in ("url", "image-set", "-webkit-image-set", "image", "src"):
|
|
114
|
+
continue
|
|
115
|
+
if index + 1 >= len(tokens) or tokens[index + 1][1] != "(":
|
|
116
|
+
continue
|
|
117
|
+
depth, body = 1, []
|
|
118
|
+
for inner_kind, inner_value in tokens[index + 2:]:
|
|
119
|
+
if inner_kind == "symbol" and inner_value == "(":
|
|
120
|
+
depth += 1
|
|
121
|
+
elif inner_kind == "symbol" and inner_value == ")":
|
|
122
|
+
depth -= 1
|
|
123
|
+
if depth == 0:
|
|
124
|
+
break
|
|
125
|
+
if name == "url":
|
|
126
|
+
body.append(inner_value[1:-1] if inner_kind == "string" else inner_value)
|
|
127
|
+
elif depth == 1 and inner_kind == "string":
|
|
128
|
+
check_resource(inner_value[1:-1])
|
|
129
|
+
if name == "url":
|
|
130
|
+
check_resource("".join(body))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Sandboxed previews have no storage; give prototypes an in-memory stand-in so their scripts keep running.
|
|
134
|
+
STORAGE_SHIM = (
|
|
135
|
+
"<script>(()=>{const mem=()=>{const m=new Map();return{get length(){return m.size},"
|
|
136
|
+
"key:i=>[...m.keys()][i]??null,getItem:k=>m.has(String(k))?m.get(String(k)):null,"
|
|
137
|
+
"setItem:(k,v)=>{m.set(String(k),String(v))},removeItem:k=>{m.delete(String(k))},clear:()=>m.clear()}};"
|
|
138
|
+
"for(const n of['localStorage','sessionStorage']){try{window[n].length}catch{"
|
|
139
|
+
"Object.defineProperty(window,n,{value:mem(),configurable:true})}}})();</script>"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def preview_csp(interactive: bool) -> str:
|
|
144
|
+
# Interactive prototypes may run their own inline scripts, still with no network or external files.
|
|
145
|
+
return PREVIEW_CSP.replace("script-src 'none'", "script-src 'unsafe-inline'") if interactive else PREVIEW_CSP
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
EMBEDDABLE = {
|
|
149
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
|
|
150
|
+
".gif": "image/gif", ".avif": "image/avif", ".svg": "image/svg+xml",
|
|
151
|
+
".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf", ".otf": "font/otf",
|
|
152
|
+
".mp4": "video/mp4", ".webm": "video/webm",
|
|
153
|
+
}
|
|
154
|
+
ATTR_REF = re.compile(r"""(?P<lead>\b(?:src|poster|href|xlink:href)\s*=\s*)(?P<q>["'])(?P<ref>[^"'#][^"']*)(?P=q)""")
|
|
155
|
+
CSS_REF = re.compile(r"""url\(\s*(?P<q>["']?)(?P<ref>[^"')\s][^"')]*)(?P=q)\s*\)""")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def embed_local_files(content: str, base: Path, root: Path, used: set[Path]) -> str:
|
|
159
|
+
"""Inline images, fonts and videos referenced relative to the candidate, as long as they stay inside the manifest folder."""
|
|
160
|
+
|
|
161
|
+
def data_url(ref: str) -> str | None:
|
|
162
|
+
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:|^//", ref):
|
|
163
|
+
return None
|
|
164
|
+
target = (base / ref.split("?")[0].split("#")[0]).resolve()
|
|
165
|
+
mime = EMBEDDABLE.get(target.suffix.lower())
|
|
166
|
+
if not mime or not target.is_file() or not target.is_relative_to(root):
|
|
167
|
+
return None
|
|
168
|
+
used.add(target)
|
|
169
|
+
return f"data:{mime};base64," + base64.b64encode(target.read_bytes()).decode("ascii")
|
|
170
|
+
|
|
171
|
+
def attr(match):
|
|
172
|
+
url = data_url(match["ref"])
|
|
173
|
+
return match.group(0) if url is None else f'{match["lead"]}{match["q"]}{url}{match["q"]}'
|
|
174
|
+
|
|
175
|
+
def css(match):
|
|
176
|
+
url = data_url(match["ref"])
|
|
177
|
+
return match.group(0) if url is None else f'url("{url}")'
|
|
178
|
+
|
|
179
|
+
return CSS_REF.sub(css, ATTR_REF.sub(attr, content))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def prepare_html(path: Path, interactive: bool = False, root: Path | None = None, used: set[Path] | None = None) -> str:
|
|
183
|
+
content = path.read_text(encoding="utf-8")
|
|
184
|
+
if root is not None:
|
|
185
|
+
content = embed_local_files(content, path.parent, root, used if used is not None else set())
|
|
186
|
+
parser = AssetCheck()
|
|
187
|
+
parser.feed(content)
|
|
188
|
+
for style in parser.styles:
|
|
189
|
+
check_css(style, path.name)
|
|
190
|
+
if parser.head_position is None:
|
|
191
|
+
raise ValueError(f"{path.name}: 候选 HTML 需要完整的 head 元素")
|
|
192
|
+
(line, column), start_tag = parser.head_position
|
|
193
|
+
head_end = sum(len(part) + 1 for part in content.split('\n')[:line - 1]) + column + len(start_tag)
|
|
194
|
+
meta = f'<meta http-equiv="Content-Security-Policy" content="{preview_csp(interactive)}">'
|
|
195
|
+
return content[:head_end] + meta + (STORAGE_SHIM if interactive else "") + content[head_end:]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def prepare_image(path: Path) -> str:
|
|
199
|
+
data = path.read_bytes()
|
|
200
|
+
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
201
|
+
mime = "image/png"
|
|
202
|
+
elif data.startswith(b"\xff\xd8\xff"):
|
|
203
|
+
mime = "image/jpeg"
|
|
204
|
+
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
|
205
|
+
mime = "image/webp"
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"{path.name}: 静态预览支持 PNG、JPEG 和 WebP")
|
|
208
|
+
return f"data:{mime};base64," + base64.b64encode(data).decode("ascii")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
LOOPBACK = {"localhost", "127.0.0.1", "::1"}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def local_url(value: str, identifier: str) -> str:
|
|
215
|
+
"""Live candidates only point at a dev server on this machine."""
|
|
216
|
+
parts = urlsplit(value)
|
|
217
|
+
if parts.scheme not in ("http", "https") or (parts.hostname or "").lower() not in LOOPBACK or parts.username or parts.password:
|
|
218
|
+
raise ValueError(f"{identifier}: url 只能是本机开发服务器地址,例如 http://localhost:5173/orders")
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_manifest(path: Path) -> tuple[dict, set[Path]]:
|
|
223
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
224
|
+
if not isinstance(raw, dict) or raw.get("schemaVersion") != 1:
|
|
225
|
+
raise ValueError("manifest.schemaVersion 必须为 1")
|
|
226
|
+
data = {"schemaVersion": 1, "project": text_field(raw, "project"),
|
|
227
|
+
"brief": text_field(raw, "brief"), "round": text_field(raw, "round", default="01")}
|
|
228
|
+
candidates = raw.get("candidates")
|
|
229
|
+
if not isinstance(candidates, list) or not candidates:
|
|
230
|
+
raise ValueError("candidates 至少需要一个候选")
|
|
231
|
+
seen = set()
|
|
232
|
+
inputs = {path, TEMPLATE.resolve()}
|
|
233
|
+
output = []
|
|
234
|
+
for candidate in candidates:
|
|
235
|
+
if not isinstance(candidate, dict):
|
|
236
|
+
raise ValueError("每个候选必须是对象")
|
|
237
|
+
identifier = text_field(candidate, "id")
|
|
238
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,63}", identifier) or identifier in seen:
|
|
239
|
+
raise ValueError(f"候选 id 无效或重复: {identifier}")
|
|
240
|
+
seen.add(identifier)
|
|
241
|
+
kind = candidate.get("kind", "html")
|
|
242
|
+
if kind not in ("html", "image", "url"):
|
|
243
|
+
raise ValueError(f"未知候选 kind: {kind}")
|
|
244
|
+
baseline = candidate.get("baseline", False)
|
|
245
|
+
interactive = candidate.get("interactive", False)
|
|
246
|
+
if not isinstance(baseline, bool) or not isinstance(interactive, bool):
|
|
247
|
+
raise ValueError(f"{identifier}: baseline 和 interactive 必须是 true 或 false")
|
|
248
|
+
if interactive and kind != "html":
|
|
249
|
+
raise ValueError(f"{identifier}: interactive 只用于 html 候选")
|
|
250
|
+
if kind == "url":
|
|
251
|
+
source = None
|
|
252
|
+
url = local_url(text_field(candidate, "url"), identifier)
|
|
253
|
+
else:
|
|
254
|
+
source = (path.parent / text_field(candidate, "source")).resolve()
|
|
255
|
+
if not source.is_relative_to(path.parent) or not source.is_file():
|
|
256
|
+
raise ValueError(f"候选 source 必须是 manifest 目录内可读文件: {identifier}")
|
|
257
|
+
inputs.add(source)
|
|
258
|
+
colors = text_list(candidate, "palette")
|
|
259
|
+
if any(not re.fullmatch(r"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})", c) for c in colors):
|
|
260
|
+
raise ValueError(f"{identifier}: palette 需要十六进制颜色")
|
|
261
|
+
output.append({
|
|
262
|
+
"id": identifier,
|
|
263
|
+
**{name: text_field(candidate, name) for name in ("name", "concept", "typography")},
|
|
264
|
+
"palette": colors, "traits": text_list(candidate, "traits"), "kind": kind,
|
|
265
|
+
"content": url if kind == "url" else prepare_html(source, interactive, path.parent, inputs) if kind == "html" else prepare_image(source),
|
|
266
|
+
"sourceLabel": url if kind == "url" else source.name,
|
|
267
|
+
"baseline": baseline,
|
|
268
|
+
"interactive": interactive,
|
|
269
|
+
})
|
|
270
|
+
if sum(c["baseline"] for c in output) > 1:
|
|
271
|
+
raise ValueError("最多只能有一个基线候选")
|
|
272
|
+
# The current version always sits first so every direction is read against it.
|
|
273
|
+
output.sort(key=lambda c: not c["baseline"])
|
|
274
|
+
data["candidates"] = output
|
|
275
|
+
canonical = json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
276
|
+
data["fingerprint"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:24]
|
|
277
|
+
return data, inputs
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build(manifest: Path, output: Path, *, force: bool = False) -> dict:
|
|
281
|
+
manifest, output = manifest.resolve(), output.resolve()
|
|
282
|
+
data, inputs = load_manifest(manifest)
|
|
283
|
+
if output in inputs or output.is_relative_to(SKILL_ROOT):
|
|
284
|
+
raise ValueError("输出不能覆盖输入或写进 Skill 安装目录")
|
|
285
|
+
if output.exists() and not force:
|
|
286
|
+
raise FileExistsError("输出已存在;使用新路径,或明确加 --force 更新")
|
|
287
|
+
template = TEMPLATE.read_text(encoding="utf-8")
|
|
288
|
+
if template.count(MARKER) != 1:
|
|
289
|
+
raise ValueError("模板数据入口缺失或重复")
|
|
290
|
+
# A closing script tag in metadata or a nested candidate cannot escape the container.
|
|
291
|
+
payload = json.dumps(data, ensure_ascii=False).replace("<", "\\u003c").replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")
|
|
292
|
+
page = template.replace(MARKER, payload)
|
|
293
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
if force:
|
|
295
|
+
temp = None
|
|
296
|
+
try:
|
|
297
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=output.parent, delete=False) as handle:
|
|
298
|
+
temp = Path(handle.name)
|
|
299
|
+
handle.write(page)
|
|
300
|
+
temp.replace(output)
|
|
301
|
+
finally:
|
|
302
|
+
if temp and temp.exists():
|
|
303
|
+
temp.unlink()
|
|
304
|
+
else:
|
|
305
|
+
with output.open("x", encoding="utf-8") as handle:
|
|
306
|
+
handle.write(page)
|
|
307
|
+
return {"output": str(output), "candidates": len(data["candidates"]), "fingerprint": data["fingerprint"], "bytes": output.stat().st_size}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main() -> int:
|
|
311
|
+
if sys.version_info < (3, 10):
|
|
312
|
+
print("需要 Python 3.10 或更新版本", file=sys.stderr)
|
|
313
|
+
return 2
|
|
314
|
+
parser = argparse.ArgumentParser(description="把本地候选与 manifest 组装成独立风格对比 HTML")
|
|
315
|
+
parser.add_argument("manifest", type=Path)
|
|
316
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
317
|
+
parser.add_argument("--force", action="store_true", help="明确允许原子更新已有输出")
|
|
318
|
+
args = parser.parse_args()
|
|
319
|
+
try:
|
|
320
|
+
print(json.dumps(build(args.manifest, args.output, force=args.force), ensure_ascii=False))
|
|
321
|
+
return 0
|
|
322
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
323
|
+
print(f"未生成对比页:{exc}", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
raise SystemExit(main())
|
|
@@ -56,6 +56,16 @@ Do NOT include specific file paths or code snippets. They may end up being outda
|
|
|
56
56
|
|
|
57
57
|
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
|
|
58
58
|
|
|
59
|
+
## UI and Design
|
|
60
|
+
|
|
61
|
+
Include this section only when the work changes what a user sees; omit it otherwise.
|
|
62
|
+
|
|
63
|
+
- The chosen visual direction and the user's stated likes and dislikes. If `atw-prototype` left `ui-direction.md` in the task directory, summarise it here and point to it — do not restate the whole direction card.
|
|
64
|
+
- The screens and states that must exist: empty, loading, success, failure, rejected actions.
|
|
65
|
+
- The target viewports (e.g. desktop 1280, mobile 390).
|
|
66
|
+
- Which project design rules apply: the `design-system.md` in the UI layer of `.atw/spec/`, if the project has one.
|
|
67
|
+
- UI acceptance criteria phrased as things you can check on a screenshot: "on mobile 390 the primary action stays visible above the keyboard", not "looks clean".
|
|
68
|
+
|
|
59
69
|
## Testing Decisions
|
|
60
70
|
|
|
61
71
|
A list of testing decisions that were made. Include:
|
|
@@ -23,6 +23,10 @@ Keep the spec tree aligned with the project:
|
|
|
23
23
|
- Delete template files that do not apply.
|
|
24
24
|
- Add new files for important local patterns the template missed.
|
|
25
25
|
|
|
26
|
+
## UI Layers
|
|
27
|
+
|
|
28
|
+
When a package renders UI, fill `design-system.md` in its UI layer from the real theme: the token or theme file, CSS variables, Tailwind config, and the shared components screens actually use. Record color roles, the type scale, spacing and radius steps, and how key components look. Where the code is inconsistent — three button styles — record the majority usage and list the rest as to-unify, rather than inventing a cleaner system.
|
|
29
|
+
|
|
26
30
|
## Content Standards
|
|
27
31
|
|
|
28
32
|
Good spec sections include:
|
|
@@ -39,6 +39,11 @@ Give each ticket its **blocking edges** — the other tickets that must complete
|
|
|
39
39
|
|
|
40
40
|
**Wide refactors are the exception to vertical slicing.** A **wide refactor** is one mechanical change — rename a column, retype a shared symbol — whose **blast radius** fans across the whole codebase, so a single edit breaks thousands of call sites at once and no vertical slice can land green. Don't force it into a tracer bullet; sequence it as **expand–contract**. First expand: add the new form beside the old so nothing breaks. Then migrate the call sites over in batches sized by blast radius (per package, per directory), each batch its own ticket blocked by the expand, keeping CI green batch to batch because the old form still exists. Finally contract: delete the old form once no caller remains, in a ticket blocked by every migrate batch. When even the batches can't stay green alone, keep the sequence but let them share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
|
|
41
41
|
|
|
42
|
+
**UI work stays inside the vertical slices.** Don't split "build the UI" into its own ticket — that is a horizontal slice. Instead, every ticket that changes what a user sees carries UI acceptance criteria next to its behavioural ones: which screen, which state, which viewport, checked against the spec's UI and Design section. Two optional exceptions, both sequenced like prefactoring and integration:
|
|
43
|
+
|
|
44
|
+
- A **design-foundation** ticket first, when several slices need the same new design tokens or shared components. Every UI ticket that uses them is blocked by it.
|
|
45
|
+
- A **visual pass** ticket last, only for multi-screen work: one consistency review across all the screens the slices produced, blocked by every UI ticket. It fixes drift between screens; it does not redesign them.
|
|
46
|
+
|
|
42
47
|
### 4. Quiz the user
|
|
43
48
|
|
|
44
49
|
Present the proposed breakdown as a numbered list. For each ticket, show:
|
|
@@ -78,6 +83,7 @@ Do NOT close or modify any parent issue.
|
|
|
78
83
|
|
|
79
84
|
- [ ] Acceptance criterion 1
|
|
80
85
|
- [ ] Acceptance criterion 2
|
|
86
|
+
- [ ] UI: <screen> / <state> / <viewport> — <what a screenshot must show> (only for tickets that change what a user sees)
|
|
81
87
|
|
|
82
88
|
</local-ticket-template>
|
|
83
89
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atw-ui
|
|
3
|
+
description: "按已选定的设计方向实现、精修和评审网站、App、后台与组件的界面:建立视觉层级与交互结构、制作素材、在目标视口取得实际画面证据、独立评审与修正、做减法后交付。用于实现阶段的正式界面、存量页面精修或改版、截图还原和界面评审。方向还没定、需要比较多个风格方向或验证业务逻辑时,先用 atw-prototype。只负责设计判断,组件归属、数据流、状态正确性和测试等代码实现质量不在范围内。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# atw-ui
|
|
7
|
+
|
|
8
|
+
以明确的设计北极星组织界面,让构图、字体、色彩、素材和交互相互呼应,形成有辨识度的表达。克制是保留最有力量的选择、删除无贡献的元素,不是把所有风格磨成中庸。视觉表现与任务完成分别验收。
|
|
9
|
+
|
|
10
|
+
方法和评审协议均在本目录内,直接使用宿主提供的文件、浏览、看图、生成与隔离执行能力。相对链接以本文件所在目录解析。
|
|
11
|
+
|
|
12
|
+
## 在 ATW 里使用
|
|
13
|
+
|
|
14
|
+
### 各阶段的用法
|
|
15
|
+
|
|
16
|
+
| 阶段 | 用法 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| discover | 不用本技能。选方向用 atw-prototype,结论写进任务目录的 `ui-direction.md` |
|
|
19
|
+
| implement | 只在 `/atw-implement` 的流水线里使用:工单涉及界面时,界面部分按本技能步骤 1–3 和 5 完成,业务逻辑仍按流水线测试先行。不在流水线之外单独用本技能写实现代码 |
|
|
20
|
+
| accept | 只读。按步骤 4 的“给已有界面评审”,对照需求文档里的界面验收标准和设计规范取证;发现的问题记成新工单或后续任务,不当场修改 |
|
|
21
|
+
| 没有 ATW 任务的小改动 | 直接使用本技能 |
|
|
22
|
+
|
|
23
|
+
实现阶段发现方向缺失或与需求文档矛盾时,停下来告诉用户,按 ATW 的回退规则处理(回到 specify 或重新探索),不在工单里临时发散新方向。
|
|
24
|
+
|
|
25
|
+
### 三类设计信息
|
|
26
|
+
|
|
27
|
+
| 信息 | 位置 | 用法 |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| 通用设计方法 | 本技能的参考资料 | 判断怎样做得好 |
|
|
30
|
+
| 本项目的设计规范:颜色用途、字号层级、间距与圆角、关键组件样式、图标库 | `.atw/spec/` 里界面所在层的 `design-system.md` | 判断用什么;是长期规定 |
|
|
31
|
+
| 本任务的设计决定:选定方向、方向卡、用户的喜欢与不喜欢、界面验收重点 | 任务目录的 `ui-direction.md` 和需求文档 | 判断这次要做成什么样 |
|
|
32
|
+
|
|
33
|
+
读取顺序:
|
|
34
|
+
|
|
35
|
+
1. 运行 `python3 ./.atw/scripts/get_context.py --mode packages`,找到工单所在的包和它的界面层。层名不一定是 `frontend`,可能是 `web`、`ui` 等;多包项目每个包可以有各自的设计规范,只读当前包的。
|
|
36
|
+
2. 读该层的 `index.md`、`design-system.md`,以及组件和代码质量相关的规范。
|
|
37
|
+
3. 读 `ui-direction.md` 和需求文档里的界面部分。
|
|
38
|
+
4. 没有 `design-system.md` 时,从现有设计变量、主题配置和共享组件里读出现状,按多数用法整理。
|
|
39
|
+
|
|
40
|
+
代码层面的事——用哪种样式方案、组件放哪、状态怎么管——以 ATW spec 为准;本技能只在这些约束内做设计判断。设计规范本身妨碍任务时说出来,交给用户决定,不悄悄绕开。
|
|
41
|
+
|
|
42
|
+
做完后,本次确立的可复用视觉约定(新增的设计变量、组件变体、统一的密度或间距规则)通过流水线里的 spec 更新步骤写回 `design-system.md`;只属于这一页的决定留在 `ui-direction.md`。
|
|
43
|
+
|
|
44
|
+
## 选择范围
|
|
45
|
+
|
|
46
|
+
- 从请求、ATW spec 的设计规范、任务目录的 `ui-direction.md`(通常由 atw-prototype 留下)、需求文档、现有页面和参考资料明确用户、主要任务、真实内容、目标设备与交付形式。检查相关位置即可,不先通读整个项目。
|
|
47
|
+
- 用户已确定的品牌、参考、页面结构、交互约束,以及 `ui-direction.md` 里选定的方向和品味优先。分清借鉴风格、重新设计与精准还原,不能自行切换目标。
|
|
48
|
+
- 仅在缺失信息会改变核心方向且无法合理推断时集中提问;可逆的细节自行决定并简述假设。不把设计工作变成强制问卷或逐步审批。
|
|
49
|
+
- 设计说明、设计稿和可运行产品是不同交付物。只要求评审时保持只读;只要求设计时不默认改业务代码。
|
|
50
|
+
|
|
51
|
+
| 当前范围 | 路径 |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| 单个元素、一处间距、文案或颜色这类小改动 | 直接改,在目标视口看改动处和相邻元素,交付时一两句话说明;不盘点现状、不启动评审 |
|
|
54
|
+
| 方向还没定,或明确要求重新探索、比较多个风格 | 交给 atw-prototype 的界面原型,选定方向后再回到这里 |
|
|
55
|
+
| 已有选定方向(`ui-direction.md` 或用户给定)的新界面 | 按步骤 1–5 执行 |
|
|
56
|
+
| 已有方向的深化或局部优化 | 保留方向,从步骤 2 进入,只检查受影响的区域与状态 |
|
|
57
|
+
| 存量项目里的精修、局部重构或改版 | 先盘点现有页面、设计变量和共享组件,留基线截图,从源头修改 |
|
|
58
|
+
| 截图还原 | 按 [布局与视口](references/layout-and-viewport.md) 的“精准还原”,以参考图为视觉基线,确定参考视口与布局约束后进入步骤 2,不发散新风格 |
|
|
59
|
+
| UI/UX 评审 | 按问题读取对应参考,只执行步骤 4 的诊断并交付证据与建议;评审现有界面、不要求重新设计时,用步骤 4 的“给已有界面评审” |
|
|
60
|
+
|
|
61
|
+
## 1. 确认方向
|
|
62
|
+
|
|
63
|
+
读 `ui-direction.md` 或用户给定的方向,确认北极星、方向卡、记忆点、用户的喜欢与不喜欢都还适用。方向缺失、彼此矛盾,或用户改变了目标时,回到 atw-prototype 选方向,不在实现中途悄悄换风格。
|
|
64
|
+
|
|
65
|
+
atw-prototype 第二段留下的纯逻辑模块和原型页面是参考,不是要提升的代码:逻辑模块可以搬进正式代码,页面按项目的组件和规范重新实现。
|
|
66
|
+
|
|
67
|
+
开始实现前把设计说明补全到可执行:用户任务、内容与主动作、选定方向、视觉关系、关键状态、设备约束和验收重点。已有说明直接更新,不另开一份。
|
|
68
|
+
|
|
69
|
+
## 2. 建立视觉与交互结构
|
|
70
|
+
|
|
71
|
+
根据当前决策按需读取,不预先加载全部参考:
|
|
72
|
+
|
|
73
|
+
| 要解决的问题 | 参考 |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| 视觉层级、字体、色彩、空间、图标、风格统一或减法精修 | [视觉语言](references/visual-language.md) |
|
|
76
|
+
| 素材选择、视频与 3D | [素材](references/media.md) |
|
|
77
|
+
| 生成配图:让图承担意思、写提示词、和页面接成一体 | [配图](references/imagery.md) |
|
|
78
|
+
| 动效手感、界面过渡、首屏动画与滚动叙事、时长与检查 | [动效](references/motion.md) |
|
|
79
|
+
| 精准还原、视口与溢出检查 | [布局与视口](references/layout-and-viewport.md) |
|
|
80
|
+
|
|
81
|
+
新产品同时交代主要内容的视觉层级和关键操作链;不要只完成漂亮的默认状态。营销页面可以表达情绪与品牌,高频工具优先识别效率和操作稳定性。
|
|
82
|
+
|
|
83
|
+
交互、表单、状态、弹窗和响应式的实现规则,项目或宿主已有相应规范时照用。
|
|
84
|
+
|
|
85
|
+
在构图阶段按 [素材](references/media.md) 判断图像能否承担主信息或情绪重心,包括后台、工具和内容产品。需要关键素材时先选定或制作它,再围绕其主体、留白与裁切安排文字和操作;不等布局填满后再补图。
|
|
86
|
+
|
|
87
|
+
既有项目先复用有效的设计变量、组件和交互习惯。发现既有模式妨碍任务时,修复对应源头;不为一次调整另造设计系统,也不把无关代码重构混入视觉工作。
|
|
88
|
+
|
|
89
|
+
## 3. 制作并取得实际证据
|
|
90
|
+
|
|
91
|
+
- 先完成代表性页面或关键操作链,检查后再延展到其他页面。只做局部修改时直接完成相关改动。
|
|
92
|
+
- 素材有明确表达作用时才制作;文字、控件与业务信息保持可编辑。生成图片中的界面仅作为视觉样本。
|
|
93
|
+
- 示例内容按真实产品的样子写。哪些是模拟数据、生成图片、未接入的动作,写在交付说明里,不写进界面:画面上不出现“示例”“待核实”“示意图”“概念稿”“按钮未接入”这类制作说明。不伪造业务成功、客户评价或产品指标。
|
|
94
|
+
- 在相关目标视口运行并查看结果。记录截图对应的页面、状态、视口与版本;长页检查整体节奏和关键局部,不能只看首屏。
|
|
95
|
+
- 在实际阅读尺寸检查中文多行标题、正文、控件和窄屏换行。文字重叠、被裁切或靠大量小字才装得下时,回到内容和布局修复,不以缩放预览或装饰精修掩盖。
|
|
96
|
+
- 同一批相关修改集中检查,复用已有会话和证据。截图保存成功不等于已经看过;构建成功不等于视觉或操作已验证。
|
|
97
|
+
|
|
98
|
+
## 4. 独立评审与修正
|
|
99
|
+
|
|
100
|
+
新页面、整体改版或影响主要构图和任务流程的变更,在初版稳定后按 [视觉评审协议](references/visual-review.md) 进行一轮独立评审,报告评分和剩余差距。局部、低风险修改由主 Agent 检查,不为每个间距调整启动评审者。
|
|
101
|
+
|
|
102
|
+
已有界面按它自己的规范评审。在已有项目里润色了一整页或多页、做完局部重构,或用户要求评审现有界面而没有要求重新设计时,用 [视觉评审协议](references/visual-review.md) 的“给已有界面评审”:标准是仓库现有的设计规范,没有成文规范时以现有实现和组件为准;怎样做得更好看,照常用视觉语言里的判断。只做一轮,不打分。单个元素的小改动不评审。
|
|
103
|
+
|
|
104
|
+
- 主 Agent 保持设计方向与集成责任。具备隔离和看图能力时,派发无历史上下文的视觉评审子 Agent;缺少任一能力时改为主 Agent 自检,并标明未独立评审。
|
|
105
|
+
- 在 `/atw-implement` 流水线里不自行派发评审者:独立视觉评审由流水线的审查步骤(atw-code-review)负责,把截图、对应的页面、状态和视口交给它。审查步骤不支持视觉评审时,主 Agent 按 [视觉评审协议](references/visual-review.md) 自检,并在交付说明里标明未独立评审。
|
|
106
|
+
- 评审者看当前画面、任务、约束和必要参考,不看制作历史、代码或旧评分。记忆点是动效时,交给评审者一段录屏或开始、中间、结束三帧,静态截图看不出它。评审以“这一体裁里顶尖设计团队会怎么做”为参照指出最大差距,并按 10 分制给出当前画面离那个标准有多远。主 Agent 按反馈修正并重新取证,不把评审偏好自动当成用户要求。
|
|
107
|
+
- 风格开始漂移、意见反复、收益不清楚或到达用户预算时,保留当前最合适的版本并说明具体取舍;未解决的必需功能仍如实列出,不能以停止美学迭代代替任务完成。
|
|
108
|
+
- 用户对实现出来的设计反复不满意,怎么调都不对时,说明问题可能出在方向本身,建议回到 atw-prototype 重新比较方向,而不是继续在细节上打转。
|
|
109
|
+
|
|
110
|
+
需要并行制作时,按独立页面、素材或区块划分所有权,给执行者相同的任务约束与设计说明;要求保留他人改动。只有阶段依赖已满足且主 Agent 同时有独立工作时才并行,不能让多个执行者同时改同一共享样式。没有并行能力时顺序完成。
|
|
111
|
+
|
|
112
|
+
## 5. 验证与交付
|
|
113
|
+
|
|
114
|
+
检查深度跟着改动走:新页面、改版和局部重构做下面全部检查;精修和基线做前后对比,再做受影响区域的细节检查,润色了一整页以上时加一轮步骤 4 的已有界面评审;小改动只看改动处和相邻元素,不做减法轮、不写评审记录。
|
|
115
|
+
|
|
116
|
+
- 对照用户任务与选定方向检查真实内容、代表性视口和改动涉及的状态。交互从真实入口走到可观察结果;保存后重新读取,失败后检查输入和当前位置是否保留。
|
|
117
|
+
- 使用现有相关检查及项目必需门禁。普通样式或文案调整以实际结果验证,不为一次检查新建长期测试体系。
|
|
118
|
+
- 修正已发现的可用性、溢出、内容和视觉问题,复查受影响部分。按视觉语言做减法,同时保留识别对象、理解规则和完成操作所需的信息。
|
|
119
|
+
- 单独做一次减法:一页只有一个主角,其余安静;逐区块删一轮文字,只放回删了会影响理解的。减法只删装饰和重复,对象名称、判断依据、当前状态和主动作必须留下。在 100% 和 200% 缩放下看关键区域。
|
|
120
|
+
- 交付请求范围内的文件或预览,说明选定方向、关键变化、实际验证和剩余项。评审记录里写明评分和删掉了哪些文字。某一步没做,就写没做和原因,不省略不提。设计说明、素材、截图及评审记录放在 ATW 任务目录(截图放在 `screenshots/`,文件名写明页面、状态和视口,供审查和验收取证),复用已有组织方式,不写进技能安装目录。
|
|
121
|
+
|
|
122
|
+
## 能力边界
|
|
123
|
+
|
|
124
|
+
没有看图能力时可以产出设计说明、实现代码或源码层面的检查,但明确“未验证实际视觉”,不生成虚假的视觉评分。没有可运行环境时不能声称交互通过;请求要求可运行界面却只能交付方案时,明确标为未完成部分。
|
|
125
|
+
|
|
126
|
+
没有生成能力时优先使用已有合适素材,或交付明确的素材需求和可用占位版本。只使用任务已授权的服务与配置,不因缺少素材自动接入付费服务;本技能不管理密钥、安装工具或发布网站。网络不可用时使用已有参考,并说明来源范围。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 配图
|
|
2
|
+
|
|
3
|
+
生成图最常见的失败不是画得不好,而是画得太满:细节越丰富越像图库照片,放进版面后没有重点,也和页面没有关系。好的配图要先承担意思,再承担气氛,最后和页面接成一个整体。
|
|
4
|
+
|
|
5
|
+
## 先让图承担意思
|
|
6
|
+
|
|
7
|
+
生成之前先写一句:这张图要让人一眼看懂什么?答不上来就先别生成。“一张好看的使用场景”不是答案;“产品把一堆混乱变成了哪几个清楚的结果”才是。
|
|
8
|
+
|
|
9
|
+
用对比把信息编进图里。整体统一成一种低调的材质或颜色,比如白模、单色、失焦或剪影,只让承载信息的少数几处有颜色、光或细节。例如白色仓库模型里只有出了异常的两排货架亮成橙色,灰度地图上只有正在配送的路线是彩色。所有地方都一样精细,就等于没有重点;第一版通常就是这样失败的。
|
|
10
|
+
|
|
11
|
+
选能和页面“接上”的主体:
|
|
12
|
+
|
|
13
|
+
- 能跨过两块色面分界的单个物件。
|
|
14
|
+
- 能延伸出画面的线:线缆、轨道、河流、胶片、纸带。
|
|
15
|
+
- 能在上面标注的空间:模型、地图、剖面、平面图。
|
|
16
|
+
|
|
17
|
+
这样的主体,之后能和页面里的线、标注和区块连成一体;一张自给自足的风景照做不到。
|
|
18
|
+
|
|
19
|
+
## 写提示词
|
|
20
|
+
|
|
21
|
+
按固定顺序写,每项一两句:
|
|
22
|
+
|
|
23
|
+
1. **主体与表现形式**:什么东西,用什么体裁呈现(照片、插画、模型或图解),体裁从页面的方向推出。
|
|
24
|
+
2. **视角与镜头**:机位的高度和角度;焦段和景深。
|
|
25
|
+
3. **构图**:主体在画面里的位置和占比,四周留多少边距,哪一块留空给文字。要整件物体完整入画,就写明“完整地放在画面内,四周留出宽松边距”,否则常被裁掉一角,之后没法排版。
|
|
26
|
+
4. **背景**:写出页面实际使用的十六进制色值,或者要求透明背景。
|
|
27
|
+
5. **材质与光**:主光方向、辅光、质感。同一页的多张图用同一个光向。
|
|
28
|
+
6. **重点**:用什么对比让哪几处成为焦点,其余部分怎样退后。
|
|
29
|
+
7. **排除**:不要文字、标志、界面、屏幕内容;透明物件写明“地面不要阴影”。
|
|
30
|
+
|
|
31
|
+
文字、界面和数据一律不让生图模型画:会画错,改不了,也不能被选中、翻译和读屏。它们放在页面上,用真实元素压在图上,或用引线连到图上。
|
|
32
|
+
|
|
33
|
+
## 生成后放进页面再判断
|
|
34
|
+
|
|
35
|
+
在真实页面、真实尺寸里看,不在图片查看器里判断。单独看很好的图,放进版面后常常主体太满、朝向不对、留白在错的位置。
|
|
36
|
+
|
|
37
|
+
不对就改提示词重新生成,并写清要改什么,例如“除了三处重点,其余全部改成白色”“把整个模型收进画面”。不要围着一张不合适的图改版式。同一批里挑最能排版的一张,不挑细节最多的那张。
|
|
38
|
+
|
|
39
|
+
## 让图和页面接成一体
|
|
40
|
+
|
|
41
|
+
**背景融合。** 不透明的图,从图的四角实际取色,页面背景用同一个值;四条边都淡出到这个颜色,不加框,也不放进圆角卡片。只淡出一侧时,其余三边在宽屏上会露出矩形接缝。淡出可以用 CSS 遮罩,也可以在导出时直接把边缘渐隐成页面色值,后者不增加样式,适合样式体积受限的页面。生成图的底色常常不均匀,靠边缘会偏色,要在实际显示尺寸下检查接缝。
|
|
42
|
+
|
|
43
|
+
**透明图。** 先放在最终背景上检查边缘、白边和残留的棋盘格;检查透明通道要看像素,不能看文件后缀。阴影不烘焙在图里,由页面按实际背景加投影,这样换了背景也对。
|
|
44
|
+
|
|
45
|
+
**跨越结构。** 透明物件放在两个色块或两个区块的分界线上,让它同时属于两边。
|
|
46
|
+
|
|
47
|
+
**图和代码接力。** 生成图负责代码做不出来的材质和光,例如金属、玻璃、模型、光影。代码负责需要精确、可编辑、会延续的部分,例如从图里延伸出去的线、路径和标注。如果图里有一段本该延续到页面的部分,就把它裁掉,改用矢量接着画。这样它能准确穿过版面,带上真实文字,并在不同宽度下重新走线。
|
|
48
|
+
|
|
49
|
+
**标注。** 在图上标注用细引线加文字,不用带底色和阴影的气泡卡片。引线端点要对准图里的真实位置:图在版面里用固定的尺寸和位置,按图片像素换算坐标。窄屏上隐藏标注,或改成图下的列表,不让它们漂移到错误的位置。
|
|
50
|
+
|
|
51
|
+
**窄屏裁切。** 不把整张大场景等比缩小到看不清。从原图裁出焦点区域另存一张给窄屏用,或用对象定位对准焦点。桌面和手机两种裁切,都要保住承载信息的那几处。
|
|
52
|
+
|
|
53
|
+
**一张图用多次。** 结尾或其他区块可以用同一张图的局部特写,比再生成一张光线和风格对不上的图更统一。
|
|
54
|
+
|
|
55
|
+
## 让一张画当舞台
|
|
56
|
+
|
|
57
|
+
另一种用法正好相反:图不讲信息,只给气氛,界面保持黑白克制,全页只用一种有笔触的画当唯一的颜色来源,例如油画、水粉、水彩或版画。
|
|
58
|
+
|
|
59
|
+
- **画当舞台,界面在上面演示。** 把真实界面的简化版放在画上,画从四周露出来。滚动时画固定不动,只换前面的界面内容,和“滚动叙事”接得上。
|
|
60
|
+
- **大胆裁切。** 只留画的一角或一个局部,接近抽象,不和界面抢。题材不必和产品字面相关,但气质要对得上:快、静、暖或者松弛。
|
|
61
|
+
- **松和紧互相衬托。** 画越松,界面越要精确、干净;两者一起出现,彼此都更好看。
|
|
62
|
+
- **用得少。** 一张画贯穿全页,或者只在两三处出现,例如价格卡的顶端和页脚;不要每个区块配一张。同一页的画统一画种和笔触。
|
|
63
|
+
- **演示用简化过的真实界面。** 次要内容虚化或换成骨架,只把要讲的那一处做清楚;数字对比直接用界面元素画出来,例如两条长短不同的进度条,而不是另做一张图表。
|
|
64
|
+
|
|
65
|
+
## 文件与说明
|
|
66
|
+
|
|
67
|
+
- 按显示尺寸的 2 倍导出,不直接内嵌原始大图。不透明的图用 JPEG 或 WebP,透明的用 WebP 或 PNG;单张控制在几百 KB 以内。内嵌成 data URL 后体积会再大约三分之一。
|
|
68
|
+
- 保留提示词、原图和处理后的版本,写清每张图的用途和处理方式。
|
|
69
|
+
- 生成图不冒充真实产品、真实场所或真实人物。虚构产品的小样,在页脚注明图片由 AI 生成。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 布局与视口
|
|
2
|
+
|
|
3
|
+
## 精准还原
|
|
4
|
+
|
|
5
|
+
先记录参考图的逻辑视口或已知像素尺寸、完整范围和固定区域。图像像素不等于逻辑视口;无法确定时说明假设。
|
|
6
|
+
|
|
7
|
+
测量主区域比例、共享对齐线、文字尺度、图片裁切与间距关系。先对齐整体几何与字体,再处理边框、阴影和细节;不因还原任务重新探索风格。
|
|
8
|
+
|
|
9
|
+
用与参考相当的视口和内容检查差异。参考没有展示的窄屏与状态根据内容约束补充,并区分“参考还原”与“合理延展”。
|
|
10
|
+
|
|
11
|
+
## 验证重点
|
|
12
|
+
|
|
13
|
+
实际检查长内容、空内容和当前改动相关状态。移动端注意视口变化、输入面板与安全边距,主操作不能被键盘或固定区域遮挡。
|
|
14
|
+
|
|
15
|
+
检查页面是否意外横向溢出、滚动是否发生在预期区域、焦点与菜单是否可见,以及缩放或重排后阅读和操作顺序是否仍然清楚。
|