@topmindspace/tms-skills 0.1.0

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/tms-skills.js +160 -0
  5. package/package.json +48 -0
  6. package/top-ppt-html/README.md +254 -0
  7. package/top-ppt-html/SKILL.md +111 -0
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
  11. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
  17. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
  19. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
  21. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
  23. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
  25. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
  26. package/top-ppt-html/assets/pptx-export.js +1944 -0
  27. package/top-ppt-html/assets/style-gallery.html +589 -0
  28. package/top-ppt-html/assets/templates/architecture.html +3728 -0
  29. package/top-ppt-html/assets/templates/engine.css +840 -0
  30. package/top-ppt-html/assets/templates/presentation.html +3738 -0
  31. package/top-ppt-html/assets/templates/research.html +4017 -0
  32. package/top-ppt-html/assets/templates/ui.js +520 -0
  33. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  35. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  36. package/top-ppt-html/assets/theme-overview.png +0 -0
  37. package/top-ppt-html/evals/prompts.csv +15 -0
  38. package/top-ppt-html/evals/rubric.schema.json +25 -0
  39. package/top-ppt-html/evals/run_evals.py +220 -0
  40. package/top-ppt-html/evals/trace.example.json +16 -0
  41. package/top-ppt-html/package.json +35 -0
  42. package/top-ppt-html/references/charts-basic.md +624 -0
  43. package/top-ppt-html/references/charts-discipline.md +108 -0
  44. package/top-ppt-html/references/charts-extended.md +482 -0
  45. package/top-ppt-html/references/charts.md +28 -0
  46. package/top-ppt-html/references/components-atoms.md +624 -0
  47. package/top-ppt-html/references/components.md +30 -0
  48. package/top-ppt-html/references/content-rules.md +490 -0
  49. package/top-ppt-html/references/design-system-engine.md +235 -0
  50. package/top-ppt-html/references/design-system.md +478 -0
  51. package/top-ppt-html/references/failure-modes.md +214 -0
  52. package/top-ppt-html/references/high-fidelity.md +127 -0
  53. package/top-ppt-html/references/icons.md +397 -0
  54. package/top-ppt-html/references/industry-benchmark.md +105 -0
  55. package/top-ppt-html/references/infographics-stats.md +308 -0
  56. package/top-ppt-html/references/infographics-structure.md +226 -0
  57. package/top-ppt-html/references/infographics.md +43 -0
  58. package/top-ppt-html/references/layout-grammar.md +315 -0
  59. package/top-ppt-html/references/layouts-architecture.md +108 -0
  60. package/top-ppt-html/references/layouts-combo.md +600 -0
  61. package/top-ppt-html/references/layouts-research.md +160 -0
  62. package/top-ppt-html/references/modes.md +254 -0
  63. package/top-ppt-html/references/outline-design.md +275 -0
  64. package/top-ppt-html/references/playbook.md +266 -0
  65. package/top-ppt-html/references/pptx-export.md +209 -0
  66. package/top-ppt-html/references/reform-plan.md +252 -0
  67. package/top-ppt-html/references/styles.md +370 -0
  68. package/top-ppt-html/references/tech-design.md +138 -0
  69. package/top-ppt-html/scripts/audit_css.py +109 -0
  70. package/top-ppt-html/scripts/audit_docs.py +176 -0
  71. package/top-ppt-html/scripts/audit_skill.py +220 -0
  72. package/top-ppt-html/scripts/audit_styles.py +351 -0
  73. package/top-ppt-html/scripts/build_examples.py +2276 -0
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -0
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
  76. package/top-ppt-html/scripts/checks_html.py +127 -0
  77. package/top-ppt-html/scripts/cross_verify.py +294 -0
  78. package/top-ppt-html/scripts/env_probe.py +158 -0
  79. package/top-ppt-html/scripts/extract_model.py +210 -0
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -0
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -0
  82. package/top-ppt-html/scripts/layout-constants.json +3377 -0
  83. package/top-ppt-html/scripts/layout_slots.json +830 -0
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
  85. package/top-ppt-html/scripts/measure_height.py +178 -0
  86. package/top-ppt-html/scripts/model-schema.json +547 -0
  87. package/top-ppt-html/scripts/negative_tests.py +307 -0
  88. package/top-ppt-html/scripts/package_skill.py +291 -0
  89. package/top-ppt-html/scripts/prepare_images.py +341 -0
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -0
  91. package/top-ppt-html/scripts/quality_gate.py +301 -0
  92. package/top-ppt-html/scripts/regression.py +308 -0
  93. package/top-ppt-html/scripts/render_compare.py +275 -0
  94. package/top-ppt-html/scripts/render_from_model.py +698 -0
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -0
  96. package/top-ppt-html/scripts/section-file-map.json +105 -0
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -0
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -0
  99. package/top-ppt-html/scripts/validate_report.py +1456 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
