@typeroll/mcp-server 0.25.0 → 0.25.2
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 +57 -21
- package/dist/bundled-content.js +3 -2
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/preview.js +1 -1
- package/dist/version.js +11 -7
- package/package.json +1 -1
- package/skills/tr-design-review.md +301 -0
- package/skills/tr-redesign-branch.md +111 -38
package/dist/tools/deploy.js
CHANGED
|
@@ -6,7 +6,7 @@ function v(version) {
|
|
|
6
6
|
export const deployTools = [
|
|
7
7
|
{
|
|
8
8
|
name: 'trigger_deploy',
|
|
9
|
-
description: "Enqueue a deploy of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. With `dry_run: true`, the deploy runs through every step EXCEPT the hosting-adapter upload — useful for validating a structural change (new collection routes, schema bump, mode switch) without risking the live site. A dry_run job that succeeds proves the build is clean; one that fails returns the same astro stack-trace in get_deploy_status.error as a real deploy would.",
|
|
9
|
+
description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. With `dry_run: true`, the deploy runs through every step EXCEPT the hosting-adapter upload — useful for validating a structural change (new collection routes, schema bump, mode switch) without risking the live site. A dry_run job that succeeds proves the build is clean; one that fails returns the same astro stack-trace in get_deploy_status.error as a real deploy would.",
|
|
10
10
|
inputSchema: {
|
|
11
11
|
environment: z.enum(['production', 'staging']).optional(),
|
|
12
12
|
dry_run: z.boolean().optional().describe('Run the build but skip the CDN upload. The job ends in succeeded/failed the same way a real deploy does.'),
|
package/dist/tools/preview.js
CHANGED
|
@@ -6,7 +6,7 @@ function v(version) {
|
|
|
6
6
|
export const previewTools = [
|
|
7
7
|
{
|
|
8
8
|
name: 'get_preview_link',
|
|
9
|
-
description: 'Mint a
|
|
9
|
+
description: 'Mint a signed URL the user (or your own browser tool) can open to SEE the rendered preview. Renders LIVE from the database with NO build, so reloading shows every edit immediately — this is the PREFERRED way to preview design/content changes as you iterate (and to verify your own edits); reach for this, not trigger_deploy, when previewing. Mint it ONCE with the long TTL (ttl_seconds: 86400 = 24h max) and REUSE that single URL across edits — internal links keep the token so it navigates the whole branch, and it stays valid until the TTL lapses (re-mint only then, never per edit). Default TTL 15 min, max 24h. Target a page (page_id), a collection item (collection_name + item_id, resolves via route_template), or a raw slug; omit all to land on the home page. (For a permanently-bookmarkable link to the COMPILED static site, deploy and share the {branch}.{project}.pages.dev alias instead — that only refreshes on trigger_deploy.)',
|
|
10
10
|
inputSchema: {
|
|
11
11
|
page_id: z.string().optional(),
|
|
12
12
|
slug: z.string().optional(),
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.2';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
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,301 @@
|
|
|
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 URL for the version under review. While iterating, use the DB-live
|
|
38
|
+
`get_preview_link` (mint once at `ttl_seconds: 86400`, reuse) — it renders
|
|
39
|
+
from the DB with no build, so fixes show on reload without re-deploying, and
|
|
40
|
+
it's the loop for the review-fix-recheck cycle. For a FINAL bit-for-bit check
|
|
41
|
+
of the compiled output before merge, deploy once (`trigger_deploy
|
|
42
|
+
version="<branch>"` → poll `get_deploy_status` → use the immutable
|
|
43
|
+
`deploy_url`, a Cloudflare Pages hash URL). Review the SAME url end to end.
|
|
44
|
+
2. The snippets below run in a browser tool's "evaluate JavaScript" (Playwright /
|
|
45
|
+
chrome-devtools / puppeteer MCP). **One origin per eval:** the iframe trick
|
|
46
|
+
needs same-origin, so run each variant's snippet on its own page (different
|
|
47
|
+
`*.pages.dev` hashes are cross-origin → `contentDocument` is null).
|
|
48
|
+
3. If you run several variants with one shared browser profile, do them
|
|
49
|
+
SEQUENTIALLY — parallel browser agents on one profile contaminate each other's
|
|
50
|
+
tabs/screenshots.
|
|
51
|
+
|
|
52
|
+
## The dimensions — run each, record the result
|
|
53
|
+
|
|
54
|
+
### 1. Responsive — width ladder 390/768/1024/1440/1920, zero overflow
|
|
55
|
+
|
|
56
|
+
Measure horizontal overflow at every width in ONE eval using same-origin iframes
|
|
57
|
+
(each iframe is its own layout viewport, so `@media` fires correctly — no 15
|
|
58
|
+
resizes):
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
async () => {
|
|
62
|
+
const url = location.href, out = [];
|
|
63
|
+
for (const w of [390,768,1024,1440,1920]) {
|
|
64
|
+
const f = document.createElement('iframe');
|
|
65
|
+
f.style.cssText = `width:${w}px;height:2400px;border:0;position:fixed;left:-99999px;top:0`;
|
|
66
|
+
document.body.appendChild(f);
|
|
67
|
+
await new Promise(r => { f.onload = r; f.src = url; });
|
|
68
|
+
await new Promise(r => setTimeout(r, 700));
|
|
69
|
+
const d = f.contentDocument, culprits = [];
|
|
70
|
+
for (const el of d.body.querySelectorAll('*')) {
|
|
71
|
+
const r = el.getBoundingClientRect();
|
|
72
|
+
if (r.right > w + 1 && r.width <= w + 40 && r.width > 4)
|
|
73
|
+
culprits.push(el.tagName.toLowerCase() + '.' + (el.className||'').toString().slice(0,40));
|
|
74
|
+
}
|
|
75
|
+
out.push({ w, hOverflow: d.documentElement.scrollWidth - w, n: culprits.length, sample: [...new Set(culprits)].slice(0,6) });
|
|
76
|
+
f.remove();
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
PASS = `hOverflow <= 0` at every width. A decorative element flagged while
|
|
83
|
+
`hOverflow` is 0 is clipped by an `overflow:hidden` parent (no scrollbar) — a
|
|
84
|
+
non-issue. Then eyeball one tablet (768) capture for stacking — but capture
|
|
85
|
+
AFTER the scroll-settle in §5.
|
|
86
|
+
|
|
87
|
+
### 2. Accessibility — compute contrast, don't eyeball
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
() => {
|
|
91
|
+
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]; };
|
|
92
|
+
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}; };
|
|
93
|
+
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); };
|
|
94
|
+
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]}; };
|
|
95
|
+
const bad=[], seen=new Set();
|
|
96
|
+
for (const el of document.body.querySelectorAll('*')) {
|
|
97
|
+
const t=[...el.childNodes].filter(n=>n.nodeType===3&&n.textContent.trim()).map(n=>n.textContent.trim()).join(' ');
|
|
98
|
+
if(!t) continue;
|
|
99
|
+
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) continue;
|
|
100
|
+
const s=getComputedStyle(el); if(s.visibility==='hidden'||s.display==='none'||+s.opacity<.1) continue;
|
|
101
|
+
const fg=P(s.color); if(!fg) continue;
|
|
102
|
+
const b=bg(el); if(b.img) continue; // can't compute over an image — eyeball hero text separately
|
|
103
|
+
const cr=R(fg.rgb,b.rgb), fs=parseFloat(s.fontSize), fw=+s.fontWeight||400;
|
|
104
|
+
const need = (fs>=24||(fs>=18.66&&fw>=700)) ? 3 : 4.5;
|
|
105
|
+
if (cr<need) { const k=t.slice(0,30)+cr.toFixed(2); if(seen.has(k))continue; seen.add(k);
|
|
106
|
+
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(',')+')'}); }
|
|
107
|
+
}
|
|
108
|
+
return { failures: bad.length, items: bad.slice(0,15) };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
PASS = 0 failures (AA: body ≥4.5:1, large/UI ≥3:1). Fix a failure by deepening
|
|
113
|
+
the offending colour token. Text over an image background is skipped — eyeball
|
|
114
|
+
those (hero overlays) for legibility separately.
|
|
115
|
+
|
|
116
|
+
Structure + alt + landmarks, same eval session:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
() => {
|
|
120
|
+
const h=[...document.querySelectorAll('h1,h2,h3,h4')].map(e=>+e.tagName[1]);
|
|
121
|
+
const skips=h.map((v,i)=>i&&v-h[i-1]>1?`${h[i-1]}->${v}`:0).filter(Boolean);
|
|
122
|
+
const imgs=[...document.querySelectorAll('img')];
|
|
123
|
+
const inputs=[...document.querySelectorAll('input:not([type=hidden]),textarea,select')];
|
|
124
|
+
const labelFor=new Set([...document.querySelectorAll('label[for]')].map(l=>l.getAttribute('for')));
|
|
125
|
+
return {
|
|
126
|
+
h1: h.filter(x=>x===1).length, levelSkips: skips,
|
|
127
|
+
imgsMissingAlt: imgs.filter(i=>i.getAttribute('alt')===null).length,
|
|
128
|
+
landmarks: ['header','nav','main','footer'].filter(t=>document.querySelector(t)),
|
|
129
|
+
unlabeledInputs: inputs.filter(i=>!(i.id&&labelFor.has(i.id))&&!i.getAttribute('aria-label')).map(i=>i.name||i.id),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
PASS = exactly one `h1`, `levelSkips` empty, `imgsMissingAlt` 0, all four
|
|
135
|
+
landmarks present, `unlabeledInputs` empty. (Decorative images SHOULD have
|
|
136
|
+
`alt=""` — that's not "missing".)
|
|
137
|
+
|
|
138
|
+
Touch targets — interactive elements ≥44px at mobile. Run in a 390px iframe;
|
|
139
|
+
EXCLUDE `aria-hidden` (the form honeypot is a visible-sized but hidden input —
|
|
140
|
+
counting it is a false positive) and inline text links inside `p`/`li`:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
async () => {
|
|
144
|
+
const f=document.createElement('iframe');
|
|
145
|
+
f.style.cssText='width:390px;height:2400px;border:0;position:fixed;left:-99999px;top:0';
|
|
146
|
+
document.body.appendChild(f);
|
|
147
|
+
await new Promise(r=>{ f.onload=r; setTimeout(r,3000); f.src=location.href; });
|
|
148
|
+
await new Promise(r=>setTimeout(r,700));
|
|
149
|
+
const d=f.contentDocument, small=[];
|
|
150
|
+
if(d) for (const el of d.querySelectorAll('a,button,input:not([type=hidden]),textarea,select,[role=button]')) {
|
|
151
|
+
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) continue;
|
|
152
|
+
const s=getComputedStyle(el); if(s.display==='none'||s.visibility==='hidden'||+s.opacity<.1) continue;
|
|
153
|
+
if(el.getAttribute('aria-hidden')==='true') continue;
|
|
154
|
+
if(el.tagName==='A'&&el.closest('p,li')) continue;
|
|
155
|
+
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)});
|
|
156
|
+
}
|
|
157
|
+
f.remove(); return { undersized: small };
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Also confirm `:focus-visible` and `prefers-reduced-motion` exist (grep the page
|
|
162
|
+
HTML: `grep -c 'focus-visible' page.html`, `grep -c 'prefers-reduced-motion'`).
|
|
163
|
+
Note honestly: presence in CSS ≠ verified per-element — tab through live if you
|
|
164
|
+
claim keyboard focus works.
|
|
165
|
+
|
|
166
|
+
### 3. Functional — console, links, form
|
|
167
|
+
|
|
168
|
+
- **Console:** read the browser tool's console messages after load. PASS = 0
|
|
169
|
+
errors/warnings.
|
|
170
|
+
- **Links:** PASS = no `href="#"`/empty; every in-page `#anchor` has a matching
|
|
171
|
+
`id`.
|
|
172
|
+
- **Form (markup — does NOT prove a live submit):** curl the page and verify the
|
|
173
|
+
`<form>` `action` is the real submit endpoint, the hidden `_token` is
|
|
174
|
+
non-empty, the honeypot is present + `aria-hidden`, required fields have
|
|
175
|
+
`required`, the email field is `type="email"`. State explicitly that you did
|
|
176
|
+
NOT submit (a live POST creates a real submission) unless you actually did.
|
|
177
|
+
|
|
178
|
+
### 4. Content — verbatim, no placeholders
|
|
179
|
+
|
|
180
|
+
`grep -Ei 'lorem|ipsum|\{\{|placeholder|TODO|FIXME' page.html` → 0. Copy matches
|
|
181
|
+
the live page (the source of truth) verbatim.
|
|
182
|
+
|
|
183
|
+
### 5. Broken / blank images (the anti-lazy-load check — run THIS before trusting any screenshot)
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
async () => {
|
|
187
|
+
const H=document.body.scrollHeight;
|
|
188
|
+
for(let y=0;y<=H;y+=400){ window.scrollTo(0,y); await new Promise(r=>setTimeout(r,120)); }
|
|
189
|
+
window.scrollTo(0,0); await new Promise(r=>setTimeout(r,1500));
|
|
190
|
+
const imgs=[...document.querySelectorAll('img')];
|
|
191
|
+
const empty=[]; // genuinely empty boxes: large, no text/img/svg/bg-image
|
|
192
|
+
for (const el of document.querySelectorAll('div,section,figure')) {
|
|
193
|
+
const r=el.getBoundingClientRect(); if(r.width<160||r.height<140) continue;
|
|
194
|
+
if((el.innerText||'').trim()||el.querySelector('img,svg,picture,canvas,video')) continue;
|
|
195
|
+
if(getComputedStyle(el).backgroundImage!=='none') continue;
|
|
196
|
+
empty.push({cls:(el.className||'').toString().slice(0,36), w:Math.round(r.width), h:Math.round(r.height)});
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
broken: imgs.filter(i=>i.complete&&i.naturalWidth===0).map(i=>i.src.slice(-45)), // real failures
|
|
200
|
+
stillLoading: imgs.filter(i=>!i.complete).map(i=>i.src.slice(-45)), // lazy, scroll first
|
|
201
|
+
emptyBoxes: empty.slice(0,8), // true placeholders
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
PASS = `broken` empty, `emptyBoxes` empty. A non-empty `emptyBoxes` is a genuine
|
|
207
|
+
unfilled illustration slot (fill it — pages shouldn't be text deserts). NOW
|
|
208
|
+
capture screenshots (the page is scrolled-and-settled, images loaded).
|
|
209
|
+
|
|
210
|
+
### 5b. Clipped artwork — the logo (and any brand image) cut off by its own frame
|
|
211
|
+
|
|
212
|
+
The single most-repeated visual bug: the header logo rendered with its top/edges
|
|
213
|
+
sliced. It produces ZERO page overflow (§1 misses it), the image isn't broken
|
|
214
|
+
(§5 misses it), and at full-page screenshot scale a few clipped pixels are easy
|
|
215
|
+
to glance past. So MEASURE it: does the artwork's rendered content touch the edge
|
|
216
|
+
of its own box on any side? Content flush against the frame (gap ≈ 0) = clipped
|
|
217
|
+
or about-to-clip. Don't just check the logo — check it, then trust the number.
|
|
218
|
+
|
|
219
|
+
For a raster/`<img>` logo, draw it to a same-origin canvas and scan the border
|
|
220
|
+
rows/cols for opaque pixels (cross-origin taints the canvas — fetch the asset to
|
|
221
|
+
a localhost file first, as in §setup, or measure on the asset directly):
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
async (url) => { // url = the logo's currentSrc, served same-origin
|
|
225
|
+
const img = new Image(); await new Promise((r,e)=>{img.onload=r;img.onerror=e;img.src=url;});
|
|
226
|
+
const h = 64, w = Math.round(h*img.naturalWidth/img.naturalHeight);
|
|
227
|
+
const c = document.createElement('canvas'); c.width=w; c.height=h;
|
|
228
|
+
const x = c.getContext('2d'); x.drawImage(img,0,0,w,h);
|
|
229
|
+
const d = x.getImageData(0,0,w,h).data, op=(px)=>d[px*4+3]>20;
|
|
230
|
+
let top=h,bot=0,left=w,right=0;
|
|
231
|
+
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);}
|
|
232
|
+
return { topGap:top, bottomGap:h-1-bot, leftGap:left, rightGap:right }; // any 0 → flush/clipped
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
PASS = every gap ≥ ~2% of the dimension. A `0` on any side means the artwork (or
|
|
237
|
+
its stroke) sits on the frame — for an SVG that's a viewBox trimmed flush to the
|
|
238
|
+
art (look for `-trim`/`-tight` in the filename); the fix is to re-export the SVG
|
|
239
|
+
with viewBox padding (e.g. widen `viewBox` by ~8% each side) so the stroke never
|
|
240
|
+
touches the edge. Verify the fix by re-running this with the patched asset. (A
|
|
241
|
+
heavy `stroke-width` + `paint-order="stroke"` outline makes a flush viewBox clip
|
|
242
|
+
visibly — and small header renders make it worse, so also check the logo isn't
|
|
243
|
+
shrunk below ~64–72px in the header.)
|
|
244
|
+
|
|
245
|
+
Also confirm no ANCESTOR clips the logo: walk the logo's parents for
|
|
246
|
+
`overflow:hidden|clip` combined with a fixed height or negative/overlap margin —
|
|
247
|
+
and always judge the logo from a screenshot of the header REGION in context,
|
|
248
|
+
never the logo element in isolation (an element screenshot re-renders the full
|
|
249
|
+
art and hides the clip).
|
|
250
|
+
|
|
251
|
+
**The inverse bug — an image FLOATING inside its frame (don't blame the file).**
|
|
252
|
+
A full-bleed illustration that renders with a margin of empty frame around it
|
|
253
|
+
usually isn't a bad asset — it's CSS. In blocks-mode the site-template's global
|
|
254
|
+
`:where(.page-content) img{ margin:1rem 0 }` (and a default `border-radius`)
|
|
255
|
+
leaks onto any `<img>` you didn't reset, so a framed hero/figure gets a 1rem gap
|
|
256
|
+
inside its frame and looks like it "floats". Before re-cropping or regenerating,
|
|
257
|
+
**open the actual image file** (`curl` the `.avif`/`.png`) — if the motif fills
|
|
258
|
+
the file edge-to-edge, the float is CSS: set `margin:0` (and `border-radius:0`)
|
|
259
|
+
on the framed `<img>` (e.g. `.your-frame img{margin:0}` or a blanket
|
|
260
|
+
`.your-scope img{margin:0}`). Measure it: the `<img>`'s `getBoundingClientRect`
|
|
261
|
+
should equal its frame's inner box (no gap). Only when the *file itself* has
|
|
262
|
+
built-in background margin (motif ≪ frame) is cropping the right fix.
|
|
263
|
+
|
|
264
|
+
### 6. Findable (SEO/meta) — curl, fast
|
|
265
|
+
|
|
266
|
+
`<title>` (≤60 chars) + meta description present + sensible; `og:title/description/image`;
|
|
267
|
+
`canonical`; `favicon` + `apple-touch-icon`; `<html lang>`; branches must be
|
|
268
|
+
`noindex`. One curl + greps covers it.
|
|
269
|
+
|
|
270
|
+
### 7. Fast (performance)
|
|
271
|
+
|
|
272
|
+
PASS signals: no render-blocking JS you didn't add; responsive variants
|
|
273
|
+
(`srcset` + AVIF/WebP) so a 1024px asset isn't shipped to a 380px slot;
|
|
274
|
+
`width`/`height` or aspect-ratio set (no layout shift); **below-fold images
|
|
275
|
+
`loading="lazy"`, above-fold `eager`**. Flag a section that eager-loads every
|
|
276
|
+
image, or a multi-hundred-KB original served when a small AVIF variant exists.
|
|
277
|
+
|
|
278
|
+
### 8. Cross-browser
|
|
279
|
+
|
|
280
|
+
The same CSS renders differently per engine. Re-check in another engine if you
|
|
281
|
+
can. If only Chromium is available, say so as UNTESTED and statically flag risky
|
|
282
|
+
props: `backdrop-filter` without fallback, `-webkit-`-only masks, `100vh` on
|
|
283
|
+
mobile (prefer `100svh`), `position:sticky` inside `overflow`.
|
|
284
|
+
|
|
285
|
+
## Deliver a scorecard + an explicit verdict
|
|
286
|
+
|
|
287
|
+
Report a table — one row per dimension, value PASS / FAIL(detail) / UNTESTED —
|
|
288
|
+
then a one-line verdict. Rules:
|
|
289
|
+
|
|
290
|
+
- "Approved" requires PASS on responsive, a11y, functional, content, SEO,
|
|
291
|
+
performance. Untested dimensions (commonly cross-browser, live form submit) are
|
|
292
|
+
listed as CAVEATS, not silently dropped — an OK with caveats is honest; an
|
|
293
|
+
unqualified "approved" off a partial pass is not.
|
|
294
|
+
- Brand FIT (palette/voice matching `brand.md`) is a direction judgment, not a
|
|
295
|
+
pass/fail defect — call it out separately so the user decides direction.
|
|
296
|
+
- If you fixed anything mid-review, just reload the DB-live preview and re-run
|
|
297
|
+
the affected dimension before signing off — no re-deploy needed (deploy only
|
|
298
|
+
for the final compiled-output check, if any).
|
|
299
|
+
|
|
300
|
+
See `tr-redesign-branch` for the surrounding branch → preview → approve → merge
|
|
301
|
+
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,86 @@ 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 DB-live preview
|
|
157
|
+
(a reused `get_preview_link`; browser tool + DOM reads), fix what you find,
|
|
158
|
+
reload, re-check. No re-deploy between fixes — the preview renders from the DB.
|
|
159
|
+
|
|
160
|
+
**Use `tr-design-review` (`read_skill tr-design-review`) for the HOW** — it has
|
|
161
|
+
the per-dimension measurement snippets (overflow ladder, computed contrast, touch
|
|
162
|
+
targets, the anti-lazy-load broken-image check) and the scorecard + verdict
|
|
163
|
+
format. The dimension summary below is the "what"; that skill is the runnable
|
|
164
|
+
routine. In particular: scroll-and-settle to trigger lazy images BEFORE any
|
|
165
|
+
full-page screenshot, or you'll report blank boxes that aren't real.
|
|
166
|
+
|
|
167
|
+
1. **Responsive** — screenshot across a width ladder (mobile / tablet / laptop /
|
|
168
|
+
desktop / wide ≈390 / 768 / 1024 / 1440 / 1920px) AND sweep the page's own
|
|
169
|
+
`@media` breakpoints (read the page-scoped `<style>`; resize a few px below +
|
|
170
|
+
above each). At EVERY width: `document.documentElement.scrollWidth <=
|
|
171
|
+
clientWidth` (no horizontal scroll), grids flip cleanly, nothing squished /
|
|
172
|
+
orphaned / overlapping, no mid-word breaks. Two sizes is not enough — bugs hide
|
|
173
|
+
in between. Also check a short/landscape viewport and 200% browser zoom.
|
|
174
|
+
2. **Visual & brand** — logo FULLY VISIBLE (not clipped by a header
|
|
175
|
+
`overflow:hidden` + overlap margin) and brand-compliant; screenshot the header
|
|
176
|
+
IN CONTEXT, never the logo element in isolation (that hides clipping).
|
|
177
|
+
Decoration robust at real size in the real browser: no hairline seam at a
|
|
178
|
+
divider (use `core/section` `divider_top`/`divider_bottom` — don't hand-roll a
|
|
179
|
+
band), no glow clipped to a hard edge, no `object-fit:cover` cropping faces, no
|
|
180
|
+
gradient fade-cutoff. Typography: body line-length ~45–75ch, consistent scale,
|
|
181
|
+
no awkward widows on headings. Palette adherence (no off-brand colours);
|
|
182
|
+
consistent spacing / alignment / radius / shadow.
|
|
183
|
+
3. **Accessibility — MEASURE, don't eyeball** — compute actual contrast ratios
|
|
184
|
+
(WCAG AA: body ≥4.5:1, large/UI ≥3:1) and fix failures by deepening the
|
|
185
|
+
offending colour token; meaningful `alt` on every image; exactly one `<h1>` +
|
|
186
|
+
no skipped heading levels; visible `:focus-visible` on every interactive
|
|
187
|
+
element; every input has an associated `<label>`; touch targets ≥44px on
|
|
188
|
+
mobile; semantic landmarks (header/nav/main/footer) + nav `aria-label`; honour
|
|
189
|
+
`prefers-reduced-motion`.
|
|
190
|
+
4. **Functional** — the form actually works (POST action correct, hidden token
|
|
191
|
+
non-empty, honeypot present + hidden; a long name/email doesn't break layout);
|
|
192
|
+
every link + in-page anchor resolves (each `#anchor` has a matching `id`; no
|
|
193
|
+
`href="#"`/`""`); ZERO console errors/warnings; interactions (menu toggle,
|
|
194
|
+
hover/focus/active) work.
|
|
195
|
+
5. **Content** — no unrendered `{{…}}` tokens in the DOM; no placeholder/lorem;
|
|
196
|
+
copy still matches the source of truth (the live page) verbatim.
|
|
197
|
+
6. **Findable (SEO/meta)** — `<title>` + meta description present + sensible;
|
|
198
|
+
`og:title`/`og:description`/`og:image`; canonical; favicon + apple-touch-icon;
|
|
199
|
+
`<html lang>`; `noindex` correct (branches must be noindex).
|
|
200
|
+
7. **Fast (performance)** — images at sane sizes (not a 2048px file shown at
|
|
201
|
+
380px without a responsive variant), modern format (avif/webp), `width`/`height`
|
|
202
|
+
or aspect-ratio set (no layout shift), below-fold lazy / above-fold eager.
|
|
203
|
+
8. **Cross-browser** — the same CSS renders differently per engine (the divider
|
|
204
|
+
seam was Chrome-only; WebKit/Firefox have their own). Re-check in another engine
|
|
205
|
+
if you can; if only Chromium is available, statically flag risky props
|
|
206
|
+
(`backdrop-filter` without fallback, `-webkit-`-only masks, `100vh` on mobile →
|
|
207
|
+
prefer `100svh`, `sticky` inside `overflow`).
|
|
149
208
|
|
|
150
209
|
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
|
-
|
|
210
|
+
fine" ≠ "looks good", and "looks good in Chrome at 1440" ≠ "works for everyone,
|
|
211
|
+
everywhere" — never report a design as approved/perfect off a glance or a partial
|
|
212
|
+
pass.
|
|
213
|
+
|
|
214
|
+
Then give the user the **DB-live preview link** to review — and use the SAME
|
|
215
|
+
link for your own verification:
|
|
216
|
+
|
|
217
|
+
- **While iterating (default):** a reused `get_preview_link` (mint once at
|
|
218
|
+
`ttl_seconds: 86400`). It renders from the database with NO build, so every
|
|
219
|
+
edit shows on reload, and one link navigates the whole branch (internal
|
|
220
|
+
links keep the token). The token URL is stable across edits — re-mint only
|
|
221
|
+
when the 24h lapses, never per edit. Do NOT deploy just to let the user (or
|
|
222
|
+
yourself) see a change.
|
|
223
|
+
- **When they want the COMPILED static site** (a permanent bookmark, a
|
|
224
|
+
stakeholder link to the built output, or a final pre-merge check): deploy
|
|
225
|
+
the branch once (`trigger_deploy version="<branch>"`) and share the stable
|
|
226
|
+
alias `https://<branch>.<project>.pages.dev` (the `<project>` is the part
|
|
227
|
+
after the hash in the returned `deploy_url`). Branch deploys are
|
|
228
|
+
`robots_blocked`, so it won't be indexed. The immutable per-deploy
|
|
229
|
+
`<hash>.pages.dev` is for your own one-off checks (a new hash each deploy).
|
|
230
|
+
|
|
231
|
+
Default: review/iterate on the reused DB-live preview; deploy only for the
|
|
232
|
+
compiled output or merge. Wait for an explicit "looks good, ship it."
|
|
162
233
|
|
|
163
234
|
### 7. Merge + deploy
|
|
164
235
|
|
|
@@ -185,9 +256,11 @@ disk cost is tiny.)
|
|
|
185
256
|
- **Skipping discovery.** "Modernize" without first reading the site
|
|
186
257
|
produces a confidently-out-of-place result. Always sample existing
|
|
187
258
|
pages.
|
|
188
|
-
- **
|
|
189
|
-
|
|
190
|
-
|
|
259
|
+
- **Deploying to preview.** Don't `trigger_deploy` after every edit just to
|
|
260
|
+
see the change — that builds static pages and the URL is only as fresh as
|
|
261
|
+
the last build. Iterate on a reused DB-live `get_preview_link` (renders from
|
|
262
|
+
the DB, reflects edits on reload); deploy only for the compiled static
|
|
263
|
+
output or merge (steps 6–7).
|
|
191
264
|
- **Auto-merge.** Don't `merge_branch` without explicit user sign-off.
|
|
192
265
|
Once merged, the only undo is another branch + reverse edits.
|
|
193
266
|
- **Header rewrites that drop the brand block.** Even when the
|