Compare commits

..
35 Commits
Author SHA1 Message Date
Paul BakausandClaude Opus 4.7 bf2bc55aa1 Fold v3.0.3 changelog into v3.0.4
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>
2026-04-28 10:40:42 -07:00
Paul BakausandClaude Opus 4.7 a923346bcc Add release tooling and bump CLI to 2.1.8
- 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>
2026-04-28 10:05:29 -07:00
Paul Bakaus 5f5e2b013d Release impeccable skill v3.0.4 2026-04-28 00:16:30 -07:00
Paul Bakaus 54f6ccf6f0 codex in auto-review became way too autonomous - significantly harden craft/shape flows 2026-04-27 23:41:23 -07:00
Paul BakausandGitHub 62ce35ac8e Merge pull request #116 from pbakaus/feat/live-modal-host-friendliness
feat(live): make picker chrome modal-host friendly (Radix, Headless UI, vaul)
2026-04-27 16:39:39 -07:00
Paul BakausandClaude Opus 4.7 630e586b01 feat(live): make picker chrome modal-host friendly (Radix, Headless UI, vaul)
Closes #113.

Picker chrome could become unclickable inside Radix Dialog portals, and
clicking it dismissed the host dialog. Three orthogonal issues surfaced
during manual verification:

1. Modal-aware chrome
   - Add `defangOutsideHandlers` and apply it to bar, picker, params
     panel, annotation overlay, global bar, and design panel host.
   - Sets `pointer-events: auto !important` on interactive chrome so
     Radix's `body { pointer-events: none }` modal scroll-lock can't
     silence our UI.
   - Stops `pointerdown` / `mousedown` / `focusin` propagation at the
     chrome boundary so DismissableLayer / FocusScope outside-handlers
     never fire for clicks that land on us.

2. detectPageTheme: misread transparent body as black
   - `getComputedStyle(body).backgroundColor` returns `rgba(0,0,0,0)`
     when no bg is set; the prior regex captured (0,0,0) and ignored
     alpha, calling every default-bg page "dark."
   - Honor alpha, walk body → html, fall back to
     `prefers-color-scheme` only when both are transparent.

3. Exit X invisible on host pages with `button { padding: ... }`
   - Every other chrome button sets padding inline; exitBtn didn't.
     Host resets like `button { padding: 0.5rem 1rem }` (in the new
     fixture, common in the wild) inflated the 24x24 button into 56x40
     and pushed the SVG into a non-rendering region — DevTools showed
     the right styles, the X just didn't paint.
   - Pin `padding: 0` + `box-sizing: border-box`, match the toggle
     icon spec (14 / stroke 1.5 / textDim → text on hover).

4. Toast no longer obscures the global bar
   - Position the toast above globalBarEl's actual rect instead of a
     fixed bottom: 16px that overlapped the bar's bottom: 14px.

