@lark-apaas/coding-steering 0.1.32-dev.6f4e4bc → 0.1.32-dev.88fb097
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/package.json +1 -1
- package/steering/design-html/skills/charts/SKILL.md +11 -4
- package/steering/design-html/skills/data-viz/SKILL.md +1 -1
- package/steering/design-html/skills/hi-fi-design/SKILL.md +1 -1
- package/steering/design-html/skills/interactive-prototype/SKILL.md +1 -17
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +2 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +56 -19
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +7 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +199 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +308 -9
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +262 -47
- package/steering/design-html/skills/slide-deck/SKILL.md +5 -3
- package/steering/design-html/skills/social-card/SKILL.md +263 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +3 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +2 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +2 -0
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +56 -14
- package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +67 -4
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +0 -1
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +75 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +10 -4
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +57 -14
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +90 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +249 -0
- package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +3 -1
- package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +2 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +49 -22
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +7 -3
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +57 -14
- package/steering/vite-react/skills/plugin-guide/SKILL.md +1 -0
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +1 -0
- package/steering/vite-react/tech.md +3 -0
- package/steering/design-html/skills/preflight/SKILL.md +0 -51
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -108
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Verify that generated slides honor every asset bound to their PPTX layout."""
|
|
3
3
|
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
4
6
|
import posixpath
|
|
5
7
|
import re
|
|
6
8
|
import sys
|
|
@@ -40,14 +42,12 @@ def bound_asset_ids(value, known_asset_ids):
|
|
|
40
42
|
|
|
41
43
|
def layout_asset_contract(pack):
|
|
42
44
|
known_asset_ids = set(pack.assets)
|
|
43
|
-
required = {}
|
|
44
45
|
owners = {}
|
|
45
46
|
for layout_name, (layout, _) in pack.layouts.items():
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for asset_id in asset_counts:
|
|
47
|
+
for asset_id, _, _ in layout_asset_instances(
|
|
48
|
+
layout, known_asset_ids, pack.canvas):
|
|
49
49
|
owners.setdefault(asset_id, set()).add(layout_name)
|
|
50
|
-
return
|
|
50
|
+
return owners
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def asset_urls(pack, asset_prefix, asset_ids):
|
|
@@ -63,10 +63,118 @@ def asset_urls(pack, asset_prefix, asset_ids):
|
|
|
63
63
|
relative = normalized_path(path)
|
|
64
64
|
if relative.startswith('assets/'):
|
|
65
65
|
relative = relative[len('assets/'):]
|
|
66
|
-
urls
|
|
66
|
+
urls.setdefault(posixpath.join(prefix, relative), set()).add(asset_id)
|
|
67
67
|
return urls
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def bound_asset_instances(value, known_asset_ids):
|
|
71
|
+
"""Return every positioned fixed-asset instance nested in slots or flow."""
|
|
72
|
+
instances = []
|
|
73
|
+
if isinstance(value, dict):
|
|
74
|
+
box = value.get('box')
|
|
75
|
+
asset_id = value.get('asset')
|
|
76
|
+
if (isinstance(box, list) and len(box) == 4
|
|
77
|
+
and isinstance(asset_id, str)
|
|
78
|
+
and asset_id in known_asset_ids):
|
|
79
|
+
instances.append((asset_id, value.get('role') or 'asset', box))
|
|
80
|
+
for key, child in value.items():
|
|
81
|
+
if key not in ('asset', 'background'):
|
|
82
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
83
|
+
elif isinstance(value, list):
|
|
84
|
+
for child in value:
|
|
85
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
86
|
+
return instances
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def layout_asset_instances(layout, known_asset_ids, canvas):
|
|
90
|
+
"""Return every fixed image instance declared by one layout."""
|
|
91
|
+
if not isinstance(layout, dict):
|
|
92
|
+
return []
|
|
93
|
+
instances = []
|
|
94
|
+
background = layout.get('background')
|
|
95
|
+
if canvas and isinstance(background, str) and background in known_asset_ids:
|
|
96
|
+
instances.append(
|
|
97
|
+
(background, 'background', [0, 0, canvas[0], canvas[1]]))
|
|
98
|
+
instances.extend(bound_asset_instances(layout, known_asset_ids))
|
|
99
|
+
return instances
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def inline_styles(value):
|
|
103
|
+
styles = {}
|
|
104
|
+
for declaration in (value or '').split(';'):
|
|
105
|
+
if ':' not in declaration:
|
|
106
|
+
continue
|
|
107
|
+
name, raw = declaration.split(':', 1)
|
|
108
|
+
styles[name.strip().lower()] = re.sub(
|
|
109
|
+
r'\s*!important\s*$', '', raw.strip().lower())
|
|
110
|
+
return styles
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def css_number(value):
|
|
114
|
+
match = re.fullmatch(r'(-?(?:\d+(?:\.\d*)?|\.\d+))(?:px)?', value or '')
|
|
115
|
+
return float(match.group(1)) if match else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def element_box(reference, canvas):
|
|
119
|
+
styles = reference['styles']
|
|
120
|
+
if reference['slide_root'] and not any(
|
|
121
|
+
name in styles for name in ('left', 'top', 'width', 'height')):
|
|
122
|
+
return [0.0, 0.0, float(canvas[0]), float(canvas[1])]
|
|
123
|
+
values = [
|
|
124
|
+
css_number(styles.get(name))
|
|
125
|
+
for name in ('left', 'top', 'width', 'height')
|
|
126
|
+
]
|
|
127
|
+
if styles.get('position') != 'absolute' or any(
|
|
128
|
+
value is None for value in values):
|
|
129
|
+
return None
|
|
130
|
+
return values
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def boxes_match(actual, expected, tolerance=1.0):
|
|
134
|
+
return actual is not None and all(
|
|
135
|
+
abs(actual_value - expected_value) <= tolerance
|
|
136
|
+
for actual_value, expected_value in zip(actual, expected)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def file_sha256(path):
|
|
141
|
+
digest = hashlib.sha256()
|
|
142
|
+
with open(path, 'rb') as stream:
|
|
143
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
|
144
|
+
digest.update(chunk)
|
|
145
|
+
return digest.hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def copied_asset_problems(pack, html_path, asset_prefix, asset_ids):
|
|
149
|
+
"""Verify copied fixed assets still contain the source PPTX bytes."""
|
|
150
|
+
problems = []
|
|
151
|
+
html_root = os.path.dirname(os.path.abspath(html_path))
|
|
152
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
153
|
+
for asset_id in sorted(asset_ids):
|
|
154
|
+
entry, _ = pack.assets.get(asset_id, (None, None))
|
|
155
|
+
path = entry.get('path') if isinstance(entry, dict) else None
|
|
156
|
+
if not isinstance(path, str) or not path:
|
|
157
|
+
continue
|
|
158
|
+
relative = normalized_path(path)
|
|
159
|
+
if relative.startswith('assets/'):
|
|
160
|
+
relative = relative[len('assets/'):]
|
|
161
|
+
source_path = os.path.join(pack.root, 'assets', *relative.split('/'))
|
|
162
|
+
copied_relative = posixpath.join(prefix, relative)
|
|
163
|
+
copied_path = os.path.join(html_root, *copied_relative.split('/'))
|
|
164
|
+
if not os.path.isfile(copied_path):
|
|
165
|
+
problems.append(
|
|
166
|
+
'固定素材 %s 未复制到项目: %s' % (asset_id, copied_relative))
|
|
167
|
+
continue
|
|
168
|
+
if not os.path.isfile(source_path):
|
|
169
|
+
problems.append(
|
|
170
|
+
'风格包中的固定素材 %s 不存在: %s' % (asset_id, path))
|
|
171
|
+
continue
|
|
172
|
+
if file_sha256(copied_path) != file_sha256(source_path):
|
|
173
|
+
problems.append(
|
|
174
|
+
'固定素材 %s 已被替换或改写,必须使用 PPTX 原文件' % asset_id)
|
|
175
|
+
return problems
|
|
176
|
+
|
|
177
|
+
|
|
70
178
|
def urls_from_attrs(attrs):
|
|
71
179
|
urls = []
|
|
72
180
|
for key, value in attrs:
|
|
@@ -85,50 +193,72 @@ class SlideAssetParser(HTMLParser):
|
|
|
85
193
|
def __init__(self):
|
|
86
194
|
super().__init__()
|
|
87
195
|
self.slides = []
|
|
88
|
-
self.
|
|
89
|
-
self._sections = []
|
|
90
|
-
self._active_slides = []
|
|
196
|
+
self._stack = []
|
|
91
197
|
self._style_depth = 0
|
|
92
198
|
self.outside_urls = []
|
|
93
199
|
|
|
94
200
|
def handle_starttag(self, tag, attrs):
|
|
95
201
|
tag = tag.lower()
|
|
202
|
+
attrs_map = {key.lower(): value for key, value in attrs}
|
|
96
203
|
urls = urls_from_attrs(attrs)
|
|
97
|
-
parent = self.
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
204
|
+
parent = self._stack[-1] if self._stack else None
|
|
205
|
+
parent_slide = parent['slide'] if parent else None
|
|
206
|
+
slide_root = bool(
|
|
207
|
+
tag == 'section' and parent and parent['tag'] == 'deck-stage')
|
|
208
|
+
slide = ({
|
|
209
|
+
'layout': attrs_map.get('data-pptx-layout'),
|
|
210
|
+
'theme': attrs_map.get('data-pptx-theme'),
|
|
211
|
+
'references': [],
|
|
212
|
+
} if slide_root else parent_slide)
|
|
213
|
+
if slide_root:
|
|
214
|
+
self.slides.append(slide)
|
|
215
|
+
styles = inline_styles(attrs_map.get('style'))
|
|
216
|
+
hidden = bool(
|
|
217
|
+
(parent and parent['hidden'])
|
|
218
|
+
or 'hidden' in attrs_map
|
|
219
|
+
or attrs_map.get('aria-hidden', '').lower() == 'true'
|
|
220
|
+
or styles.get('display') == 'none'
|
|
221
|
+
or styles.get('visibility') in ('hidden', 'collapse')
|
|
222
|
+
or styles.get('content-visibility') == 'hidden'
|
|
223
|
+
or css_number(styles.get('width')) == 0
|
|
224
|
+
or css_number(styles.get('height')) == 0
|
|
225
|
+
or (
|
|
226
|
+
css_number(styles.get('opacity')) is not None
|
|
227
|
+
and css_number(styles.get('opacity')) <= 0
|
|
102
228
|
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
229
|
+
)
|
|
230
|
+
if slide is not None and (urls or attrs_map.get('data-pptx-asset')):
|
|
231
|
+
slide['references'].append({
|
|
232
|
+
'asset': attrs_map.get('data-pptx-asset'),
|
|
233
|
+
'hidden': hidden,
|
|
234
|
+
'slide_root': slide_root,
|
|
235
|
+
'source': attrs_map.get('src'),
|
|
236
|
+
'styles': styles,
|
|
237
|
+
'tag': tag,
|
|
238
|
+
'urls': urls,
|
|
239
|
+
})
|
|
240
|
+
elif urls:
|
|
109
241
|
self.outside_urls.extend(urls)
|
|
110
242
|
if tag == 'style':
|
|
111
243
|
self._style_depth += 1
|
|
112
244
|
if tag not in VOID_TAGS:
|
|
113
|
-
self.
|
|
245
|
+
self._stack.append({
|
|
246
|
+
'hidden': hidden,
|
|
247
|
+
'slide': slide,
|
|
248
|
+
'tag': tag,
|
|
249
|
+
})
|
|
114
250
|
|
|
115
251
|
def handle_startendtag(self, tag, attrs):
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.outside_urls.extend(urls_from_attrs(attrs))
|
|
252
|
+
self.handle_starttag(tag, attrs)
|
|
253
|
+
if tag.lower() not in VOID_TAGS:
|
|
254
|
+
self.handle_endtag(tag)
|
|
120
255
|
|
|
121
256
|
def handle_endtag(self, tag):
|
|
122
257
|
tag = tag.lower()
|
|
123
258
|
if tag == 'style' and self._style_depth:
|
|
124
259
|
self._style_depth -= 1
|
|
125
|
-
if tag
|
|
126
|
-
|
|
127
|
-
if slide is not None:
|
|
128
|
-
self.slides.append(slide)
|
|
129
|
-
self._active_slides.pop()
|
|
130
|
-
if tag not in VOID_TAGS and self._tags:
|
|
131
|
-
self._tags.pop()
|
|
260
|
+
if tag not in VOID_TAGS and self._stack:
|
|
261
|
+
self._stack.pop()
|
|
132
262
|
|
|
133
263
|
def handle_data(self, data):
|
|
134
264
|
if not self._style_depth:
|
|
@@ -139,10 +269,13 @@ class SlideAssetParser(HTMLParser):
|
|
|
139
269
|
def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
140
270
|
"""Return violations of the asset contract declared by each layout."""
|
|
141
271
|
pack = Pack(pack_dir)
|
|
142
|
-
|
|
272
|
+
owners = layout_asset_contract(pack)
|
|
143
273
|
known_urls = asset_urls(pack, asset_prefix, owners)
|
|
144
|
-
|
|
145
|
-
|
|
274
|
+
asset_urls_by_id = {
|
|
275
|
+
asset_id: url
|
|
276
|
+
for url, asset_ids in known_urls.items()
|
|
277
|
+
for asset_id in asset_ids
|
|
278
|
+
}
|
|
146
279
|
|
|
147
280
|
with open(html_path, encoding='utf-8') as stream:
|
|
148
281
|
text = stream.read()
|
|
@@ -150,10 +283,13 @@ def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
|
150
283
|
parser.feed(text)
|
|
151
284
|
parser.close()
|
|
152
285
|
|
|
153
|
-
problems =
|
|
286
|
+
problems = copied_asset_problems(pack, html_path, asset_prefix, owners)
|
|
287
|
+
if not parser.slides:
|
|
288
|
+
problems.append(
|
|
289
|
+
'没有识别到 deck-stage 的直属 slide section,无法核验模板页型')
|
|
290
|
+
return problems
|
|
154
291
|
for url in parser.outside_urls:
|
|
155
|
-
asset_id
|
|
156
|
-
if asset_id:
|
|
292
|
+
for asset_id in sorted(known_urls.get(normalized_path(url), ())):
|
|
157
293
|
problems.append(
|
|
158
294
|
'模板资产 %s 出现在 slide section 外,无法核验页型归属' % asset_id)
|
|
159
295
|
for number, slide in enumerate(parser.slides, 1):
|
|
@@ -164,23 +300,102 @@ def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
|
164
300
|
if layout not in pack.layouts:
|
|
165
301
|
problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
|
|
166
302
|
continue
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
303
|
+
layout_entry = pack.layouts[layout][0]
|
|
304
|
+
explicit_theme = slide['theme']
|
|
305
|
+
default_theme = pack.design.data.get('default-theme')
|
|
306
|
+
active_theme = explicit_theme or default_theme
|
|
307
|
+
if explicit_theme and explicit_theme not in pack.themes:
|
|
308
|
+
problems.append(
|
|
309
|
+
'第 %d 页声明了不存在的模板主题: %s' % (number, explicit_theme))
|
|
310
|
+
continue
|
|
311
|
+
layout_themes = (
|
|
312
|
+
(layout_entry.get('themes') or [])
|
|
313
|
+
if isinstance(layout_entry, dict) else []
|
|
171
314
|
)
|
|
315
|
+
if active_theme and layout_themes and active_theme not in layout_themes:
|
|
316
|
+
problems.append(
|
|
317
|
+
'第 %d 页页型 %s 不支持当前主题 %s;允许主题: %s。'
|
|
318
|
+
'若确需切换,显式声明 data-pptx-theme'
|
|
319
|
+
% (number, layout, active_theme, '、'.join(layout_themes)))
|
|
320
|
+
continue
|
|
321
|
+
expected = layout_asset_instances(
|
|
322
|
+
layout_entry, set(pack.assets), pack.canvas)
|
|
323
|
+
actual = []
|
|
324
|
+
for reference in slide['references']:
|
|
325
|
+
referenced_ids = set()
|
|
326
|
+
normalized_urls = [normalized_path(url) for url in reference['urls']]
|
|
327
|
+
for url in normalized_urls:
|
|
328
|
+
referenced_ids.update(known_urls.get(url, ()))
|
|
329
|
+
asset_id = reference['asset']
|
|
330
|
+
if not asset_id:
|
|
331
|
+
for referenced_id in sorted(referenced_ids):
|
|
332
|
+
problems.append(
|
|
333
|
+
'第 %d 页模板资产 %s 缺少 data-pptx-asset 实例标记'
|
|
334
|
+
% (number, referenced_id))
|
|
335
|
+
continue
|
|
336
|
+
if asset_id not in pack.assets:
|
|
337
|
+
problems.append(
|
|
338
|
+
'第 %d 页声明了不存在的模板资产: %s' % (number, asset_id))
|
|
339
|
+
continue
|
|
340
|
+
expected_url = asset_urls_by_id.get(asset_id)
|
|
341
|
+
uses_expected_source = (
|
|
342
|
+
len(normalized_urls) == 1
|
|
343
|
+
and normalized_urls[0] == expected_url
|
|
344
|
+
and (
|
|
345
|
+
reference['tag'] != 'img'
|
|
346
|
+
or (
|
|
347
|
+
reference['source']
|
|
348
|
+
and normalized_path(reference['source']) == expected_url
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
if not uses_expected_source:
|
|
353
|
+
problems.append(
|
|
354
|
+
'第 %d 页固定实例 %s 未引用对应的 PPTX 原素材'
|
|
355
|
+
% (number, asset_id))
|
|
356
|
+
if reference['hidden']:
|
|
357
|
+
problems.append(
|
|
358
|
+
'第 %d 页固定实例 %s 不可隐藏' % (number, asset_id))
|
|
359
|
+
actual.append({
|
|
360
|
+
'asset': asset_id,
|
|
361
|
+
'box': element_box(reference, pack.canvas),
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
used_asset_counts = Counter(instance['asset'] for instance in actual)
|
|
172
365
|
for asset_id in sorted(used_asset_counts):
|
|
173
366
|
if layout not in owners.get(asset_id, set()):
|
|
174
367
|
allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
|
|
175
368
|
problems.append(
|
|
176
369
|
'第 %d 页页型 %s 不得使用 %s;只允许: %s'
|
|
177
370
|
% (number, layout, asset_id, allowed))
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
371
|
+
unmatched = list(actual)
|
|
372
|
+
for asset_id, _, expected_box in expected:
|
|
373
|
+
matching_index = next((
|
|
374
|
+
index for index, instance in enumerate(unmatched)
|
|
375
|
+
if instance['asset'] == asset_id
|
|
376
|
+
and boxes_match(instance['box'], expected_box)
|
|
377
|
+
), None)
|
|
378
|
+
if matching_index is not None:
|
|
379
|
+
unmatched.pop(matching_index)
|
|
380
|
+
continue
|
|
381
|
+
same_asset = next((
|
|
382
|
+
instance for instance in unmatched
|
|
383
|
+
if instance['asset'] == asset_id
|
|
384
|
+
), None)
|
|
385
|
+
if same_asset:
|
|
386
|
+
problems.append(
|
|
387
|
+
'第 %d 页固定实例 %s 的位置尺寸必须为 %s,当前为 %s'
|
|
388
|
+
% (number, asset_id, expected_box, same_asset['box']))
|
|
389
|
+
unmatched.remove(same_asset)
|
|
390
|
+
else:
|
|
391
|
+
problems.append(
|
|
392
|
+
'第 %d 页页型 %s 缺少固定实例 %s,位置尺寸应为 %s'
|
|
393
|
+
% (number, layout, asset_id, expected_box))
|
|
394
|
+
for instance in unmatched:
|
|
395
|
+
if layout in owners.get(instance['asset'], set()):
|
|
181
396
|
problems.append(
|
|
182
|
-
'第 %d 页页型 %s
|
|
183
|
-
% (number, layout,
|
|
397
|
+
'第 %d 页页型 %s 额外使用了固定实例 %s'
|
|
398
|
+
% (number, layout, instance['asset']))
|
|
184
399
|
return problems
|
|
185
400
|
|
|
186
401
|
|
|
@@ -45,6 +45,8 @@ metadata:
|
|
|
45
45
|
|
|
46
46
|
deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**在幻灯片 `<section>` 元素上自行设置 position/inset/width/height。
|
|
47
47
|
|
|
48
|
+
组件撑满的只是 `<section>` 本身,section 内的容器高度仍由内容撑开;高度由内容撑开的容器上,`justify-content` 等垂直分布声明静默无效。要垂直居中或上下分布,先给承载声明的容器高度(如 `height: 100%`)。
|
|
49
|
+
|
|
48
50
|
### 使用 PPTX 风格包
|
|
49
51
|
|
|
50
52
|
如果前一步产出了 `pptx-style-extract` 风格包,必须把它当作本 deck 的设计系统:
|
|
@@ -104,7 +106,7 @@ deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**
|
|
|
104
106
|
|
|
105
107
|
- **动效。** 动效服务于叙事——引导视线、分层揭示信息、平滑衔接页面——而不是炫技或填空。默认克制,始终以不干扰阅读为底线。deck 动效的形态是**翻到该页时播放一次的入场 / 分步揭示**,不做环境循环——无限循环的装饰动画会持续争夺注意力。实现用 CSS 动画(幻灯片保持可直编的静态 HTML),两条契约(细节见 deck-stage.js 头部 Authoring guidance):
|
|
106
108
|
- 动画门控在 `[data-deck-active]` 与 `prefers-reduced-motion: no-preference` 上——组件在激活页维护该属性,翻页即触发;需要 JS 编排时监听组件的 `slidechange` 事件。
|
|
107
|
-
-
|
|
109
|
+
- 基础样式写的就是**动画结束后的最终画面**,`@keyframes` 只描述进入它的路径——判据:删掉所有 `animation` 声明,静止画面应一个像素不变(缩略图栏、print、reduced-motion 渲染的正是这个无动画基态)。
|
|
108
110
|
|
|
109
111
|
- **层次靠版式,不靠特效。** 页内层级由字号、字重、色块、边框、分隔线和留白建立;内容卡片和区块默认平面化——不加 box-shadow、发光、玻璃拟态(backdrop-filter + 半透明底),渐变默认只用于图上文字的保护渐变(见「图片呈现」)和数据可视化的连续色带。深色底 + 紫蓝渐变 + 发光卡片的「科技感」组合是模型默认值而非设计选择(frontend-design 校准清单第 4 种长相),除非品牌 / brief 明确要求,不要用它。
|
|
110
112
|
|
|
@@ -135,7 +137,7 @@ deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**
|
|
|
135
137
|
|
|
136
138
|
## 规划步骤
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
务必完成以下步骤:
|
|
139
141
|
|
|
140
142
|
1. 如果不清楚受众、期望的品牌风格,先提问。
|
|
141
143
|
2. 写出完整的标题序列。选择**一种**语法风格(例如短主题名词短语或简短陈述句),确保适合内容,并用该风格写出每一个标题。回头通读一遍,判断一个人**仅凭标题**能否跟上整个演示的脉络。标题应像书的章节——用直白的语言告诉读者接下来是什么。审阅这些标题并按需修订。将它们写入 scratchpad.md 文件。
|
|
@@ -162,4 +164,4 @@ deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**
|
|
|
162
164
|
- 没有被动空洞:边框 / 底色圈出的范围与其内容相称
|
|
163
165
|
- 页面视觉重量在画布上分布均衡,没有大片区域读成「缺了东西」
|
|
164
166
|
- 每页能指出视觉锚点;版式原型的重复经得起「内容使然还是没做选择」的追问
|
|
165
|
-
-
|
|
167
|
+
- 带动效的元素在缩略图栏和打印视图下与激活页一致(基础样式即动画终态——删掉 `animation` 声明,静止画面不变)
|