* feat(site): scaffold Astro migration, convert 3 pages
Phase 1+2 of the Astro migration:
- Astro v6.2.1 installed, srcDir: 'site', static output to build/
- Shared layout: Base.astro (head, fonts, meta, slots), Header.astro
(star count in one place: 23k), Footer.astro
- CSS moved from public/css/ to site/styles/ (9 files, @import chains
resolve via Vite)
- Three pages converted: privacy, cases/neo-mirai, live-mode
(all return 200 on astro dev)
Remaining: designing, slop, homepage, content collections (docs),
JS migration, server/index.js deletion, build.js cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): migrate all 6 main pages to Astro
Converts the remaining pages:
- designing/index.html → site/pages/designing/index.astro (551 lines)
- slop/index.html → site/pages/slop/index.astro (909 lines)
- index.html → site/pages/index.astro (1278 lines, the homepage)
Base.astro gains OG meta tag props, before-header/after-header
slots (for grain overlay and section nav), and configurable mainId.
Homepage uses link tags to public/css/ instead of frontmatter CSS
imports to avoid esbuild choking on :has() in main.css. Curly
braces inside <code> elements (CSS snippets in changelog) escaped
with HTML entities to prevent Astro JSX expression parsing.
All 6 pages return 200 on astro dev. Branch: feat/astro-migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): content collections for docs and tutorials
Replaces the 1532-line build-sub-pages.js generator with Astro v6
content collections:
- 24 skill editorial files move to site/content/skills/
- 4 tutorial files move to site/content/tutorials/
- site/content.config.ts defines both collections with glob loaders
- site/pages/docs/[...slug].astro reads skills collection + command
metadata from source/skills/ at build time
- site/pages/docs/index.astro renders the command grid grouped by
category (create, evaluate, refine, simplify, harden, system)
- site/pages/tutorials/ mirrors the pattern with ordered index
- Doc.astro layout provides sidebar nav, breadcrumbs, and related-
command chips from the COMMAND_RELATIONSHIPS data
- Category/relationship data extracted to site/data/sub-pages-data.ts
All 15 tested pages return 200: 6 main pages + 5 docs + 2 tutorials
+ 2 index pages. The old generator is not yet deleted (Task #6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): move JS source from public/js/ to site/scripts/
Moves all 49 JS files (app.js + 48 in js/) into site/scripts/.
Vite now processes them through its module bundler instead of
serving them raw from public/.
app.js import paths updated from ./js/X to ./X (the js/ nesting
is gone since app.js now lives alongside the subdirectories).
Homepage and live-mode page switch from <script is:inline src="/app.js">
to Vite-processed <script> imports, so tree-shaking, bundling,
and minification happen automatically at build time.
public/js/ still exists for now (cleanup in Task #6) and the
generated/counts.js build output path needs updating there too.
@paper-design/shaders added to npm dependencies (was missing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(site): delete old Bun server, generator, and duplicated files
Cleanup after the Astro migration:
Deleted:
- server/index.js (233 lines, replaced by `astro dev`)
- scripts/build-sub-pages.js (1532 lines, replaced by content collections)
- scripts/lib/render-page.js (247 lines, replaced by Base.astro layout)
- content/site/partials/header.html (replaced by Header.astro component)
- public/index.html, privacy.html, designing/, live-mode/, cases/
(replaced by .astro pages in site/pages/)
- public/css/ (moved to site/styles/)
- public/js/ old source files (moved to site/scripts/)
- public/app.js (moved to site/scripts/app.js)
Kept in public/:
- antipattern-examples/ (standalone HTML demos, not Astro pages)
- antipattern-images/, assets/, neo-mirai/ (static assets)
- js/detect-antipatterns-browser.js (referenced by antipattern examples)
- js/generated/counts.js (build output from scripts/build.js)
- _data/api/ (generated API data, now written to public/ so Astro
passes it through to build/)
Updated:
- astro.config.mjs: added redirects (skills->docs, cheatsheet->docs,
gallery->slop, neon-mirai->neo-mirai, etc.)
- package.json: dev->astro dev, build->build:skills+build:site,
preview->astro preview
- scripts/build.js: removed buildStaticSite(), generateSubPages(),
static-asset copying. API data writes to public/_data/ instead of
build/_data/. Site-header validator is a no-op (shared component).
Em-dash validator scans site/components + site/layouts, not pages
(pages contain content from other sources like detector descriptions).
- .gitignore: removed public/slop/ entry
Tests: 186/186 pass. Skills build: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): fix redirect config for Astro compatibility
Move the dynamic /skills/:id -> /docs/:id redirect to public/_redirects
(Cloudflare Pages native format) since Astro's redirect config can't
handle dynamic routes that don't match existing page patterns.
Remove duplicate trailing-slash redirect entries that caused warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): switch remaining pages from /css/ link tags to frontmatter imports
Doc.astro, docs/index, tutorials/index, and tutorials/[slug] were
still using <link href="/css/sub-pages.css"> which pointed at the
deleted public/css/ directory. Switched to frontmatter CSS imports
(import '../../styles/sub-pages.css') which Vite resolves from
site/styles/.
Homepage also switches from link tags to frontmatter imports for
main.css and sub-pages.css — the esbuild error that originally
forced the link-tag workaround was caused by unescaped curly braces
in the HTML content (since fixed), not by the CSS itself.
All pages verified visually in Chrome: homepage hero, foundation
grid, docs index (card grid with categories), docs detail (sidebar +
editorial content + visual mockups), designing (core loop diagram),
privacy, tutorials. Header renders with 23k stars on every page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): fix edge-to-edge sections, broken API paths, CSS links
Three fixes:
1. Homepage sections sat on the viewport edge because Base.astro's
<main> lacked the site-content class (provides max-width + padding).
Added mainClass prop to Base.astro; homepage sets mainClass="site-content".
2. "Failed to load commands" because app.js fetched /api/commands
which only existed in the old Bun server's routing. Updated to
fetch from /_data/api/commands.json (the static JSON files that
build:skills writes to public/_data/).
3. CSS reference fix (previous commit was incomplete): Doc.astro,
docs/index, tutorials pages all used <link href="/css/sub-pages.css">
pointing at deleted public/css/. Switched to frontmatter imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): add sidebar to docs index page
The docs index was using Base.astro directly without the skills-layout
grid, so it rendered without a sidebar. Added the same sidebar structure
from Doc.astro (category-grouped command list) and wrapped the content
in the skills-layout grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): extract footer CSS to shared file, import in Base.astro
Footer was unstyled on sub-pages because footer CSS lived only in
main.css (loaded by the homepage) not in sub-pages.css. Extracted
the 95 lines of footer rules into site/styles/footer.css and
imported it in Base.astro so every page gets footer styles regardless
of which page-specific CSS it loads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(demos): move landing-demo into repo, add as slop specimens
Moves ~/code/landing-demo/ into demos/landing-demo/ (without
node_modules or the redundant .claude/.agents skill copies — the
repo root's skill is found by walking up). PRODUCT.md, DESIGN.md,
DESIGN.json, PROMPT.md, and SCRIPT.md stay in place so running
Claude from demos/landing-demo/ picks up the project context.
Also copies both pages as slop specimens to public/antipattern-examples/
with the detector script baked in:
- new-slop-2026.html (Fraunces + warm cream editorial monoculture)
- old-slop-2022.html (purple gradient + glassmorphism + neon glow)
These can be linked from the slop page gallery alongside the
existing 11 synthetic specimens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(slop): replace single demo iframe with Then vs Now comparison
The "See it" section (01) on the slop page now shows two side-by-side
browser frames: 2022 slop (purple gradients, glassmorphism, neon glow)
and 2026 slop (Fraunces, warm cream, editorial restraint). Both run
the detector overlay live — hover either to see which rules fire.
Replaces the single visual-mode-demo.html iframe. Responsive: stacks
vertically on viewports below 900px.
Caption: "Same engine, different decade, both flagged."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): switch to single-frame era toggle, center the section
Replaces the side-by-side dual-iframe layout with a single large
frame and a segmented 2022/2026 toggle. Clicking the toggle swaps
which iframe is visible (both pre-loaded, instant switch). Browser
chrome title updates to match the active era.
Centers the lede text and toggle above the frame for visual
cohesion with the full-width iframe below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): left-align See It section, toggle inline with lede
Moves the era toggle to the right of the lede paragraph using a
flex row (align-items: flex-end). Left-aligned text + right-docked
toggle matches the rest of the page's flow instead of standing out
as a centered island. Stacks vertically on narrow viewports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): left-align iframe, remove max-width and auto margin
The visual-mode-preview had max-width: 1040px + margin: 0 auto
which centered it within the column. Override both in the
.slop-then-now context so the frame fills the full content width
flush with the text above. Caption left-aligned to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(site): update star count to 24k (24,062)
One file, one edit. The Astro migration working as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): regenerate pnpm-lock.yaml for astro + shaders deps
Cloudflare Pages uses pnpm with frozen-lockfile. The lockfile was
stale after adding astro, @astrojs/cloudflare, and
@paper-design/shaders via npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): resolve 3 bugbot review issues
1. Restore public/slop/ to .gitignore — prevents accidental legacy
generator output from conflicting with the Astro page.
2. Move astro and @paper-design/shaders to devDependencies — these
are site-build tools, not CLI runtime deps. Removes @astrojs/cloudflare
entirely (unused; static output mode needs no adapter).
3. Fix Astro wiping build:skills output — CF config (_headers,
_redirects, _routes.json) and API data now write to public/ so
Astro copies them through. Dist ZIPs copy to build/_data/dist/
as a post-build step (after Astro finishes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): merge duplicate devDependencies, use npx for astro CLI
The previous commit created a second devDependencies key in
package.json. JSON doesn't support duplicate keys — pnpm ignored
the first block (with astro), so `astro build` wasn't found.
Merged astro and @paper-design/shaders into the existing
devDependencies block. Changed `astro build/dev/preview` to
`npx astro build/dev/preview` so pnpm finds the local binary
on Cloudflare Pages (which doesn't add node_modules/.bin to PATH
by default).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(demos): remove private demo script and prompt from public repo
SCRIPT.md contained a detailed conference talk script with personal
delivery strategies, rehearsed Q&A answers, and venue details.
PROMPT.md contained the origin brief for the demo page. Neither
belongs in a public repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): gitignore generated public/ artifacts, consolidate redirects
1. Generated files written to public/ by build:skills (API data,
CF config, browser detector, counts.js) are now gitignored.
Prevents noisy diffs and merge conflicts from committed build
artifacts.
2. Removed duplicate redirects from astro.config.mjs. All redirects
now live in one place: the _redirects file generated by
scripts/build.js (which Cloudflare Pages processes natively).
Eliminates the dual-maintenance risk where the two sources
could drift apart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Star count was 21k on sub-pages and the header partial, 22k on the
homepage. Updated all seven source files to 23k (actual: 23,692).
Changelog section gains a curated "Highlights since v3.0" block
above the full version history, which now collapses behind a
disclosure toggle. Fixes the vertical bloat from 9 entries in
three weeks while keeping v3.0's anchor content visible.
Also fixes a nesting bug where an orphan </div> from the old
changelog-list wrapper prematurely closed the changelog-section,
breaking the two-column changelog+FAQ grid layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The seven-lane list in Phase C departure mode was acting as a menu:
the model ran "furthest from editorial" as its selection criterion and
converged on Swiss-grid / Terminal / Industrial-signage every time.
Replaced with a brand-voice derivation process (read personality
words, imagine physical experiences, derive visual directions).
Explicitly names the failure mode so the model can't fall into it.
Phase D family-pass labels are now open-ended nouns, not a fixed
vocabulary list that re-anchored the same categories.
Reinforced parameter generation: Phase C (both modes) now requires
naming 2-3 parameter knobs alongside each variant during planning,
not as an afterthought. The freeform bias paragraph aligns with
the budget table (2-3 for large compositions, not 1-2) and frames
0-param heroes as mistakes, not judgment calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SAFE_TAGS skipped <a> and <button> categorically to avoid noise on
inline links and unstyled controls. The blanket skip overshot: a
pill-style anchor or styled button with its own opaque background
was silently exempted from the contrast check, so a "Get started"
button with charcoal text on near-black background (~2:1) read as
fine to both the CLI and the browser overlay.
The bail in checkColors now permits <a> and <button> when they have
their own opaque background AND direct text. Inline links and bare
controls keep skipping. checkElementColorsDOM no longer short-circuits
before reaching checkColors so the exception fires on the browser path.
Adds readOwnBackgroundColor() helper to handle jsdom's missing
shorthand decomposition; falls back to parsing the inline style attr
when getComputedStyle returns empty (real browsers always decompose,
so the fallback is a no-op there).
Fixture gains four cases: pill-style <a> low-contrast (flag),
<button> low-contrast (flag), inline <a> with no own bg (pass),
pill-style <a> with high contrast (pass). Three new tests assert
the right flags fire and the no-regression cases stay clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The service worker's webNavigation.onCompleted handler only cleared
csInjected when DevTools was open. The popup-only flow never
registered the tab in devtoolsTabs, so a page reload left a stale
csInjected: true even though the content script had been destroyed.
The next popup scan saw the stale flag, skipped re-injection, and
silently sent its scan request to a tab with no listener — popup
UI got stuck on "Scanning..." indefinitely.
Reset is now unconditional (page reload always destroys the content
script regardless of which UI is open). Auto-rescan stays gated to
DevTools, since the popup is strictly user-driven.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures live.md Step 4 into four phases: identity extraction
(Phase A, non-skippable, falls back to CSS variables and computed
styles when DESIGN.md is absent), default vs departure mode pick
(Phase B), variant planning by primary axis or aesthetic lane
(Phase C), and a family-level squint test before the sentence
pass (Phase D). Default mode preserves identity and varies
expression; departure mode only triggers on explicit signals
(PRODUCT.md anti-references calling out the current surface, or
the user prompt asking for departure).
Adds reflex-reject aesthetic lanes to brand.md as a parallel to
the existing font reflex-reject list. Editorial-typographic is
the first entry. Expands SKILL.md's category-reflex check to two
altitudes (theme + palette from category, then aesthetic family
from category + anti-references).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Fraunces, Geist (Sans/Mono), Mona Sans, Plus Jakarta Sans,
Space Grotesk, Recoleta, and Instrument Sans to OVERUSED_FONTS.
Brand-domain exceptions for Geist on vercel.com / nextjs.org /
v0.app and Mona Sans on github.com / githubnext.com.
Updates four should-pass fixtures that used the now-flagged faces
as "good examples"; switches to Newsreader + Karla. Adds positive
assertions for Fraunces and Geist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(live): switch live-poll to execFileSync, validate ids strictly
live-poll.mjs built the live-accept invocation with execSync and string
interpolation of event.id and event.variantId. Both fields originate in
the browser; validateEvent only checked truthiness, so shell metacharacters
in either field would land in the shell-parsed command.
Real exploitability is gated by the per-session token (loopback only,
unguessable UUID), so risk is low. The construction itself is structurally
unsafe though, and the fix is small.
- live-poll.mjs: execSync(string) → execFileSync('node', argv). Drops the
hand-rolled single-quote wrap for --param-values; execFileSync passes
each arg as a discrete argv slot, no shell parsing.
- live-server.mjs validateEvent: tighten id and variantId to match the
actual generator shapes (8 hex chars and 1-3 digit numeric strings).
Defense in depth so any value reaching downstream code is inert by
construction.
- live-server.test.mjs: add three regression tests covering accept/discard
rejection of shell-metachar ids and non-numeric variantIds. Update the
three existing fixture ids to match the new pattern.
Reported in #122.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: refresh pnpm-lock.yaml to match package.json
Cloudflare Pages runs pnpm install --frozen-lockfile and was failing on
ERR_PNPM_OUTDATED_LOCKFILE: the lockfile was missing entries for
@ai-sdk/anthropic, @ai-sdk/openai, @anthropic-ai/claude-agent-sdk,
@anthropic-ai/sdk, @google/genai, ai, modern-screenshot, zod, and had
stale specifiers for jsdom, marked, playwright, wrangler, puppeteer.
Drift was introduced when package.json was last edited without a lockfile
regen. Running pnpm install --lockfile-only resolves it; verified with
pnpm install --frozen-lockfile (clean install succeeds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the per-component tag prefixes, the changelog-label convention
that the release script matches against, the cleanliness gates, the
attached artifacts, and the manual post-release steps for the CLI
(npm publish) and the extension (Chrome Web Store upload).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HTML changelog source lives 12 spaces deep inside its containers,
so list items emitted by htmlToMarkdown carried that indentation. Four
or more leading spaces in markdown is a code block, so all bullets
after the first (which the final .trim() rescued) rendered as code on
the GitHub release page. Strip leading whitespace per line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v3.0.3 was never installable as a distinct version: the manifest jumped
3.0.2 → 3.0.4 in a single commit (5f5e2b0), so plugin users picked up
the craft/shape hardening and the modal-host live-picker fix together
with the 3.0.4 work. Merging the changelog matches what actually
shipped and keeps a single GitHub release for that batch of changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/release.mjs tags and publishes GitHub releases for the three
independently versioned components (skill, cli, extension). Refuses on
dirty tree, unpushed HEAD, missing changelog entry, or stale build
outputs. Skill release attaches dist/universal.zip; extension release
runs build:extension and attaches dist/extension.zip. Prints a manual
next-step hint for npm publish (CLI) and Chrome Web Store upload.
- package.json: bump CLI to 2.1.8, add release:{skill,cli,ext} scripts.
- public/index.html: add CLI v2.1.8 changelog entry covering the
Windows path fix (#95) and border-radius detector hardening. Adopt
"CLI v" / "Extension v" prefix convention to disambiguate components
in the shared changelog timeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Added tests/windows-path-fix.test.js to package.json's test script so
the regression suite actually runs in CI; without this the file lived
on disk but no command picked it up. Verified with bun run test:
170 bun tests / 3 files, all green.
- Rebased onto current main. The PR's second hunk (live-mode browser
script load) no longer applies because that code path was extracted
into source/skills/impeccable/scripts/live-*.mjs during the live-mode
rewrite. The remaining puppeteer site at line 2700 still had the bug
and now uses fileURLToPath, matching the PR's intent.
- The added test file's mix of ESM imports + require/__dirname runs
cleanly under Bun's test runner; left as-is to preserve the PR's
authorship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseRadiusToPx("50%", 0) used to return 0, and resolveBorderRadiusPx's
"if (fromComputed !== null) return fromComputed" guard short-circuited
with that 0 before ever consulting longhand / inline / stylesheet
fallbacks. Callers that gate on `> 0` (border-accent-on-rounded and
isCardLike's hasRadius) silently lost findings the old
parseFloat(style.borderRadius) === 50 heuristic happened to keep.
In jsdom this is reachable any time style.width resolves to "auto" or
an empty string — parseFloat yields NaN, the `|| 0` fallback turns it
into 0, and any percent radius collapses to nothing. Real-world cards
with `width: 100%` hit this on every load.
Fix: when widthPx is 0 / missing, return the raw percentage number
instead. The percent-to-px conversion only makes sense with a width
reference; without one, the value still serves as a positive presence
signal for boolean checks. The icon-tile circle exclusion is
unaffected because that rule already gates on `siblingWidth >= 32`.
Caught by Cursor Bugbot on PR #115.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds resolveBorderRadiusPx(el, style, widthPx, win), a helper that walks
computed style → longhand → inline DOM → raw style attribute → matching
stylesheet rules to recover a pixel value, converting % to px when
needed.
Three jsdom adapter sites now use it: checkElementBorders (via a new
optional resolvedRadius param threaded from detectHtml), the icon-tile
sibling check in checkElementIconTile, and isCardLike's hasRadius gate.
Browser DOM adapters hit the fast path on the first line since real
getComputedStyle resolves both shorthand and percentages.
Background: from jsdom 29.0.2 onward, getComputedStyle(el).borderRadius
returns "" for the shorthand and "0" for longhand reads when the rule
used the shorthand. checkIconTile relied on parseFloat(borderRadius) >=
width/2 to exclude circular avatars; that comparison broke and circles
got false-flagged as icon-tile-stack. jsdom 29.1.0 has a separate
parser crash on <h*> + linear-gradient inline style which keeps the
pin at exactly 29.0.0 for now, but landing the helper means we can
move forward as soon as the gradient crash is fixed upstream without
touching detector code again.
The change is also strictly more correct than the old parseFloat
approach: percentage values now convert to actual pixel sizes, so
checkIconTile no longer relies on parseFloat("50%") == 50 happening
to satisfy `>= width/2` only for elements <= 100px wide.
bun run test passes (174/174); bun run build:browser and
bun run build:extension regenerated to mirror the helper into
bundled artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- @ai-sdk/anthropic 3.0.69 → 3.0.71
- @anthropic-ai/claude-agent-sdk 0.2.110 → 0.2.119
- ai 6.0.162 → 6.0.168
- playwright 1.58.2 → 1.59.1
- wrangler 4.75.0 → 4.85.0
- puppeteer 24.39.1 → 24.42.0 (optional)
- marked range floor bumped to 16.4.2 (already installed)
jsdom is intentionally pinned to exact 29.0.0. From 29.0.2 onward,
getComputedStyle(el).borderRadius returns "" (empty string) instead
of "50%" for percentage values that the engine can't resolve to px
without layout. checkIconTile relies on parseFloat(borderRadius) ≥
width/2 to exclude circular avatars; with the empty string, all
circles get re-flagged as icon-tile-stack. Real browsers resolve
the percentage so the public-site overlay and Chrome extension are
unaffected — only the Node/jsdom path used by `npx impeccable detect`
on HTML files breaks. Hardening the detector to read raw stylesheet
rules as a fallback is a follow-up; pinning is the safe move today.
Skipped: marked 16 → 18 (major bump, unrelated to this work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
package.json was updated to ^0.91.1 in d26ccac (live-mode E2E LLM
agent), but the lockfile was not committed alongside. The next
bun install bumped @anthropic-ai/sdk from 0.81.0 to 0.91.1 to match
the declared range. The nested resolution under
@anthropic-ai/claude-agent-sdk stays at 0.81.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Marketplace source moved from "./" to "./plugin", a thin generated
subtree containing only the plugin manifest and the impeccable skill.
Per-version plugin cache shrinks ~378× (~770 KB instead of ~291 MB),
and the lockfile is no longer included in the source path so the
cache extraction never runs bun install. (#107)
- skills field in plugin.json now ends with a trailing slash to match
the documented schema (code.claude.com/docs/en/plugins-reference,
every directory example uses ./path/). Three reporters converged on
this fix because Claude Code's plugin loader skips command
registration on some setups when the slash is missing. (#86)
- Anti-patterns maintenance agent moved out of .claude/agents/ into
CLAUDE.md / AGENTS.md as concise inline guidance, since it is
repo-internal dev workflow, not user-facing. The plugin was also
the only place this agent was exposed to install users.
- Skills version bumped to 3.0.2 so existing users pick up the new
install path on next /plugin update.
- Top-level harness directories (.claude/skills/, .cursor/skills/, ...)
intentionally stay where they are; npx skills add reads them
directly from the GitHub repo and that path is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The configure row's text input filled its background with translucent
magenta (BP.accentSoft) on focus. Composited against the dark bar surface
this produced a murky purple where the browser's default placeholder
gray washed out — flagged in a real session as "godawful styling, gray
text on dark magenta really hurts my eyes". Fix: focus state shows an
accent-colored border only, no fill; placeholder color is set explicitly
to BP.textDim via a one-shot stylesheet so it reads in both themes.
tests/live-e2e/agent.mjs: runAgentLoop's wrapTarget now accepts either a
static {classes,tag,elementId} (test fixture mode) OR a function that
derives the target from each generate event (real-use mode where the
picked element is unknown ahead of time).
tools/live-loop.mjs: standalone runner that attaches the LLM agent to a
running live-server. Used as a test-harness shortcut for validating live
mode out of band; in production the user's coding agent (Claude Code,
Cursor, etc.) plays this role directly via the live skill spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tests/live-e2e/agents/llm-agent.mjs: a Claude-backed VariantAgent that
implements the same one-method interface as the fake agent
(generateVariants(event, context) → { scopedCss, variants[] }). Default
model claude-haiku-4-5; override via IMPECCABLE_E2E_LLM_MODEL.
Prompt caching is on — the system prompt (instructions + the live-mode
spec from reference/live.md) is the cacheable prefix. First call writes
~10K tokens to cache; subsequent fixtures pay only the cache-read rate.
JSON output is validated for shape (scopedCss, variants[N].innerHtml),
with light error messages on parse failure.
tests/live-e2e.test.mjs: read IMPECCABLE_E2E_AGENT (fake|llm). When 'llm',
construct the LLM agent and skip the case cleanly if ANTHROPIC_API_KEY is
unset. Param-manifest assertions are gated to fake mode (LLM may emit
zero-param "fixed point" variants per the live.md spec). The accepted-h1
class assertion now allows hero-title as one of multiple classes so an
LLM agent that adds classes alongside the original still passes.
Test timeouts widen for LLM mode: 25s first-pass on conditional-render
fixtures (vs 5s for fake), 60s on direct waits (vs 30s). Without these,
the LLM's 3-8s generate latency races the orchestration's state-loss
recovery window.
tests/live-e2e/ui.mjs: clickGo retries up to 3× on stability failures.
Required because conditional-render fixtures (modal/tabs) animate the bar
mid-transition when preActions trigger framework HMR; a single click can
land during a re-render and Playwright's stability gate times out.
Pass rate on a typical sweep: 18/19 in LLM mode, 19/19 in fake mode.
The modal fixture's intrinsic state-loss flake (Fast Refresh resetting
useState(open) when source changes) is amplified by LLM latency and may
need a re-run; documented in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
startVariantObserver's "dominated" check only matched when the variant
wrapper was added directly as a mutation's addedNode. SvelteKit (and any
framework whose HMR replaces a whole subtree on edit) adds the wrapper as
a descendant of an added <main> or similar — the observer ignored those
mutations and the session stayed in GENERATING forever even with all 3
variants present in the DOM.
Surfaced by the LLM-agent E2E run on vite8-sveltekit. The fake-agent path
masked the issue because its splice timing happened before Vite's reload
finalized; the slower LLM call shifted timing into the failure window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligning to the design system is now non-optional, drift gets named by
root cause (missing token / one-off / conceptual), and a new Information
Architecture & Flow dimension covers the user-flow shape that polish
previously left to chance. Folds the missing pieces from the deprecated
normalize skill into the v3.0.1 changelog bullet rather than a new bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents `bun run test:live-e2e`, the IMPECCABLE_E2E_ONLY scope env var,
the IMPECCABLE_E2E_DEBUG diagnostic flag, the one-time
`npx playwright install chromium` setup, and why the suite is kept off
the default `bun run test` path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CSP meta-tag auto-patch (live-inject.mjs)
When the user's HTML carries <meta http-equiv="Content-Security-Policy">,
the cross-origin load of /live.js and the SSE/POST stream back to
localhost:PORT are both blocked. Insert: append http://localhost:PORT to
script-src and connect-src, plus blob: to img-src (the shader overlay),
stash the original content value as a base64 data-impeccable-csp-original
attribute. Remove: decode the marker and restore the original verbatim.
Header-based CSP (Next/Nuxt/SvelteKit configs) intentionally untouched —
those flow through the existing detect-csp.mjs reference path.
JSX-aware accept (live-accept.mjs)
- Carbonize stash now emits style={{ display: 'contents' }} for JSX targets
instead of style="display: contents" (HTML form). React 19 was throwing
"Failed to set indexed property [0] on CSSStyleDeclaration" on the
string form because it iterated chars onto the style object.
- extractCss now matches </style> anywhere on a line, not just at line
start. Previously a JSX template-literal close like `}</style> would
leak the backtick + brace into the carbonize stash, breaking JSX.
- Carbonize stash wraps the CSS body in {` … `} for JSX targets so curly
braces in CSS rules don't get parsed as JSX expressions.
Conditional-render UX (live-browser.js)
- Drop the 2s-then-window.location.reload() fallback in the SSE 'done'
handler. That reload was masking a real failure mode: when the picked
element lives inside conditional render (closed modal, hidden tab,
other-route), Fast Refresh remounts the parent and state resets, so
the variants land in source but never reach the DOM. Reload also reset
state to default, leaving the user stuck.
- Replace with a 6s contextual toast: "Variants ready. If the picked
element isn't visible, retrace the path that revealed it — they'll
appear automatically." The MutationObserver stays armed and
auto-transitions to CYCLING once the variants finally mount.
- Pick-time heads-up: when the picked element is inside [role="dialog"],
[data-state="open"], a multi-tab tabpanel, or an aria-expanded
collapsible, fire a brief upfront toast so the user knows what to
expect if state resets during generation.
Hydration race (live-browser.js)
- SvelteKit (and any framework that hydrates after HTML parse) was
failing post-Vite-page-reload because init() ran resumeSession()
before the variant wrapper hydrated into the DOM. The OLD reload
fallback masked this by triggering a second reload whose hydration
benefited from warm cache. Without that, fix it properly: install a
scout MutationObserver in init() that retries resumeSession() once
[data-impeccable-variants] lands in the DOM.
Shader overlay (live-browser.js)
- WebGL fallback in showShaderOverlay used Object.assign(img.style,
canvas.style, …), which throws on modern Chromium because
CSSStyleDeclaration's indexed properties are not writable. Use
cssText to copy positioning instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 fixtures (11 styling/build variants + 4 conditional-render scenarios + 4
meta-frameworks) drive the entire user flow end-to-end: handshake, pick,
configure, Go, cycle, accept, carbonize cleanup. Each fixture installs real
deps, boots the framework dev server, and runs Playwright Chromium against a
deterministic fake agent that produces realistic variants (colocated style
with @scope rules, full data-impeccable-params manifests covering range +
steps + toggle, JSX/HTML/Svelte syntax-aware rendering).
The agent is pluggable via a one-method interface — generateVariants(event) —
so a future LLM-backed agent slots in by implementing the same shape. The
orchestrator handles wrap, file write, accept, and carbonize cleanup
deterministically regardless of which agent is plugged in.
Schema extensions (tests/framework-fixtures/README.md): runtime block adds
preActions / reloadProbe / pickSelector / scheme / ignoreHTTPSErrors so
fixtures can drive conditional UI (modal, tab, route) before pick and verify
the carbonized variant survives a reload.
Static fixture suite filtered to skip dirs without fixture.json so empty
scaffold dirs no longer break discovery. Total: 178 static checks, 19 E2E
full cycles, ~107s wall clock for the E2E suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .prose p rule (specificity 0,1,1) was overriding .docs-viz-caption's
intended margin-top, leaving 0px between the caption and the cards/file/
report above it on every docs page that uses the hero block.
Bump specificity with .docs-viz-hero .docs-viz-caption and set
margin: 16px 0 0 so the caption sits 16px below the visual and lets the
hero's 24px padding alone control the bottom gap. Symmetric inside the
cream box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
public/index.html and public/privacy.html had stale
`<script src="http://localhost:8400/live.js">` scaffolding from a
local /impeccable live session. On impeccable.style (Cloudflare) this
fired Chrome's private-network-access prompt on every page load. The
inject is dev-only; normally stripped by live-server.mjs stop, but
these two slipped through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
typography.md pointed at SKILL.md's `<font_selection_procedure>` and
`<reflex_fonts_to_reject>` XML tags, which were removed in the v3
consolidation and moved into brand.md as the "Font selection procedure"
and "Reflex-reject list" sections. Agents loading typography.md via the
craft flow were chasing content that no longer existed. Now points at
brand.md with correct section names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
colorize.md: the brand-register paragraph claimed "a dominant color can own
the page" and "accent rate stays ≤10%" in the same breath. SKILL.md scopes
the ≤10% rule to Restrained only; Committed / Full palette / Drenched
exceed it on purpose, and brand.md explicitly encourages those strategies.
Rewritten to defer to the color-strategy ladder.
critique.md: two cross-references still pointed at "Step 4" / "Step 5"
after those headers were renamed to "Ask the User" / "Recommended Actions".
Swapped the references to the new names so the flow is self-consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged ten tactical items from ehmo/typecraft-guide-skill into the typography
reference at the upstream author's request: dark-mode weight/tracking/leading
compensation, font-display: optional vs swap, preload-critical-weight-only,
variable fonts for 3+ weights, clamp() max-to-min ratio bound, container/
font-size coupling to preserve measure, text-wrap: balance / pretty,
font-optical-sizing: auto, quantified ALL-CAPS tracking (5-12%), and the
paragraph-rhythm rule (space OR indent, never both).
Skipped: platform-specific tables (iOS/Android/print), confidence markers,
severity-graded report format, academic sources, and the punctuation
subsection (em-dash prescription conflicts with the project copy rule).
Attribution lives in NOTICE.md, not inside the skill content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime fix in live-browser.js: the 2s static-server fallback in handleAccept
now swaps the outer wrapper with the `[data-impeccable-variant="N"]` div itself
(+ display:contents), matching what live-accept.mjs writes to disk. Scope rules
anchored on the variant attribute keep matching on the non-HMR path, so the
accepted design no longer flashes unstyled until reload. Propagated to all
harness script copies.
/designing:
- §03 Polish redesigned as drenched magenta masthead: commands live in the band,
three title/description columns beneath on cream.
- §04 Maintain redesigned as architectural poster diptych: extract + document
vizzes become the hero element, caption below.
- §05 Interop section removed.
- §05 (was §06) "Pick a register" renamed to "Brand, or product." with a
two-lane hairline-divided layout and an auto-selected framing in the sub.
Live mode status: BETA → ALPHA across the periodic table, magazine spread,
and docs callout, reflecting real-world-testing readiness.
Skill bootstrap: removed the `<post-update-cleanup>` block from source/SKILL.md
(the source repo is the origin; running cleanup-deprecated here would touch
legitimate source). CLAUDE.md and AGENTS.md now document the skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/designing §01 is reframed around the words → pictures → code arc.
The phase sub covers teach, shape, and craft in one breath; the body
splits into two labeled micro-steps ("teach · in words" and
"shape + craft · in pictures") so the new image-gen spread reads as
step 2 of the same flow instead of a separate topic. Generated plates
from GPT Image 2 sit as an editorial two-up beside their brand and
hi-fi captions. Also fixes a long-standing font-size mismatch where
<code> inside .designing-phase-sub fell back to the browser's default
monospace.
Homepage: inserts a new "Visualize, then build" slot at position 02 of
The Case carousel, compact two-plate visual, shifts the remaining
eight slots to 03-09.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Section 7 no longer reads as default-zero: composition-sized targets,
freeform bias toward 1–2 dials on non-tiny surfaces, hard cap of four.
Cross-link freeform to §7 in the action loader; sync all harness copies.
Made-with: Cursor
Top nav was at six items and growing. The anti-patterns catalog and the
visual-mode overlay demo were always two views of the same subject (the
rule set and seeing it caught in place), so collapsing them into one page
reduces nav weight and puts the catalog + live demo next to each other.
Four sections in one scroll: See it (iframe demo), Try it live (specimen
gallery), The catalog (full rule list), Run it yourself (invocation
methods). Sidebar TOC nests the catalog sections under "The catalog" for
deep linking. 301s from the old URLs preserve external links.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unify the design-system panel's data shape around DESIGN.md frontmatter
as the primary source of truth; the sidecar carries only what Stitch's
frontmatter schema can't (extensions + live component HTML + narrative).
Also fix a long-standing build bug that destroyed per-project config.
Shape changes:
- Server /design-system.json now returns { parsed, sidecar, hasMd,
hasSidecar, mdNewerThanJson, parseError?, sidecarError? }. No more
mode switching; both layers ship when present and the panel merges.
- Panel consolidates renderSidecarVisual + renderParsedMdVisual into a
single renderDesignVisual that merges frontmatter primitives with
sidecar extensions.colorMeta / typographyMeta. Helpers for color,
typography, radii model-building. Parsed-md narrative synthesis
survives as a fallback when no sidecar.
- DESIGN.json rewritten at schemaVersion 2: extensions.{colorMeta,
typographyMeta, shadows, motion, breakpoints} + components (with
refersTo pointing back to frontmatter component keys) + narrative.
Token primitives no longer duplicated in the sidecar.
Build fix:
- scripts/build.js:634 wiped .claude/skills/ (and every other harness
dir) on each rebuild, then recopied from dist. After commit b0feed0
unbundled per-project config.json from dist, the sync destroyed the
user's live-mode config on every build without replacing it.
- Added stashPerProjectArtifacts / restorePerProjectArtifacts in
scripts/lib/utils.js. Hoisted PER_PROJECT_SCRIPT_ARTIFACTS to a
module-level export so build.js and readSourceFiles share one
source of truth. Build now preserves config.json across the sync.
Verified in browser: panel renders 10 colors, 9 typography roles, 3
shadows, 6 grouped components, 9 rules, 25 do/don't items, all merged
correctly from frontmatter + v2 sidecar with zero console errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two loopholes in the critique procedure let the model shortcut past its own rigor: "SHOULD delegate" with a broad "if sub-agents are not available" escape, and "Browser visualization (when available)" framing that made the [Human] detector-overlay tab read as optional color. Both get rationalized away under context pressure even though the isolation is what makes the combined score honest and the overlay is the user-facing deliverable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopt the Stitch google-labs-code/design.md format's two-layer shape:
YAML frontmatter carries machine-readable tokens, prose body covers the
six canonical sections. The sidecar DESIGN.json now extends frontmatter
rather than duplicating it.
- document.md: new frontmatter section, Step 2b staging, sidecar scoped
to extensions, Do's/Don'ts now cite PRODUCT.md anti-references by name,
OKLCH vs hex presented as project posture not mandate.
- design-parser.mjs: tiny YAML-subset reader, exposes model.frontmatter,
schemaVersion bumped to 2, prose-scraping fallback intact.
- live-browser.js: "basic view" CTA copy reflects frontmatter-first model.
- DESIGN.md: add frontmatter with 10 colors, 9 type roles, 7 components;
OKLCH values direct per The OKLCH-Only Rule.
- tests/design-parser.test.mjs: coverage for no-frontmatter, Stitch-shape,
missing-terminator, comment handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /designing page: the missing "how do I actually use this tool?" story,
organized around a four-phase core loop. Start, iterate, polish, maintain.
Text-dominant editorial sections, cards are rare, spacing pulls from
DESIGN.md's scale only.
Hero runs as a single editorial spread: title and lede on the left,
a compass loop on the right. The compass is a thin ink circle with
cardinal phase labels at N/E/S/W, clockwise arrow glyphs at 45°
transit points, a slow orbiting accent dot, and an italic impeccable
wordmark in the middle. Collapses to vertical stack below 880px.
Phase sections below the hero are full width. Each opens with a
numbered eyebrow, italic Cormorant title (spine), and a 60ch sub.
Appendix sections (register cross-link, DESIGN.md interop, what to
avoid, CTA) use a quieter title tier in roman to separate spine from
supporting material. All separators are 1px mist hairline, no 2px
black rules; rhythm comes from variable padding and margins.
Adds /live-mode hero-frame polish, new top-nav entry (Home · Designing
· Docs · Anti-Patterns · Live · Overlay), footer link, and the server
route for /designing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Config drift was a real tripwire for projects with static generators: new
HTML files get added, never make it into config.files, silently skip
injection. Two additions.
config.files entries now accept glob patterns (**, *, ?) expanded via
fs.globSync in live-inject. Multi-page projects can write
["public/**/*.html"] once and never maintain the list again. New optional
exclude field filters out matched files (email templates, demo fixtures).
HARD_EXCLUDES of node_modules and .git are enforced regardless of user
config so vendor trees can never receive a tracking script.
live.mjs now runs a drift scan after inject: walks common page-source
roots (public/, src/, app/, pages/) and reports HTML files not covered
by the resolved inject targets. Respects user excludes so intentional
omissions aren't flagged. Output JSON carries configDrift: { orphans,
orphanCount, hint } or null. live.md documents the agent flow for
surfacing drift to users without auto-mutating the config.
Unbundle config.json from the distributable skill: it's a per-project
artifact, not skill code. readSourceFiles now skips any PER_PROJECT_ARTIFACTS
during source scan so build output to .claude/ .cursor/ etc never ships
one project's inject targets to another's install. The per-harness
copies stay gitignored via the existing **/skills/impeccable/scripts/config.json
rule; each consuming project writes its own on first /impeccable live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without comments or strokes the screenshot is pure visual anchoring,
biasing the model toward the existing rendering and fighting the
three-distinct-directions brief. Local blob still drives the shader
overlay; upload and screenshotPath are gated on annotation presence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 1 is now skill-only (the outdated "powers visual mode" claim
and the global CLI install are gone). Chrome extension stays at
step 3, the CLI moves to a new optional step 4 with a link to npm,
and "Stay updated" shifts to step 5. Step 2 collapses its three
numbered sub-steps into a compact command/caption table so the
"Use it" panel matches step 1's height instead of towering over it.
Also: keep code chips and the npm link on one line each (nowrap)
so the caption wraps only on natural prose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vertical 8-tab list ate ~430px of viewport on mobile; panel content
sat far below the fold. At ≤900px, the tab list becomes a sticky
horizontal scrollable pill strip (47px tall) under the header, with
edge-fade mask, pill-shaped buttons, ink-filled active state, and
the active pill auto-centered as rotation advances or users tap.
Desktop layout unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- why-panel tab swap is a proper opacity+transform crossfade (display:grid
stack area) instead of display:none jump; 650/800ms ease-out.
- tab progress indicator animates linearly (timer, not eased).
- Panel 01: dropped redundant "Every command reads this…" footer; moved the
commands meta into the visual as a right-side sidebar aside PRODUCT.md.
- Panel 02: "Browse the full catalog →" moved under the Gallery of Shame;
pattern category tabs are now always a single-row horizontal scroll with
JS-tracked edge-fade mask and chevron affordances; click centers the
selected tab inside the strip (never scrolls the page).
- Panel 04: "register" → "mode" in body, labels, meta for plain-language.
- Panel 05: removed redundant "Works in Claude Code…" meta.
- Panel 06: removed "Spec-compliant. Interoperable. Not a proprietary
sidecar." meta.
- .language-content grid gap reduced from --spacing-lg to --spacing-sm so
the commands palette sits closer to the section lead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a coarse-controls ("Tune") popover that slides out from behind the
contextual bar via clip-path, showing 2-4 per-variant knobs (range / steps /
toggle) driven by a `data-impeccable-params` manifest. Range/toggle drive
CSS custom properties on the variant wrapper; steps toggle a data
attribute. Values reset per variant; on accept, current values are passed
through live-poll to live-accept as an `impeccable-param-values` comment
for the carbonize cleanup step to bake in.
Other live-UI work in this change:
- Theme-aware palette (barPaletteForTheme) now drives the contextual bar,
action picker, and tune popover. Dark sand on light pages, paper on
dark. Detection has a localStorage dev override for QA.
- Action picker chips get inline SVG icons (wand / bars / funnel / sparkle /
type ramp / circles / grid / devices / curve / star / bolt) stacked
above the label; selection state recolors via currentColor.
- Accept button switched to saturated site magenta with paper text.
- Cycle dots reworked: solid accent for active, neutral for arrived,
hairline ring for pending. No more magenta-on-gray noise.
- Tune chip sits in the cycling row with a count pill badge; open state
uses accentSoft bg + accent text (no ad-hoc white border).
- Popover uses the bar's palette with a deeper surface (surfaceDeep),
lives behind the bar via z-index so a 6px overlap reads as tucked under
it, and animates with clip-path inset() for reliable slide behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reorder and relabel all 8 tabs (Prevents AI slop, Brand + Product design, Ships code not mockups, Fluent in DESIGN.md, etc.)
- Flip every panel to title → body → visual → meta so the claim leads and the visual serves as proof
- Move "The Case" section header above the tab layout, matching the other sections
- Let panel visuals span the full right column; cap only the text at 60ch
- Wrap the right-side content in a paper card; strip .why-visual chrome to avoid nested cards
- Add a heading to panel 02 (Prevents AI slop) so it matches the others
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove section 03 (The Antidote) entirely as a standalone section.
Bring the original pattern list (patterns-tabbed via /api/patterns)
and gallery-stack-container back intact, and move them into a new
8th tab in The Case: "Anti-patterns, caught". The new tab panel
shows the list + gallery side-by-side on wide viewports, stacked on
narrow. Keeps the existing app.js wiring (renderPatternsWithTabs
and initGalleryStack both auto-discover via IDs).
Section numbers renumbered: Case 04→03, Live 05→04, Install 06→05,
Changelog 07→06, FAQ 08→07. Sticky nav updated to match.
Replaced the tall page-screenshot "before" images with the
self-contained inline visual examples from the /anti-patterns
catalog (VISUAL_EXAMPLES dict). Three before/after pairs:
1. Gradient Text — purple-blue "Build the Future" gradient text vs.
a solid italic Cormorant "Build the future." with magenta emphasis.
2. Side-Tab Cards — the ban signature, thick colored left stripe on
an otherwise neutral card vs. a hairline-bordered card with a mono
kicker and italic title.
3. Hero-Metric Template — the SaaS gradient "10M+" with tiny stats
vs. a single "+18.4%" italic insight with real explanation and
"from checkout fixes" causal tail.
Panels are now ~140px tall (was 4:3 aspect of full-page screenshots),
so the whole section is roughly half as tall. No large images, no
risk of the "Before" screenshots reading as Impeccable output. The
After panels sit on warm cream (brand surface) while the Before
panels use neutral cool white to further distance them visually.
The "wall of three screenshots" alone looked, at a glance, like
examples of pages made WITH Impeccable. Wrong signal. Replace each
card with a vertical before/after pair: anti-pattern screenshot on
top, hand-built refined alternative underneath. Pair labels use
colored pills (before = crimson, after = green) on the image corners
so the contrast is immediate. Three pairs shown:
1. Purple Gradients → warm editorial hero (cream bg, italic serif
title with magenta emphasis word, squared ink CTA, no gradient)
2. Cardocalypse → flat data table (mono labels, display numbers,
green delta, hairline rules between rows, no nested boxes)
3. Hero-Metric Template → one honest insight (single 52px italic
number with percent accent, body explanation mentioning the
actual reason, subtle sparkline)
Lead copy updated: "Before, and what Impeccable steers toward instead."
After mocks are pure inline HTML/CSS — no new image assets needed,
no font dependencies, fully respect brand (Cormorant italic display,
Instrument Sans body, magenta accent, warm cream bg, sharp CTAs).
Previous layout was a wall of text (three numbered stat rows plus a
side gallery). User feedback: "seeing is believing." Drop the stats
entirely, lead with three big anti-pattern screenshots in a 3-up grid
(Purple Gradients / Cardocalypse / Hero-Metric Template), tight
one-line lead above, footer row with "Browse all 25" link + a small
mono meta line that still carries the key claims (deterministic rules,
LLM layer, browser overlay) in a single sentence.
Images use 4:3 aspect, hover lifts them slightly with a magenta-soft
outline glow, labels are italic display face + mono accent number.
The whole section now reads at a glance: headline + three pictures +
one link.
Content: rewrite the stats to actually sell the differentiator.
- Row 1: "25 Named anti-patterns" — vs. the short prose lists in
comparable skills (Anthropic's frontend-design, assorted "AI design"
skills).
- Row 2: "2 Detection layers" — deterministic (source + DOM, no LLM)
+ LLM semantic pass via /impeccable critique.
- Row 3: "3 Surfaces" — CLI for PR gating, Chrome extension on any
page, overlay inside /impeccable critique.
Lead copy amplified: "list a handful of don'ts in a document and hope
the model remembers" vs. "actively detects them."
Layout: grid-template-columns is now minmax(0, 1fr) minmax(320px,
420px), align-items: start. The row fills the section's full width,
left column sits left-aligned against the intro text, gallery stays
to the right in its natural 320-420px range. Stat numbers shifted
to magenta accent + bolder 3rem italic, rows separated by a dashed
hairline, with bold ink lead-words ("Named anti-patterns" etc.) in
the descriptions for scannability.
- Drop the black rule above the support list (it read as a hard hat
over the column).
- Drop the phase-caption strip below the frame (the demo already
carries its own state visually; the narrated loop underneath was
noise).
- Rewrite the four support cells with stronger POV:
- Start: the actual commands, /impeccable live and stop.
- Your agent, your code: emphasises that it's the same AI you've
been working with, reading your real project context, generating
real edits.
- Accept = write: canvas-like iteration, production output. The
point almost no other AI design tool can claim.
- Skill-native: no external tools or IDE extensions, works across
every harness the skill targets.
- Drop CSP-patch bullet (edge case, not worth homepage real estate).
- Drop the "Accepts into ... via HMR" framing — accepts work because
of the skill loop, not because of HMR per se.
Four responses to feedback:
1. Language: fold "Pick any command to see it in action / Full command
reference →" into the section-lead. The whole commands-header row
is gone; the fisheye panel now sits much closer to the lead copy.
2. Antidote: full two-column editorial split. Left column carries the
lead + a three-row stats block (25 / 6 / 3) and the "Browse full
catalog" CTA as a squared editorial link. Right column is the
Gallery of Shame card stack. No more empty right-of-gallery void.
3. The Case: tab labels switch from Cormorant italic serif to body
sans. Tab numbers stay mono. Less aesthetic noise, cleaner reading.
4. Live Mode:
- Layout split: the frame now sits in the left column with its
caption; the support list ("Works on / Accepts into / CSP /
Overlay") moves to a narrow right column, rule-delimited, with
magenta-kicker-above-ink style per cell. Under the support list
on desktop, stacks on mobile.
- Skeleton page: nav bar (logo + 3 links + CTA) plus heading +
body lines drawn in at 30% opacity behind the target card, so
the iteration reads as "I clicked an element on an actual page."
- Variant cards rewritten with distinct design language per card:
V1 editorial column (rule-top, italic display, text-link button),
V2 brutalist ink ticket (ink slab, magenta accent stripe, stamp
glyph, sans bold), V3 playful dashed postcard (stickers, italic
Cormorant, pill CTA). The demo now actually shows three visually
different directions instead of three similar cards.
Antidote (task b): drop the duplicated pattern tabs (content lives on
/anti-patterns for the full list). Keep the Gallery of Shame as the
signature visual and add a "Browse the full catalog" link underneath.
Section is roughly 40% shorter vertically.
Live Mode demo (task faithful): rewrite to match the real picker flow
one-for-one. Now two separate bars:
- Global bar: dark pill, bottom of the frame, persistent for the
whole session. / + Pick (active) + Detect + DESIGN.md + divider + ✕.
- Contextual bar: LIGHT paper-backed pill that floats above the
picked element, mirroring buildConfigureRow() in live-browser.js.
Dark command pill, transparent input, ×N count, magenta Go button.
Morphs through configure → generating (spinner) → cycling (prev /
counter / next / discard / Accept) → accepted (green ✓ written to
source).
Contextual bar positions itself below the target when space allows,
else above. New JS positionCtx() mirrors the real positionBar()
logic. prefers-reduced-motion still freezes on a representative
"cycling, variant 3" still.
Timeline unchanged in steps but step captions updated to match the
new two-bar split ("Contextual bar appears." etc.).
Two homepage tweaks:
- Section 02 (The Language) gets a pill-style Palette / Periodic view
toggle at the top-right of the intro row. Palette (fisheye command
gallery) is default; Periodic (the periodic table) is behind a tab
click. Collapses the section's vertical footprint substantially
without losing the signature periodic-table visualization.
- Section 04 renamed "Why Impeccable" → "The Case". Matches the
editorial voice of "The Language" / "The Antidote" / "The Editors"
(changelog signature). Sticky nav label Case.
Section 05 is now "Live Mode" with a self-contained real-DOM animated
demo loop. Browser chrome + stage + picker bar + simulated cursor,
running through the full Live Mode flow:
hover → outline → click → open command picker → pick "delight" →
type "more playful" → draw stroke → Go → generating → three
variants cycled → accept → write-to-source confirmation → reset
Three distinct card variants morph in place so the cycling reads as
real iteration, not a slideshow of the same card. Picker bar states
(idle / configuring / generating / cycling / accepted) are driven by
data-phase and matching CSS selectors. Simulated cursor animates
between targets on an expo-out curve; annotations draw a scribble
stroke and pop a comment tag. IntersectionObserver gates the timeline
so it only plays while visible, and prefers-reduced-motion freezes
on a cycling / variant 3 still.
Supporting row below the frame adds the three honest limitations:
frameworks supported (Vite/Next/SvelteKit/Astro/Nuxt), writes to real
source via HMR, CSP-strict apps get a one-time dev-only patch.
Top-nav rename "Visual Mode" → "Overlay" (points at /visual-mode
which is still the overlay-focused sub-page). Sticky section-nav
updated too.
New file: public/js/components/live-demo.js with the timeline driver.
Three polish items on the Why Impeccable section:
- Move the "04 / Why Impeccable" section header into the left column
next to the tab list. The right-side panel now aligns vertically
with the heading instead of sitting offset below it.
- Replace the outer border-top + per-tab border-bottom with an
adjacent-sibling combinator (li + li). No more hairline above the
first tab or below the last one.
- Rework the active-state progress bar. Previously the is-active
border-left-color was fully magenta, which hid the countdown
animation underneath it. Now the active state is driven entirely
by a 2px progress column that either sits full-height (paused or
user-selected) or fills from top to bottom over the cycle window
on a gentle ease curve. The countdown is actually visible now.
Default browser focus-visible ring was a heavy blue outline that
clashed with the dark capsule. Inject a palette-aware inner box-shadow
ring (accentSoft + accent) scoped to the global bar buttons via a
one-time <style> tag. Keeps keyboard focus visible without the
visual noise.
Previously Escape while picking just hid the highlight and set state
to IDLE, leaving the global bar's Pick button visually active. Next
pick attempt fell into a broken state where the button looked on but
no picker ran. Route through togglePick() so the flag, the button,
the UI, and the state all flip together.
Small copy fix on Why panel 04: Figma stamp now reads "last touched
Q3 2025" (was 2024; should reflect closer to the current calendar).
Previous hardening emitted a 7-line todo array and a 10-line ASCII-bar
stderr banner on every accept event, both printed to the agent's
transcript. Per-event overhead added up fast on multi-variant sessions.
Keep the three-layer defence but shrink the per-event noise:
- todo is now a single string: "REQUIRED before next poll: carbonize
cleanup in FILE. See reference/live.md ..."
- stderr is one line with the same pointer.
- reference/live.md keeps the full five-step checklist (loaded once
per session, so its verbosity is a fixed cost — no repetition tax).
Attention signal still triple-redundant: stdout todo, stderr line,
reference section.
After an LLM-triggered session where the carbonize cleanup got skipped
entirely (the instruction was buried as a single bullet among four
cases in live.md, and `_acceptResult.handled: true` felt like a
"done" signal), add three redundant reinforcements:
1. live-accept.mjs now emits a `todo` array on the event payload when
carbonize is true, listing all five cleanup steps plus a pointer to
reference/live.md. The agent reads this as part of the event JSON.
2. live-poll.mjs prints a loud multi-line stderr banner on every
carbonize=true event. Even agents that parse only stdout JSON see
stderr output and can't treat the event as handled without action.
3. reference/live.md pulls the carbonize branch out of the "Handle
accept" bullet list into a dedicated "Required after accept
(carbonize)" section with a numbered five-step checklist, a
rationale paragraph for why skipping is bad, and an explicit
"do not poll again until the file is clean" instruction.
The three layers are deliberately redundant: a future LLM that ignores
the reference text should still be caught by the stderr banner or the
in-event todo, and vice versa.
Per reference/live.md's carbonize instructions (which I should have
followed immediately after the accept event):
- Moved the inline @scope CSS into public/css/workflow.css, rewritten
to use the real .why-visual--v2 class instead of
[data-impeccable-variant="2"].
- Removed the <style data-impeccable-css="afb9794d"> block from index.html.
- Removed both impeccable-carbonize-start / -end marker comments.
- Unwrapped the <div data-impeccable-variant="2" style="display: contents">
wrapper around the accepted variant content.
- Dropped the dead @scope rules for variants 1, 3, 4 entirely.
Only load-bearing element classes remain (why-visual--v2, v2-side,
v2-label, v2-canvas-*, v2-new-*) and they're all styled via the
central stylesheet now.
Alignment: .why-section had its own max-width + horizontal padding,
which made the whole section sit inside a narrower column than every
other section. Drop the extra wrapping; match the .antidote-section /
.visual-mode-section pattern (padding: 2xl 0, border-top).
Panel 04: scrap the "your IDE + AI agent" browser-chrome surface
(confusing, implied a code editor with a fake browser top bar, and
never mentioned what the point actually is). Replace with two side-by-
side tiles: dark "Your AI agent" tile + warm "Your browser / Live Mode"
tile, joined by a magenta "+" glyph. The line underneath lands the
point: two surfaces you already have open, zero handoff.
Addressing feedback that the first round didn't sell the differentiation:
01 Intentional design: scrap the "generic AI card vs warm card" comparison
(the slop card actually looked good, which undermined the point). Show
PRODUCT.md as a real file with fields filled in (Users, Register,
Brand voice, Anti-references) and a pulse-dot footer signaling it's
loaded on every command. The point is that Impeccable knows specific
things about the product before it designs anything.
03 Production codebases: expand the terminal from a single "reading
DESIGN.md" line to a full codebase walk. Tokens from tokens.css,
merged tailwind theme, 34 components listed with variants inferred,
DESIGN.md as one of many inputs. DESIGN.md is shallow; the real work
is traversing source.
04 Where you code: scrap the card grid. Replaced with a two-row
waterfall-vs-unified flow. Old way (greyed, strikethrough): Figma →
Handoff → Ticket → Engineer → QA drift. New way: a single surface
with Design / Code / Ship chips inside a fake IDE chrome. Pulls the
real argument instead of just showing logos.
05 DESIGN.md: picker-sidebar-style rich tiles. Accent color with
8-step tonal ramp, Aa specimens for display + body, component chips +
input preview. Stitch spec badge up top. Matches the visual vocabulary
of the live-mode DESIGN.md sidebar.
Panels 02, 06, 07 unchanged per feedback that they were working.
Per-panel storytelling visualizations, pure HTML/CSS, no image assets:
- 01 Intentional design: "Generic AI" dark/purple gradient card vs.
warm editorial card with /impeccable vocabulary side by side.
- 02 Brand and product, both: tiny brand mock (italic display headline)
vs. product mock (mono/stats rows).
- 03 Production codebases: dark terminal showing /impeccable polish
reading DESIGN.md tokens and component APIs.
- 04 Where you code: prompt bar with blinking caret + 4×2 grid of
harness logos (Claude, Cursor, Codex, Gemini, Copilot, Antigravity,
Kiro, OpenCode).
- 05 DESIGN.md: a file-view of the six Stitch sections with a
"Stitch spec" badge, plus an interop tagline.
- 06 CI/CD: terminal showing `impeccable detect` failing CI with
three issues and exit 1.
- 07 Chrome extension: browser chrome + floating extension popup
listing detections and two magenta outline boxes over "page content".
Auto-rotation: 7s per tab, pauses on hover, stops entirely on any
click/keyboard interaction (user-initiated navigation wins). Thin
magenta progress bar animates on the active tab's left accent as
the rotation progresses. IntersectionObserver gates the whole timer
so it only runs while the section is on screen. prefers-reduced-motion
disables the auto-rotation and the progress animation.
Dropped the "Seven reasons..." lead line.
New section 04 with a vertical tab list on the left and a panel on the
right. Seven items covering the actual differentiators:
1. Intentional design — PRODUCT.md + DESIGN.md + shape as the opposite
of one-shot-mock tools.
2. Brand and product, both — the register split, called out against
frontend-design-style single-mode skills.
3. Production codebases — reads your design system, daily-driver.
4. Where you code — IDE-native, no canvas, no handoff.
5. DESIGN.md — Google Stitch spec-compliant, interoperable.
6. CI/CD-ready — CLI + JSON output for PR gates.
7. Chrome extension — 25 checks, one click, any page.
Section numbers bumped: Visual 04→05, Install 05→06, Changelog 06→07,
FAQ 07→08. Sticky nav updated to match.
Arrow-key tab navigation, aria-selected, hidden attr on inactive
panels, subtle fade on switch, respects prefers-reduced-motion.
Vertical layout on desktop, stacked on mobile.
The DO/DONT extractor in readPatterns stopped finding anything after
the skill went human-authored (new SKILL.md uses plain bullets, one-word
section headings, no DO:/DON'T: prefixes). Replace the extractor with
a hand-curated category list: six categories, three or four DOs and
DON'Ts each. Editorial tone, tight, deliberately a teaser — the full
catalog still lives on /anti-patterns.
The legacy SKILL.md parser is retained in the file as _legacyReadPatterns
in case we want to revive it later with a different format.
Small CSS fix: .faq-question was display:flex with justify-content:
space-between, so inline <code> in a summary got treated as its own
flex item and pushed apart. Switched to relative+absolute positioning
so the + icon sits in the right margin and text flows naturally.
Changelog font size reduced to 0.9375rem for the expanded v3.0 entry.
New FAQ entry on pinning standalone commands back. Pin section added
to /docs/impeccable editorial.
- v3.0 changelog now covers 6 items (was 2): Live Mode, PRODUCT.md,
DESIGN.md (with Google Stitch spec compliance), brand/product
registers, plus the existing consolidation + pin/unpin.
- New FAQ entry answering "how do I get /critique back" via pinning.
- Added Pin commands back as shortcuts section to /docs/impeccable
editorial so it's findable outside the FAQ.
- Reduced changelog item font size (0.9375rem) to match length.
- Swept user-facing copy: .impeccable.md -> PRODUCT.md, removed three
stale npx impeccable live references (the CLI subcommand no longer
exists; live is /impeccable live inside the skill).
- Historical v1.5.0 changelog entry preserved as-is (period-accurate).
Three related site scroll bugs:
1. initAnchorScroll and initHashTracking both called scrollTo with
`behavior: 'auto'`, which defers to CSS `scroll-behavior`. Because
sub-pages.css set `html { scroll-behavior: smooth }`, every anchor
jump and reload-hash-restore animated — despite a code comment
explicitly stating "Instant anchor scroll — no smooth scrolling".
Switch to `behavior: 'instant'` so the JS wins.
2. The reload-hash restore used a fixed `setTimeout(100)` to compute
target position. At 100ms, async Google Fonts (Cormorant Garamond
italic) has not swapped in, so `getBoundingClientRect().top` is
computed against fallback metrics and mislanded by hundreds of
pixels. Retry on `document.fonts.ready` and on window `load`.
3. Remove `scroll-behavior: smooth` from sub-pages.css entirely —
it was silently fighting the JS and made long-page anchor clicks
feel sluggish.
The inline pre-restore wasn't actually fixing a timing issue — the
fix was the fonts.ready + load retries. Since live.js's own
top-level block runs before DOMContentLoaded and we can do the same
retries there, we don't need an inline script injected into every
user page. Simpler HTML, single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scrollTo(y) clamps to the current document.scrollHeight, which is
several hundred pixels short of the final value until async-loaded
fonts swap in (Cormorant Garamond italic grew consulting-section
layout by ~585px in the logs). The initial synchronous scroll was
clamping to ~6165 even though the Go-time target was 6749.5.
Retry on document.fonts.ready and on the window load event, both of
which fire once the document reaches its final height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
external live.js is fetched, so by the time it runs the browser has
already queued its reload-scroll animation and history.scrollRestoration
='manual' has no effect. Inject a tiny inline synchronous <script> into
the same block live-inject writes, BEFORE the external live.js tag. It
sets scrollRestoration='manual' and does window.scrollTo(0, savedY)
during HTML parse — before the browser can animate anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
startScrollLock calls stopScrollLock at the top as a reset. I had
clearScrollY() inside stopScrollLock, so every Go sequence was:
writeScrollY(6749.5) → startScrollLock → stopScrollLock → clearScrollY
— the persisted value was wiped right after being written, so resume
after reload read null and locked to 0.
Move clearScrollY to the three genuine session-end sites (hideBar
error path, confirmed/accept, cleanup/discard). stopScrollLock no
longer touches persistent storage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete bugs from the diagnostic logs:
1. saveSession was writing scrollY alongside state, so every call during
resumeSession clobbered the Go-time value with whatever the browser
had left us at (typically 0). Move scrollY to its own localStorage
key, touched only at Go and on user-scroll reanchor.
2. history.scrollRestoration='manual' was being set inside init() at
DOMContentLoaded — by then the browser has already started animating
its restore, especially with scroll-behavior: smooth on html. Apply
it at script parse time, and apply the saved scrollY immediately
there too, before the browser's animation starts.
3. Corrections only fired on MutationObserver. A programmatic smooth
scroll (browser restore animation, or another script calling
scrollIntoView) produces zero DOM mutations — so we never caught it
walking scrollY from 0 up to 4800+ in the recorded session. Snap
back on every scroll event, gated by a 250ms user-gesture window so
we don't fight real user scrolls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log target-Y at Go, every mutation that triggers a correction (with the
mutation type + added nodes), every correct-or-noop (with from/to/delta),
every reanchor, and every external scroll event >5px. Lets us see which
step is actually moving the page during wrap / variant insert.
Element-based scroll tracking broke every time: Bun's HMR destroys the
target element, the browser's scroll anchoring picks a different nearby
element (e.g. the #downloads CTA) as its new anchor, and the page jumps
to wherever that surviving element is. My element-based correction then
computes against a replaced DOM node with stale / wrong geometry.
The primitive the user actually cares about is window.scrollY — they
want the page to stay where it is, regardless of which element survives
the patch. Pin scrollY directly: capture it at session start, restore it
on every mutation inside the wrapper, re-anchor on user scroll, store it
in saveSession for reload-resume.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>