@lark-apaas/coding-steering 0.1.32-dev.b2f659e → 0.1.32-dev.b6564ba
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 +1 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +21 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +120 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +121 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +24 -3
- 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 +82 -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
package/package.json
CHANGED
|
@@ -145,13 +145,20 @@ metadata:
|
|
|
145
145
|
```jsx
|
|
146
146
|
function EChart({ option, style }) {
|
|
147
147
|
const ref = React.useRef(null);
|
|
148
|
+
const chartRef = React.useRef(null);
|
|
148
149
|
React.useEffect(() => {
|
|
149
150
|
const el = ref.current;
|
|
150
151
|
const chart = echarts.init(el);
|
|
151
|
-
chart
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return () => {
|
|
152
|
+
chartRef.current = chart;
|
|
153
|
+
const resizeObserver = new ResizeObserver(() => chart.resize());
|
|
154
|
+
resizeObserver.observe(el);
|
|
155
|
+
return () => {
|
|
156
|
+
resizeObserver.disconnect();
|
|
157
|
+
chart.dispose();
|
|
158
|
+
};
|
|
159
|
+
}, []);
|
|
160
|
+
React.useEffect(() => {
|
|
161
|
+
chartRef.current.setOption(option);
|
|
155
162
|
}, [option]);
|
|
156
163
|
return <div ref={ref} style={{ width: '100%', minHeight: 300, ...style }} />;
|
|
157
164
|
}
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
创建高保真、精细打磨的设计。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
遵循以下通用设计流程:
|
|
15
15
|
1. 澄清关键信息:能从需求、附件、截图或常见模式合理推断的,直接继续;只在关键信息缺失且会影响设计方向时才向用户提问
|
|
16
16
|
2. 查找现有 UI kit 并收集设计上下文——复制所有相关组件,阅读所有相关示例;如果找不到且会影响核心设计方向,再向用户询问
|
|
17
17
|
3. 在文件开头写下假设、上下文和设计推理(把自己当作初级设计师,用户是你的主管),放好设计占位,并尽早展示给用户
|
|
@@ -24,20 +24,4 @@ metadata:
|
|
|
24
24
|
|
|
25
25
|
**面向桌面的场景**(管理后台、内部工具、数据密集型仪表盘)——不需要重排为移动端布局,但必须设 `min-width`(通常 1024px–1200px),窄于此宽度时整体水平滚动,而不是让布局被挤压变形。
|
|
26
26
|
|
|
27
|
-
brief 未指明时默认按终端用户产品处理。
|
|
28
|
-
|
|
29
|
-
## 宣告可升级为全栈应用
|
|
30
|
-
|
|
31
|
-
宿主为交互原型提供「升级为全栈应用」入口,把纯前端原型转成带服务端的真实应用。入口是否出现,取决于原型有没有向父窗口宣告:
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
function announceUpgrade() {
|
|
35
|
-
window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'interactive-prototype' }, '*');
|
|
36
|
-
}
|
|
37
|
-
announceUpgrade();
|
|
38
|
-
// 宿主在 iframe 'load' 时重置能力声明,脚本早于 load 执行时补一次
|
|
39
|
-
if (document.readyState !== 'complete') window.addEventListener('load', announceUpgrade, { once: true });
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- 重复宣告无副作用;宁可多发,也不要因时序错过让入口不出现。
|
|
43
|
-
- 只宣告,不实现:原型侧不写升级逻辑,转全栈由宿主发起。
|
|
27
|
+
brief 未指明时默认按终端用户产品处理。
|
|
@@ -130,7 +130,7 @@ Read `<pack_dir>/design.md` first, especially `## Usage`, `## Hard Rules`, color
|
|
|
130
130
|
When generating a deck:
|
|
131
131
|
|
|
132
132
|
1. Call `copy_starter_component` with `kind: "deck-stage.js"`.
|
|
133
|
-
2. Build `<deck-stage width="<canvas width>" height="<canvas height>">` using the `canvas` declared in `layouts.md` — source decks are not always 16:9, and a default-sized stage shifts every coordinate on the page. Each slide is one static `<section data-pptx-layout="<chosen archetype>">`.
|
|
133
|
+
2. Build `<deck-stage width="<canvas width>" height="<canvas height>">` using the `canvas` declared in `layouts.md` — source decks are not always 16:9, and a default-sized stage shifts every coordinate on the page. Each slide is one static `<section data-pptx-layout="<chosen archetype>">`. For a multi-theme pack, choose layouts compatible with `default-theme` by default. When a page intentionally switches theme, add `data-pptx-theme="<theme>"`; the theme must be listed by that layout's `themes`.
|
|
134
134
|
3. Inline CSS variables from `design.md` into the HTML `<style>` block using a `--ppt-*` prefix.
|
|
135
135
|
4. **位移动画用独立的 `translate` 属性**:`@keyframes fadeUp { from{opacity:0; translate:0 24px} to{opacity:1; translate:0 0} }`。`transform` 是单一属性,动画里碰它会覆盖掉元素原有的那条(`left:50%; transform:translateX(-50%)` 的居中就此丢失);`translate` / `rotate` / `scale` 各自独立,与已有 `transform` 叠加。
|
|
136
136
|
5. Map `layouts.md` slots to absolute-positioned elements inside each section: expand `box: [x,y,w,h]` mechanically to `left/top/width/height`, then apply the slot's `css` declaration string unchanged. `box` owns geometry; `css` owns all rendering style, including the template's text padding, typography, alignment, line height, letter spacing, and rotation. Do not reinterpret PPTX fields or replace slot CSS with your own type scale. A slot with `asset` is a fixed image element: use that exact asset at that box on that archetype only; do not omit or replace it. Do not reflow any of this as generic web grids.
|
|
@@ -1243,7 +1243,21 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1243
1243
|
sample_pages_of_layout = defaultdict(list)
|
|
1244
1244
|
for slide_part, layout_part in lay_of_slide.items():
|
|
1245
1245
|
sample_pages_of_layout[layout_part].append(slide_no(slide_part))
|
|
1246
|
+
sampled_theme_counts = Counter(
|
|
1247
|
+
theme_of_master.get(master_of.get(layout_part))
|
|
1248
|
+
for layout_part in lay_of_slide.values()
|
|
1249
|
+
)
|
|
1250
|
+
sampled_theme_counts.pop(None, None)
|
|
1246
1251
|
default_theme = topo.get('default')
|
|
1252
|
+
if sampled_theme_counts:
|
|
1253
|
+
highest = max(sampled_theme_counts.values())
|
|
1254
|
+
leaders = sorted(
|
|
1255
|
+
theme for theme, count in sampled_theme_counts.items()
|
|
1256
|
+
if count == highest
|
|
1257
|
+
)
|
|
1258
|
+
if default_theme not in leaders:
|
|
1259
|
+
default_theme = leaders[0]
|
|
1260
|
+
topo['default'] = default_theme
|
|
1247
1261
|
multi = len(topo.get('themes') or []) > 1
|
|
1248
1262
|
|
|
1249
1263
|
rows = []
|
|
@@ -2394,7 +2408,9 @@ def emit_manifest(d, assets, vision_groups, ldir, archetypes=()):
|
|
|
2394
2408
|
' TODO: 一句话说清这套模板的视觉性格(底色 / 主色 / 字形 / 版面骨架),给消费模型定调。']
|
|
2395
2409
|
themes = d['theme_topology'].get('themes') or ['single']
|
|
2396
2410
|
if themes != ['single'] and len(themes) > 1:
|
|
2397
|
-
|
|
2411
|
+
default_theme = d['theme_topology'].get('default') or themes[0]
|
|
2412
|
+
L += ['themes: [%s]' % ', '.join(themes),
|
|
2413
|
+
'default-theme: %s' % default_theme]
|
|
2398
2414
|
if assets:
|
|
2399
2415
|
L.append('assets:')
|
|
2400
2416
|
for a in assets:
|
|
@@ -2921,6 +2937,8 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
2921
2937
|
L.append(' %s:' % a['name'])
|
|
2922
2938
|
if a.get('role'):
|
|
2923
2939
|
L.append(' role: %s' % a['role'])
|
|
2940
|
+
if a.get('theme'):
|
|
2941
|
+
L.append(' themes: [%s]' % a['theme'])
|
|
2924
2942
|
if a['bg']:
|
|
2925
2943
|
L.append(' background: %s' % a['bg'])
|
|
2926
2944
|
fl = a.get('flow')
|
|
@@ -3056,6 +3074,8 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
3056
3074
|
'页数多于页型时,挑最接近的一个原样套用它的 slot:用不到的槽删掉,'
|
|
3057
3075
|
'内容比槽多就按同类槽的间距等距加,**坐标一律沿用该页型给的那套,不要自己另起网格**。'
|
|
3058
3076
|
'每个生成页面的 `<section>` 都写 `data-pptx-layout="<页型名>"`,'
|
|
3077
|
+
'多主题包默认只选兼容 `default-theme` 的页型;确需切换时,该页同时写 '
|
|
3078
|
+
'`data-pptx-theme="<主题名>"`,且主题必须属于该页型的 `themes`。'
|
|
3059
3079
|
'交付前据此核验该页型绑定的背景与图片资产均已使用,且没有跨页型误用。' % sidecar,
|
|
3060
3080
|
'3. **按页型给的形态落元素** —— 页型给 `flow` 就用流式,给 `slots` 就用绝对,'
|
|
3061
3081
|
'两者只会出现一个。'
|
|
@@ -15,6 +15,7 @@ from draft import (asset_vision_contexts, bound_visual_candidates, # noqa: E402
|
|
|
15
15
|
cover_background_media,
|
|
16
16
|
emit_asset_vision_groups, emit_layouts, emit_manifest,
|
|
17
17
|
fullscreen_overlay_media,
|
|
18
|
+
layouts_from_template,
|
|
18
19
|
needs_asset_judgment, preserve_image_bearing_groups,
|
|
19
20
|
select_asset_vision_groups, slide_image_marks, slot_style,
|
|
20
21
|
visual_slot_candidates)
|
|
@@ -24,6 +25,125 @@ from package import (FONTSIZE_RE, Fail, apply_asset_decisions, # noqa: E402
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class LayoutCssTest(unittest.TestCase):
|
|
28
|
+
def test_template_layouts_use_sampled_theme_as_default_and_keep_theme_scope(self):
|
|
29
|
+
dark_master = 'ppt/slideMasters/slideMaster1.xml'
|
|
30
|
+
light_master = 'ppt/slideMasters/slideMaster2.xml'
|
|
31
|
+
dark_layout = 'ppt/slideLayouts/slideLayout1.xml'
|
|
32
|
+
light_layout = 'ppt/slideLayouts/slideLayout2.xml'
|
|
33
|
+
data = {
|
|
34
|
+
'layouts': [
|
|
35
|
+
{'part': dark_layout, 'name': 'Dark content', 'used_by_slides': 2},
|
|
36
|
+
{'part': light_layout, 'name': 'Light content', 'used_by_slides': 0},
|
|
37
|
+
],
|
|
38
|
+
'slides': [],
|
|
39
|
+
'theme_topology': {
|
|
40
|
+
'themes': ['light', 'dark'],
|
|
41
|
+
'default': 'light',
|
|
42
|
+
'per_master': [
|
|
43
|
+
{'master': dark_master, 'theme_label': 'dark'},
|
|
44
|
+
{'master': light_master, 'theme_label': 'light'},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
'reference_graph': {
|
|
48
|
+
'master_of_layout': {
|
|
49
|
+
dark_layout: dark_master,
|
|
50
|
+
light_layout: light_master,
|
|
51
|
+
},
|
|
52
|
+
'layout_of_slide': {
|
|
53
|
+
'ppt/slides/slide1.xml': dark_layout,
|
|
54
|
+
'ppt/slides/slide2.xml': dark_layout,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'background_composites': {},
|
|
58
|
+
}
|
|
59
|
+
shapes = [
|
|
60
|
+
{
|
|
61
|
+
'part': part,
|
|
62
|
+
'layer': 'layout',
|
|
63
|
+
'kind': 'sp',
|
|
64
|
+
'box': {'x': 100, 'y': 100, 'w': 800, 'h': 100},
|
|
65
|
+
'ph': {'type': 'title', 'idx': '1'},
|
|
66
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title', 'sz_px': 40}]}]},
|
|
67
|
+
}
|
|
68
|
+
for part in (dark_layout, light_layout)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
archetypes = layouts_from_template(data, shapes, 1920, 1080)
|
|
72
|
+
|
|
73
|
+
self.assertEqual('dark', data['theme_topology']['default'])
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
{'content': 'dark', 'content-2': 'light'},
|
|
76
|
+
{item['name']: item['theme'] for item in archetypes},
|
|
77
|
+
)
|
|
78
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
79
|
+
emit_layouts(archetypes, output_dir)
|
|
80
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
81
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
82
|
+
layouts = stream.read()
|
|
83
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
84
|
+
manifest = stream.read()
|
|
85
|
+
self.assertIn(' themes: [dark]', layouts)
|
|
86
|
+
self.assertIn(' themes: [light]', layouts)
|
|
87
|
+
self.assertIn('default-theme: dark', manifest)
|
|
88
|
+
|
|
89
|
+
def test_template_twin_layouts_keep_topology_but_scope_retained_layout(self):
|
|
90
|
+
dark_master = 'ppt/slideMasters/slideMaster1.xml'
|
|
91
|
+
light_master = 'ppt/slideMasters/slideMaster2.xml'
|
|
92
|
+
dark_layout = 'ppt/slideLayouts/slideLayout1.xml'
|
|
93
|
+
light_layout = 'ppt/slideLayouts/slideLayout2.xml'
|
|
94
|
+
data = {
|
|
95
|
+
'layouts': [
|
|
96
|
+
{'part': dark_layout, 'name': 'Content', 'used_by_slides': 2},
|
|
97
|
+
{'part': light_layout, 'name': 'Content', 'used_by_slides': 0},
|
|
98
|
+
],
|
|
99
|
+
'slides': [],
|
|
100
|
+
'theme_topology': {
|
|
101
|
+
'themes': ['light', 'dark'],
|
|
102
|
+
'default': 'light',
|
|
103
|
+
'per_master': [
|
|
104
|
+
{'master': dark_master, 'theme_label': 'dark'},
|
|
105
|
+
{'master': light_master, 'theme_label': 'light'},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
'reference_graph': {
|
|
109
|
+
'master_of_layout': {
|
|
110
|
+
dark_layout: dark_master,
|
|
111
|
+
light_layout: light_master,
|
|
112
|
+
},
|
|
113
|
+
'layout_of_slide': {
|
|
114
|
+
'ppt/slides/slide1.xml': dark_layout,
|
|
115
|
+
'ppt/slides/slide2.xml': dark_layout,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
'background_composites': {},
|
|
119
|
+
}
|
|
120
|
+
shapes = [
|
|
121
|
+
{
|
|
122
|
+
'part': part,
|
|
123
|
+
'layer': 'layout',
|
|
124
|
+
'kind': 'sp',
|
|
125
|
+
'box': {'x': 100, 'y': 100, 'w': 800, 'h': 100},
|
|
126
|
+
'ph': {'type': 'title', 'idx': '1'},
|
|
127
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title', 'sz_px': 40}]}]},
|
|
128
|
+
}
|
|
129
|
+
for part in (dark_layout, light_layout)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
archetypes = layouts_from_template(data, shapes, 1920, 1080)
|
|
133
|
+
|
|
134
|
+
self.assertEqual(1, len(archetypes))
|
|
135
|
+
self.assertEqual('dark', archetypes[0]['theme'])
|
|
136
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
137
|
+
emit_layouts(archetypes, output_dir)
|
|
138
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
139
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
140
|
+
layouts = stream.read()
|
|
141
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
142
|
+
manifest = stream.read()
|
|
143
|
+
self.assertIn(' themes: [dark]', layouts)
|
|
144
|
+
self.assertIn('themes: [light, dark]', manifest)
|
|
145
|
+
self.assertIn('default-theme: dark', manifest)
|
|
146
|
+
|
|
27
147
|
def test_text_slot_emits_rendering_style_as_css_only(self):
|
|
28
148
|
shape = {
|
|
29
149
|
'text': {
|
|
@@ -128,6 +128,31 @@ class LayoutAssetContractTest(unittest.TestCase):
|
|
|
128
128
|
'---\n'
|
|
129
129
|
)
|
|
130
130
|
|
|
131
|
+
def write_theme_pack(self, root):
|
|
132
|
+
with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
|
|
133
|
+
stream.write(
|
|
134
|
+
'---\n'
|
|
135
|
+
'themes: [dark, light]\n'
|
|
136
|
+
'default-theme: dark\n'
|
|
137
|
+
'layouts: layouts.md\n'
|
|
138
|
+
'---\n'
|
|
139
|
+
)
|
|
140
|
+
with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
|
|
141
|
+
stream.write(
|
|
142
|
+
'---\n'
|
|
143
|
+
'canvas: 1920x1080\n'
|
|
144
|
+
'layouts:\n'
|
|
145
|
+
' dark-content:\n'
|
|
146
|
+
' role: content\n'
|
|
147
|
+
' themes: [dark]\n'
|
|
148
|
+
' slots: []\n'
|
|
149
|
+
' light-content:\n'
|
|
150
|
+
' role: content\n'
|
|
151
|
+
' themes: [light]\n'
|
|
152
|
+
' slots: []\n'
|
|
153
|
+
'---\n'
|
|
154
|
+
)
|
|
155
|
+
|
|
131
156
|
def test_logo_is_limited_to_every_archetype_with_its_slot(self):
|
|
132
157
|
with tempfile.TemporaryDirectory() as root:
|
|
133
158
|
self.write_pack(root)
|
|
@@ -200,6 +225,102 @@ class LayoutAssetContractTest(unittest.TestCase):
|
|
|
200
225
|
validate_layout_assets(root, html, 'assets/pptx-volcengine'),
|
|
201
226
|
)
|
|
202
227
|
|
|
228
|
+
def test_missing_deck_slides_is_rejected_even_without_fixed_assets(self):
|
|
229
|
+
with tempfile.TemporaryDirectory() as root:
|
|
230
|
+
self.write_theme_pack(root)
|
|
231
|
+
html = self.write_html(root, '<main><section>普通页面</section></main>')
|
|
232
|
+
|
|
233
|
+
self.assertEqual(
|
|
234
|
+
['没有识别到 deck-stage 的直属 slide section,无法核验模板页型'],
|
|
235
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def test_layout_outside_default_theme_requires_explicit_theme(self):
|
|
239
|
+
with tempfile.TemporaryDirectory() as root:
|
|
240
|
+
self.write_theme_pack(root)
|
|
241
|
+
html = self.write_html(
|
|
242
|
+
root,
|
|
243
|
+
'<deck-stage>'
|
|
244
|
+
'<section data-pptx-layout="light-content">正文</section>'
|
|
245
|
+
'</deck-stage>',
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.assertEqual(
|
|
249
|
+
['第 1 页页型 light-content 不支持当前主题 dark;'
|
|
250
|
+
'允许主题: light。若确需切换,显式声明 data-pptx-theme'],
|
|
251
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def test_explicit_non_default_theme_must_match_layout(self):
|
|
255
|
+
with tempfile.TemporaryDirectory() as root:
|
|
256
|
+
self.write_theme_pack(root)
|
|
257
|
+
html = self.write_html(
|
|
258
|
+
root,
|
|
259
|
+
'<deck-stage>'
|
|
260
|
+
'<section data-pptx-layout="light-content" '
|
|
261
|
+
'data-pptx-theme="light">正文</section>'
|
|
262
|
+
'</deck-stage>',
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
self.assertEqual(
|
|
266
|
+
[],
|
|
267
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def test_unknown_explicit_theme_is_rejected(self):
|
|
271
|
+
with tempfile.TemporaryDirectory() as root:
|
|
272
|
+
self.write_theme_pack(root)
|
|
273
|
+
html = self.write_html(
|
|
274
|
+
root,
|
|
275
|
+
'<deck-stage>'
|
|
276
|
+
'<section data-pptx-layout="dark-content" '
|
|
277
|
+
'data-pptx-theme="brand-new">正文</section>'
|
|
278
|
+
'</deck-stage>',
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
self.assertEqual(
|
|
282
|
+
['第 1 页声明了不存在的模板主题: brand-new'],
|
|
283
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def test_pack_without_fixed_assets_still_accepts_valid_layout(self):
|
|
287
|
+
with tempfile.TemporaryDirectory() as root:
|
|
288
|
+
self.write_theme_pack(root)
|
|
289
|
+
html = self.write_html(
|
|
290
|
+
root,
|
|
291
|
+
'<deck-stage>'
|
|
292
|
+
'<section data-pptx-layout="dark-content">正文</section>'
|
|
293
|
+
'</deck-stage>',
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
self.assertEqual(
|
|
297
|
+
[],
|
|
298
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def test_non_mapping_layout_entry_does_not_crash_theme_validation(self):
|
|
302
|
+
with tempfile.TemporaryDirectory() as root:
|
|
303
|
+
self.write_theme_pack(root)
|
|
304
|
+
with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
|
|
305
|
+
stream.write(
|
|
306
|
+
'---\n'
|
|
307
|
+
'canvas: 1920x1080\n'
|
|
308
|
+
'layouts:\n'
|
|
309
|
+
' malformed: unavailable\n'
|
|
310
|
+
'---\n'
|
|
311
|
+
)
|
|
312
|
+
html = self.write_html(
|
|
313
|
+
root,
|
|
314
|
+
'<deck-stage>'
|
|
315
|
+
'<section data-pptx-layout="malformed">正文</section>'
|
|
316
|
+
'</deck-stage>',
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
self.assertEqual(
|
|
320
|
+
[],
|
|
321
|
+
validate_layout_assets(root, html, 'assets/pptx-theme'),
|
|
322
|
+
)
|
|
323
|
+
|
|
203
324
|
def test_logo_in_global_css_is_rejected(self):
|
|
204
325
|
with tempfile.TemporaryDirectory() as root:
|
|
205
326
|
self.write_pack(root)
|
|
@@ -207,6 +207,7 @@ class SlideAssetParser(HTMLParser):
|
|
|
207
207
|
tag == 'section' and parent and parent['tag'] == 'deck-stage')
|
|
208
208
|
slide = ({
|
|
209
209
|
'layout': attrs_map.get('data-pptx-layout'),
|
|
210
|
+
'theme': attrs_map.get('data-pptx-theme'),
|
|
210
211
|
'references': [],
|
|
211
212
|
} if slide_root else parent_slide)
|
|
212
213
|
if slide_root:
|
|
@@ -270,8 +271,6 @@ def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
|
270
271
|
pack = Pack(pack_dir)
|
|
271
272
|
owners = layout_asset_contract(pack)
|
|
272
273
|
known_urls = asset_urls(pack, asset_prefix, owners)
|
|
273
|
-
if not known_urls:
|
|
274
|
-
return []
|
|
275
274
|
asset_urls_by_id = {
|
|
276
275
|
asset_id: url
|
|
277
276
|
for url, asset_ids in known_urls.items()
|
|
@@ -285,6 +284,10 @@ def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
|
285
284
|
parser.close()
|
|
286
285
|
|
|
287
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
|
|
288
291
|
for url in parser.outside_urls:
|
|
289
292
|
for asset_id in sorted(known_urls.get(normalized_path(url), ())):
|
|
290
293
|
problems.append(
|
|
@@ -297,8 +300,26 @@ def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
|
297
300
|
if layout not in pack.layouts:
|
|
298
301
|
problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
|
|
299
302
|
continue
|
|
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 []
|
|
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
|
|
300
321
|
expected = layout_asset_instances(
|
|
301
|
-
|
|
322
|
+
layout_entry, set(pack.assets), pack.canvas)
|
|
302
323
|
actual = []
|
|
303
324
|
for reference in slide['references']:
|
|
304
325
|
referenced_ids = set()
|
|
@@ -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` 声明,静止画面不变)
|