bigpowers 2.4.0 → 2.5.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.
- package/.pi/package.json +2 -2
- package/.pi/prompts/align-grid.md +102 -0
- package/.pi/prompts/compose-workflow.md +1 -1
- package/.pi/prompts/diagnose-root.md +1 -1
- package/.pi/prompts/evolve-skill.md +1 -1
- package/.pi/prompts/grill-with-docs.md +1 -1
- package/.pi/prompts/research-first.md +1 -1
- package/.pi/prompts/reset-baseline.md +1 -1
- package/.pi/prompts/run-evals.md +1 -1
- package/.pi/prompts/scope-work.md +1 -1
- package/.pi/prompts/search-skills.md +1 -1
- package/.pi/prompts/setup-environment.md +1 -1
- package/.pi/prompts/simulate-agents.md +1 -1
- package/.pi/prompts/slice-tasks.md +1 -1
- package/.pi/prompts/stocktake-skills.md +1 -1
- package/.pi/prompts/verify-work.md +1 -1
- package/.pi/skills/align-grid/SKILL.md +104 -0
- package/.pi/skills/assess-impact/SKILL.md +2 -1
- package/.pi/skills/audit-code/SKILL.md +1 -0
- package/.pi/skills/build-epic/SKILL.md +1 -0
- package/.pi/skills/change-request/SKILL.md +1 -0
- package/.pi/skills/commit-message/SKILL.md +1 -0
- package/.pi/skills/compose-workflow/SKILL.md +2 -1
- package/.pi/skills/craft-skill/SKILL.md +1 -0
- package/.pi/skills/deepen-architecture/SKILL.md +1 -0
- package/.pi/skills/define-language/SKILL.md +2 -1
- package/.pi/skills/define-success/SKILL.md +2 -1
- package/.pi/skills/delegate-task/SKILL.md +1 -0
- package/.pi/skills/design-interface/SKILL.md +2 -1
- package/.pi/skills/develop-tdd/SKILL.md +1 -0
- package/.pi/skills/diagnose-root/SKILL.md +2 -1
- package/.pi/skills/dispatch-agents/SKILL.md +1 -0
- package/.pi/skills/edit-document/SKILL.md +1 -0
- package/.pi/skills/elaborate-spec/SKILL.md +2 -1
- package/.pi/skills/enforce-first/SKILL.md +2 -1
- package/.pi/skills/evolve-skill/SKILL.md +2 -1
- package/.pi/skills/execute-plan/SKILL.md +1 -0
- package/.pi/skills/fix-bug/SKILL.md +1 -0
- package/.pi/skills/grill-me/SKILL.md +2 -1
- package/.pi/skills/grill-with-docs/SKILL.md +2 -1
- package/.pi/skills/guard-git/SKILL.md +1 -0
- package/.pi/skills/hook-commits/SKILL.md +1 -0
- package/.pi/skills/inspect-quality/SKILL.md +2 -1
- package/.pi/skills/investigate-bug/SKILL.md +2 -1
- package/.pi/skills/kickoff-branch/SKILL.md +2 -1
- package/.pi/skills/map-codebase/SKILL.md +2 -1
- package/.pi/skills/migrate-spec/SKILL.md +1 -0
- package/.pi/skills/model-domain/SKILL.md +1 -0
- package/.pi/skills/orchestrate-project/SKILL.md +1 -0
- package/.pi/skills/organize-workspace/SKILL.md +2 -1
- package/.pi/skills/plan-refactor/SKILL.md +1 -0
- package/.pi/skills/plan-release/SKILL.md +2 -1
- package/.pi/skills/plan-work/SKILL.md +2 -1
- package/.pi/skills/release-branch/SKILL.md +2 -1
- package/.pi/skills/request-review/SKILL.md +1 -0
- package/.pi/skills/research-first/SKILL.md +2 -1
- package/.pi/skills/reset-baseline/SKILL.md +2 -1
- package/.pi/skills/respond-review/SKILL.md +1 -0
- package/.pi/skills/run-evals/SKILL.md +2 -1
- package/.pi/skills/run-planning/SKILL.md +2 -1
- package/.pi/skills/scope-work/SKILL.md +2 -1
- package/.pi/skills/search-skills/SKILL.md +2 -1
- package/.pi/skills/seed-conventions/SKILL.md +1 -0
- package/.pi/skills/session-state/SKILL.md +1 -0
- package/.pi/skills/setup-environment/SKILL.md +2 -1
- package/.pi/skills/simulate-agents/SKILL.md +2 -1
- package/.pi/skills/slice-tasks/SKILL.md +2 -1
- package/.pi/skills/spike-prototype/SKILL.md +2 -1
- package/.pi/skills/stocktake-skills/SKILL.md +2 -1
- package/.pi/skills/survey-context/SKILL.md +1 -0
- package/.pi/skills/terse-mode/SKILL.md +2 -1
- package/.pi/skills/trace-requirement/SKILL.md +2 -1
- package/.pi/skills/using-bigpowers/SKILL.md +2 -1
- package/.pi/skills/validate-fix/SKILL.md +2 -1
- package/.pi/skills/verify-work/SKILL.md +2 -1
- package/.pi/skills/visual-dashboard/SKILL.md +1 -0
- package/.pi/skills/wire-observability/SKILL.md +1 -0
- package/.pi/skills/write-document/SKILL.md +1 -0
- package/CHANGELOG.md +14 -0
- package/SKILL-INDEX.md +34 -33
- package/align-grid/SKILL.md +108 -0
- package/align-grid/scripts/grid_tokens.py +201 -0
- package/align-grid/scripts/verify_grid.js +140 -0
- package/dashboard/src/web/client.html +191 -249
- package/package.json +1 -1
- package/scripts/generate-reference-tables.sh +1 -1
- package/scripts/sync-skills.sh +22 -10
- package/scripts/validate-skill-yaml.py +73 -0
- package/visual-dashboard/scripts/cockpit.html +123 -16
- package/visual-dashboard/scripts/frame-template.html +181 -45
- package/countable-story-format.md +0 -293
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
grid_tokens.py — Müller-Brockmann editorial grid scaffold generator.
|
|
4
|
+
|
|
5
|
+
Emits a battle-tested, self-contained CSS + JS scaffold for building an
|
|
6
|
+
editorial/magazine webpage on a REAL, VISIBLE, VERIFIED modular grid:
|
|
7
|
+
|
|
8
|
+
• ONE source of truth: all grid params live in :root CSS variables.
|
|
9
|
+
• The grid-toggle OVERLAY reads the SAME variables and lives in the SAME
|
|
10
|
+
content box as the content, so its columns ARE the content columns
|
|
11
|
+
(this is the fix for the "grid is just slapped on top / misaligned" bug
|
|
12
|
+
that happens when the overlay is a full-width sibling of a centered
|
|
13
|
+
max-width container).
|
|
14
|
+
• Subgrid "bands" so every element is placed by column LINE, not eyeballed.
|
|
15
|
+
• Vertical rhythm locked to an 8px baseline (24px leading).
|
|
16
|
+
• Runtime OPTICAL ALIGNMENT: display type is nudged so its INK (not its box)
|
|
17
|
+
lands on the column line — large letterforms carry a left side-bearing, so
|
|
18
|
+
a headline whose box is on the grid still looks misaligned vs body text.
|
|
19
|
+
|
|
20
|
+
No network, no credentials. Deterministic.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 grid_tokens.py # print CSS + JS block
|
|
24
|
+
python3 grid_tokens.py --scaffold # print a full minimal HTML page
|
|
25
|
+
python3 grid_tokens.py --cols 12 --baseline 8 --gutter 24 --margin 72 \
|
|
26
|
+
--maxw 1296 --accent "#e4002b"
|
|
27
|
+
"""
|
|
28
|
+
import argparse, sys
|
|
29
|
+
|
|
30
|
+
def build(cfg):
|
|
31
|
+
c = cfg
|
|
32
|
+
lh = c.baseline * 3 # leading = 3 baselines
|
|
33
|
+
css = f""":root{{
|
|
34
|
+
--cols:{c.cols};
|
|
35
|
+
--bl:{c.baseline}px; /* baseline unit */
|
|
36
|
+
--lh:{lh}px; /* leading = 3 x baseline */
|
|
37
|
+
--gutter:{c.gutter}px;
|
|
38
|
+
--margin:{c.margin}px;
|
|
39
|
+
--pad:{c.baseline*12}px; /* spread top/bottom pad (x baseline) */
|
|
40
|
+
--maxw:{c.maxw}px;
|
|
41
|
+
|
|
42
|
+
--paper:#ffffff;
|
|
43
|
+
--ink:#111315;
|
|
44
|
+
--ink-soft:#5b6066;
|
|
45
|
+
--accent:{c.accent};
|
|
46
|
+
|
|
47
|
+
--g-col:rgba(228,0,43,.075); /* column field fill (re-tint to taste) */
|
|
48
|
+
--g-edge:rgba(228,0,43,.40); /* column edge / margin line */
|
|
49
|
+
--g-base:rgba(0,150,140,.34); /* major baseline line ({lh}px) */
|
|
50
|
+
--g-base-min:rgba(0,150,140,.12);/* minor baseline line ({c.baseline}px) */
|
|
51
|
+
}}
|
|
52
|
+
*{{box-sizing:border-box;}}
|
|
53
|
+
body{{margin:0;background:var(--paper);color:var(--ink);
|
|
54
|
+
font-family:"Inter",system-ui,sans-serif;font-size:16px;line-height:var(--lh);
|
|
55
|
+
-webkit-font-smoothing:antialiased;}}
|
|
56
|
+
img{{display:block;width:100%;height:100%;object-fit:cover;}}
|
|
57
|
+
|
|
58
|
+
/* ---- spread + grid scaffold (ONE source of truth) ---- */
|
|
59
|
+
.spread{{position:relative;width:100%;}}
|
|
60
|
+
.wrap{{position:relative;max-width:var(--maxw);margin:0 auto;padding:var(--pad) var(--margin);}}
|
|
61
|
+
.grid{{display:grid;grid-template-columns:repeat(var(--cols),1fr);
|
|
62
|
+
column-gap:var(--gutter);row-gap:var(--lh);}}
|
|
63
|
+
/* a band spans all columns and re-exposes them as a subgrid so children
|
|
64
|
+
align to the SAME lines as everything else on the page */
|
|
65
|
+
.band{{grid-column:1 / -1;display:grid;grid-template-columns:subgrid;
|
|
66
|
+
column-gap:var(--gutter);row-gap:var(--lh);align-items:start;}}
|
|
67
|
+
@supports not (grid-template-columns:subgrid){{
|
|
68
|
+
.band{{grid-template-columns:repeat(var(--cols),1fr);}}
|
|
69
|
+
}}
|
|
70
|
+
/* place children with: style="grid-column: <startline> / <endline>" */
|
|
71
|
+
|
|
72
|
+
/* ---- the grid OVERLAY (same content box -> columns match exactly) ---- */
|
|
73
|
+
.guides{{position:absolute;inset:0;pointer-events:none;z-index:60;opacity:0;
|
|
74
|
+
transition:opacity .26s ease;}}
|
|
75
|
+
body.grid-on .guides{{opacity:1;}}
|
|
76
|
+
.guides .cols{{position:absolute;top:0;bottom:0;left:var(--margin);right:var(--margin);
|
|
77
|
+
display:grid;grid-template-columns:repeat(var(--cols),1fr);column-gap:var(--gutter);}}
|
|
78
|
+
.guides .col{{background:var(--g-col);
|
|
79
|
+
box-shadow:inset 1px 0 0 var(--g-edge),inset -1px 0 0 var(--g-edge);position:relative;}}
|
|
80
|
+
.guides .col span{{position:absolute;top:{c.baseline*4}px;left:0;right:0;text-align:center;
|
|
81
|
+
font-family:"Space Mono",monospace;font-size:10px;line-height:1;color:var(--accent);}}
|
|
82
|
+
.guides .rows{{position:absolute;left:var(--margin);right:var(--margin);top:var(--pad);bottom:0;
|
|
83
|
+
background-image:
|
|
84
|
+
repeating-linear-gradient(to bottom,var(--g-base) 0 1px,transparent 1px var(--lh)),
|
|
85
|
+
repeating-linear-gradient(to bottom,var(--g-base-min) 0 1px,transparent 1px var(--bl));}}
|
|
86
|
+
.guides .mline{{position:absolute;top:0;bottom:0;width:1px;background:var(--g-edge);}}
|
|
87
|
+
.guides .mline.l{{left:var(--margin);}} .guides .mline.r{{right:var(--margin);}}
|
|
88
|
+
|
|
89
|
+
/* ---- vertical rhythm helpers (keep ALL spacing a multiple of --bl) ----
|
|
90
|
+
line-heights for display type MUST be px multiples of --bl, never unitless,
|
|
91
|
+
or the box height drifts off the baseline. Media heights = multiples of --lh
|
|
92
|
+
so photo top AND bottom land on lines. */
|
|
93
|
+
.toggle{{position:fixed;top:18px;right:18px;z-index:200;display:flex;align-items:center;gap:10px;
|
|
94
|
+
background:var(--ink);color:#fff;border:none;cursor:pointer;font-family:"Space Mono",monospace;
|
|
95
|
+
font-size:12px;letter-spacing:.14em;text-transform:uppercase;padding:11px 14px;}}
|
|
96
|
+
.toggle .dot{{width:9px;height:9px;border-radius:50%;background:#555;}}
|
|
97
|
+
body.grid-on .toggle{{background:var(--accent);}} body.grid-on .toggle .dot{{background:#fff;}}"""
|
|
98
|
+
|
|
99
|
+
js = """/* toggle: button + 'G' key */
|
|
100
|
+
var btn=document.getElementById('gridToggle');
|
|
101
|
+
function setGrid(on){document.body.classList.toggle('grid-on',on);
|
|
102
|
+
if(btn){btn.setAttribute('aria-pressed',on?'true':'false');
|
|
103
|
+
var l=btn.querySelector('.lbl'); if(l) l.textContent=on?'Hide grid':'Show grid';}}
|
|
104
|
+
if(btn) btn.addEventListener('click',function(){setGrid(!document.body.classList.contains('grid-on'));});
|
|
105
|
+
document.addEventListener('keydown',function(e){
|
|
106
|
+
if((e.key==='g'||e.key==='G')&&!e.metaKey&&!e.ctrlKey&&!e.altKey){
|
|
107
|
+
setGrid(!document.body.classList.contains('grid-on'));}});
|
|
108
|
+
|
|
109
|
+
/* populate every overlay's column guides (numbered) */
|
|
110
|
+
document.querySelectorAll('.guides .cols').forEach(function(h){
|
|
111
|
+
var n=getComputedStyle(document.documentElement).getPropertyValue('--cols').trim()||'12';
|
|
112
|
+
for(var i=1;i<=parseInt(n,10);i++){var c=document.createElement('div');c.className='col';
|
|
113
|
+
var s=document.createElement('span');s.textContent=i;c.appendChild(s);h.appendChild(c);}});
|
|
114
|
+
|
|
115
|
+
/* ---- OPTICAL ALIGNMENT --------------------------------------------------
|
|
116
|
+
Large display glyphs carry a left side-bearing: the ink sits inside the
|
|
117
|
+
layout box, so a headline whose BOX is on the column line still LOOKS
|
|
118
|
+
indented (or overhangs) vs body text. Measure each display glyph's actual
|
|
119
|
+
ink offset and nudge the element so its visible ink lands on the line.
|
|
120
|
+
Scales with fluid type; re-runs after the webfont loads and on resize.
|
|
121
|
+
Add the selector list to match your display elements. */
|
|
122
|
+
(function(){
|
|
123
|
+
var cvs=document.createElement('canvas'),ctx=cvs.getContext('2d');
|
|
124
|
+
var sel='.masthead, .numeral, .shead h2, .h2b'; /* <-- your display selectors */
|
|
125
|
+
function align(){
|
|
126
|
+
document.querySelectorAll(sel).forEach(function(el){
|
|
127
|
+
el.style.marginLeft='0px';
|
|
128
|
+
var cs=getComputedStyle(el),ch=(el.textContent||'').trim().charAt(0); if(!ch) return;
|
|
129
|
+
if(cs.textTransform==='uppercase') ch=ch.toUpperCase();
|
|
130
|
+
ctx.font=cs.fontStyle+' '+cs.fontWeight+' '+cs.fontSize+' '+cs.fontFamily;
|
|
131
|
+
ctx.textAlign='left';
|
|
132
|
+
var abl=ctx.measureText(ch).actualBoundingBoxLeft; /* +ve = ink overhangs left */
|
|
133
|
+
if(isFinite(abl)) el.style.marginLeft=abl.toFixed(2)+'px'; /* ink -> on the line */
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if(document.fonts&&document.fonts.ready){document.fonts.ready.then(align);}
|
|
137
|
+
align();
|
|
138
|
+
var t;window.addEventListener('resize',function(){clearTimeout(t);t=setTimeout(align,120);});
|
|
139
|
+
})();"""
|
|
140
|
+
|
|
141
|
+
band = """ <!-- a band: children placed by column LINE -->
|
|
142
|
+
<div class="band">
|
|
143
|
+
<div style="grid-column:1 / 6;"><!-- text col --></div>
|
|
144
|
+
<figure style="grid-column:6 / 13;"><!-- image col (height = x --lh) --></figure>
|
|
145
|
+
</div>"""
|
|
146
|
+
|
|
147
|
+
overlay = """ <div class="guides" aria-hidden="true">
|
|
148
|
+
<div class="cols"></div><div class="rows"></div>
|
|
149
|
+
<div class="mline l"></div><div class="mline r"></div>
|
|
150
|
+
</div>"""
|
|
151
|
+
|
|
152
|
+
if cfg.scaffold:
|
|
153
|
+
return f"""<!DOCTYPE html>
|
|
154
|
+
<html lang="en"><head><meta charset="UTF-8">
|
|
155
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
156
|
+
<title>Editorial — modular grid</title>
|
|
157
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
158
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
159
|
+
<style>
|
|
160
|
+
{css}
|
|
161
|
+
</style></head>
|
|
162
|
+
<body>
|
|
163
|
+
<button class="toggle" id="gridToggle" aria-pressed="false"><span class="dot"></span><span class="lbl">Show grid</span></button>
|
|
164
|
+
|
|
165
|
+
<section class="spread">
|
|
166
|
+
<div class="wrap">
|
|
167
|
+
<div class="grid">
|
|
168
|
+
{band}
|
|
169
|
+
</div>
|
|
170
|
+
{overlay}
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
|
|
174
|
+
<script>
|
|
175
|
+
{js}
|
|
176
|
+
</script>
|
|
177
|
+
</body></html>"""
|
|
178
|
+
else:
|
|
179
|
+
return ("/* ===== CSS (paste in <style>) ===== */\n" + css +
|
|
180
|
+
"\n\n/* ===== JS (paste in <script>, after the DOM) ===== */\n" + js +
|
|
181
|
+
"\n\n/* ===== band markup pattern ===== */\n" + band +
|
|
182
|
+
"\n\n/* ===== per-spread overlay markup ===== */\n" + overlay + "\n")
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
ap = argparse.ArgumentParser(description="Müller-Brockmann editorial grid scaffold generator")
|
|
186
|
+
ap.add_argument("--cols", type=int, default=12)
|
|
187
|
+
ap.add_argument("--baseline", type=int, default=8, help="baseline unit in px (leading = 3x)")
|
|
188
|
+
ap.add_argument("--gutter", type=int, default=24)
|
|
189
|
+
ap.add_argument("--margin", type=int, default=72)
|
|
190
|
+
ap.add_argument("--maxw", type=int, default=1296)
|
|
191
|
+
ap.add_argument("--accent", default="#e4002b")
|
|
192
|
+
ap.add_argument("--scaffold", action="store_true", help="emit a full minimal HTML page")
|
|
193
|
+
cfg = ap.parse_args()
|
|
194
|
+
for name, v in (("gutter", cfg.gutter), ("margin", cfg.margin)):
|
|
195
|
+
if v % cfg.baseline != 0:
|
|
196
|
+
print(f"# WARNING: --{name} ({v}) is not a multiple of --baseline ({cfg.baseline}); "
|
|
197
|
+
f"vertical/spacing rhythm will drift off the grid.", file=sys.stderr)
|
|
198
|
+
sys.stdout.write(build(cfg) + "\n")
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* verify_grid.js — prove an editorial page actually sits on its grid.
|
|
4
|
+
*
|
|
5
|
+
* Renders the page with headless Chrome (Puppeteer) and asserts, at several
|
|
6
|
+
* viewport widths (including > and < the content max-width, to catch the
|
|
7
|
+
* centered-container drift):
|
|
8
|
+
*
|
|
9
|
+
* 1. COLUMN ADHERENCE — every placed `.band > *` element's left edge snaps
|
|
10
|
+
* to a column START line and its right edge to a column END line (~0px).
|
|
11
|
+
* NB: build BOTH the start-set and end-set of x-coords. A grid item that
|
|
12
|
+
* spans "to line N" ends at the FAR side of the gutter, so naive single
|
|
13
|
+
* edge math falsely reports a one-gutter (gutter-px) error.
|
|
14
|
+
* 2. OVERLAY MATCH — each `.guides .col` rect equals the computed column
|
|
15
|
+
* rect (~0px), i.e. the overlay really is the content grid.
|
|
16
|
+
* 3. BASELINE — text element tops, modulo the baseline unit, ~0.
|
|
17
|
+
* 4. OPTICAL INK — display elements' visible INK-left equals the column
|
|
18
|
+
* line (measure canvas actualBoundingBoxLeft with the LOADED font).
|
|
19
|
+
* CAVEAT: side-bearing is font-specific. In a sandbox the webfont is often
|
|
20
|
+
* absent and canvas falls back to a different grotesque (we measured -16px
|
|
21
|
+
* fallback vs -7px real Inter). To verify optics offline, EMBED the real
|
|
22
|
+
* webfont via @font-face(local TTF). In production the page's runtime JS
|
|
23
|
+
* measures the actually-loaded font, so it is correct for the user.
|
|
24
|
+
*
|
|
25
|
+
* Env / args:
|
|
26
|
+
* CHROME = path to chrome binary (required)
|
|
27
|
+
* PUP = path to puppeteer-core module (required)
|
|
28
|
+
* arg1 = file:// URL or http URL of the page (default: file://$PWD/index.html)
|
|
29
|
+
* --widths=1440,1180,900 --baseline=8
|
|
30
|
+
*
|
|
31
|
+
* Sandbox chrome flags that work here:
|
|
32
|
+
* --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader
|
|
33
|
+
* (file:// works for non-ES-module pages; CLI --screenshot can hang on tall
|
|
34
|
+
* pages, so we drive via Puppeteer and screenshot per-viewport.)
|
|
35
|
+
*/
|
|
36
|
+
const puppeteer = require(process.env.PUP || 'puppeteer-core');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const url = (args.find(a => !a.startsWith('--'))) ||
|
|
41
|
+
('file://' + path.join(process.cwd(), 'index.html'));
|
|
42
|
+
const opt = k => { const a = args.find(x => x.startsWith('--' + k + '=')); return a ? a.split('=')[1] : null; };
|
|
43
|
+
const widths = (opt('widths') || '1440,1180,900').split(',').map(Number);
|
|
44
|
+
const BL = Number(opt('baseline') || 8);
|
|
45
|
+
|
|
46
|
+
(async () => {
|
|
47
|
+
const browser = await puppeteer.launch({
|
|
48
|
+
executablePath: process.env.CHROME, headless: 'new',
|
|
49
|
+
args: ['--no-sandbox','--disable-gpu','--disable-dbus','--use-gl=angle','--use-angle=swiftshader','--hide-scrollbars']
|
|
50
|
+
});
|
|
51
|
+
const page = await browser.newPage();
|
|
52
|
+
let failed = false;
|
|
53
|
+
|
|
54
|
+
for (const W of widths) {
|
|
55
|
+
await page.setViewport({ width: W, height: 1000, deviceScaleFactor: 1 });
|
|
56
|
+
await page.goto(url, { waitUntil: 'load', timeout: 25000 });
|
|
57
|
+
try { await page.evaluate(() => document.fonts && document.fonts.ready); } catch (e) {}
|
|
58
|
+
await new Promise(r => setTimeout(r, 500));
|
|
59
|
+
|
|
60
|
+
const res = await page.evaluate((BL) => {
|
|
61
|
+
const OPT = '.opt-align'; // display elements: optically aligned by INK, not box
|
|
62
|
+
const grid = document.querySelector('.grid');
|
|
63
|
+
const cs = getComputedStyle(grid);
|
|
64
|
+
const tracks = cs.gridTemplateColumns.split(' ').map(parseFloat);
|
|
65
|
+
const gap = parseFloat(cs.columnGap);
|
|
66
|
+
const gr = grid.getBoundingClientRect();
|
|
67
|
+
// build column START (L) and END (R) coordinate sets
|
|
68
|
+
const L = [], R = []; let x = gr.left;
|
|
69
|
+
for (let i = 0; i < tracks.length; i++) { L.push(x); x += tracks[i]; R.push(x); if (i < tracks.length - 1) x += gap; }
|
|
70
|
+
const nr = (v, arr) => arr.reduce((m, e) => Math.min(m, Math.abs(e - v)), 1e9);
|
|
71
|
+
const nearest = (v, arr) => arr.reduce((b, e) => Math.abs(e - v) < Math.abs(b - v) ? e : b, arr[0]);
|
|
72
|
+
|
|
73
|
+
// 1. column adherence — exclude optical display elements (their box is
|
|
74
|
+
// deliberately offset by the glyph side-bearing so the INK lands on
|
|
75
|
+
// the line; they are validated by check 4 instead).
|
|
76
|
+
let colErr = 0, worst = null;
|
|
77
|
+
document.querySelectorAll('.band > *').forEach(el => {
|
|
78
|
+
if (el.matches(OPT)) return;
|
|
79
|
+
const r = el.getBoundingClientRect(); if (r.width < 2) return;
|
|
80
|
+
const e = Math.max(nr(r.left, L), nr(r.right, R));
|
|
81
|
+
if (e > colErr) { colErr = e; worst = (el.className || el.tagName).toString().slice(0, 28); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 2. overlay match
|
|
85
|
+
let ovErr = 0;
|
|
86
|
+
document.querySelectorAll('.guides .cols .col').forEach((c, i) => {
|
|
87
|
+
const r = c.getBoundingClientRect();
|
|
88
|
+
if (L[i] != null) ovErr = Math.max(ovErr, Math.abs(r.left - L[i]));
|
|
89
|
+
if (R[i] != null) ovErr = Math.max(ovErr, Math.abs(r.right - R[i]));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 3. baseline (tops modulo BL, per spread relative to its rows-top)
|
|
93
|
+
let baseErr = 0;
|
|
94
|
+
document.querySelectorAll('.spread').forEach(sp => {
|
|
95
|
+
const rowsEl = sp.querySelector('.guides .rows'); if (!rowsEl) return;
|
|
96
|
+
const top = rowsEl.getBoundingClientRect().top;
|
|
97
|
+
sp.querySelectorAll('.body,.lede,.cap,.toc li,.dishes li,.kicker').forEach(el => {
|
|
98
|
+
const t = el.getBoundingClientRect().top - top; const m = ((t % BL) + BL) % BL;
|
|
99
|
+
baseErr = Math.max(baseErr, Math.min(m, BL - m));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 4. optical ink offset — each display element's visible INK-left must
|
|
104
|
+
// sit on ITS OWN column line (the nearest column-start to its box),
|
|
105
|
+
// not always line 1 (headlines can start on any column).
|
|
106
|
+
const cvs = document.createElement('canvas'), ctx = cvs.getContext('2d');
|
|
107
|
+
let inkErr = 0, inkWorst = null;
|
|
108
|
+
document.querySelectorAll(OPT).forEach(el => {
|
|
109
|
+
const c = getComputedStyle(el); let ch = (el.textContent || '').trim().charAt(0); if (!ch) return;
|
|
110
|
+
if (c.textTransform === 'uppercase') ch = ch.toUpperCase();
|
|
111
|
+
ctx.font = c.fontStyle + ' ' + c.fontWeight + ' ' + c.fontSize + ' ' + c.fontFamily; ctx.textAlign = 'left';
|
|
112
|
+
const abl = ctx.measureText(ch).actualBoundingBoxLeft;
|
|
113
|
+
const box = el.getBoundingClientRect().left;
|
|
114
|
+
const target = nearest(box, L); // the column line this element sits on
|
|
115
|
+
const ink = box - abl; // visible ink-left
|
|
116
|
+
const e = Math.abs(ink - target);
|
|
117
|
+
if (e > inkErr) { inkErr = e; inkWorst = (el.className || '').toString().slice(0, 20) + ' "' + ch + '"'; }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
track: +tracks[0].toFixed(1),
|
|
122
|
+
maxColErrPx: +colErr.toFixed(2), worstCol: worst,
|
|
123
|
+
overlayErrPx: +ovErr.toFixed(2),
|
|
124
|
+
maxBaselineOffPx: +baseErr.toFixed(2),
|
|
125
|
+
maxInkOffPx: +inkErr.toFixed(2), worstInk: inkWorst,
|
|
126
|
+
fontFamily: getComputedStyle(document.querySelector('.masthead') || document.body).fontFamily.split(',')[0]
|
|
127
|
+
};
|
|
128
|
+
}, BL);
|
|
129
|
+
|
|
130
|
+
// baseline tolerance = half a baseline unit (element border-box top vs line is a proxy; leading does the real work)
|
|
131
|
+
const pass = res.maxColErrPx <= 0.5 && res.overlayErrPx <= 0.5 && res.maxBaselineOffPx <= (BL / 2) && res.maxInkOffPx <= 1.0;
|
|
132
|
+
if (!pass) failed = true;
|
|
133
|
+
console.log(`[${pass ? 'PASS' : 'FAIL'}] vw=${W} col=${res.maxColErrPx}px overlay=${res.overlayErrPx}px ` +
|
|
134
|
+
`baseline=${res.maxBaselineOffPx}px ink=${res.maxInkOffPx}px ` +
|
|
135
|
+
`(worstCol=${res.worstCol}, worstInk=${res.worstInk}, font=${res.fontFamily})`);
|
|
136
|
+
}
|
|
137
|
+
await browser.close();
|
|
138
|
+
if (failed) { console.error('GRID VERIFY: FAIL'); process.exit(1); }
|
|
139
|
+
console.log('GRID VERIFY: PASS');
|
|
140
|
+
})().catch(e => { console.error('ERR', e.message); process.exit(2); });
|