3
+ *
4
+ * 从 layout-constants.json + layout_slots.json 派生「语义槽位 → 英寸矩形」。
5
+ * B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源),
6
+ * 本模块是把「槽位语义」从散落坐标收拢成可测接口的第一步。
7
+ *
8
+ * 纪律:改几何只改 layout-constants.json;改槽位只改 layout_slots.json。
9
+ * 新增页型先扩 layout_slots.json,再在此补 regionOf 分支,最后写双引擎。
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const ROOT = path.join(__dirname, '..');
17
+ const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
18
+ // 布局 IR:优先 layout-constants.layoutSlots(v9 单源合并);否则回落 layout_slots.json
19
+ let SLOTS = LC.layoutSlots;
20
+ if (!SLOTS || !SLOTS.pageTypes) {
21
+ SLOTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout_slots.json'), 'utf8'));
22
+ }
23
+
24
+ const INFO_TYPES = {
25
+ sankey: 1, treemap: 1, boxplot: 1, network: 1, marimekko: 1, streamgraph: 1,
26
+ };
27
+
28
+ function pageBox() {
29
+ const p = LC.page;
30
+ return { pw: p.pw, ph: p.ph, mx: p.mx, cw: p.pw - 2 * p.mx };
31
+ }
32
+
33
+ /** 内容区垂直范围(含注释层预留) */
34
+ function contentBand(opts) {
35
+ const L = LC.pageTypes.layout;
36
+ const C = LC.pageTypes.common;
37
+ const withNote = !!(opts && opts.withNote);
38
+ const top = (opts && opts.top != null) ? opts.top : C.bodyY;
39
+ const bottom = withNote ? L.contentBottomWithNote : L.contentBottom;
40
+ return { top, bottom, h: Math.max(0, bottom - top) };
41
+ }
42
+
43
+ /**
44
+ * 语义槽位 → {x,y,w,h}(英寸)
45
+ * 未识别页型/槽位时返回 null(调用方回落自身逻辑,不中断)。
46
+ */
47
+ function regionOf(pageType, slotId, opts) {
48
+ const type = String(pageType || 'points');
49
+ const slot = String(slotId || '');
50
+ const { mx, cw } = pageBox();
51
+ const C = LC.pageTypes.common;
52
+ const L = LC.pageTypes.layout;
53
+ const PT = LC.pageTypes[type] || {};
54
+
55
+ // ── chrome:页头 ──
56
+ if (slot === 'head' || slot === 'chrome') {
57
+ return {
58
+ x: mx, y: C.headEyebrowY, w: cw,
59
+ h: Math.max(0.4, (C.headRuleY || 1.95) - C.headEyebrowY),
60
+ titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY,
61
+ };
62
+ }
63
+
64
+ // ── annotation:so-what / 来源 / note ──
65
+ if (slot === 'annotation' || slot === 'soWhat') {
66
+ const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.soWhatY != null)
67
+ ? LC.pageTypes.exhibit.soWhatY : 6.05;
68
+ return { x: mx, y, w: cw, h: 0.62 };
69
+ }
70
+ if (slot === 'footnote') {
71
+ const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.footnoteY != null)
72
+ ? LC.pageTypes.exhibit.footnoteY
73
+ : ((LC.pageTypes.note && LC.pageTypes.note.y) || 6.55);
74
+ return { x: mx, y, w: cw - 1.2, h: (LC.pageTypes.note && LC.pageTypes.note.h) || 0.32 };
75
+ }
76
+
77
+ // ── 页型主从区域 ──
78
+ if (type === 'points') {
79
+ const band = contentBand(opts);
80
+ if (slot === 'primary' || slot === 'list') {
81
+ return { x: mx, y: PT.listY || band.top, w: cw, h: Math.max(0.8, band.bottom - (PT.listY || band.top)) };
82
+ }
83
+ if (slot === 'metrics') {
84
+ return { x: mx, y: PT.metricY || 4.7, w: cw, h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)) };
85
+ }
86
+ }
87
+
88
+ if (type === 'metrics') {
89
+ if (slot === 'primary') {
90
+ return {
91
+ x: mx, y: PT.bandY || 3.1, w: cw, h: PT.cardH || 2.3,
92
+ valY: PT.valY, valH: PT.valH, capY: PT.capY, capH: PT.capH,
93
+ gap: PT.gap, maxW: PT.maxW,
94
+ };
95
+ }
96
+ }
97
+
98
+ if (type === 'table') {
99
+ if (slot === 'primary') {
100
+ const y = PT.y || L.contentTop;
101
+ return {
102
+ x: mx, y, w: cw,
103
+ h: Math.max(1, ((opts && opts.bottom) || L.contentBottom) - y),
104
+ rowH: PT.rowH, rowHMin: PT.rowHMin, headRowH: PT.headRowH,
105
+ };
106
+ }
107
+ }
108
+
109
+ if (type === 'bar') {
110
+ if (slot === 'primary') {
111
+ const y = PT.chartY || 2.95;
112
+ const bottom = (opts && opts.bottom != null) ? opts.bottom
113
+ : ((opts && opts.withNote) ? L.contentBottomWithNote : L.contentBottom);
114
+ return { x: mx + (PT.chartX || 0), y, w: cw - (PT.chartW || 0), h: Math.max(1.2, bottom - y) };
115
+ }
116
+ if (slot === 'hbar') {
117
+ const y0 = PT.hbarY0 || 2.6;
118
+ const y1 = (opts && opts.bottom != null) ? opts.bottom : (PT.hbarY1 || 6.25);
119
+ return { x: mx, y: y0, w: cw, h: Math.max(1.2, y1 - y0) };
120
+ }
121
+ }
122
+
123
+ if (type === 'donut') {
124
+ if (slot === 'primary') {
125
+ const y = PT.chartY || 2.95;
126
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
127
+ return { x: mx, y, w: cw, h: Math.max(1.2, bottom - y) };
128
+ }
129
+ }
130
+
131
+ if (type === 'exhibit') {
132
+ if (slot === 'primary') {
133
+ const y = PT.chartY || 2.95;
134
+ const h = Math.max(1.2, (L.contentBottomWithNote - y));
135
+ return { x: mx, y, w: cw, h, badgeY: PT.badgeY };
136
+ }
137
+ if (slot === 'secondary') {
138
+ return { x: mx, y: PT.badgeY || L.contentTop, w: cw * 0.32, h: 0.4 };
139
+ }
140
+ }
141
+
142
+ if (type === 'points') {
143
+ const band = contentBand(opts);
144
+ if (slot === 'primary' || slot === 'list') {
145
+ return {
146
+ x: mx, y: PT.listY || band.top, w: cw,
147
+ h: Math.max(0.8, (opts && opts.bottom != null ? opts.bottom : band.bottom) - (PT.listY || band.top)),
148
+ rowH: PT.rowH, rowHMin: PT.rowHMin, markSize: PT.markSize,
149
+ };
150
+ }
151
+ if (slot === 'metrics') {
152
+ return {
153
+ x: mx, y: PT.metricY || 4.7, w: cw,
154
+ h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)),
155
+ metricW: PT.metricW, metricValH: PT.metricValH, metricCapY: PT.metricCapY,
156
+ };
157
+ }
158
+ }
159
+
160
+ if (type === 'split') {
161
+ const SP = PT;
162
+ const top = SP.chartY || L.contentTop;
163
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
164
+ if (slot === 'left') {
165
+ return {
166
+ x: mx, y: top, w: cw * (SP.leftW || 0.55),
167
+ h: Math.max(1.2, bottom - top),
168
+ tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
169
+ };
170
+ }
171
+ if (slot === 'right') {
172
+ const rx = mx + cw * (SP.rightX != null ? SP.rightX : 0.585);
173
+ return {
174
+ x: rx, y: top, w: cw * (SP.rightW || 0.415),
175
+ h: Math.max(1.2, bottom - top),
176
+ tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
177
+ };
178
+ }
179
+ }
180
+
181
+ if (type === 'twocol') {
182
+ const gap = PT.colGap || 0.5;
183
+ const colW = (cw - gap) / 2;
184
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
185
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
186
+ if (slot === 'primary' || slot === 'left') {
187
+ return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
188
+ }
189
+ if (slot === 'secondary' || slot === 'right') {
190
+ return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
191
+ }
192
+ }
193
+
194
+ if (type === 'diagram' || type === 'lane') {
195
+ if (slot === 'primary') {
196
+ const arch = LC.pageTypes.arch || {};
197
+ const y = (opts && opts.mode === 'architecture' && arch.fullStartY != null)
198
+ ? arch.fullStartY
199
+ : (PT.bodyStartY || 2.9);
200
+ const b = (opts && opts.mode === 'architecture' && arch.fullEndY != null)
201
+ ? arch.fullEndY
202
+ : ((opts && opts.bottom != null) ? opts.bottom : L.contentBottom);
203
+ return {
204
+ x: mx, y, w: cw, h: Math.max(1.2, b - y),
205
+ layerBarW: PT.layerBarW, layerGap: PT.layerGap, maxLayerH: PT.maxLayerH,
206
+ laneH: arch.laneH, laneHeadW: arch.laneHeadW, laneGap: arch.laneGap,
207
+ stepGap: arch.stepGap, stepMaxW: arch.stepMaxW,
208
+ };
209
+ }
210
+ }
211
+
212
+ // ── 剩余高频页型 ──
213
+ if (type === 'kpi') {
214
+ if (slot === 'primary' || slot === 'hero') {
215
+ return {
216
+ x: mx, y: PT.heroY || 2.9, w: cw * (PT.heroW || 0.52), h: PT.heroH || 1.6,
217
+ dividerX: PT.dividerX, metricY0: PT.metricY0, metricRowH: PT.metricRowH,
218
+ };
219
+ }
220
+ if (slot === 'metrics') {
221
+ const dx = PT.dividerX != null ? PT.dividerX : 0.56;
222
+ return {
223
+ x: mx + cw * dx + 0.35, y: PT.metricY0 || 2.95,
224
+ w: cw * (1 - dx) - 0.5, h: Math.max(0.6, L.contentBottom - (PT.metricY0 || 2.95)),
225
+ };
226
+ }
227
+ }
228
+
229
+ if (type === 'comparison') {
230
+ const gap = PT.panelGap || 0.4;
231
+ const panelW = (cw - gap) / 2;
232
+ const y = PT.panelY || 2.55;
233
+ const h = PT.panelH || 3.4;
234
+ if (slot === 'primary' || slot === 'left') {
235
+ return { x: mx, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
236
+ }
237
+ if (slot === 'right' || slot === 'secondary') {
238
+ return { x: mx + panelW + gap, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
239
+ }
240
+ }
241
+
242
+ if (type === 'quote') {
243
+ if (slot === 'primary') {
244
+ return {
245
+ x: mx + 1.05, y: PT.textY || 2.7, w: cw - 2.1, h: PT.textH || 2.3,
246
+ markY: PT.markY, authorY: PT.authorY, contextY: PT.contextY,
247
+ };
248
+ }
249
+ }
250
+
251
+ if (type === 'heatmap') {
252
+ if (slot === 'primary') {
253
+ const y = PT.y || 2.6;
254
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
255
+ return {
256
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
257
+ labelW: PT.labelW, headH: PT.headH, cellH: PT.cellH, cellGap: PT.cellGap,
258
+ };
259
+ }
260
+ }
261
+
262
+ if (type === 'bullet') {
263
+ if (slot === 'primary') {
264
+ const y = PT.y || 2.6;
265
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
266
+ return {
267
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
268
+ rowH: PT.rowH, labelW: PT.labelW, valW: PT.valW, barH: PT.barH,
269
+ };
270
+ }
271
+ }
272
+
273
+ if (type === 'pyramid') {
274
+ if (slot === 'primary') {
275
+ const y = PT.y || 2.35;
276
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
277
+ return {
278
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
279
+ rowH: PT.rowH, gap: PT.gap, minW: PT.minW, labelW: PT.labelW,
280
+ };
281
+ }
282
+ }
283
+
284
+ if (type === 'steps') {
285
+ if (slot === 'primary') {
286
+ const y = PT.y || 3.05;
287
+ return {
288
+ x: mx, y, w: cw, h: PT.barH || 1.35,
289
+ gap: PT.gap, arrowW: PT.arrowW, numH: PT.numH, titleH: PT.titleH,
290
+ pad: PT.pad, maxPerRow: PT.maxPerRow,
291
+ };
292
+ }
293
+ }
294
+
295
+ if (type === 'cards') {
296
+ if (slot === 'primary') {
297
+ const y = PT.startY || 3.0;
298
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
299
+ return { x: mx, y, w: cw, h: Math.max(1, bottom - y), gap: PT.gap, maxH: PT.maxH, titleH: PT.titleH };
300
+ }
301
+ }
302
+
303
+ if (type === 'timeline') {
304
+ if (slot === 'primary') {
305
+ return {
306
+ x: mx, y: PT.labelY || 2.95, w: cw,
307
+ h: Math.max(1, L.contentBottom - (PT.labelY || 2.95)),
308
+ axisY: PT.axisY, dotY: PT.dotY, dotR: PT.dotR, nameY: PT.nameY,
309
+ descY: PT.descY, descH: PT.descH,
310
+ };
311
+ }
312
+ }
313
+
314
+ if (type === 'image') {
315
+ if (slot === 'primary') {
316
+ const y = Math.max(PT.y || 2.3, (opts && opts.top) || 0);
317
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
318
+ const capH = (opts && opts.caption) ? 0.32 : 0;
319
+ return {
320
+ x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y - capH)),
321
+ halfW: PT.halfW, halfGap: PT.halfGap, capY: PT.capY, capH: PT.capH || 0.3,
322
+ radius: PT.radius, gridGap: PT.gridGap, gridCols: PT.gridCols,
323
+ };
324
+ }
325
+ }
326
+
327
+ if (type === 'halftable') {
328
+ if (slot === 'primary' || slot === 'left') {
329
+ const r = LC.pageTypes.research || {};
330
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
331
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
332
+ return { x: mx, y: top, w: cw * (r.halfTableW || 0.52), h: Math.max(1, bottom - top) };
333
+ }
334
+ if (slot === 'secondary' || slot === 'right') {
335
+ const r = LC.pageTypes.research || {};
336
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
337
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
338
+ return {
339
+ x: mx + cw * (r.halfChartX || 0.57), y: top,
340
+ w: cw * (r.halfChartW || 0.43), h: Math.max(1, bottom - top),
341
+ };
342
+ }
343
+ }
344
+
345
+ if (type === 'matrix') {
346
+ if (slot === 'primary') {
347
+ const r = LC.pageTypes.research || {};
348
+ const y = r.matrixY || 2.3;
349
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
350
+ return {
351
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
352
+ labelW: r.matrixLabelW, cellH: r.matrixCellH,
353
+ };
354
+ }
355
+ }
356
+
357
+ if (type === 'threecol') {
358
+ const r = LC.pageTypes.research || {};
359
+ const gap = r.col3Gap || 0.38;
360
+ const colW = (cw - 2 * gap) / 3;
361
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
362
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
363
+ if (slot === 'primary' || slot === 'left') {
364
+ return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
365
+ }
366
+ if (slot === 'middle') {
367
+ return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
368
+ }
369
+ if (slot === 'right' || slot === 'secondary') {
370
+ return { x: mx + 2 * (colW + gap), y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
371
+ }
372
+ }
373
+
374
+ // 信息图页型:内容区主图
375
+ if (INFO_TYPES[type] && (slot === 'primary')) {
376
+ const y = PT.y || L.contentTop;
377
+ const h = PT.h || 4.1;
378
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
379
+ return { x: mx, y, w: cw, h: Math.max(1.2, Math.min(h, bottom - y)) };
380
+ }
381
+
382
+ // 默认:内容区全宽
383
+ if (slot === 'primary' || slot === 'secondary' || slot === 'left' || slot === 'right') {
384
+ const band = contentBand(opts);
385
+ return { x: mx, y: band.top, w: cw, h: band.h };
386
+ }
387
+ return null;
388
+ }
389
+
390
+ /** 槽位是否在 layout_slots.json 中声明为 required */
391
+ function slotRequired(pageType, slotId) {
392
+ const def = (SLOTS.pageTypes || {})[pageType];
393
+ if (!def) return false;
394
+ const s = (def.slots || []).find(x => x.id === slotId);
395
+ return !!(s && s.required);
396
+ }
397
+
398
+ /** 列出页型已声明槽位 id */
399
+ function slotIds(pageType) {
400
+ const def = (SLOTS.pageTypes || {})[pageType];
401
+ return (def && def.slots) ? def.slots.map(s => s.id) : [];
402
+ }
403
+
404
+ module.exports = {
405
+ LC,
406
+ SLOTS,
407
+ pageBox,
408
+ contentBand,
409
+ regionOf,
410
+ slotRequired,
411
+ slotIds,
412
+ };
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · HTML 页高与容器裁切真值测量(可选 · 需 playwright)
4
+
5
+ 静态估算(validate_report 页高溢出)是交付前硬门禁的保守近似,但它看不到
6
+ 真实布局:「表格被半幅容器横向裁掉一列」这类缺陷在 strict 全绿时照样发生。
7
+ 本脚本用无头浏览器量真实布局高度与容器裁切,供深度交付 / 排版争议时对照。
8
+
9
+ 用法:
10
+ python scripts/measure_height.py report.html
11
+ python scripts/measure_height.py report.html --json
12
+ python scripts/measure_height.py report.html --width 1920 --height 1080
13
+
14
+ 缺 playwright / chromium 时打印指引并 exit 0(不阻断交付链)。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ try:
24
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
25
+ except Exception:
26
+ pass
27
+
28
+
29
+ def main() -> int:
30
+ ap = argparse.ArgumentParser(description='HTML 页高真值测量(Playwright)')
31
+ ap.add_argument('html', help='报告 HTML 路径')
32
+ ap.add_argument('--width', type=int, default=1920)
33
+ ap.add_argument('--height', type=int, default=1080)
34
+ ap.add_argument('--json', action='store_true')
35
+ ap.add_argument('--tolerance-px', type=int, default=8,
36
+ help='band 高度相对视口的容差(超过则记 overflow)')
37
+ args = ap.parse_args()
38
+
39
+ html = Path(args.html)
40
+ if not html.exists():
41
+ print(f'错误:HTML 不存在 {html}')
42
+ return 2
43
+
44
+ try:
45
+ from playwright.sync_api import sync_playwright
46
+ except ImportError:
47
+ # 托管 Python 可能未装 playwright;回落到本机已装 playwright 的解释器(不阻断)
48
+ import shutil
49
+ import subprocess
50
+ candidates = []
51
+ for name in ('python', 'python3', 'py'):
52
+ exe = shutil.which(name)
53
+ if exe:
54
+ candidates.append(exe)
55
+ # 常见用户安装路径不写死绝对盘符/用户名;仅用 PATH / py launcher
56
+ for extra in (
57
+ r'py',
58
+ ):
59
+ if Path(extra).exists() or shutil.which(extra):
60
+ candidates.append(extra)
61
+ alt = None
62
+ for exe in candidates:
63
+ try:
64
+ probe = subprocess.run(
65
+ [exe, '-c', 'import playwright; print(1)'],
66
+ capture_output=True, text=True, timeout=8)
67
+ if probe.returncode == 0 and '1' in (probe.stdout or ''):
68
+ alt = exe
69
+ break
70
+ except Exception:
71
+ continue
72
+ if not alt:
73
+ print('跳过:未安装 playwright(pip install playwright && playwright install chromium)')
74
+ print('静态估算仍由 validate_report.py --strict 把关。')
75
+ return 0
76
+ # 用有 playwright 的解释器重跑本脚本(避免在无 playwright 的解释器里空转)
77
+ import subprocess as sp
78
+ cmd = [alt, str(Path(__file__).resolve()), str(html),
79
+ '--width', str(args.width), '--height', str(args.height),
80
+ '--tolerance-px', str(args.tolerance_px)]
81
+ if args.json:
82
+ cmd.append('--json')
83
+ raise SystemExit(sp.call(cmd))
84
+
85
+ url = html.resolve().as_uri()
86
+ results = []
87
+ with sync_playwright() as p:
88
+ try:
89
+ browser = p.chromium.launch(headless=True)
90
+ except Exception as e:
91
+ print(f'跳过:无法启动 chromium({e})。请执行 playwright install chromium。')
92
+ return 0
93
+ page = browser.new_page(viewport={'width': args.width, 'height': args.height})
94
+ page.goto(url, wait_until='load', timeout=60000)
95
+ # 关掉工具栏对页高的影响:尽量贴近演示全屏内容区
96
+ page.evaluate("""() => {
97
+ const bar = document.querySelector('.bar');
98
+ if (bar) bar.style.display = 'none';
99
+ document.documentElement.style.setProperty('--bar-h', '0px');
100
+ }""")
101
+ page.wait_for_timeout(200)
102
+ bands = page.evaluate("""(tol) => {
103
+ const vh = window.innerHeight;
104
+ const nodes = [...document.querySelectorAll('section.band')];
105
+ return nodes.map((el, i) => {
106
+ const r = el.getBoundingClientRect();
107
+ const h = Math.round(el.offsetHeight);
108
+ const flow = el.classList.contains('band--flow');
109
+ const id = el.id || ('band-' + (i + 1));
110
+ return {
111
+ index: i + 1,
112
+ id,
113
+ height: h,
114
+ viewport: vh,
115
+ overflow: !flow && h > vh + tol,
116
+ flow,
117
+ title: (el.querySelector('.shead__title, h1, h2')?.textContent || '').trim().slice(0, 40),
118
+ };
119
+ });
120
+ }""", args.tolerance_px)
121
+ # 容器级裁切真值:静态估算只看页高,看不到「表格/卡片被归属容器横向或纵向裁掉」——
122
+ # 裁切比压缩更糟(数据直接不可见),且 strict 全绿时依然可能发生。
123
+ clipped = page.evaluate("""() => {
124
+ const sel = '.card,.panel,.metric,.sowhat,.exhibit,.kpi,.note,.tbl-wrap,td,th';
125
+ const out = [];
126
+ document.querySelectorAll(sel).forEach(el => {
127
+ if (getComputedStyle(el).overflow === 'visible') return;
128
+ const dw = el.scrollWidth - el.clientWidth;
129
+ const dh = el.scrollHeight - el.clientHeight;
130
+ if (dw > 3 || dh > 3) {
131
+ const band = el.closest('section.band');
132
+ out.push({
133
+ container: (el.className || el.tagName).toString().slice(0, 30),
134
+ page: band ? ([...document.querySelectorAll('section.band')].indexOf(band) + 1) : 0,
135
+ overflowX: dw > 3 ? dw : 0,
136
+ overflowY: dh > 3 ? dh : 0,
137
+ text: (el.textContent || '').trim().slice(0, 24),
138
+ });
139
+ }
140
+ });
141
+ return out;
142
+ }""")
143
+ browser.close()
144
+
145
+ over = [b for b in bands if b['overflow']]
146
+ payload = {
147
+ 'file': str(html),
148
+ 'viewport': f'{args.width}x{args.height}',
149
+ 'bands': len(bands),
150
+ 'overflow_count': len(over),
151
+ 'overflow': over,
152
+ 'clipped_count': len(clipped),
153
+ 'clipped': clipped,
154
+ 'details': bands,
155
+ }
156
+ if args.json:
157
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
158
+ else:
159
+ print(f'页高真值测量 · {html.name} @ {args.width}×{args.height}')
160
+ print(f' band 数 {len(bands)} · 超一屏 {len(over)} · 容器裁切 {len(clipped)}')
161
+ for b in bands:
162
+ mark = '✗' if b['overflow'] else ('·' if b['flow'] else '✓')
163
+ print(f' {mark} 第{b["index"]}页 {b["id"]:16} {b["height"]:4}px {b["title"]}')
164
+ if clipped:
165
+ print(' ── 容器级裁切(内容越过归属容器,铁律 11)──')
166
+ for c in clipped[:8]:
167
+ axis = f'横向 {c["overflowX"]}px' if c['overflowX'] else f'纵向 {c["overflowY"]}px'
168
+ print(f' ✗ 第{c["page"]}页 {c["container"]} {axis} 「{c["text"]}」')
169
+ print(' 处理:换组合版式/减列/缩表 → 调容器宽度 → 拆页;不要靠容器滚动条遮掩')
170
+ if over:
171
+ print(' 处理:列表化/精炼 → 压缩间距 → 多列 → 拆页;长结构页加 band--flow')
172
+ if not over and not clipped:
173
+ print(' 结论:全部固定页高 band 未超视口,且无容器级裁切')
174
+ return 0 if not over and not clipped else 1
175
+
176
+
177
+ if __name__ == '__main__':
178
+ sys.exit(main())