Test coverage: new `vite8-react-radix-dialog` fixture exercises the
full pick → Go → cycle → Accept loop with `@radix-ui/react-dialog`
+ `Portal` + `Overlay` + `Content`. Without the fix, clicking Go
dismisses the dialog and unmounts the picked element. All 20 live
e2e fixtures pass; all 180 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:22:00 -07:00
Paul BakausandGitHub 39bec7c08c Merge pull request #115 from pbakaus/feat/harden-jsdom-border-radius
feat(detector): harden border-radius reads against jsdom CSS regressions
2026-04-27 14:55:12 -07:00
Paul BakausandGitHub e3d488e123 Merge pull request #101 from voidborne-d/fix/windows-detect-path-drive-letter
fix: use fileURLToPath for Windows path resolution
2026-04-27 14:49:13 -07:00
Paul BakausandClaude Opus 4.7 668263843f test: wire windows-path-fix into bun test script + rebase notes
- 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>
2026-04-27 14:46:58 -07:00
voidborne-dandPaul Bakaus 94b315ef63 fix: use fileURLToPath for Windows path resolution (#95)
On Windows, `new URL(import.meta.url).pathname` returns `/C:/...`
(with a leading slash). Passing that to `path.resolve()` or
`path.join()` causes Node to prepend the drive letter again, producing
doubled paths like `C:\C:\Users\...\detect-antipatterns-browser.js`.

Replace both occurrences (puppeteer scan at ~L2690 and live detect at
~L3506) with `fileURLToPath(import.meta.url)` from `node:url`, which
correctly strips the leading slash on Windows while remaining a no-op
on POSIX.

Add regression tests verifying the source no longer uses the raw
`.pathname` accessor for local path construction and that
`fileURLToPath` handles both Windows and POSIX file URLs correctly.

Closes #95
2026-04-27 14:44:25 -07:00
Paul BakausandClaude Opus 4.7 28875097b0 fix(detector): preserve percent-radius signal when width is missing
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>
2026-04-27 14:33:54 -07:00
Paul BakausandClaude Opus 4.7 65bbd6cb5f feat(detector): harden border-radius reads against jsdom CSS regressions
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>
2026-04-27 14:09:49 -07:00
17fe31baa9 chore: bump in-range deps; pin jsdom to 29.0.0
- @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>
2026-04-27 13:59:06 -07:00
427128e073 chore: reconcile bun.lock with @anthropic-ai/sdk ^0.91.1
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>
2026-04-27 13:59:06 -07:00
70a9246401 fix(plugin): slim Claude Code install (291MB→770KB) + fix skills path
- 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>
2026-04-27 13:59:06 -07:00
Paul BakausandGitHub 8548003cc1 Merge pull request #111 from vivshaw/main
Rename all references to `Neon Mirai` -> `Neo Mirai`, to match case study site's actual content
2026-04-27 11:07:48 -07:00
vivshaw 579006cda5 fix: add missing trailing-slash redirect for /cases/neon-mirai/ 2026-04-26 19:30:09 -04:00
vivshaw ceb0ef8f67 chore: rename all references Neon Mirai -> Neo Mirai to match site title 2026-04-26 19:10:46 -04:00
Paul BakausandClaude Opus 4.7 25e6c820aa chore(site): bump GitHub star count to 22k
Live count is 21,826 stars; rounding up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:35:37 -07:00
Paul BakausandClaude Opus 4.7 6e96f62803 fix(live): readable freeform input on dark bar + tools/live-loop.mjs
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>
2026-04-25 01:35:32 -07:00
Paul Bakaus 74f16d6310 Add Neon Mirai showcase 2026-04-25 01:23:51 -07:00
Paul BakausandClaude Opus 4.7 d26ccac1be feat(test): pluggable LLM agent for live-mode E2E suite
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>
2026-04-25 01:06:06 -07:00
Paul BakausandClaude Opus 4.7 4310352423 fix(live): variant observer detects wrappers added as descendants
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>
2026-04-25 01:05:28 -07:00
Paul BakausandClaude Opus 4.7 89ffd73d4b improve(polish): make polish a true superset of retired /normalize
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>
2026-04-25 00:56:35 -07:00
Paul BakausandClaude Opus 4.7 4fa02bf573 docs: add live-mode E2E test instructions to CLAUDE.md and AGENTS.md
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>
2026-04-24 23:44:14 -07:00
Paul BakausandClaude Opus 4.7 7baf77a457 chore: bump impeccable skill to v3.0.1
User-facing changes shipped in this patch:
- Live mode runs in strict-CSP apps (auto-patches meta CSP, reverts on stop)
- Live mode survives conditional-render content (modal/tab/collapsible)
- Live mode no longer breaks JSX projects (carbonize stash + accept rewrite)
- SvelteKit hydration race fixed
- Headless Chromium WebGL fallback fixed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:39:49 -07:00
Paul BakausandClaude Opus 4.7 c3e18fe664 fix(live): four bugs surfaced by E2E suite + CSP meta auto-patch
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>
2026-04-24 23:37:45 -07:00
Paul BakausandClaude Opus 4.7 c8de59d81e test(live): add full-cycle E2E framework-fixture suite with pluggable agent
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>
2026-04-24 23:37:04 -07:00
Paul Bakaus d29a690797 Fix Neon Mirai active navigation 2026-04-24 23:18:23 -07:00
Paul Bakaus d340f075e8 Improve Neon Mirai manifesto artwork 2026-04-24 23:10:34 -07:00
Paul Bakaus 7de610c620 Add Neon Mirai conference example 2026-04-24 17:22:59 -07:00
Paul BakausandClaude Opus 4.7 25353448e2 fix(site): restore docs-viz-caption top margin squashed by .prose p
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>
2026-04-24 15:45:59 -07:00
Paul BakausandClaude Opus 4.7 346ce25952 docs(site): add image gen bullet and Live Mode alpha tag to v3.0 changelog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:21:01 -07:00
Paul BakausandClaude Opus 4.7 f5e82162c1 fix(site): strip dev-only live.js inject tags from production HTML
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>
2026-04-23 18:21:26 -07:00
Paul BakausandGitHub 6816558d7a Merge pull request #109 from pbakaus/3.0
v3.0: single /impeccable skill, Live Mode, /designing orientation, visualize-first
2026-04-23 18:08:34 -07:00
448 changed files with 30347 additions and 1652 deletions
+23 -2
View File
@@ -7,7 +7,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .agents/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `$impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `$impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `$impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `$impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -28,7 +47,7 @@ If the output is already in this session's conversation history, don't re-run. E
`$impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `$impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `$impeccable teach`, then resume the user's original task with the fresh context. If the original task was `$impeccable craft`, resume into `$impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `$impeccable document` for more on-brand output"*), then proceed.
@@ -141,6 +160,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `$impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `$<command>` invokes `$impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -29,7 +29,7 @@ Analyze where motion would improve the experience:
- Who's the audience? (Motion-sensitive users? Power users who want speed?)
- What matters most? (One hero animation vs many micro-interactions?)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: Respect `prefers-reduced-motion`. Always provide non-animated alternatives for users who need them.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
@@ -28,7 +28,7 @@ Analyze what makes the design feel too safe or boring:
- Who's the audience? (What will resonate?)
- What are the constraints? (Brand guidelines, accessibility, performance)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: "Bolder" doesn't mean chaotic or garish. It means distinctive, memorable, and confident. Think intentional drama, not random chaos.
@@ -30,7 +30,7 @@ Analyze the current state and identify opportunities:
- **Wayfinding**: Helping users navigate and understand structure
- **Delight**: Moments of visual interest and personality
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: More color ≠ better. Strategic color beats rainbow vomit every time. Every color should have a purpose.
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run $impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run $impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -166,7 +166,7 @@ Provocative questions that might unlock better solutions:
### Ask the User
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
**After presenting findings**, use targeted questions based on what was actually found. STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. These answers will shape the action plan.
Ask questions along these lines (adapt to the specific findings; do NOT ask generic questions):
@@ -37,7 +37,7 @@ Identify where delight would enhance (not distract from) the experience:
- **Helpful surprises**: Anticipating needs before users ask (productivity tools)
- **Sensory richness**: Satisfying sounds, smooth animations (creative tools)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: Delight should enhance usability, never obscure it. If users notice the delight more than accomplishing their goal, you've gone too far.
@@ -21,7 +21,7 @@ Analyze what makes the design feel complex or cluttered:
- What can be removed, hidden, or combined?
- What's the 20% that delivers 80% of value?
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: Simplicity is not about removing features - it's about removing obstacles between users and their goals. Every element should justify its existence.
@@ -66,7 +66,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P
- An existing `DESIGN.md` is stale (the design has drifted).
- Before a large redesign, to capture the current state as a reference.
If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user the existing file and ask the user directly to clarify what you cannot infer. whether to refresh, overwrite, or merge.
If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user the existing file and STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. whether to refresh, overwrite, or merge.
## Two paths
@@ -6,7 +6,7 @@ Identify reusable patterns, components, and design tokens, then extract and cons
Find the design system, component library, or shared UI directory. Understand its structure: component organization, naming conventions, design token structure, import/export conventions.
**CRITICAL**: If no design system exists, ask the user directly to clarify what you cannot infer. before creating one. Understand the preferred location and structure first.
**CRITICAL**: If no design system exists, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. before creating one. Understand the preferred location and structure first.
## Step 2: Identify Patterns
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
@@ -14,7 +14,7 @@ Push an interface past conventional limits. This isn't just about visual effects
This command has the highest potential to misfire. Do NOT jump straight into implementation. You MUST:
1. **Think through 2-3 different directions**: consider different techniques, levels of ambition, and aesthetic approaches. For each direction, briefly describe what the result would look and feel like.
2. **ask the user directly to clarify what you cannot infer.** to present these directions and get the user's pick before writing any code. Explain trade-offs (browser support, performance cost, complexity).
2. **STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.** to present these directions and get the user's pick before writing any code. Explain trade-offs (browser support, performance cost, complexity).
3. Only proceed with the direction the user confirms.
Skipping this step risks building something embarrassing that needs to be thrown away.
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
@@ -28,7 +28,7 @@ Analyze what makes the design feel too intense:
- What's working? (Don't throw away good ideas)
- What's the core message? (Preserve what matters)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
If any of these are unclear from the codebase, STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
**CRITICAL**: "Quieter" doesn't mean boring or generic. It means refined, sophisticated, and easier on the eyes. Think luxury, not laziness.
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to $impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use $impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `$impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: ask the user directly to clarify what you cannot infer. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `$impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase.
STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `$impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally ask the user directly to clarify what you cannot infer. whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state — modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow — only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+2 -2
View File
@@ -12,12 +12,12 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "3.0.0",
"version": "3.0.4",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
},
"source": "./",
"source": "./plugin",
"category": "design",
"homepage": "https://impeccable.style",
"tags": ["design", "frontend", "ui", "ux", "skills", "commands"]
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "3.0.0",
"version": "3.0.4",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
},
"homepage": "https://impeccable.style",
"repository": "https://github.com/pbakaus/impeccable",
"skills": "./.claude/skills"
"skills": "./.claude/skills/"
}
-257
View File
@@ -1,257 +0,0 @@
---
name: anti-patterns
description: Use when adding, modifying, or debugging an anti-pattern detection rule in this repo. Walks through the TDD recipe, the rule schema, all five plug-in points, jsdom constraints, and the post-implementation checklist. Trigger this for any work touching src/detect-antipatterns.mjs, tests/fixtures/antipatterns/, or extension/detector/.
tools: Read, Edit, Write, Glob, Grep, Bash, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp
---
# Anti-Pattern Engine Maintenance
This agent handles every step of adding or modifying an anti-pattern detection rule in the impeccable repo. The rule engine is wired into many places — tests, browser bundle, extension detector, extension panel JSON, homepage count, and the skill content — and missing a step causes silent drift between them.
## The five things that need to stay in sync
When you add a rule, all of these update or get regenerated:
| Where | What | How it stays in sync |
|---|---|---|
| `src/detect-antipatterns.mjs` `ANTIPATTERNS` | Rule metadata (id, category, name, description, skillSection, skillGuideline) and the detection logic (`checkXxx`) | **Hand-edited.** Source of truth. |
| `src/detect-antipatterns-browser.js` | Browser-bundled engine for the public site overlay | Generated by `bun run build:browser` |
| `extension/detector/detect.js` | Browser-bundled engine for the Chrome extension | Generated by `bun run build:extension` |
| `extension/detector/antipatterns.json` | Rule list (id, name, category, description) for the extension's devtools panel — drives rule toggles UI | Generated by `bun run build:extension` |
| `public/js/generated/counts.js` | `DETECTION_COUNT` integer for homepage display | Generated by `bun run build` |
| `source/skills/impeccable/SKILL.md` and `reference/*.md` | Design guidance that a human or LLM reads. Can reference anti-patterns in its own voice. | **Hand-edited**, alongside the rule. Drift is a code-review concern, not a programmatic one. |
The CLI (`bin/cli.js`) imports `ANTIPATTERNS` directly from `src/detect-antipatterns.mjs` — no separate sync needed.
## Rule schema
Each entry in the `ANTIPATTERNS` array (around src/detect-antipatterns.mjs:77) looks like this:
```js
{
id: 'icon-tile-stack', // kebab-case, unique, stable
category: 'slop', // 'slop' or 'quality' (see below)
name: 'Icon tile stacked above heading', // human-readable, used in extension UI
description: // 12 sentences. Used in CLI output, extension tooltips, web overlay labels
'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
skillSection: 'Typography', // OPTIONAL. The logical skill section this rule maps to; used for /docs/impeccable deep-links.
skillGuideline: 'large icons with rounded corners above every heading', // OPTIONAL. Canonical short phrasing for the rule; used in CLI output and as a linkable fragment.
}
```
### Categories
- **`slop`** = "AI tells". Patterns that scream *AI generated this*. Things like purple gradients, gradient text, dark glow accents, thick side borders, icon-tile-stacks. Flagging these is about taste and freshness, not correctness.
- **`quality`** = real design or accessibility issues regardless of who wrote the code. WCAG contrast, line length, padding, line height, justified text, skipped headings, etc.
If you're not sure, ask: *"would a human designer who's careful and tasteful still ship this?"* If no, it's `quality`. If they would (because it works fine, it just looks templated), it's `slop`.
### `skillSection` values
The value is used by `scripts/build-sub-pages.js` to build deep links into the impeccable docs page. Use one of the logical sections the skill groups rules under (e.g. `Typography`, `Color`, `Layout`, `Motion`, `Visual Details`). If the section you pick matches an `### Heading` somewhere in the skill body, the deep link will land precisely; otherwise it falls back to the section's top. Omit entirely for rules that don't have a natural home in the skill.
### `skillGuideline` phrasing
Canonical short phrasing for the rule (36 words). Used as the CLI output label when `npx impeccable detect` reports a violation, and as human-readable text in the extension's devtools panel. The skill's prose may or may not echo this phrasing verbatim — the skill is the design-guidance document, not a rule manifest.
Examples: `'AI color palette'`, `'large icons with rounded corners above every heading'`, `'WCAG AA contrast'`.
Omit if the rule doesn't need a short label (rare — only niche a11y-only rules).
## The TDD recipe (always do it in this order)
This order is non-negotiable. Fixture and failing test before implementation. The full suite must run between the rule going in and you committing.
### 1. Write the fixture (two-column convention)
A single HTML file at `tests/fixtures/antipatterns/{rule-id}.html` with two columns: left = should-flag, right = should-pass. Each test case carries a unique heading text so the test can match snippets back to expectations.
Convention skeleton:
```html
<!DOCTYPE html>
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; padding: 24px; }
.col h2 { font-size: 14px; text-transform: uppercase; }
/* ... per-case styles with EXPLICIT pixel dimensions (jsdom can't lay out) ... */
</style>
</head>
<body>
<div class="grid">
<div class="col" data-col="flag">
<h2>Should flag</h2>
<!-- 46 cases that should be flagged, each with a unique <h3> text -->
</div>
<div class="col" data-col="pass">
<h2>Should pass</h2>
<!-- 58 cases that should NOT be flagged: cover every false-positive shape you can think of -->
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>
```
The script tag at the bottom is critical — it lets you load the fixture in the browser via `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` (served by `server/index.js:62` route for `/fixtures/*`).
**Should-pass cases must cover the false-positive shapes you can think of in advance.** A good fixture has 5+ pass cases. The icon-tile-stack fixture covers: round avatar, wide thumbnail, side-by-side, no-icon, too-tiny, too-huge.
### 2. Write the failing test
Add to `tests/detect-antipatterns-fixtures.test.mjs` in its own `describe` block. Use the snippet-substring matching pattern — the test parses heading text out of each finding's snippet and asserts membership against expected lists:
```js
describe('detectHtml — {rule-id}', () => {
const SHOULD_FLAG = ['Heading One', 'Heading Two', /* ... */];
const SHOULD_PASS = ['Pass Heading One', /* ... */];
it('{rule-id}: flags only the should-flag column', async () => {
const f = await detectHtml(path.join(FIXTURES, '{rule-id}.html'));
const flagged = new Set();
for (const r of f) {
if (r.antipattern !== '{rule-id}') continue;
const m = (r.snippet || '').match(/"([^"]+)"/);
if (m) flagged.add(m[1]);
}
for (const text of SHOULD_FLAG) {
assert.ok(flagged.has(text), `expected "${text}" to be flagged`);
}
for (const text of SHOULD_PASS) {
assert.ok(!flagged.has(text), `"${text}" should NOT be flagged`);
}
});
});
```
For this to work, the rule's snippet **must include the heading text in quotes**. See "Snippet conventions" below.
Run `node --test tests/detect-antipatterns-fixtures.test.mjs` and **watch it fail**. If it doesn't fail, your test is wrong.
### 3. Add the rule definition
Add a new entry to the `ANTIPATTERNS` array in `src/detect-antipatterns.mjs`. Place it in the right category section (slop or quality). Fill in all fields including `skillSection` and `skillGuideline`.
### 4. Implement the pure check function
Add a `checkXxx(opts)` function alongside the others (`checkColors`, `checkBorders`, `checkMotion`, `checkGlow`, `checkIconTile`, etc.). The pure function takes a plain options object — no DOM access — and returns an array of `{ id, snippet }`. This makes it testable and reusable across the browser/Node adapters.
Example shape (see `checkIconTile` in src/detect-antipatterns.mjs for a real one):
```js
function checkXxx(opts) {
const { tag, /* whatever fields the rule needs */ } = opts;
if (SAFE_TAGS.has(tag)) return [];
// ... your detection logic ...
if (matches) {
return [{ id: 'rule-id', snippet: `... "${headingText}"` }];
}
return [];
}
```
### 5. Add the two adapters
Two adapters wrap the pure function with environment-specific input gathering:
- **`checkElementXxxDOM(el)`** — for the browser. Uses `getComputedStyle(el)` and `el.getBoundingClientRect()`.
- **`checkElementXxx(el, tag, window)`** — for jsdom (Node). Uses `window.getComputedStyle(el)` and **must read explicit pixel dimensions from `parseFloat(style.width)`** instead of bounding rects, because **jsdom does not lay out**`getBoundingClientRect()` returns 0×0 for everything.
If your rule needs vertical positioning info (e.g. "icon must be above heading"), that check is browser-only — gate it behind `if (headingTop && siblingBottom)` so the Node path skips it. The structural checks alone (sizes, sibling identity, classes) are enough for the fixture.
### 6. Wire into both element-iteration loops
Two loops iterate every element on the page. You need to add your DOM-adapter call to **both**:
- **Browser loop** at src/detect-antipatterns.mjs:1837 (`for (const el of document.querySelectorAll('*'))` with the `findings` spread). Add a line like:
```js
...checkElementXxxDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
```
- **Node (jsdom) loop** at src/detect-antipatterns.mjs:2058 (in `detectHtml`). Add a block like:
```js
for (const f of checkElementXxx(el, tag, window)) {
findings.push(finding(f.id, filePath, f.snippet));
}
```
Forgetting one of these is the most common mistake — the test passes but the live page doesn't show anything (or vice versa).
### 7. Decide whether the skill needs an update
If the rule introduces a new design concept not already covered by the impeccable skill, update `source/skills/impeccable/SKILL.md` (or the appropriate register file in `reference/editorial.md` / `reference/product.md`) to teach the concept. The skill is a design-guidance document — it doesn't need to echo every rule verbatim, and one skill line can cover multiple engine rules. Only add prose if there's a real gap in the guidance.
### 8. Run the build (regenerates everything)
```bash
bun run build && bun run build:browser && bun run build:extension
```
This regenerates:
- `src/detect-antipatterns-browser.js` (public-site detector)
- `extension/detector/detect.js` (extension detector)
- `extension/detector/antipatterns.json` (extension rule list, includes description)
- `public/js/generated/counts.js` (DETECTION_COUNT)
### 9. Run the test suite
```bash
bun run test
```
166 unit tests + N fixture tests, including your new one. All should be green.
### 10. Verify on a live page in the browser
Don't skip this. The jsdom path uses `parseFloat(style.width)` and the browser path uses `getBoundingClientRect()` — they can disagree. The fixture test catches one path; manual browser verification catches the other.
```
http://localhost:3000/fixtures/antipatterns/{rule-id}.html
http://localhost:3000/antipattern-examples/{your-example}.html (if relevant)
http://localhost:3000/ (no false positives on real pages)
```
Use the chrome MCP tools (`mcp__claude-in-chrome__navigate` + `mcp__claude-in-chrome__javascript_tool`) to inject `window.impeccableScan()` and read `.impeccable-overlay` / `.impeccable-label` from the DOM to verify. Don't try to screenshot — the overlays are decorative; read them programmatically.
## Snippet conventions
The fixture-test convention extracts the heading text from a finding's snippet using regex `/"([^"]+)"/` — so **wrap the identifying heading text in straight double quotes** in your snippet. Examples:
- `'80x80px icon tile above h3 "Lightning Fast"'`
- `'4.5:1 (need 4.5:1) — text #808080 on #3b82f6'` ← uses element identifiers instead, since this rule isn't anchored to a heading
If your rule isn't naturally anchored to a heading, pick another stable identifier (a class name, the parent element's text, etc.) and document the test pattern in the test itself.
## jsdom constraints (the most common gotcha)
- **No layout.** `getBoundingClientRect()` returns `0×0` always. Read `parseFloat(style.width)` and `parseFloat(style.height)` instead — jsdom does honor explicit pixel widths in `<style>` and inline styles.
- **`background:` shorthand isn't decomposed.** `style.backgroundColor` and `style.backgroundImage` may be empty even when `style="background: ..."` is set. The existing `resolveBackground()` and `resolveGradientStops()` helpers (src/detect-antipatterns.mjs:631 and src/detect-antipatterns.mjs:670) handle this — use them.
- **Computed colors are normalized in real browsers, not in jsdom.** A browser returns `rgb(59, 130, 246)`; jsdom may return the original hex. The `parseGradientColors()` helper handles both.
- **No SAFE_TAGS skipping for parent walks.** When walking ancestors, you don't get the `SAFE_TAGS` filter the main loop applies — be explicit.
## Where to find concrete example rules to learn from
- Simplest border check: **`side-tab`** — `checkBorders()` at src/detect-antipatterns.mjs:312
- Color/contrast with gradient handling: **`low-contrast`** — `checkColors()` at src/detect-antipatterns.mjs:339
- Element-relationship check (siblings): **`icon-tile-stack`** — `checkIconTile()` at src/detect-antipatterns.mjs:425
- Page-level / cross-element: **`flat-type-hierarchy`** — `checkPageTypography()` at src/detect-antipatterns.mjs:1080
- Motion/animation: **`bounce-easing`** — `checkMotion()` at src/detect-antipatterns.mjs:425
## Pre-commit checklist
Before you commit a new rule:
- [ ] Test passes: `bun run test` is green
- [ ] Build passes: `bun run build && bun run build:browser && bun run build:extension` is green
- [ ] Live verification: rule fires on a real page and produces zero false positives on the homepage `http://localhost:3000/`
- [ ] Both element loops were updated (browser DOM at line ~1846 + Node jsdom at line ~2058)
- [ ] Snippet format matches the test's extraction regex
- [ ] Fixture covers ≥4 should-flag and ≥5 should-pass cases
- [ ] Skill reviewed: if the rule introduces a new design concept, the relevant skill file teaches it
- [ ] Commit only the relevant files — `git status` will show many unrelated stale skill builds; do not stage them
## Things that have bitten previous sessions
- **Forgot to run `bun run build:extension`** — extension JSON went stale, missing the new rule. Symptom: extension panel doesn't show toggle for new rule. Fix: always run all three build commands.
- **Forgot to update both loops** — test passed in jsdom but live browser was silent (or vice versa). Fix: grep for an existing rule's adapter call and copy its placement.
- **Wrote the fixture without explicit pixel dimensions** — jsdom returned 0×0 and the rule never matched. Fix: always set `width: Npx; height: Npx` in CSS for fixture elements, or use inline style attributes.
+24 -3
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
@@ -13,7 +13,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .claude/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -34,7 +53,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -147,6 +166,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. STOP and call the AskUserQuestion tool to clarify.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. STOP and call the AskUserQuestion tool to clarify.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
STOP and call the AskUserQuestion tool to clarify. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
STOP and call the AskUserQuestion tool to clarify. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: STOP and call the AskUserQuestion tool to clarify. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: STOP and call the AskUserQuestion tool to clarify. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
STOP and call the AskUserQuestion tool to clarify. Focus only on what you couldn't infer from the codebase.
STOP and call the AskUserQuestion tool to clarify. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), STOP and call the AskUserQuestion tool to clarify. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), STOP and call the AskUserQuestion tool to clarify. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally STOP and call the AskUserQuestion tool to clarify. whether they'd like a brief summary of PRODUCT.md appended to CLAUDE.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally STOP and call the AskUserQuestion tool to clarify. Ask whether they'd like a brief summary of PRODUCT.md appended to CLAUDE.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+24 -3
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
---
@@ -9,7 +9,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .cursor/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -30,7 +49,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -143,6 +162,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: ask the user directly to clarify what you cannot infer. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: ask the user directly to clarify what you cannot infer. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase.
ask the user directly to clarify what you cannot infer. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally ask the user directly to clarify what you cannot infer. whether they'd like a brief summary of PRODUCT.md appended to .cursorrules for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .cursorrules for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+24 -3
View File
@@ -1,14 +1,33 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
---
Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft.
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .gemini/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -29,7 +48,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -142,6 +161,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: ask the user directly to clarify what you cannot infer. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: ask the user directly to clarify what you cannot infer. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase.
ask the user directly to clarify what you cannot infer. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally ask the user directly to clarify what you cannot infer. whether they'd like a brief summary of PRODUCT.md appended to GEMINI.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to GEMINI.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+24 -3
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
@@ -11,7 +11,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .github/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -32,7 +51,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -145,6 +164,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: ask the user directly to clarify what you cannot infer. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: ask the user directly to clarify what you cannot infer. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase.
ask the user directly to clarify what you cannot infer. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally ask the user directly to clarify what you cannot infer. whether they'd like a brief summary of PRODUCT.md appended to .github/copilot-instructions.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .github/copilot-instructions.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+24 -3
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
---
@@ -9,7 +9,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .kiro/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -30,7 +49,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -143,6 +162,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
+6 -4
View File
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
+1 -1
View File
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
+29 -9
View File
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: ask the user directly to clarify what you cannot infer. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: ask the user directly to clarify what you cannot infer. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase.
ask the user directly to clarify what you cannot infer. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), ask the user directly to clarify what you cannot infer. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally ask the user directly to clarify what you cannot infer. whether they'd like a brief summary of PRODUCT.md appended to .kiro/settings.json for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .kiro/settings.json for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {
@@ -149,13 +149,16 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const replacement = [];
if (cssContent) {
const isJsx = commentSyntax.open === '{/*';
replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-start ' + id + ' ' + commentSyntax.close);
replacement.push(indent + '<style data-impeccable-css="' + id + '">');
// JSX targets need the CSS body wrapped in a template literal so that the
// `{` and `}` in CSS rules don't get parsed as JSX expressions.
replacement.push(indent + '<style data-impeccable-css="' + id + '">' + (isJsx ? '{`' : ''));
// Re-indent CSS content to match
for (const cssLine of cssContent) {
replacement.push(indent + cssLine.trimStart());
}
replacement.push(indent + '</style>');
replacement.push(indent + (isJsx ? '`}</style>' : '</style>'));
if (paramValues && Object.keys(paramValues).length > 0) {
// Preserve the user's knob positions for the carbonize-cleanup agent
// to bake into the final CSS when it collapses scoped rules.
@@ -169,8 +172,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
// in a data-impeccable-variant="N" div with `display: contents` (so layout
// isn't affected). The carbonize agent strips this attribute + wrapper when
// it moves the CSS to a proper stylesheet.
//
// Style attribute syntax has to follow the host file's flavor — JSX files
// need the object form, otherwise React 19 throws "Failed to set indexed
// property [0] on CSSStyleDeclaration" while parsing the string char-by-char.
if (cssContent) {
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" style="display: contents">');
const isJsx = commentSyntax.open === '{/*';
const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"';
replacement.push(indent + '<div data-impeccable-variant="' + variantNum + '" ' + styleAttr + '>');
replacement.push(...restored);
replacement.push(indent + '</div>');
} else {
@@ -344,7 +353,11 @@ function extractCss(lines, block, id) {
}
if (inStyle) {
if (line.trimStart().startsWith('</style>')) break;
// Detect </style> anywhere on the line — JSX template-literal closes
// (`}</style>`) put the close mid-line, and we don't want to absorb the
// template-literal punctuation as CSS content.
const closeIdx = line.indexOf('</style>');
if (closeIdx !== -1) break;
content.push(line);
}
}
+212 -25
View File
@@ -197,6 +197,45 @@
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// Modal-aware chrome: keep our floating UI clickable inside Radix /
// Headless UI / vaul portals.
//
// Two host-page behaviors break us when the picked element lives inside a
// modal dialog:
//
// 1. Modal scroll-lock disables outside pointer events. Radix's
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
// while a modal is open and only restores `auto` on the layer. Our
// chrome inherits `none` from <body> and becomes unclickable.
// 2. The dialog's outside-interaction handler (Radix's
// `usePointerDownOutside`) listens at document level and dismisses
// the dialog whenever a `pointerdown` lands outside the layer node.
// Our chrome is a sibling of <body>, so Radix classifies our clicks
// as outside and tears the dialog down mid-task.
//
// We can't reliably re-parent our chrome into the dialog subtree (z-index
// stacking, scroll containers, theming all become host-page concerns), so
// we defang both behaviors at our root:
//
// - `pointer-events: auto !important` overrides the inherited `none`.
// - Stop `pointerdown` / `mousedown` propagation so the document-level
// dismiss listener never fires for our clicks.
// - Stop `focusin` propagation so any focus shifts inside our chrome
// don't read as "focus moved outside the dialog" to focus traps.
//
// Click events still bubble normally — only the early pointer/focus
// signals that drive outside-interaction detection are silenced.
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
if (!rootEl) return;
if (setPointerEvents) {
rootEl.style.setProperty('pointer-events', 'auto', 'important');
}
const stop = (e) => e.stopPropagation();
rootEl.addEventListener('pointerdown', stop);
rootEl.addEventListener('mousedown', stop);
rootEl.addEventListener('focusin', stop);
}
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
@@ -336,6 +375,11 @@
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
document.body.appendChild(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
@@ -811,6 +855,7 @@
maxWidth: '520px', minWidth: '320px',
});
document.body.appendChild(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
@@ -905,7 +950,12 @@
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
// Freeform input. Focus state shows an accent-colored border only —
// an earlier version tinted the background with `BP.accentSoft`, which
// composited against the dark bar surface to a murky purple where the
// browser's default placeholder gray was unreadable. Placeholder color
// is set explicitly via a one-shot stylesheet keyed off this input's id
// so it picks up the bar's `textDim` token in both themes.
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
@@ -916,15 +966,20 @@
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: BP.text,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
transition: 'border-color 0.15s ease',
});
if (!document.getElementById(PREFIX + '-input-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-input-style';
s.textContent =
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
document.head.appendChild(s);
}
input.addEventListener('focus', () => {
input.style.borderColor = BP.hairline;
input.style.background = BP.accentSoft;
input.style.borderColor = BP.accent;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
@@ -1320,6 +1375,7 @@
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
@@ -1433,6 +1489,10 @@
paramsPanelEl.appendChild(paramsPanelBody);
document.body.appendChild(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
@@ -2011,7 +2071,16 @@
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
if (n.nodeType !== 1) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
dominated = true; break;
}
}
@@ -2126,17 +2195,20 @@
}
break;
}
// HMR didn't propagate in time. Give it a 2s grace window, then
// reload the page. resumeSession counts variants off the rendered
// DOM on load and transitions straight to CYCLING — reload is the
// one universal recovery path: HTML, JSX/TSX, Vue, Svelte, static
// servers, anything. We used to try DOMParser on the raw source,
// but JSX expressions aren't valid HTML and the parse fails.
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast — better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
saveSession();
window.location.reload();
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'error':
@@ -2236,6 +2308,60 @@
showBar('configure');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel — only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
@@ -2694,7 +2820,13 @@ void main() {
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.id = PREFIX + '-shader';
Object.assign(img.style, canvas.style, { outline: '2px dashed ' + C.brand, outlineOffset: '-2px' });
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
img.style.cssText = canvas.style.cssText;
img.style.outline = '2px dashed ' + C.brand;
img.style.outlineOffset = '-2px';
document.body.appendChild(img);
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
return;
@@ -2942,8 +3074,16 @@ void main() {
function showToast(message, duration) {
if (toastEl) toastEl.remove();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect — its height varies
// with hover-expanded labels — and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
@@ -3066,13 +3206,33 @@ void main() {
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
const bg = getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return 'light';
const [, r, g, b] = m;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that — and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * +r + 0.7152 * +g + 0.0722 * +b) / 255;
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
@@ -3275,15 +3435,24 @@ void main() {
});
inner.appendChild(divider);
// Exit (subtle × on the right)SVG for baseline-free centering
// Exit × on the right — intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar — the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '26px', height: '26px', borderRadius: '6px',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"/></svg>';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
@@ -3301,6 +3470,7 @@ void main() {
});
document.body.appendChild(globalBarEl);
defangOutsideHandlers(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
@@ -3513,6 +3683,11 @@ void main() {
designShadow.appendChild(root);
document.body.appendChild(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
@@ -4577,6 +4752,18 @@ void main() {
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+134 -6
View File
@@ -88,10 +88,15 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const updated = removeTag(content, config.commentSyntax);
const detagged = removeTag(content, config.commentSyntax);
const updated = revertCspMeta(detagged);
if (updated === content) return { file: relFile, removed: false, note: 'no tag present' };
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, removed: true };
return {
file: relFile,
removed: detagged !== content,
cspReverted: updated !== detagged,
};
});
console.log(JSON.stringify({ ok: true, results }));
return;
@@ -109,11 +114,18 @@ Output (JSON):
const absFile = path.resolve(process.cwd(), relFile);
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
const content = fs.readFileSync(absFile, 'utf-8');
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
const withTag = insertTag(withoutOld, config, port);
if (withTag === withoutOld) {
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
}
const updated = patchCspMeta(withTag, port);
fs.writeFileSync(absFile, updated, 'utf-8');
return { file: relFile, inserted: true };
return {
file: relFile,
inserted: true,
cspPatched: updated !== withTag,
};
});
const anyInserted = results.some((r) => r.inserted);
console.log(JSON.stringify({ ok: anyInserted, port, results }));
@@ -296,6 +308,121 @@ function removeTag(content, _syntax) {
return content;
}
// ---------------------------------------------------------------------------
// Content-Security-Policy meta-tag patcher
//
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
// the cross-origin load of /live.js (and the SSE/POST connection back to
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
//
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
// and stash the original `content` value in a `data-impeccable-csp-original`
// attribute (base64) so revert is exact.
//
// On remove: detect the marker attribute, decode it, restore the original
// content value verbatim, drop the marker.
//
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
// shared helpers) is NOT patched here — those need framework-specific config
// edits and are handled via the existing detect-csp.mjs reference output.
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
const out = [];
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
let m;
while ((m = tagRe.exec(content)) !== null) {
const attrs = m[1];
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
return out;
}
function getAttr(attrs, name) {
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
const m = attrs.match(re);
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
const m = csp.match(re);
if (m) {
const tokens = m[4].trim().split(/\s+/);
if (tokens.includes(origin)) return csp;
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const attrs = tag.attrs;
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
const contentAttr = getAttr(attrs, 'content');
if (!contentAttr) continue;
const original = contentAttr.value;
let patched = original;
patched = appendOriginToDirective(patched, 'script-src', origin);
patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
if (patched === original) continue;
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
const newTag = tag.full.replace(attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
export function revertCspMeta(content) {
const tags = findCspMetaTags(content);
if (tags.length === 0) return content;
let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
if (!origAttr) continue;
const contentAttr = getAttr(tag.attrs, 'content');
if (!contentAttr) continue;
let originalValue;
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
catch { continue; }
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
const newTag = tag.full.replace(tag.attrs, newAttrs);
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
return result;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
@@ -306,3 +433,4 @@ if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
// patchCspMeta + revertCspMeta are exported above where they're defined.
+24 -3
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 3.0.0
version: 3.0.4
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
@@ -13,7 +13,26 @@ Designs and iterates production-grade frontend interfaces. Real working code, co
## Setup (non-optional)
Two steps before any design work. Both are required. Skipping either produces generic output that ignores the project.
Before any design work or file edits, pass these gates. Skipping them produces generic output that ignores the project.
| Gate | Required check | If fail |
|---|---|---|
| Context | The PRODUCT.md / DESIGN.md loader result is known from `node .opencode/skills/impeccable/scripts/load-context.mjs`. | Run the loader before continuing. |
| Product | PRODUCT.md exists and is not empty or placeholder (`[TODO]` markers, <200 chars). | Run `/impeccable teach`, refresh context, then resume. Never synthesize PRODUCT.md from the user's original prompt alone. |
| Command | The matching command reference is loaded when a sub-command is used. | Load the reference before continuing. |
| Craft | `/impeccable craft` has a user-confirmed shape brief for this task. `teach` / PRODUCT.md never counts as shape. | Run `/impeccable shape` and wait for explicit brief confirmation. |
| Image | Required visual probes / mocks are generated or skipped with a reason. | Resolve the image-generation gate in `shape.md` or `craft.md` before code. |
| Mutation | All active gates above pass. | Do not edit project files yet. |
Codex-style agents must state this before editing files:
```text
IMPECCABLE_PREFLIGHT: context=pass product=pass command_reference=pass shape=pass|not_required image_gate=pass|skipped:<reason> mutation=open
```
For `/impeccable craft`, `shape=pass` is only valid after a separate user response approving the shape design brief, or when the user provided an already-confirmed brief in the request. Do not mark `shape=pass` after writing PRODUCT.md, summarizing assumptions, or drafting an unconfirmed brief yourself.
Other harnesses should follow the same checklist when they can expose this state.
### 1. Context gathering
@@ -34,7 +53,7 @@ If the output is already in this session's conversation history, don't re-run. E
`/impeccable live` already warms context via `live.mjs` — if you've run `live.mjs`, don't also run `load-context.mjs` this session.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context.
If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work.
If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed.
@@ -147,6 +166,8 @@ Plus two management commands — `pin <command>` and `unpin <command>`, detailed
Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`.
If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target.
## Pin / Unpin
**Pin** creates a standalone shortcut so `/<command>` invokes `/impeccable <command>` directly. **Unpin** removes it. The script writes to every harness directory present in the project.
@@ -122,7 +122,8 @@ Use appropriate techniques for each animation:
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
- transform and opacity for reliable movement
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth
```
### JavaScript Animation
@@ -134,9 +135,10 @@ Use appropriate techniques for each animation:
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect
- **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
@@ -152,7 +154,7 @@ Use appropriate techniques for each animation:
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Animate layout properties casually (`width`, `height`, `top`, `left`, margins) when transform, FLIP, or grid-based techniques would work
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
@@ -22,7 +22,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
+104 -37
View File
@@ -1,12 +1,41 @@
# Craft Flow
Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful.
Build a feature with impeccable UX and UI quality through a structured process: shape the design, land the visual direction, build real production code, then inspect and improve in-browser until the result meets a high-end studio bar.
## Build Gate
Craft cannot build until all of these are true:
1. PRODUCT context is valid and current.
2. The shape design brief is explicitly confirmed by the user for this task, unless the user already provided a confirmed brief.
3. Implementation references from the brief are loaded.
4. The shape visual probe decision is recorded: generated, skipped with reason, or already resolved.
5. The north-star mock decision is recorded: generated, skipped with reason, or not applicable.
PRODUCT.md and `teach` answers do **not** satisfy the shape gate. They are project context only. A compact self-authored brief does not satisfy the shape gate either. `shape=pass` requires a separate user response approving the shape brief or an already-confirmed brief supplied by the user.
Invalid image-skip reasons include: "the final implementation will be semantic HTML/CSS/SVG", "the diagram should stay editable", "a raster mock would not be used directly", or "the product is fictional." Generated probes and mocks are direction artifacts; they are not implementation assets.
## Craft Contract
Craft is not a first pass. It is a loop with these required artifacts:
1. Confirmed design brief from `shape`.
2. Approved visual direction, from generated probes / mocks when image generation is available.
3. Mock fidelity inventory: the visible ingredients from the approved direction that must survive into code.
4. Semantic, functional implementation using the project's real stack and conventions.
5. Browser evidence across relevant viewports.
6. At least one critique-and-fix pass after the first browser inspection, unless the first pass has no material defects.
Do not let generated mockups replace interface structure, copy, accessibility, responsive behavior, or state design. But do treat the approved mock as a concrete visual contract for composition, hierarchy, density, atmosphere, signature motifs, image needs, and distinctive visual moves. "North star" means "preserve the important visible ingredients in semantic code," not "use it as loose mood."
## Step 1: Shape the Design
Run /impeccable shape, passing along whatever feature description the user provided.
Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
Wait for the design brief to be fully confirmed by the user before proceeding. The brief is your blueprint, and every implementation decision should trace back to it.
If this craft run resumed after `teach` created PRODUCT.md, run shape now. Do not treat the teach interview, PRODUCT.md, or a summary of project context as a substitute for shape. Shape is task-specific and must cover scope, content/states, visual direction, constraints, anti-goals, probes when applicable, and explicit brief confirmation.
If the user has already run /impeccable shape and has a confirmed design brief, skip this step and use the existing brief.
@@ -24,15 +53,17 @@ Then add references based on the brief's needs:
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
## Step 3: North Star Mock (Capability-Gated)
## Step 3: Land the Visual Direction (Capability-Gated)
Before implementation, generate a small set of high-fidelity visual comps when all of these are true:
Before implementation, generate high-fidelity visual comps when all of these are true:
- The work is **net-new** or visually open-ended enough that composition exploration will improve the build.
- The brief's scope is **mid-fi, high-fi, or production-ready**.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default for **both brand and product work**.
When those conditions are met, this step is mandatory for **both brand and product work** in Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Do not skip this step because the eventual UI should be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### Purpose
@@ -40,25 +71,43 @@ Use the mock step to find a stronger visual lane than code-first generation woul
### What to generate
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief.
Generate **1 to 3** high-fidelity north-star comps based on the confirmed brief. If shape already produced direction probes, use those results as input and generate a more resolved mock from the winning lane, not another unrelated exploration.
- For brand work, push visual identity, composition, and mood aggressively.
- For product work, still push hierarchy, topology, density, and tone, but keep the comps grounded in realistic product structure and states.
- For landing pages and long-form brand surfaces, show enough of the next section or second fold to establish the system beyond the hero.
The comps must be genuinely different in primary visual direction, not just color variants.
### After generation
### Approval loop
Choose a direction with the user, or if the user delegated the decision, pick the strongest one and explain why.
Show the comps and ask what should carry forward. If the user asks for changes or the best direction is still weak, generate a focused revision before implementation. Continue until one direction is approved, or until the user explicitly delegates the choice.
If the user delegates, pick the strongest direction and explain the decision using the brief, not personal taste.
Before moving to implementation, summarize:
- What to carry into code
- What **not** to literalize from the mock
Treat the mock as a **north star**, not a screenshot to trace. Do **not** let it override the confirmed brief.
This summary is required before Step 4. It is the handoff between visual exploration and semantic implementation.
## Step 4: Asset Extraction (Optional)
### Mock fidelity inventory
Before building, inventory the approved mock's major visible ingredients:
- Hero silhouette and dominant composition.
- Signature motifs: planets, devices, portraits, charts, route lines, insets, badges, or other memorable objects.
- Nav and primary CTA treatment.
- Section sequence visible in the mock, especially the second fold.
- Image-native content the concept depends on.
- Typography, density, color/material treatment, and motion cues.
For each ingredient, decide how it will be implemented: semantic HTML/CSS/SVG, generated asset, sourced project asset, icon library, canvas/WebGL, or an explicitly accepted omission. Do not substitute a different hero composition or new visual driver after approval unless the user approves the change.
Treat the mock as a **north star**, not a screenshot to trace. Do **not** rasterize core UI text or let the mock override the confirmed brief. But if the live result lacks the mock's major visible ingredients, the implementation is wrong.
## Step 4: Asset Extraction (Need-Gated)
If the chosen direction includes image-native visual ingredients that would materially improve the implementation, generate them as separate assets before building.
@@ -74,53 +123,71 @@ Good candidates:
- decorative marks
- non-semantic scene elements
For travel, editorial, portfolio, venue, product showcase, entertainment, education, or any other image-led brand surface, visual assets are usually core content, not decoration. Do not ship abstract CSS panels where the approved mock or subject matter calls for real imagery, generated plates, illustrations, maps, product/object renders, or destination scenes.
Do **not** export assets for core UI text, navigation, body copy, or any structure that should stay semantic and editable in code.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets.
Usually **1 to 5** extracted assets is enough. If the design can be built cleanly in HTML/CSS/SVG, prefer that over raster assets. If the mock contains major visual content that cannot be built credibly in code, asset extraction is not optional.
## Step 5: Build
## Step 5: Build to Production Quality
Implement the feature following the design brief. Work in this order:
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
1. **Structure first**: HTML/semantic structure for the primary state. No styling yet.
2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy.
3. **Typography and color**: Apply the type scale and color system.
4. **Interactive states**: Hover, focus, active, disabled.
5. **Edge case states**: Empty, loading, error, overflow, first-run.
6. **Motion**: Purposeful transitions and animations (if appropriate).
7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context.
### Production bar
### During Build
- Test with real (or realistic) data at every step, not placeholder text
- Check each state as you build it, not all at the end
- If you discover a design question, stop and ask rather than guessing
- Every visual choice should trace back to something in the design brief or the chosen north-star direction
- Keep text semantic, layout real, and interactions accessible. Do not turn the mock into a pile of rasterized UI
- If assets were extracted, use them intentionally. They support the build; they do not replace interface structure
- Use real or realistic content. Remove placeholder copy, placeholder images, dead links, fake controls, and unused scaffold before presenting.
- Preserve the approved mock's major ingredients. Missing hero objects, missing world/product imagery, different section structure, downgraded CTA/nav treatment, or generic replacements for distinctive motifs are blocking defects unless the user accepted the change.
- Build semantically first: real headings, landmarks, labels, form associations, button/link semantics, accessible names, and state announcements where needed.
- Calibrate spacing, alignment, grid placement, and vertical rhythm deliberately. Do not accept default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment.
- Make typography intentional: chosen font loading strategy, clear hierarchy, readable measure, stable line breaks, tuned wrapping, and no overflow at mobile or large desktop sizes.
- Design realistic state coverage: default, hover where supported, focus-visible, active, disabled, loading, error, success, empty, overflow, long text, short text, and first-run states where relevant.
- Make interaction quality feel finished: keyboard paths, touch targets, feedback timing, scroll behavior, transitions between states, and no hover-only functionality.
- Use icons from the project's established icon set when available. If no set exists, choose a coherent library or use accessible text controls; do not mix unrelated icon styles.
- Optimize imagery and media: correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset` / `picture` for raster assets, and no project-referenced asset left outside the workspace.
- Make motion feel premium: use atmospheric blur, filter, mask, shadow, or reveal effects when they improve the experience; avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion.
- Preserve maintainability: reusable local patterns, clear component boundaries, project conventions, no rasterized UI text, and no hard-coded one-off hacks when a better local pattern exists.
- Fit the technical context: production build passes, no obvious console errors, no avoidable layout shift, no needless dependency, and no broken asset path.
- If you discover a design question that materially changes the brief or approved direction, stop and ask rather than guessing.
## Step 6: Visual Iteration
## Step 6: Browser-Based Iteration
**This step is critical.** Do not stop after the first implementation pass.
Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback.
Open the result in a browser. In Codex, use browser-use or equivalent browser automation when available; otherwise use Playwright or ask the user for screenshots. Inspect screenshots, not just DOM or terminal output.
Iterate through these checks visually:
### Required viewport pass
Check the experience at the viewports that matter for the brief. Default minimum:
- Mobile narrow
- Tablet or small laptop
- Desktop wide
For each viewport, capture or inspect the rendered state and look for visual defects: overlap, clipping, weak hierarchy, off-grid alignment, awkward whitespace, cramped controls, unreadable type, broken imagery, hover-only functionality, layout shift, and text overflow.
### Critique and fix loop
After the first browser pass, write a short critique for yourself and patch the implementation. Repeat browser inspection after fixes. Continue until no material issues remain against this checklist:
1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies.
2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink?
6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing.
2. **Does it match the approved mock?** Compare screenshots against the mock fidelity inventory: hero silhouette, major motifs, imagery, nav/CTA, section sequence, density, color/materials, and second-fold substance. Missing major ingredients are P0 defects.
3. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention.
4. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations.
5. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought.
6. **Check responsive behavior.** The design should adapt compositionally, not merely shrink.
7. **Check craft details.** Spacing consistency, optical alignment, type hierarchy, color contrast, image quality, icon coherence, interactive feedback, motion timing, and focus treatment.
8. **Check performance basics.** No obviously oversized images, avoidable layout thrash, blocking animations, or heavy assets without a reason.
After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights."
The exit bar is not "it works." It is: the rendered result looks intentional at all checked viewports, all expected states are handled, no placeholders remain unless explicitly accepted, and the implementation quality would be defensible in a high-end studio review.
## Step 7: Present
Present the result to the user:
- Show the feature in its primary state
- Summarize the browser/viewports checked and the most important fixes made after inspection
- Walk through the key states (empty, error, responsive)
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock
- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients.
- Note any remaining limitations or follow-up risks honestly
- Ask: "What's working? What isn't?"
Iterate based on feedback. Good design is rarely right on the first pass.
@@ -38,9 +38,19 @@ Timing matters more than easing. These durations feel right for most UI:
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
## Premium Motion Materials
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects.
Use the right material for the effect:
- **Transform / opacity**: movement, press feedback, simple reveals, list choreography.
- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions.
- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions.
- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state.
- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly.
The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it.
## Staggered Animations
@@ -109,10 +109,10 @@ elements.forEach((el, i) => {
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `transform` and `opacity` for reliable movement, but allow blur, filters, masks, clip paths, shadows, and color shifts when they create meaningful polish
- Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
- Bound expensive paint areas for blur/filter/shadow effects (smaller and isolated is faster)
### Animation Performance
@@ -4,17 +4,17 @@ Perform a meticulous final pass to catch all the small details that separate goo
## Design System Discovery
Before polishing, understand the system you are polishing toward:
Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: color tokens, spacing scale, typography styles, component API.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established?
3. **Identify drift**: Where does the target feature deviate from the system? Hard-coded values that should be tokens, custom components that duplicate shared ones, spacing that doesn't match the scale.
1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
If a design system exists, polish should align the feature with it. If none exists, polish against the conventions visible in the codebase.
If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask — never guess at design system principles.**
## Pre-Polish Assessment
Understand the current state and goals:
Understand the current state and goals before touching anything:
1. **Review completeness**:
- Is it functionally complete?
@@ -22,13 +22,18 @@ Understand the current state and goals:
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
3. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
@@ -50,6 +55,16 @@ Work through these dimensions methodically:
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Information Architecture & Flow
Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
- **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
- **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
- **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
- **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
- **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
@@ -89,7 +104,7 @@ Every interactive element needs all states:
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
@@ -158,6 +173,8 @@ Every interactive element needs all states:
Go through systematically:
- [ ] Aligned to the design system (drift named and resolved by root cause)
- [ ] Information architecture and flow shape match neighboring features
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
@@ -183,12 +200,15 @@ Go through systematically:
**NEVER**:
- Polish before it's functionally complete
- Polish without aligning to the design system — that's decoration on drift
- Guess at design system principles instead of asking when something is ambiguous
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
- Perfect one thing while leaving others rough (consistent quality level)
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns or flows that diverge from established ones
## Final Verification
+20 -5
View File
@@ -12,7 +12,18 @@ Most AI-generated UIs fail not because of bad code, but because of skipped think
**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later.
Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. STOP and call the `question` tool to clarify.
This is a required interaction, not optional guidance. Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. STOP and call the `question` tool to clarify.
### Interview cadence
Discovery must include at least one user-answer round unless PRODUCT.md, DESIGN.md, or an already-confirmed brief directly answers the needed design inputs. With a sparse prompt, do **not** synthesize a complete brief for confirmation on the first response.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Treat PRODUCT.md and DESIGN.md as anchors; they reduce repeated questions but do **not** replace shape for craft. Shape is task-specific.
- Round 1 should clarify purpose, audience/context, and success or emotional outcome.
- Round 2 should clarify content/data/states and scope/fidelity.
- Round 3 should clarify visual direction, constraints, and anti-goals when still unresolved.
### Purpose & Context
- What is this feature for? What problem does it solve?
@@ -63,7 +74,11 @@ After the discovery interview, generate a small set of visual direction probes *
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
- The current harness has **built-in image generation capability** (for example, Codex with a native image tool). Do **not** ask the user to set up external APIs, shell scripts, or one-off tooling just to do this.
When those conditions are met, this step is the default. Use it to explore visual lanes, not to replace the brief.
When those conditions are met, this step is mandatory for Codex and any harness with built-in image generation. Use native image generation; in Codex, use the built-in `image_gen` tool via the imagegen skill. If image generation is unavailable, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed.
Use probes to explore visual lanes, not to replace the brief.
Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration.
### What to generate
@@ -89,11 +104,11 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase and proceed directly to the design brief.
If image generation is unavailable, or the task doesn't benefit from it, skip this phase only with a one-line reason and proceed directly to the design brief.
## Phase 2: Design Brief
After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this command complete.
After the interview and any required probes, synthesize everything into a structured design brief. Present it to the user for explicit confirmation before considering this command complete. Stop after asking for confirmation; do not proceed to craft or implementation in the same response unless the user has already approved the brief.
### Brief Structure
@@ -131,6 +146,6 @@ Anything unresolved that the implementer should resolve during build.
---
STOP and call the `question` tool to clarify. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions.
STOP and call the `question` tool to clarify. Ask for explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the brief is confirmed.
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
+23 -4
View File
@@ -21,11 +21,13 @@ Decision tree:
- **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
- **PRODUCT.md exists, DESIGN.md missing**: skip to Step 5 — offer to run `/impeccable document` for DESIGN.md.
- **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
- **Both exist**: STOP and call the `question` tool to clarify. which to refresh. Skip the one the user doesn't want changed.
- **Both exist**: STOP and call the `question` tool to clarify. Ask which file to refresh. Skip the one the user doesn't want changed.
- **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
Never silently overwrite an existing file. Always confirm first.
If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
## Step 2: Explore the codebase
Before asking questions, thoroughly scan the project to discover what you can:
@@ -48,7 +50,22 @@ Note what you've learned and what remains unclear. This exploration feeds both P
## Step 3: Ask strategic questions (for PRODUCT.md)
STOP and call the `question` tool to clarify. Focus only on what you couldn't infer from the codebase.
STOP and call the `question` tool to clarify. Ask only about what you couldn't infer from the codebase.
### Interview mode, not confirmation mode
If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
- Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
- Ask **2-3 questions per round**, then wait for answers.
- Use inferred answers as hypotheses or options, not as finished facts.
- Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
- Round 1 should establish register, users/purpose, and desired outcome.
- Round 2 should establish brand personality or references, anti-references, and accessibility needs.
### Minimum viable interview
Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
### Register (ask first — it shapes everything below)
@@ -56,7 +73,7 @@ Every design task is either **brand** (marketing, landing, campaign, long-form c
If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface — does that match your intent, or should we treat it differently?"*
If the signal is genuinely split (e.g. a product with a big marketing landing), STOP and call the `question` tool to clarify. which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
If the signal is genuinely split (e.g. a product with a big marketing landing), STOP and call the `question` tool to clarify. Ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
### Users & Purpose
- Who uses this? What's their context when using it?
@@ -79,6 +96,8 @@ Skip questions where the answer is already clear. **Do NOT ask about colors, fon
## Step 4: Write PRODUCT.md
Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
Synthesize into a strategic document:
```markdown
@@ -134,4 +153,4 @@ Summarize:
If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context.
Optionally STOP and call the `question` tool to clarify. whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
Optionally STOP and call the `question` tool to clarify. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there.
@@ -1,10 +1,10 @@
{
"craft": {
"description": "Full shape-then-build flow with visual iteration. Plans the UX with /impeccable shape, loads the right reference files, then builds and iterates visually until the result is delightful. Use when building a new feature end-to-end.",
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"argumentHint": "[feature description]"
},
"teach": {
"description": "Gathers design context for a project. Runs a short discovery interview and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.",
"argumentHint": ""
},
"document": {
@@ -84,7 +84,7 @@
"argumentHint": "[target]"
},
"shape": {
"description": "Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.",
"description": "Plan UX and UI before code. Runs a required multi-round discovery interview, uses visual probes when available, and produces a user-confirmed design brief for implementation.",
"argumentHint": "[feature to shape]"
},
"typeset": {

Some files were not shown because too many files have changed in this diff Show More