@typeroll/mcp-server 0.24.0 → 0.25.1
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/AGENTS.md +60 -13
- package/dist/bundled-content.js +3 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -1
- package/dist/tools/pages.js +9 -2
- package/dist/version.js +11 -0
- package/package.json +1 -1
- package/skills/tr-design-review.md +298 -0
- package/skills/tr-redesign-branch.md +109 -38
package/dist/server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { siteTools } from './tools/sites.js';
|
|
|
27
27
|
import { domainTools } from './tools/domain.js';
|
|
28
28
|
import { skillTools } from './tools/skills.js';
|
|
29
29
|
import { fail } from './tools/helpers.js';
|
|
30
|
+
import { VERSION } from './version.js';
|
|
30
31
|
const PERM_RANK = { read: 0, write: 1, admin: 2 };
|
|
31
32
|
/**
|
|
32
33
|
* Classify a tool by name into the minimum permission needed. We keep this
|
|
@@ -48,7 +49,7 @@ function effectFor(name) {
|
|
|
48
49
|
}
|
|
49
50
|
return 'write';
|
|
50
51
|
}
|
|
51
|
-
const DEFAULT_INFO = { name: 'typeroll', version:
|
|
52
|
+
const DEFAULT_INFO = { name: 'typeroll', version: VERSION };
|
|
52
53
|
/**
|
|
53
54
|
* Server-level instructions — returned in the MCP `initialize` response and
|
|
54
55
|
* surfaced to the model by every client (Claude Code stdio AND the hosted
|
package/dist/tools/pages.js
CHANGED
|
@@ -236,13 +236,20 @@ export const pageTools = [
|
|
|
236
236
|
// the block-tree mutation tools (add/update/move/remove/convert).
|
|
237
237
|
{
|
|
238
238
|
name: 'get_page_preview',
|
|
239
|
-
description:
|
|
239
|
+
description: "Get the WHOLE page rendered as one self-contained HTML document — header partial + block-rendered body + footer partial, with the site's settings CSS variables, global styles, and the tree-shaken block-CSS bundle all inlined, exactly as deployed. This is the single artifact for UNDERSTANDING what a page looks like and how its CSS actually cascades (get_page_blocks gives the editable block tree; this gives the rendered result). Returns { rendered_html, internal_links[] }. Pass annotate:true to tag every element with data-block-id + data-block-type so you can map the rendered HTML back to the block to edit. For a clickable preview URL use get_preview_link instead.",
|
|
240
240
|
inputSchema: {
|
|
241
241
|
page_id: z.string(),
|
|
242
|
+
annotate: z
|
|
243
|
+
.boolean()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe('Tag every block root with data-block-id (the authored block id) + data-block-type so you can map a rendered element back to the exact block to mutate. Off by default.'),
|
|
242
246
|
version: versionParam,
|
|
243
247
|
},
|
|
244
248
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
245
|
-
const
|
|
249
|
+
const query = { ...(v(args.version) ?? {}) };
|
|
250
|
+
if (args.annotate)
|
|
251
|
+
query.annotate = 'true';
|
|
252
|
+
const res = await client.get(siteId, `pages/${encodeURIComponent(args.page_id)}/preview`, query);
|
|
246
253
|
return ok(res);
|
|
247
254
|
}),
|
|
248
255
|
},
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Server version reported in the MCP `initialize` handshake.
|
|
2
|
+
//
|
|
3
|
+
// MUST be a plain literal — NOT a runtime `require('../package.json')`. This
|
|
4
|
+
// module is bundled into the portal's /api/mcp route (hosted MCP), where
|
|
5
|
+
// `import.meta.url` points at the bundled file and `../package.json` does not
|
|
6
|
+
// resolve — a require there throws at module load and 500s the whole route.
|
|
7
|
+
// The literal works in every context (standalone npm package + bundled portal).
|
|
8
|
+
//
|
|
9
|
+
// Keep it in lockstep with package.json: tests/version.test.ts asserts
|
|
10
|
+
// VERSION === package.json.version, so a bump that forgets this line fails CI.
|
|
11
|
+
export const VERSION = '0.25.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Model Context Protocol server for the Typeroll public API. Use with Claude Code or any MCP-compatible client to manage a Typeroll site.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tr-design-review
|
|
3
|
+
description: Use to review a deployed/previewed Typeroll page like a designer — a MEASURED multi-dimension pass (responsive, a11y, functional, content, SEO, performance) that emits a per-dimension scorecard and an explicit OK verdict. Run it before telling the user a design is approved; it's the "how" for tr-redesign-branch's approval round.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review a design — measured, not glanced
|
|
7
|
+
|
|
8
|
+
A design review is a MEASUREMENT, not a look. The failure mode is reporting
|
|
9
|
+
"looks good" off a couple of screenshots — which silently misses overflow at
|
|
10
|
+
untested widths, sub-AA contrast, broken/blank images, and small touch targets.
|
|
11
|
+
This skill is the deterministic routine: per dimension, a check you RUN (a
|
|
12
|
+
browser-eval snippet or a curl), and a scorecard you fill with PASS / FAIL /
|
|
13
|
+
UNTESTED. Never report "approved" off a partial pass — list what you didn't test
|
|
14
|
+
as caveats.
|
|
15
|
+
|
|
16
|
+
`tr-redesign-branch` step 6 lists the dimensions (the "what"). This is the "how".
|
|
17
|
+
|
|
18
|
+
## Cardinal rule: a screenshot is evidence, not proof
|
|
19
|
+
|
|
20
|
+
**Full-page screenshots lie about lazy-loaded images.** A page with
|
|
21
|
+
`loading="lazy"` images below the fold will screenshot with BLANK boxes where
|
|
22
|
+
those images sit — they hadn't entered the viewport when the capture fired. If
|
|
23
|
+
you trust that, you will report a non-existent "empty illustration box" gap.
|
|
24
|
+
(This has happened — on a real review, across three variants at once.)
|
|
25
|
+
|
|
26
|
+
So, always:
|
|
27
|
+
|
|
28
|
+
- **Before any full-page capture**, scroll the whole page to trigger lazy loads
|
|
29
|
+
and let it settle (snippet in §5), THEN screenshot.
|
|
30
|
+
- **Verify every suspected blank/broken image via the DOM** (`naturalWidth` after
|
|
31
|
+
scroll), never from the screenshot. A real broken image has `complete === true
|
|
32
|
+
&& naturalWidth === 0`; a lazy one that just hasn't loaded has `complete ===
|
|
33
|
+
false` — scroll it into view and re-check before calling it broken.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
1. Get a stable URL for the version under review: `trigger_deploy version="<branch>"`
|
|
38
|
+
→ poll `get_deploy_status` → use the immutable `deploy_url` (a Cloudflare Pages
|
|
39
|
+
hash URL — it never changes, so the URL you OK is bit-for-bit what ships if
|
|
40
|
+
merged). Or `get_preview_link` for an in-portal render. Review the SAME url end
|
|
41
|
+
to end.
|
|
42
|
+
2. The snippets below run in a browser tool's "evaluate JavaScript" (Playwright /
|
|
43
|
+
chrome-devtools / puppeteer MCP). **One origin per eval:** the iframe trick
|
|
44
|
+
needs same-origin, so run each variant's snippet on its own page (different
|
|
45
|
+
`*.pages.dev` hashes are cross-origin → `contentDocument` is null).
|
|
46
|
+
3. If you run several variants with one shared browser profile, do them
|
|
47
|
+
SEQUENTIALLY — parallel browser agents on one profile contaminate each other's
|
|
48
|
+
tabs/screenshots.
|
|
49
|
+
|
|
50
|
+
## The dimensions — run each, record the result
|
|
51
|
+
|
|
52
|
+
### 1. Responsive — width ladder 390/768/1024/1440/1920, zero overflow
|
|
53
|
+
|
|
54
|
+
Measure horizontal overflow at every width in ONE eval using same-origin iframes
|
|
55
|
+
(each iframe is its own layout viewport, so `@media` fires correctly — no 15
|
|
56
|
+
resizes):
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
async () => {
|
|
60
|
+
const url = location.href, out = [];
|
|
61
|
+
for (const w of [390,768,1024,1440,1920]) {
|
|
62
|
+
const f = document.createElement('iframe');
|
|
63
|
+
f.style.cssText = `width:${w}px;height:2400px;border:0;position:fixed;left:-99999px;top:0`;
|
|
64
|
+
document.body.appendChild(f);
|
|
65
|
+
await new Promise(r => { f.onload = r; f.src = url; });
|
|
66
|
+
await new Promise(r => setTimeout(r, 700));
|
|
67
|
+
const d = f.contentDocument, culprits = [];
|
|
68
|
+
for (const el of d.body.querySelectorAll('*')) {
|
|
69
|
+
const r = el.getBoundingClientRect();
|
|
70
|
+
if (r.right > w + 1 && r.width <= w + 40 && r.width > 4)
|
|
71
|
+
culprits.push(el.tagName.toLowerCase() + '.' + (el.className||'').toString().slice(0,40));
|
|
72
|
+
}
|
|
73
|
+
out.push({ w, hOverflow: d.documentElement.scrollWidth - w, n: culprits.length, sample: [...new Set(culprits)].slice(0,6) });
|
|
74
|
+
f.remove();
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
PASS = `hOverflow <= 0` at every width. A decorative element flagged while
|
|
81
|
+
`hOverflow` is 0 is clipped by an `overflow:hidden` parent (no scrollbar) — a
|
|
82
|
+
non-issue. Then eyeball one tablet (768) capture for stacking — but capture
|
|
83
|
+
AFTER the scroll-settle in §5.
|
|
84
|
+
|
|
85
|
+
### 2. Accessibility — compute contrast, don't eyeball
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
() => {
|
|
89
|
+
const L = c => { const a = c.map(v => (v/=255, v<=.03928?v/12.92:((v+.055)/1.055)**2.4)); return .2126*a[0]+.7152*a[1]+.0722*a[2]; };
|
|
90
|
+
const P = c => { const m = c.match(/rgba?\(([^)]+)\)/); if(!m) return null; const p = m[1].split(',').map(parseFloat); return {rgb:[p[0],p[1],p[2]], a:p[3]??1}; };
|
|
91
|
+
const R = (f,b) => { const x=L(f),y=L(b),h=Math.max(x,y),l=Math.min(x,y); return (h+.05)/(l+.05); };
|
|
92
|
+
const bg = el => { let e=el; while(e){ const s=getComputedStyle(e); if(s.backgroundImage!=='none') return {img:1}; const c=P(s.backgroundColor); if(c&&c.a>.5) return {rgb:c.rgb}; e=e.parentElement; } return {rgb:[255,255,255]}; };
|
|
93
|
+
const bad=[], seen=new Set();
|
|
94
|
+
for (const el of document.body.querySelectorAll('*')) {
|
|
95
|
+
const t=[...el.childNodes].filter(n=>n.nodeType===3&&n.textContent.trim()).map(n=>n.textContent.trim()).join(' ');
|
|
96
|
+
if(!t) continue;
|
|
97
|
+
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) continue;
|
|
98
|
+
const s=getComputedStyle(el); if(s.visibility==='hidden'||s.display==='none'||+s.opacity<.1) continue;
|
|
99
|
+
const fg=P(s.color); if(!fg) continue;
|
|
100
|
+
const b=bg(el); if(b.img) continue; // can't compute over an image — eyeball hero text separately
|
|
101
|
+
const cr=R(fg.rgb,b.rgb), fs=parseFloat(s.fontSize), fw=+s.fontWeight||400;
|
|
102
|
+
const need = (fs>=24||(fs>=18.66&&fw>=700)) ? 3 : 4.5;
|
|
103
|
+
if (cr<need) { const k=t.slice(0,30)+cr.toFixed(2); if(seen.has(k))continue; seen.add(k);
|
|
104
|
+
bad.push({txt:t.slice(0,45), ratio:+cr.toFixed(2), need, fs:Math.round(fs), fw, color:s.color, bg:'rgb('+b.rgb.join(',')+')'}); }
|
|
105
|
+
}
|
|
106
|
+
return { failures: bad.length, items: bad.slice(0,15) };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
PASS = 0 failures (AA: body ≥4.5:1, large/UI ≥3:1). Fix a failure by deepening
|
|
111
|
+
the offending colour token. Text over an image background is skipped — eyeball
|
|
112
|
+
those (hero overlays) for legibility separately.
|
|
113
|
+
|
|
114
|
+
Structure + alt + landmarks, same eval session:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
() => {
|
|
118
|
+
const h=[...document.querySelectorAll('h1,h2,h3,h4')].map(e=>+e.tagName[1]);
|
|
119
|
+
const skips=h.map((v,i)=>i&&v-h[i-1]>1?`${h[i-1]}->${v}`:0).filter(Boolean);
|
|
120
|
+
const imgs=[...document.querySelectorAll('img')];
|
|
121
|
+
const inputs=[...document.querySelectorAll('input:not([type=hidden]),textarea,select')];
|
|
122
|
+
const labelFor=new Set([...document.querySelectorAll('label[for]')].map(l=>l.getAttribute('for')));
|
|
123
|
+
return {
|
|
124
|
+
h1: h.filter(x=>x===1).length, levelSkips: skips,
|
|
125
|
+
imgsMissingAlt: imgs.filter(i=>i.getAttribute('alt')===null).length,
|
|
126
|
+
landmarks: ['header','nav','main','footer'].filter(t=>document.querySelector(t)),
|
|
127
|
+
unlabeledInputs: inputs.filter(i=>!(i.id&&labelFor.has(i.id))&&!i.getAttribute('aria-label')).map(i=>i.name||i.id),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
PASS = exactly one `h1`, `levelSkips` empty, `imgsMissingAlt` 0, all four
|
|
133
|
+
landmarks present, `unlabeledInputs` empty. (Decorative images SHOULD have
|
|
134
|
+
`alt=""` — that's not "missing".)
|
|
135
|
+
|
|
136
|
+
Touch targets — interactive elements ≥44px at mobile. Run in a 390px iframe;
|
|
137
|
+
EXCLUDE `aria-hidden` (the form honeypot is a visible-sized but hidden input —
|
|
138
|
+
counting it is a false positive) and inline text links inside `p`/`li`:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
async () => {
|
|
142
|
+
const f=document.createElement('iframe');
|
|
143
|
+
f.style.cssText='width:390px;height:2400px;border:0;position:fixed;left:-99999px;top:0';
|
|
144
|
+
document.body.appendChild(f);
|
|
145
|
+
await new Promise(r=>{ f.onload=r; setTimeout(r,3000); f.src=location.href; });
|
|
146
|
+
await new Promise(r=>setTimeout(r,700));
|
|
147
|
+
const d=f.contentDocument, small=[];
|
|
148
|
+
if(d) for (const el of d.querySelectorAll('a,button,input:not([type=hidden]),textarea,select,[role=button]')) {
|
|
149
|
+
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) continue;
|
|
150
|
+
const s=getComputedStyle(el); if(s.display==='none'||s.visibility==='hidden'||+s.opacity<.1) continue;
|
|
151
|
+
if(el.getAttribute('aria-hidden')==='true') continue;
|
|
152
|
+
if(el.tagName==='A'&&el.closest('p,li')) continue;
|
|
153
|
+
if(r.height<44||r.width<44) small.push({tag:el.tagName.toLowerCase(), txt:(el.innerText||el.value||el.getAttribute('aria-label')||'').trim().slice(0,24), w:Math.round(r.width), h:Math.round(r.height)});
|
|
154
|
+
}
|
|
155
|
+
f.remove(); return { undersized: small };
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Also confirm `:focus-visible` and `prefers-reduced-motion` exist (grep the page
|
|
160
|
+
HTML: `grep -c 'focus-visible' page.html`, `grep -c 'prefers-reduced-motion'`).
|
|
161
|
+
Note honestly: presence in CSS ≠ verified per-element — tab through live if you
|
|
162
|
+
claim keyboard focus works.
|
|
163
|
+
|
|
164
|
+
### 3. Functional — console, links, form
|
|
165
|
+
|
|
166
|
+
- **Console:** read the browser tool's console messages after load. PASS = 0
|
|
167
|
+
errors/warnings.
|
|
168
|
+
- **Links:** PASS = no `href="#"`/empty; every in-page `#anchor` has a matching
|
|
169
|
+
`id`.
|
|
170
|
+
- **Form (markup — does NOT prove a live submit):** curl the page and verify the
|
|
171
|
+
`<form>` `action` is the real submit endpoint, the hidden `_token` is
|
|
172
|
+
non-empty, the honeypot is present + `aria-hidden`, required fields have
|
|
173
|
+
`required`, the email field is `type="email"`. State explicitly that you did
|
|
174
|
+
NOT submit (a live POST creates a real submission) unless you actually did.
|
|
175
|
+
|
|
176
|
+
### 4. Content — verbatim, no placeholders
|
|
177
|
+
|
|
178
|
+
`grep -Ei 'lorem|ipsum|\{\{|placeholder|TODO|FIXME' page.html` → 0. Copy matches
|
|
179
|
+
the live page (the source of truth) verbatim.
|
|
180
|
+
|
|
181
|
+
### 5. Broken / blank images (the anti-lazy-load check — run THIS before trusting any screenshot)
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
async () => {
|
|
185
|
+
const H=document.body.scrollHeight;
|
|
186
|
+
for(let y=0;y<=H;y+=400){ window.scrollTo(0,y); await new Promise(r=>setTimeout(r,120)); }
|
|
187
|
+
window.scrollTo(0,0); await new Promise(r=>setTimeout(r,1500));
|
|
188
|
+
const imgs=[...document.querySelectorAll('img')];
|
|
189
|
+
const empty=[]; // genuinely empty boxes: large, no text/img/svg/bg-image
|
|
190
|
+
for (const el of document.querySelectorAll('div,section,figure')) {
|
|
191
|
+
const r=el.getBoundingClientRect(); if(r.width<160||r.height<140) continue;
|
|
192
|
+
if((el.innerText||'').trim()||el.querySelector('img,svg,picture,canvas,video')) continue;
|
|
193
|
+
if(getComputedStyle(el).backgroundImage!=='none') continue;
|
|
194
|
+
empty.push({cls:(el.className||'').toString().slice(0,36), w:Math.round(r.width), h:Math.round(r.height)});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
broken: imgs.filter(i=>i.complete&&i.naturalWidth===0).map(i=>i.src.slice(-45)), // real failures
|
|
198
|
+
stillLoading: imgs.filter(i=>!i.complete).map(i=>i.src.slice(-45)), // lazy, scroll first
|
|
199
|
+
emptyBoxes: empty.slice(0,8), // true placeholders
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
PASS = `broken` empty, `emptyBoxes` empty. A non-empty `emptyBoxes` is a genuine
|
|
205
|
+
unfilled illustration slot (fill it — pages shouldn't be text deserts). NOW
|
|
206
|
+
capture screenshots (the page is scrolled-and-settled, images loaded).
|
|
207
|
+
|
|
208
|
+
### 5b. Clipped artwork — the logo (and any brand image) cut off by its own frame
|
|
209
|
+
|
|
210
|
+
The single most-repeated visual bug: the header logo rendered with its top/edges
|
|
211
|
+
sliced. It produces ZERO page overflow (§1 misses it), the image isn't broken
|
|
212
|
+
(§5 misses it), and at full-page screenshot scale a few clipped pixels are easy
|
|
213
|
+
to glance past. So MEASURE it: does the artwork's rendered content touch the edge
|
|
214
|
+
of its own box on any side? Content flush against the frame (gap ≈ 0) = clipped
|
|
215
|
+
or about-to-clip. Don't just check the logo — check it, then trust the number.
|
|
216
|
+
|
|
217
|
+
For a raster/`<img>` logo, draw it to a same-origin canvas and scan the border
|
|
218
|
+
rows/cols for opaque pixels (cross-origin taints the canvas — fetch the asset to
|
|
219
|
+
a localhost file first, as in §setup, or measure on the asset directly):
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
async (url) => { // url = the logo's currentSrc, served same-origin
|
|
223
|
+
const img = new Image(); await new Promise((r,e)=>{img.onload=r;img.onerror=e;img.src=url;});
|
|
224
|
+
const h = 64, w = Math.round(h*img.naturalWidth/img.naturalHeight);
|
|
225
|
+
const c = document.createElement('canvas'); c.width=w; c.height=h;
|
|
226
|
+
const x = c.getContext('2d'); x.drawImage(img,0,0,w,h);
|
|
227
|
+
const d = x.getImageData(0,0,w,h).data, op=(px)=>d[px*4+3]>20;
|
|
228
|
+
let top=h,bot=0,left=w,right=0;
|
|
229
|
+
for(let y=0;y<h;y++)for(let xx=0;xx<w;xx++)if(op(y*w+xx)){top=Math.min(top,y);bot=Math.max(bot,y);left=Math.min(left,xx);right=Math.max(right,xx);}
|
|
230
|
+
return { topGap:top, bottomGap:h-1-bot, leftGap:left, rightGap:right }; // any 0 → flush/clipped
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
PASS = every gap ≥ ~2% of the dimension. A `0` on any side means the artwork (or
|
|
235
|
+
its stroke) sits on the frame — for an SVG that's a viewBox trimmed flush to the
|
|
236
|
+
art (look for `-trim`/`-tight` in the filename); the fix is to re-export the SVG
|
|
237
|
+
with viewBox padding (e.g. widen `viewBox` by ~8% each side) so the stroke never
|
|
238
|
+
touches the edge. Verify the fix by re-running this with the patched asset. (A
|
|
239
|
+
heavy `stroke-width` + `paint-order="stroke"` outline makes a flush viewBox clip
|
|
240
|
+
visibly — and small header renders make it worse, so also check the logo isn't
|
|
241
|
+
shrunk below ~64–72px in the header.)
|
|
242
|
+
|
|
243
|
+
Also confirm no ANCESTOR clips the logo: walk the logo's parents for
|
|
244
|
+
`overflow:hidden|clip` combined with a fixed height or negative/overlap margin —
|
|
245
|
+
and always judge the logo from a screenshot of the header REGION in context,
|
|
246
|
+
never the logo element in isolation (an element screenshot re-renders the full
|
|
247
|
+
art and hides the clip).
|
|
248
|
+
|
|
249
|
+
**The inverse bug — an image FLOATING inside its frame (don't blame the file).**
|
|
250
|
+
A full-bleed illustration that renders with a margin of empty frame around it
|
|
251
|
+
usually isn't a bad asset — it's CSS. In blocks-mode the site-template's global
|
|
252
|
+
`:where(.page-content) img{ margin:1rem 0 }` (and a default `border-radius`)
|
|
253
|
+
leaks onto any `<img>` you didn't reset, so a framed hero/figure gets a 1rem gap
|
|
254
|
+
inside its frame and looks like it "floats". Before re-cropping or regenerating,
|
|
255
|
+
**open the actual image file** (`curl` the `.avif`/`.png`) — if the motif fills
|
|
256
|
+
the file edge-to-edge, the float is CSS: set `margin:0` (and `border-radius:0`)
|
|
257
|
+
on the framed `<img>` (e.g. `.your-frame img{margin:0}` or a blanket
|
|
258
|
+
`.your-scope img{margin:0}`). Measure it: the `<img>`'s `getBoundingClientRect`
|
|
259
|
+
should equal its frame's inner box (no gap). Only when the *file itself* has
|
|
260
|
+
built-in background margin (motif ≪ frame) is cropping the right fix.
|
|
261
|
+
|
|
262
|
+
### 6. Findable (SEO/meta) — curl, fast
|
|
263
|
+
|
|
264
|
+
`<title>` (≤60 chars) + meta description present + sensible; `og:title/description/image`;
|
|
265
|
+
`canonical`; `favicon` + `apple-touch-icon`; `<html lang>`; branches must be
|
|
266
|
+
`noindex`. One curl + greps covers it.
|
|
267
|
+
|
|
268
|
+
### 7. Fast (performance)
|
|
269
|
+
|
|
270
|
+
PASS signals: no render-blocking JS you didn't add; responsive variants
|
|
271
|
+
(`srcset` + AVIF/WebP) so a 1024px asset isn't shipped to a 380px slot;
|
|
272
|
+
`width`/`height` or aspect-ratio set (no layout shift); **below-fold images
|
|
273
|
+
`loading="lazy"`, above-fold `eager`**. Flag a section that eager-loads every
|
|
274
|
+
image, or a multi-hundred-KB original served when a small AVIF variant exists.
|
|
275
|
+
|
|
276
|
+
### 8. Cross-browser
|
|
277
|
+
|
|
278
|
+
The same CSS renders differently per engine. Re-check in another engine if you
|
|
279
|
+
can. If only Chromium is available, say so as UNTESTED and statically flag risky
|
|
280
|
+
props: `backdrop-filter` without fallback, `-webkit-`-only masks, `100vh` on
|
|
281
|
+
mobile (prefer `100svh`), `position:sticky` inside `overflow`.
|
|
282
|
+
|
|
283
|
+
## Deliver a scorecard + an explicit verdict
|
|
284
|
+
|
|
285
|
+
Report a table — one row per dimension, value PASS / FAIL(detail) / UNTESTED —
|
|
286
|
+
then a one-line verdict. Rules:
|
|
287
|
+
|
|
288
|
+
- "Approved" requires PASS on responsive, a11y, functional, content, SEO,
|
|
289
|
+
performance. Untested dimensions (commonly cross-browser, live form submit) are
|
|
290
|
+
listed as CAVEATS, not silently dropped — an OK with caveats is honest; an
|
|
291
|
+
unqualified "approved" off a partial pass is not.
|
|
292
|
+
- Brand FIT (palette/voice matching `brand.md`) is a direction judgment, not a
|
|
293
|
+
pass/fail defect — call it out separately so the user decides direction.
|
|
294
|
+
- If you fixed anything mid-review, re-deploy and re-run the affected dimension
|
|
295
|
+
before signing off.
|
|
296
|
+
|
|
297
|
+
See `tr-redesign-branch` for the surrounding branch → preview → approve → merge
|
|
298
|
+
flow; this skill is its measured approval round.
|
|
@@ -71,6 +71,34 @@ Save the response's `id` — pass it as `version=<id>` on every
|
|
|
71
71
|
subsequent call. Branches default `robots_blocked: true` so a
|
|
72
72
|
half-finished redesign won't be indexed.
|
|
73
73
|
|
|
74
|
+
### 2b. Write the design spec (REQUIRED — before you build)
|
|
75
|
+
|
|
76
|
+
Every redesign branch MUST carry a written design spec. Without it the
|
|
77
|
+
design choices live only in the agent's head, so a later "just tweak the
|
|
78
|
+
illustration style / palette" means re-deriving everything by hand (this
|
|
79
|
+
gap cost a real project a full reverse-engineering pass). Write it BEFORE
|
|
80
|
+
building so it guides the work, and keep it in sync as the design evolves.
|
|
81
|
+
|
|
82
|
+
Save it as a markdown doc with the project (e.g. `design-spec.md`, or
|
|
83
|
+
`prompts/design-system.md`) — or, if there's no local working dir, as an
|
|
84
|
+
unlisted page on the branch. It must capture:
|
|
85
|
+
|
|
86
|
+
- **Palette** — every role + hex (background, surface, primary, accent,
|
|
87
|
+
text, borders), and where the variant *diverges* from the brand and why.
|
|
88
|
+
- **Typography** — fonts + weights/sizes per role.
|
|
89
|
+
- **Illustration / imagery style** — the exact image-gen prompt prefix
|
|
90
|
+
(tone, palette, formspråk, framing rules), so the imagery can be
|
|
91
|
+
regenerated or restyled on its own without touching layout. State the
|
|
92
|
+
business/concept constraints the imagery must respect (a wrong-concept
|
|
93
|
+
image is worse than none).
|
|
94
|
+
- **Section structure** — the page's sections in order + each one's
|
|
95
|
+
treatment (band colour, layout).
|
|
96
|
+
- **Rationale** — one line per major choice: *why* this direction.
|
|
97
|
+
|
|
98
|
+
When the user later says "adjust just the illustrations" or "change the
|
|
99
|
+
palette", you edit the spec first, then apply — the spec is the source of
|
|
100
|
+
truth for the design intent.
|
|
101
|
+
|
|
74
102
|
### 3. Iterate on the branch
|
|
75
103
|
|
|
76
104
|
For each redesign step:
|
|
@@ -122,43 +150,84 @@ bulk_replace_text pattern="OldCo" replacement="NewCo" dry_run=false version="<br
|
|
|
122
150
|
|
|
123
151
|
### 6. Approval round
|
|
124
152
|
|
|
125
|
-
**
|
|
126
|
-
checks (copy present,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
**Self-review is a multi-DIMENSION pass, not a glance — and most of it you
|
|
154
|
+
MEASURE, not eyeball.** Structural checks (copy present, images return 200) are
|
|
155
|
+
NOT a design review; never report "approved" off them. Don't just list the bugs
|
|
156
|
+
you happened to notice — walk every dimension below on the deployed branch
|
|
157
|
+
(browser tool + DOM reads), fix what you find, re-deploy, re-check.
|
|
158
|
+
|
|
159
|
+
**Use `tr-design-review` (`read_skill tr-design-review`) for the HOW** — it has
|
|
160
|
+
the per-dimension measurement snippets (overflow ladder, computed contrast, touch
|
|
161
|
+
targets, the anti-lazy-load broken-image check) and the scorecard + verdict
|
|
162
|
+
format. The dimension summary below is the "what"; that skill is the runnable
|
|
163
|
+
routine. In particular: scroll-and-settle to trigger lazy images BEFORE any
|
|
164
|
+
full-page screenshot, or you'll report blank boxes that aren't real.
|
|
165
|
+
|
|
166
|
+
1. **Responsive** — screenshot across a width ladder (mobile / tablet / laptop /
|
|
167
|
+
desktop / wide ≈390 / 768 / 1024 / 1440 / 1920px) AND sweep the page's own
|
|
168
|
+
`@media` breakpoints (read the page-scoped `<style>`; resize a few px below +
|
|
169
|
+
above each). At EVERY width: `document.documentElement.scrollWidth <=
|
|
170
|
+
clientWidth` (no horizontal scroll), grids flip cleanly, nothing squished /
|
|
171
|
+
orphaned / overlapping, no mid-word breaks. Two sizes is not enough — bugs hide
|
|
172
|
+
in between. Also check a short/landscape viewport and 200% browser zoom.
|
|
173
|
+
2. **Visual & brand** — logo FULLY VISIBLE (not clipped by a header
|
|
174
|
+
`overflow:hidden` + overlap margin) and brand-compliant; screenshot the header
|
|
175
|
+
IN CONTEXT, never the logo element in isolation (that hides clipping).
|
|
176
|
+
Decoration robust at real size in the real browser: no hairline seam at a
|
|
177
|
+
divider (use `core/section` `divider_top`/`divider_bottom` — don't hand-roll a
|
|
178
|
+
band), no glow clipped to a hard edge, no `object-fit:cover` cropping faces, no
|
|
179
|
+
gradient fade-cutoff. Typography: body line-length ~45–75ch, consistent scale,
|
|
180
|
+
no awkward widows on headings. Palette adherence (no off-brand colours);
|
|
181
|
+
consistent spacing / alignment / radius / shadow.
|
|
182
|
+
3. **Accessibility — MEASURE, don't eyeball** — compute actual contrast ratios
|
|
183
|
+
(WCAG AA: body ≥4.5:1, large/UI ≥3:1) and fix failures by deepening the
|
|
184
|
+
offending colour token; meaningful `alt` on every image; exactly one `<h1>` +
|
|
185
|
+
no skipped heading levels; visible `:focus-visible` on every interactive
|
|
186
|
+
element; every input has an associated `<label>`; touch targets ≥44px on
|
|
187
|
+
mobile; semantic landmarks (header/nav/main/footer) + nav `aria-label`; honour
|
|
188
|
+
`prefers-reduced-motion`.
|
|
189
|
+
4. **Functional** — the form actually works (POST action correct, hidden token
|
|
190
|
+
non-empty, honeypot present + hidden; a long name/email doesn't break layout);
|
|
191
|
+
every link + in-page anchor resolves (each `#anchor` has a matching `id`; no
|
|
192
|
+
`href="#"`/`""`); ZERO console errors/warnings; interactions (menu toggle,
|
|
193
|
+
hover/focus/active) work.
|
|
194
|
+
5. **Content** — no unrendered `{{…}}` tokens in the DOM; no placeholder/lorem;
|
|
195
|
+
copy still matches the source of truth (the live page) verbatim.
|
|
196
|
+
6. **Findable (SEO/meta)** — `<title>` + meta description present + sensible;
|
|
197
|
+
`og:title`/`og:description`/`og:image`; canonical; favicon + apple-touch-icon;
|
|
198
|
+
`<html lang>`; `noindex` correct (branches must be noindex).
|
|
199
|
+
7. **Fast (performance)** — images at sane sizes (not a 2048px file shown at
|
|
200
|
+
380px without a responsive variant), modern format (avif/webp), `width`/`height`
|
|
201
|
+
or aspect-ratio set (no layout shift), below-fold lazy / above-fold eager.
|
|
202
|
+
8. **Cross-browser** — the same CSS renders differently per engine (the divider
|
|
203
|
+
seam was Chrome-only; WebKit/Firefox have their own). Re-check in another engine
|
|
204
|
+
if you can; if only Chromium is available, statically flag risky props
|
|
205
|
+
(`backdrop-filter` without fallback, `-webkit-`-only masks, `100vh` on mobile →
|
|
206
|
+
prefer `100svh`, `sticky` inside `overflow`).
|
|
149
207
|
|
|
150
208
|
Fix what you find and re-check before involving the user. "Looks structurally
|
|
151
|
-
fine" ≠ "looks good"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
209
|
+
fine" ≠ "looks good", and "looks good in Chrome at 1440" ≠ "works for everyone,
|
|
210
|
+
everywhere" — never report a design as approved/perfect off a glance or a partial
|
|
211
|
+
pass.
|
|
212
|
+
|
|
213
|
+
Then send the user a **stable preview URL** — and ALWAYS prefer a stable
|
|
214
|
+
one over a link that goes stale:
|
|
215
|
+
|
|
216
|
+
- **Stable (use this for sharing / repeat viewing):** the deployed branch's
|
|
217
|
+
alias URL, `https://<branch>.<project>.pages.dev`. It always serves that
|
|
218
|
+
branch's *latest* deploy, so the same link keeps working across every
|
|
219
|
+
re-deploy. Get it by deploying the branch once (`trigger_deploy
|
|
220
|
+
version="<branch>"`); the `<project>` segment is the part after the hash
|
|
221
|
+
in the returned `deploy_url` (e.g. `abc123.<project>.pages.dev`). Curl it
|
|
222
|
+
once to confirm it resolves. (Branch deploys are `robots_blocked`, so it
|
|
223
|
+
won't be indexed.)
|
|
224
|
+
- **Avoid as the shared link:** the immutable per-deploy hash URL (a new
|
|
225
|
+
hash every deploy — the user's bookmark dies) and a `get_preview_link`
|
|
226
|
+
token (expires). Those are fine for your own one-off in-editor checks,
|
|
227
|
+
not as the canonical link you hand the user.
|
|
228
|
+
|
|
229
|
+
Default rule: when the user will look more than once, give the stable alias.
|
|
230
|
+
Wait for an explicit "looks good, ship it."
|
|
162
231
|
|
|
163
232
|
### 7. Merge + deploy
|
|
164
233
|
|
|
@@ -185,9 +254,11 @@ disk cost is tiny.)
|
|
|
185
254
|
- **Skipping discovery.** "Modernize" without first reading the site
|
|
186
255
|
produces a confidently-out-of-place result. Always sample existing
|
|
187
256
|
pages.
|
|
188
|
-
- **
|
|
189
|
-
|
|
190
|
-
|
|
257
|
+
- **Handing out a link that goes stale.** Don't give the user the
|
|
258
|
+
immutable per-deploy hash URL or an expiring `get_preview_link` token as
|
|
259
|
+
the canonical link — share the stable branch alias
|
|
260
|
+
`https://<branch>.<project>.pages.dev` (see step 6). `get_preview_link`
|
|
261
|
+
is for your own one-off in-editor checks.
|
|
191
262
|
- **Auto-merge.** Don't `merge_branch` without explicit user sign-off.
|
|
192
263
|
Once merged, the only undo is another branch + reverse edits.
|
|
193
264
|
- **Header rewrites that drop the brand block.** Even when the
|