mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c6df216b | ||
|
|
d154a5feb3 | ||
|
|
ac2237a7a9 | ||
|
|
b5c203f38a | ||
|
|
856b90e52b | ||
|
|
c332c7aa91 | ||
|
|
9a5d0e71a9 | ||
|
|
7e0ce5e6b1 | ||
|
|
18fa503d44 | ||
|
|
04709eadf0 | ||
|
|
efedf2d3d3 | ||
|
|
f67add25e5 | ||
|
|
6b507e0c6a | ||
|
|
c1e1104e31 | ||
|
|
1f760aff61 | ||
|
|
8660d3aa22 | ||
|
|
11dfad81da | ||
|
|
a701ee613a | ||
|
|
99e68370b9 | ||
|
|
fdb9e7c6f8 | ||
|
|
9ec904302b | ||
|
|
54d9f05ea5 | ||
|
|
638af20566 | ||
|
|
5881a0843b | ||
|
|
27af49f190 | ||
|
|
bf2bc55aa1 | ||
|
|
a923346bcc | ||
|
|
c812d76b6f | ||
|
|
7cfa7759f5 | ||
|
|
9a76c7bc17 | ||
|
|
c6ca721786 | ||
|
|
4f66eb9c08 | ||
|
|
5f5e2b013d | ||
|
|
54f6ccf6f0 | ||
|
|
62ce35ac8e | ||
|
|
b93d36dcfe |
@@ -7,11 +7,30 @@ 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
|
||||
|
||||
Two files at the project root, case-insensitive:
|
||||
Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd).
|
||||
|
||||
- **PRODUCT.md** — required. Users, brand, tone, anti-references, strategic principles.
|
||||
- **DESIGN.md** — optional, strongly recommended. Colors, typography, elevation, components.
|
||||
@@ -22,13 +41,13 @@ Load both in one call:
|
||||
node .agents/skills/impeccable/scripts/load-context.mjs
|
||||
```
|
||||
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`.
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from.
|
||||
|
||||
If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `$impeccable teach` or `$impeccable document` (they rewrite the files), or the user manually edited one.
|
||||
|
||||
`$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.
|
||||
|
||||
@@ -101,7 +120,10 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit
|
||||
|
||||
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
|
||||
|
||||
**Category-reflex check.** If someone could guess the theme and palette from the category name alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the training-data reflex. Rework the scene sentence and color strategy until the answer is no longer obvious from the domain.
|
||||
**Category-reflex check.** Run at two altitudes — the second one catches what the first one misses.
|
||||
|
||||
- **First-order:** if someone could guess the theme + palette from the category alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
|
||||
- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references — "AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode" — it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -141,6 +163,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -29,6 +29,16 @@ Training-data defaults. Ban list — look further:
|
||||
|
||||
Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
|
||||
|
||||
### Reflex-reject aesthetic lanes
|
||||
|
||||
Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex — the trap one tier deeper than picking a Fraunces font. Look further.
|
||||
|
||||
- **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
|
||||
|
||||
(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
|
||||
|
||||
The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins — variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in [live.md](live.md).
|
||||
|
||||
### Pairing and voice
|
||||
|
||||
Distinctive + refined is the goal — the specific shape depends on the brand:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,24 +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.
|
||||
|
||||
## Real Example: Neo Mirai
|
||||
## Build Gate
|
||||
|
||||
Neo Mirai is the full craft loop in public. A retro-futurist AI design conference started with generated brand and hi-fi reference images, then shipped as a responsive static site in `public/neo-mirai`.
|
||||
Craft cannot build until all of these are true:
|
||||
|
||||
Repro command:
|
||||
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.
|
||||
|
||||
```bash
|
||||
$impeccable craft retro-futurist AI design conference website
|
||||
```
|
||||
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.
|
||||
|
||||
The important detail is the artifact chain: brand toolkit, north-star mock, semantic implementation, regenerated assets, browser iteration, responsive fixes. The mock was not treated as a screenshot to trace. It was used as direction for a real page.
|
||||
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.
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -52,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.
|
||||
|
||||
@@ -86,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
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
|
||||
node .agents/skills/impeccable/scripts/live.mjs
|
||||
```
|
||||
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `$impeccable document` for the matching DESIGN.md.
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent — extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `$impeccable document` for the matching DESIGN.md.
|
||||
|
||||
`serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname).
|
||||
|
||||
@@ -74,7 +74,7 @@ Reading annotations precisely:
|
||||
### 2. Wrap the element
|
||||
|
||||
```bash
|
||||
node .agents/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
|
||||
node .agents/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
|
||||
```
|
||||
|
||||
Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
@@ -82,9 +82,12 @@ Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
- `--element-id` ← `event.element.id`
|
||||
- `--classes` ← `event.element.classes` joined with commas
|
||||
- `--tag` ← `event.element.tagName`
|
||||
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
|
||||
|
||||
The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only — do not use it for normal element lookups.
|
||||
|
||||
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"` — read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
|
||||
|
||||
Output on success: `{ file, insertLine, commentSyntax }`.
|
||||
|
||||
**Fallback errors.** Wrap only writes into files it judges to be source (tracked by git, not marked GENERATED, not listed in config's `generatedFiles`). If it can't land on a source file, it errors without writing — accepting a variant into a generated file is silent data loss. Three shapes:
|
||||
@@ -101,38 +104,81 @@ If `event.action` is `impeccable` (the default freeform action), use SKILL.md's
|
||||
|
||||
Any other `event.action` (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `adapt`, `animate`, `delight`, `overdrive`): Read `reference/<action>.md` before planning. Each sub-command encodes a specific discipline; skipping its reference produces generic output. Those files may require specific params; layer them on top of the §7 budget, not instead of it.
|
||||
|
||||
### 4. Plan three genuinely distinct directions
|
||||
### 4. Plan three variants — identity first, then mode, then axes
|
||||
|
||||
Before writing a single line of code, name each variant.
|
||||
The wrong frame for live mode is "show three different design directions." Live runs on an existing surface — the brand has already been chosen. The job is variation **within identity**, not selection between identities. Failure mode: three editorial-typographic variants on a brief that wasn't editorial. Bigger failure mode: three off-brand variants the user can't accept because they don't look like their product.
|
||||
|
||||
**For freeform (`action` is `impeccable`, or the user supplied a free prompt):** each variant must anchor to a different **archetype** — a real-world design analogue specific enough to be recognizable at a glance. Not "modern landing page." Not "minimal product hero." Examples:
|
||||
Four phases. Do them in order.
|
||||
|
||||
- *Broadsheet masthead with rule-divided columns* (think NYT print edition)
|
||||
- *Klim Type Foundry specimen page* (dense, technical, catalog-driven)
|
||||
- *Japanese print-poster minimalism with a single oversize glyph*
|
||||
- *Bloomberg Terminal status bar*
|
||||
- *Condé Nast Traveler feature layout*
|
||||
#### Phase A: Extract the identity (non-skippable)
|
||||
|
||||
Then commit each variant to a different **primary axis** of difference:
|
||||
The existing surface has an identity already. Read it before planning anything. Sources, in priority order:
|
||||
|
||||
1. **DESIGN.md** if loaded — read the visual system fields (palette, type pairing, motion, components). This is the authoritative answer.
|
||||
2. **CSS custom properties** in the page's stylesheets (`:root { --color-...; --font-...; ... }`) — these are de-facto tokens.
|
||||
3. **Computed styles** on the picked element and its parent — colors, fonts, spacing scales, corner radii.
|
||||
4. **Sibling components on the page** — what visual rhetoric do existing components use? (Asymmetric or centered? Dense or airy? Bold or quiet?)
|
||||
|
||||
Write down what you see in **one sentence**. The sentence describes the surface that's actually on screen — it is not aspirational, not opinionated, not edited toward what the brand "should" be. Capture, in roughly this order:
|
||||
|
||||
- The dominant surface color and accent color, by hex or token name (use the actual values, not categories like "warm" or "neutral").
|
||||
- The type pairing — the actual font names loaded, primary first.
|
||||
- The layout topology — how the dominant elements are arranged (stacked / side-by-side / grid / asymmetric / overlay).
|
||||
- The surface treatment — corners, borders, shadows, density of decoration.
|
||||
- The voice tone you read off the copy itself, not off the aesthetic feel.
|
||||
|
||||
Be specific. "Modern" is not a color, "elegant" is not a type pairing, "clean" is not a layout. If you can't extract a real value for an axis, skip it rather than fabricate. The point is to record what is, not to describe what you wish it were.
|
||||
|
||||
Do not include adjectives that name an aesthetic family ("editorial-leaning", "terminal-flavored", "brutalist") — those are conclusions, not data. They belong to Phase C lane selection in departure mode, not to identity description. Letting them sneak into Phase A is how the identity-lock collapses into a self-fulfilling prophecy.
|
||||
|
||||
This sentence is the **identity lock**. Every variant must be readable as the same brand if rendered side by side. Skipping this phase is the primary cause of off-brand variants. Absence of DESIGN.md is never an excuse — extract from CSS and computed styles instead.
|
||||
|
||||
#### Phase B: Pick mode (default vs departure)
|
||||
|
||||
**Default mode** — the existing identity is preserved. Variants vary expression axes within it. *This is the right mode for ~90% of live sessions.* The user picked an element on a real product they're shipping; they expect variants of *their* hero, not three different brands' heroes.
|
||||
|
||||
**Departure mode** — the existing identity is rejected. Variants propose alternatives consistent with PRODUCT.md voice. Trigger only when at least one is true:
|
||||
|
||||
- PRODUCT.md anti-references explicitly call out the current surface ("the current `index.html` is itself an example"; "diffuse away from this"; "the page on screen is the failure"). Generic anti-references that describe what to avoid in general do **not** trigger departure mode — only ones that point at *this* surface specifically.
|
||||
- The user's freeform prompt explicitly asks for departure ("rebuild this from scratch", "what if it weren't editorial at all", "show me something completely different").
|
||||
|
||||
If you're unsure, you're in default mode. The cost of being wrong about default is "three on-brand variants with similar feel" — recoverable, the user picks none. The cost of being wrong about departure is "three off-brand variants" — unrecoverable, the user is annoyed.
|
||||
|
||||
#### Phase C: Plan three variants
|
||||
|
||||
**Default mode.** Each variant commits to a different **primary axis** of difference, while preserving the identity sentence. The six axes:
|
||||
|
||||
1. **Hierarchy** — which element commands the eye?
|
||||
2. **Layout topology** — stacked / side-by-side / grid / asymmetric / overlay
|
||||
3. **Typographic system** — pairing, scale ratio, case/weight strategy
|
||||
4. **Color strategy** — Restrained / Committed / Full palette / Drenched
|
||||
3. **Typographic system** — pairing logic, scale ratio, case/weight strategy *within the available faces*
|
||||
4. **Color strategy** — which existing palette role carries the surface (Restrained / Committed / Full palette / Drenched). Use the brand's existing palette tokens, not new colors.
|
||||
5. **Density** — minimal / comfortable / dense
|
||||
6. **Structural decomposition** — merge, split, progressive disclosure
|
||||
|
||||
Three variants → three DIFFERENT primary axes, not three riffs on color.
|
||||
Three variants → three DIFFERENT axes. The trio reads as *the same brand at three angles*. Do not introduce new fonts, new palette hues, or new aesthetic-family signals — those belong to departure mode.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for one dark-neutral-accent, one light-drenched, one full-palette-saturated — three color worlds, not three shades of the same.
|
||||
**Departure mode.** Each variant anchors to a different **aesthetic lane**, drawn from non-monoculture options. Lanes (illustrative, not exhaustive):
|
||||
|
||||
**The squint test (before writing code).** Write the three one-sentence descriptions side by side:
|
||||
- Tech-minimal (Stripe / Linear / Vercel-restraint)
|
||||
- Brutalist-utility (system fonts, raw HTML defaults, Craigslist-energy)
|
||||
- Terminal-native (actual code surface, syntax-highlighted UI as the design)
|
||||
- Industrial-signage (dimensional type, arrow systems, ISO standards, wayfinding)
|
||||
- Technical-drawing (isometric line art, callout numbers, blueprint blue, exploded-view diagrams)
|
||||
- Drenched-saturated (Liquid Death / Mailchimp full-palette / single-hue commitment)
|
||||
- Swiss-grid-rigorous (visible grid as voice, tight type, accumulated systems)
|
||||
|
||||
> V1: Broadsheet masthead, ruled columns, 24px ink on cream.
|
||||
> V2: Enormous italic title, catalog spec rows, heavy monospace data.
|
||||
> V3: Card-framed poster with one oversize glyph, magenta veil.
|
||||
Avoid the **reflex-reject lanes** in [brand.md](brand.md). Don't trade one monoculture for another. *Editorial-typographic is currently a reflex-reject lane* — three variants that all read as "magazine cover" is the second-order training reflex, the trap one tier deeper than SaaS-cream.
|
||||
|
||||
If two of them rhyme ("both use big type" / "both are stacks of sections" / "both feature the CTA prominently"), rework the offender. Freeform variants failing the squint test is the primary failure mode of this flow — three-of-the-same with minor styling tweaks.
|
||||
#### Phase D: Squint test
|
||||
|
||||
**Default mode squint.** Read each variant's identity sentence and compare to the locked identity from Phase A. If any variant has drifted to a different palette, type voice, or visual rhetoric, it has crossed into departure mode by accident — rework. Then check that each variant commits to a different primary axis. Three "tighter density" variants is failure.
|
||||
|
||||
**Departure mode squint.** Two passes, family before sentence:
|
||||
|
||||
1. **Family pass.** Label each variant with one design-family word: *editorial / brutalist / terminal / signage / drenched / swiss / technical-drawing / tech-minimal*. If any two variants share a family label, rework. *This pass is non-negotiable in departure mode and catches the editorial-monoculture failure that the sentence pass misses.*
|
||||
2. **Sentence pass.** Write three one-sentence descriptions side by side. If two of them rhyme ("both feature big type" / "both are stacks of sections" / "both center the CTA"), rework the offender.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for three color worlds, not three shades of the same.
|
||||
|
||||
**For action-specific invocations**, each variant must vary along the dimension the action names:
|
||||
|
||||
@@ -150,7 +196,13 @@ If two of them rhyme ("both use big type" / "both are stacks of sections" / "bot
|
||||
|
||||
### 5. Apply the freeform prompt (if present)
|
||||
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. "Make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. The interpretations stay within whichever mode you picked in Phase B.
|
||||
|
||||
In **default mode**, the prompt narrows the axes you choose, not the identity. *"Make it feel more confident"* → variant 1 amplifies hierarchy (one element commands the eye), variant 2 commits the existing accent color (Committed strategy on the brand's hue), variant 3 tightens density and removes decorative slack. Three different axes, same brand.
|
||||
|
||||
In **departure mode**, the prompt narrows the lanes you draw from, not the families. *"Make it feel like a newspaper front page"* would itself be a departure-mode prompt; honor it but pick three meaningfully different newspaper-adjacent lanes (broadsheet vs. tabloid vs. trade journal), and run the family pass to confirm they don't collapse into one.
|
||||
|
||||
When the prompt and PRODUCT.md anti-references conflict (the prompt asks for X, the anti-references ban X), the anti-references win — they describe the brand's standing position, the prompt is one moment.
|
||||
|
||||
### 6. Write all variants in a single edit
|
||||
|
||||
@@ -181,6 +233,25 @@ The first variant has no `display: none` (visible by default). All others do. If
|
||||
|
||||
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
|
||||
|
||||
**Author every `:scope` rule with a descendant combinator.** The `@scope` boundary is the **variant wrapper `<div data-impeccable-variant="N">`**, not the element you're designing. A bare `:scope { background: cream; }` styles the wrapper, not the inner replacement, so the cream lands on a `display: contents` shell while the actual element keeps page defaults. Always step in: `:scope > .card`, `:scope > section`, `:scope .hero-title`, etc. The fake test agent's CSS in `tests/live-e2e/agent.mjs` is a faithful template — every rule starts `:scope > ...`.
|
||||
|
||||
**JSX / TSX target files.** Wrap `<style>` content in a template literal so the CSS `{` / `}` aren't parsed as JSX expressions, and use `className=` / `style={{…}}` on every variant element. Keep `data-impeccable-*` attributes as-is — they're plain strings:
|
||||
|
||||
```tsx
|
||||
<style data-impeccable-css="SESSION_ID">{`
|
||||
@scope ([data-impeccable-variant="1"]) { ... }
|
||||
@scope ([data-impeccable-variant="2"]) { ... }
|
||||
`}</style>
|
||||
<div data-impeccable-variant="1">
|
||||
{/* variant 1 */}
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style={{ display: 'none' }}>
|
||||
{/* variant 2 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
The wrap script already gives you a single-rooted JSX wrapper — a `<div data-impeccable-variants="…">` outer element with the marker comments tucked inside. Drop the variants block above into the "Variants: insert below this line" comment and the source stays valid TSX.
|
||||
|
||||
### 7. Parameters (composition-sized, 0–4 per variant)
|
||||
|
||||
Each variant can expose **coarse** knobs alongside the full HTML/CSS replacement. The browser docks a small panel to the right of the outline with one control per parameter. The user drags/clicks and sees instant feedback: there is zero regeneration cost because the knob toggles a CSS variable or data attribute that the variant's scoped CSS is already authored against.
|
||||
@@ -246,6 +317,16 @@ node .agents/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --fil
|
||||
|
||||
Then run `live-poll.mjs` again immediately.
|
||||
|
||||
### Aborting an in-flight session
|
||||
|
||||
If wrap or generation fails after the browser has flipped to GENERATING (e.g. wrap landed on the wrong source branch and you've already reverted it, or generation hit an unrecoverable error), tell the **browser** so its bar resets to PICKING:
|
||||
|
||||
```bash
|
||||
node .agents/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"
|
||||
```
|
||||
|
||||
Don't run `live-accept --discard` for this — that's a pure file mutator, the browser doesn't see it, and the bar gets stuck on the GENERATING dots forever (the user has to refresh). `--discard` is only correct when the **browser** initiated the discard (user clicked ✕ during CYCLING) and the agent is just running source-side cleanup the browser already triggered.
|
||||
|
||||
## Handle fallback
|
||||
|
||||
When wrap returns `fallback: "agent-driven"`, the deterministic flow doesn't apply. Pick up here.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -104,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`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -105,15 +105,22 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...restored,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
return {};
|
||||
@@ -127,8 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -149,7 +162,6 @@ 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);
|
||||
// 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.
|
||||
@@ -177,7 +189,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
// 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) {
|
||||
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);
|
||||
@@ -187,9 +198,9 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
}
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
@@ -218,6 +229,72 @@ function findMarkerBlock(id, lines) {
|
||||
return (start !== -1 && end !== -1) ? { start, end } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the line range to REPLACE (vs. just the marker range to extract
|
||||
* from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE
|
||||
* the `<div data-impeccable-variants="ID">` outer wrapper so the picked
|
||||
* element's JSX slot keeps a single child — a Fragment `<></>` would have
|
||||
* solved the multi-sibling case but failed inside `asChild` / cloneElement
|
||||
* parents with "Invalid prop supplied to React.Fragment".
|
||||
*
|
||||
* That means the marker block is enclosed by the wrapper `<div>` opener
|
||||
* (with `data-impeccable-variants="ID"`) and its matching `</div>`. We
|
||||
* walk back to the opener and forward to the closer so accept/discard
|
||||
* remove the entire scaffold, not just the inner markers.
|
||||
*
|
||||
* Marker lines themselves stay where they were so extractOriginal /
|
||||
* extractVariant / extractCss continue to walk the same range.
|
||||
*/
|
||||
function expandReplaceRange(block, lines, isJsx) {
|
||||
if (!isJsx) return { start: block.start, end: block.end };
|
||||
|
||||
let { start, end } = block;
|
||||
|
||||
// Walk back for the wrapper `<div data-impeccable-variants="..."` opener.
|
||||
// The attr may sit on a continuation line of a multi-line opening tag, so
|
||||
// also walk to the line that actually contains `<div`.
|
||||
for (let i = start - 1; i >= Math.max(0, start - 12); i--) {
|
||||
if (/data-impeccable-variants=/.test(lines[i])) {
|
||||
let opener = i;
|
||||
while (opener > 0 && !/<div\b/.test(lines[opener])) opener--;
|
||||
start = opener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk forward to the matching `</div>` by div-depth tracking from the
|
||||
// wrapper opener. Operate on JOINED text instead of per-line: a
|
||||
// multi-line self-closing JSX `<div\n className="spacer"\n/>` would
|
||||
// fool per-line regex tracking (the `<div` line matches openRe but the
|
||||
// `/>` line never matches selfCloseRe since it needs `<div` on the same
|
||||
// line). That left depth permanently over-counted and the wrapper's
|
||||
// outer `</div>` orphaned after accept/discard. Single regex with
|
||||
// `[^>]*?` (which spans newlines in JS) handles either form correctly.
|
||||
const joined = lines.slice(start).join('\n');
|
||||
// Match either `<div … />` (self-close, group 1 is `/`), `<div … >`
|
||||
// (open, group 1 is empty), or `</div>`.
|
||||
const tagRe = /<div\b[^>]*?(\/?)>|<\/div\s*>/g;
|
||||
let depth = 0;
|
||||
let m;
|
||||
while ((m = tagRe.exec(joined)) !== null) {
|
||||
const isClose = m[0].startsWith('</');
|
||||
const isSelfClose = !isClose && m[1] === '/';
|
||||
if (isClose) depth--;
|
||||
else if (!isSelfClose) depth++;
|
||||
if (depth <= 0) {
|
||||
// m.index is offset within `joined`; convert back to a file line.
|
||||
const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
|
||||
const candidateEnd = start + linesBefore;
|
||||
if (candidateEnd >= end) {
|
||||
end = candidateEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wrapper lines into a single string with `<style>` elements removed so
|
||||
* marker matching and div-depth tracking aren't confused by:
|
||||
@@ -345,7 +422,7 @@ function extractCss(lines, block, id) {
|
||||
// Same-line open + close: extract inner text.
|
||||
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
||||
if (sameLine) {
|
||||
const inner = sameLine[1];
|
||||
const inner = stripJsxTemplateWrap(sameLine[1]);
|
||||
return inner.length > 0 ? inner.split('\n') : null;
|
||||
}
|
||||
inStyle = true;
|
||||
@@ -362,7 +439,60 @@ function extractCss(lines, block, id) {
|
||||
}
|
||||
}
|
||||
|
||||
return content.length > 0 ? content : null;
|
||||
if (content.length === 0) return null;
|
||||
return stripJsxTemplateLines(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a
|
||||
* `<style>` element in a JSX/TSX file. The agent may write the wrap with
|
||||
* `{` and `}` directly attached to the `<style>` tags, on their own lines,
|
||||
* or attached to the first/last CSS lines — all three are JSX-legal.
|
||||
*
|
||||
* Stripping is required because handleAccept re-wraps the CSS itself when
|
||||
* carbonizing. Without this, two consecutive accepts (or a previously-
|
||||
* accepted variants block being carbonized) would produce nested
|
||||
* `{` `{` … `}` `}`, which oxc rejects with "Expected `}` but found `@`".
|
||||
*/
|
||||
function stripJsxTemplateLines(content) {
|
||||
const out = content.slice();
|
||||
|
||||
// Drop any leading blank lines so we don't miss a `{` line buried below
|
||||
// them; same for trailing.
|
||||
while (out.length > 0 && out[0].trim() === '') out.shift();
|
||||
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Leading `{`: own line, or attached to the first CSS line.
|
||||
const firstTrim = out[0].trimStart();
|
||||
if (firstTrim === '{`') {
|
||||
out.shift();
|
||||
} else if (firstTrim.startsWith('{`')) {
|
||||
const idx = out[0].indexOf('{`');
|
||||
out[0] = out[0].slice(0, idx) + out[0].slice(idx + 2);
|
||||
if (out[0].trim() === '') out.shift();
|
||||
}
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Trailing `` ` `` `}`: own line, or attached to the last CSS line.
|
||||
const lastIdx = out.length - 1;
|
||||
const lastTrim = out[lastIdx].trimEnd();
|
||||
if (lastTrim === '`}') {
|
||||
out.pop();
|
||||
} else if (lastTrim.endsWith('`}')) {
|
||||
const text = out[lastIdx];
|
||||
const idx = text.lastIndexOf('`}');
|
||||
out[lastIdx] = text.slice(0, idx) + text.slice(idx + 2);
|
||||
if (out[lastIdx].trim() === '') out.pop();
|
||||
}
|
||||
|
||||
return out.length > 0 ? out : null;
|
||||
}
|
||||
|
||||
function stripJsxTemplateWrap(text) {
|
||||
const lines = text.split('\n');
|
||||
const stripped = stripJsxTemplateLines(lines);
|
||||
return stripped ? stripped.join('\n') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2622,11 +2622,15 @@
|
||||
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.body).backgroundColor ||
|
||||
getComputedStyle(document.documentElement).backgroundColor ||
|
||||
'#ffffff'
|
||||
);
|
||||
// The walk already passed through <body> and <html>; if they had been
|
||||
// opaque we would have returned. Falling through with the previous
|
||||
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
|
||||
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
|
||||
// never set its own bg, which is truthy and short-circuits the chain to
|
||||
// transparent-black — modern-screenshot then renders the capture on a
|
||||
// black canvas and the shader overlay flashes solid black during load.
|
||||
// The browser canvas defaults to white, so we do too.
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
// Capture the element (with current annotations baked in) and return a PNG
|
||||
|
||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
||||
|
||||
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;
|
||||
// The tagRe captures any whitespace between the last attribute and the
|
||||
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||
// a replace would land it BEFORE that trailing space, leaving a double
|
||||
// space inside attrs and clobbering the space before `/>`. Split off
|
||||
// the trailing whitespace, splice the marker into the attribute body,
|
||||
// and re-append the original trailing whitespace so a self-closing
|
||||
// `<meta … />` round-trips byte-for-byte.
|
||||
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||
const newTag = tag.full.replace(attrs, newAttrs);
|
||||
|
||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
@@ -146,13 +146,12 @@ Options:
|
||||
? ['--id', event.id, '--discard']
|
||||
: ['--id', event.id, '--variant', event.variantId];
|
||||
if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
|
||||
// Pass through a JSON blob; the shell-safe wrap uses single quotes because
|
||||
// values are finite {id, number|string|boolean} pairs from a validated payload.
|
||||
scriptArgs.push('--param-values', `'${JSON.stringify(event.paramValues).replace(/'/g, "'\\''")}'`);
|
||||
scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
`node "${acceptScript}" ${scriptArgs.join(' ')}`,
|
||||
const out = execFileSync(
|
||||
'node',
|
||||
[acceptScript, ...scriptArgs],
|
||||
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
|
||||
);
|
||||
event._acceptResult = JSON.parse(out.trim());
|
||||
|
||||
@@ -21,11 +21,16 @@ import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './load-context.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// PID file in the project root so both the server and agent can find it
|
||||
// predictably (os.tmpdir() varies across platforms).
|
||||
const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
|
||||
// PRODUCT.md / DESIGN.md / DESIGN.json live wherever load-context.mjs resolves.
|
||||
// Keeps live-server in sync with the loader when users keep the docs in
|
||||
// .agents/context/, docs/, or a path set via IMPECCABLE_CONTEXT_DIR.
|
||||
const CONTEXT_DIR = resolveContextDir(process.cwd());
|
||||
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
||||
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
||||
|
||||
@@ -113,7 +118,7 @@ function hasProjectContext() {
|
||||
// concern, surfaced by the design panel's own empty state. Legacy
|
||||
// .impeccable.md is auto-migrated to PRODUCT.md by load-context.mjs.
|
||||
try {
|
||||
fs.accessSync(path.join(process.cwd(), 'PRODUCT.md'), fs.constants.R_OK);
|
||||
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -131,11 +136,21 @@ const VISUAL_ACTIONS = [
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
|
||||
// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars)
|
||||
// and variantIds via String(small integer). Restrict to those shapes so
|
||||
// any value that reaches a downstream child_process or DOM selector is
|
||||
// inert by construction.
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
||||
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
||||
|
||||
function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
||||
switch (msg.type) {
|
||||
case 'generate':
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!isValidId(msg.id)) return 'generate: missing or malformed id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
|
||||
@@ -145,8 +160,8 @@ function validateEvent(msg) {
|
||||
if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array';
|
||||
return null;
|
||||
case 'accept':
|
||||
if (!msg.id) return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
if (!isValidId(msg.id)) return 'accept: missing or malformed id';
|
||||
if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
|
||||
if (msg.paramValues !== undefined) {
|
||||
if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
|
||||
return 'accept: paramValues must be an object';
|
||||
@@ -154,7 +169,7 @@ function validateEvent(msg) {
|
||||
}
|
||||
return null;
|
||||
case 'discard':
|
||||
return msg.id ? null : 'discard: missing id';
|
||||
return isValidId(msg.id) ? null : 'discard: missing or malformed id';
|
||||
case 'exit':
|
||||
return null;
|
||||
case 'prefetch':
|
||||
@@ -315,8 +330,8 @@ function createRequestHandler({ detectScript, livePath }) {
|
||||
const token = url.searchParams.get('token');
|
||||
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
||||
|
||||
const mdPath = path.join(process.cwd(), 'DESIGN.md');
|
||||
const jsonPath = path.join(process.cwd(), 'DESIGN.json');
|
||||
const mdPath = path.join(CONTEXT_DIR, 'DESIGN.md');
|
||||
const jsonPath = path.join(CONTEXT_DIR, 'DESIGN.json');
|
||||
const mdStat = statOrNull(mdPath);
|
||||
const jsonStat = statOrNull(jsonPath);
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ Element identification (at least one required):
|
||||
|
||||
Optional:
|
||||
--file PATH Source file to search in (skips auto-detection)
|
||||
--text TEXT Picked element's textContent. Used to disambiguate when
|
||||
classes/tag match multiple sibling elements (e.g. a list
|
||||
of <Card>s with the same className). Pass the first ~80
|
||||
chars of event.element.textContent.
|
||||
--help Show this help message
|
||||
|
||||
Output (JSON):
|
||||
@@ -53,6 +57,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const tag = argVal(args, '--tag');
|
||||
const query = argVal(args, '--query');
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -115,17 +120,67 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find the element, trying each query in priority order.
|
||||
// Pass tag hint so findElement can reject matches inside wrong element types
|
||||
// and walk backward to the real opener on multi-line JSX tags.
|
||||
// Find the element, trying each query in priority order. When `--text` is
|
||||
// supplied, collect every candidate the queries surface and disambiguate
|
||||
// by the picked element's textContent. Without `--text`, fall back to the
|
||||
// legacy first-match behavior so unmodified callers keep working.
|
||||
let match = null;
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
if (text) {
|
||||
const candidates = [];
|
||||
for (const q of queries) {
|
||||
const all = findAllElements(lines, q, tag);
|
||||
for (const c of all) {
|
||||
if (!candidates.some((x) => x.startLine === c.startLine)) {
|
||||
candidates.push(c);
|
||||
}
|
||||
}
|
||||
// Once a more-specific query (ID, full className combo) yielded a unique
|
||||
// result, stop — falling through to the loose tag+single-class query
|
||||
// would readmit the siblings we just disambiguated past.
|
||||
if (candidates.length === 1) break;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
if (candidates.length === 1) {
|
||||
match = candidates[0];
|
||||
} else {
|
||||
const filtered = filterByText(candidates, lines, text);
|
||||
if (filtered.length === 1) {
|
||||
match = filtered[0];
|
||||
} else if (filtered.length === 0) {
|
||||
// Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
|
||||
// browser-side textContent doesn't appear literally in source. Fall
|
||||
// back to first-match rather than refusing — this is the same
|
||||
// behavior unmodified callers see, just preserved.
|
||||
match = candidates[0];
|
||||
} else {
|
||||
// Multiple candidates ALSO match the text. Truly ambiguous — refuse
|
||||
// rather than pick wrong, and hand the agent the candidate locations
|
||||
// so it can disambiguate by reading the file.
|
||||
console.error(JSON.stringify({
|
||||
error: 'element_ambiguous',
|
||||
fallback: 'agent-driven',
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
candidates: filtered.map((c) => ({
|
||||
startLine: c.startLine + 1,
|
||||
endLine: c.endLine + 1,
|
||||
})),
|
||||
hint: 'Multiple source elements match both classes/tag and textContent. Pass --element-id, a more specific --text, or write the wrapper manually. See "Handle fallback" in live.md.',
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const { startLine, endLine } = match;
|
||||
@@ -133,17 +188,48 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
const indent = lines[startLine].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the original element
|
||||
// Extract the original element. Reindent under the wrapper while preserving
|
||||
// the relative depth between lines — `l.trimStart()` would strip ALL leading
|
||||
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
|
||||
// to a single uniform indent, so on accept/discard the round-trip restores
|
||||
// the inner element at its parent's depth instead of nested inside it.
|
||||
// Strip only the COMMON minimum leading whitespace across the picked lines;
|
||||
// `deindentContent` on the accept side already mirrors this convention.
|
||||
const originalLines = lines.slice(startLine, endLine + 1);
|
||||
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
|
||||
const originalBaseIndent = minLeadingSpaces(originalLines);
|
||||
const reindentOriginal = (extra) => originalLines
|
||||
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
|
||||
.join('\n');
|
||||
const originalIndented = reindentOriginal(' ');
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
// either type-errors or renders a literal CSS string).
|
||||
const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"';
|
||||
|
||||
// Build the wrapper
|
||||
const wrapperLines = [
|
||||
// JSX/TSX guard: the picked element occupies a single JSX child slot
|
||||
// (inside `return (...)`, an array `.map(...)`, an `asChild` branch, or
|
||||
// any other expression position). Replacing it with `comment + <div> +
|
||||
// comment` yields three adjacent siblings — invalid JSX. We can't use a
|
||||
// Fragment `<></>` either: parents that clone children (Radix `asChild`,
|
||||
// Headless UI, etc.) hit "Invalid prop supplied to React.Fragment" when
|
||||
// they try to pass an `id` through.
|
||||
//
|
||||
// Solution: keep the wrapper `<div>` as the single JSX-slot child and
|
||||
// tuck both marker comments INSIDE it. accept/discard then expands its
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
reindentOriginal(' '),
|
||||
indent + ' </div>',
|
||||
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
@@ -163,13 +249,24 @@ The agent should insert variant HTML at insertLine.`);
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
// Calculate insert line (the "insert below this line" comment)
|
||||
const insertLine = startLine + 6; // 0-indexed in the new file
|
||||
// Calculate insert line (the "insert below this line" comment).
|
||||
// 0-indexed file position. Both HTML and JSX wrappers have 6 lines above
|
||||
// the insert marker (HTML: start-comment + outer-div + Original-comment +
|
||||
// original-div + content + close-original-div; JSX: outer-div +
|
||||
// start-comment + Original-comment + original-div + content +
|
||||
// close-original-div). Multi-line originals push the marker by their
|
||||
// extra line count.
|
||||
const insertLine = startLine + 6 + (originalLines.length - 1);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
startLine: startLine + 1, // 1-indexed for the agent
|
||||
endLine: startLine + wrapperLines.length, // 1-indexed
|
||||
// wrapperLines is an array but one element (the original-content slot)
|
||||
// is a `\n`-joined multi-line string, so the actual file-row count is
|
||||
// wrapperLines.length + (originalLines.length - 1). Without the offset,
|
||||
// endLine pointed inside the wrapper for any picked element that
|
||||
// spanned more than one source line.
|
||||
endLine: startLine + wrapperLines.length + (originalLines.length - 1), // 1-indexed
|
||||
insertLine: insertLine + 1, // 1-indexed: where variants go
|
||||
commentSyntax: commentSyntax,
|
||||
originalLineCount: originalLines.length,
|
||||
@@ -310,6 +407,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
|
||||
* line to find the actual tag opener. When `tag` is provided, opener candidates
|
||||
* must match that tag name.
|
||||
*/
|
||||
/**
|
||||
* Return the smallest leading-whitespace count across a set of lines,
|
||||
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
|
||||
* the common base indent of a multi-line picked element so reindenting
|
||||
* under the wrapper preserves the relative depth between lines.
|
||||
*/
|
||||
function minLeadingSpaces(lines) {
|
||||
let min = Infinity;
|
||||
for (const l of lines) {
|
||||
if (l.trim() === '') continue;
|
||||
const m = l.match(/^(\s*)/);
|
||||
if (m && m[1].length < min) min = m[1].length;
|
||||
}
|
||||
return min === Infinity ? 0 : min;
|
||||
}
|
||||
|
||||
function findElement(lines, query, tag = null) {
|
||||
// Iterate all matches — the first substring hit isn't always the right one.
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -330,6 +443,69 @@ function findElement(lines, query, tag = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like findElement, but returns every match. Used for ambiguity detection
|
||||
* when the agent passes --text: when the same className appears on multiple
|
||||
* sibling elements (a list of cards, repeated section variants, etc.),
|
||||
* first-match silently lands on the wrong branch. Returning all matches lets
|
||||
* the caller narrow by textContent or fail with a structured ambiguity error.
|
||||
*/
|
||||
function findAllElements(lines, query, tag = null) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].includes(query)) continue;
|
||||
const stripped = lines[i].trim();
|
||||
if (stripped.startsWith('<!--') || stripped.startsWith('{/*') || stripped.startsWith('//')) continue;
|
||||
if (lines[i].includes('data-impeccable-variant')) continue;
|
||||
const openerLine = findOpenerLine(lines, i, tag);
|
||||
if (openerLine === -1) continue;
|
||||
if (seen.has(openerLine)) continue; // multiple matches inside the same element
|
||||
seen.add(openerLine);
|
||||
const endLine = findClosingLine(lines, openerLine);
|
||||
out.push({ startLine: openerLine, endLine });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a candidate set to those whose source body matches a meaningful
|
||||
* prefix of the picked element's textContent. The compare strips tags and
|
||||
* JSX expressions, then checks two whitespace normalizations side-by-side:
|
||||
*
|
||||
* - single-space ("hero two second card body")
|
||||
* - no-whitespace ("herotwosecondcardbody")
|
||||
*
|
||||
* Both are needed because `el.textContent` concatenates sibling text without
|
||||
* inserting whitespace (e.g. `<h1>Hero Two</h1><p>Second…</p>` reads as
|
||||
* `"Hero TwoSecond…"`), while the source has whitespace between tags. If
|
||||
* EITHER normalization matches, the candidate keeps. A snippet shorter than
|
||||
* 8 chars after stripping is too weak to disambiguate — the caller falls
|
||||
* back to first-match.
|
||||
*/
|
||||
function filterByText(candidates, lines, text) {
|
||||
const trimmed = text.replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 80);
|
||||
// Too short to disambiguate. Return [] so the caller's `filtered.length
|
||||
// === 0` branch fires (fall back to first-match) — the previous
|
||||
// `candidates.slice()` return forced `filtered.length > 1` and surfaced
|
||||
// a spurious `element_ambiguous` error on every short-text picker event
|
||||
// with multiple candidates.
|
||||
if (trimmed.length < 8) return [];
|
||||
const targetSpaced = trimmed;
|
||||
const targetCompact = trimmed.replace(/\s+/g, '');
|
||||
|
||||
return candidates.filter((c) => {
|
||||
const body = lines.slice(c.startLine, c.endLine + 1).join(' ');
|
||||
const inner = body
|
||||
.replace(/<[^>]*>/g, ' ') // strip HTML/JSX tags
|
||||
.replace(/\{[^}]*\}/g, ' ') // strip JSX expressions
|
||||
.toLowerCase();
|
||||
const sourceSpaced = inner.replace(/\s+/g, ' ').trim();
|
||||
const sourceCompact = inner.replace(/\s+/g, '');
|
||||
return sourceSpaced.includes(targetSpaced) || sourceCompact.includes(targetCompact);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a match line to the real tag opener. If the match line itself opens
|
||||
* a tag, return it. Otherwise walk up to 10 lines backward looking for the
|
||||
|
||||
@@ -13,11 +13,21 @@
|
||||
* design: string | null, // DESIGN.md contents
|
||||
* designPath: string | null,
|
||||
* migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md
|
||||
* contextDir: string, // absolute path of the directory the files were found in
|
||||
* }
|
||||
*
|
||||
* Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The
|
||||
* Google DESIGN.md convention is uppercase at repo root; Kiro-style and
|
||||
* lowercase variants are also matched so users don't get punished for case.
|
||||
*
|
||||
* Lookup directory resolution (first match wins):
|
||||
* 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
|
||||
* 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat)
|
||||
* 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/
|
||||
* 4. cwd as a default "no context found" location
|
||||
*
|
||||
* Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root;
|
||||
* fallback directories are read-only as far as auto-rename is concerned.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -26,15 +36,52 @@ import path from 'node:path';
|
||||
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
|
||||
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
|
||||
const LEGACY_NAMES = ['.impeccable.md'];
|
||||
const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
||||
|
||||
/**
|
||||
* Resolve the directory that holds PRODUCT.md / DESIGN.md / DESIGN.json for
|
||||
* this project. Exported so other scripts (e.g. live-server.mjs) can read the
|
||||
* design files from the same location the loader uses.
|
||||
*/
|
||||
export function resolveContextDir(cwd = process.cwd()) {
|
||||
// 1. Explicit override
|
||||
const envDir = process.env.IMPECCABLE_CONTEXT_DIR;
|
||||
if (envDir && envDir.trim()) {
|
||||
const trimmed = envDir.trim();
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
}
|
||||
|
||||
// 2. cwd wins if any canonical or legacy file is there. We check legacy too
|
||||
// so the auto-migration path in loadContext stays predictable.
|
||||
if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present;
|
||||
// legacy `.impeccable.md` does not pull the lookup into a fallback dir.
|
||||
for (const rel of FALLBACK_DIRS) {
|
||||
const candidate = path.resolve(cwd, rel);
|
||||
if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Nothing found — keep the historical "default to cwd" behaviour so the
|
||||
// caller's `hasProduct === false` branch still fires the same way.
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function loadContext(cwd = process.cwd()) {
|
||||
let migrated = false;
|
||||
const contextDir = resolveContextDir(cwd);
|
||||
|
||||
// 1. Look for PRODUCT.md (case-insensitive)
|
||||
let productPath = firstExisting(cwd, PRODUCT_NAMES);
|
||||
// 1. Look for PRODUCT.md (case-insensitive) in the resolved dir
|
||||
let productPath = firstExisting(contextDir, PRODUCT_NAMES);
|
||||
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists, rename in place
|
||||
if (!productPath) {
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename
|
||||
// it in place. We only migrate at the root — fallback dirs are read-only
|
||||
// so we don't surprise users by mutating files under docs/ or .agents/.
|
||||
if (!productPath && contextDir === cwd) {
|
||||
const legacyPath = firstExisting(cwd, LEGACY_NAMES);
|
||||
if (legacyPath) {
|
||||
const newPath = path.join(cwd, 'PRODUCT.md');
|
||||
@@ -50,7 +97,7 @@ export function loadContext(cwd = process.cwd()) {
|
||||
}
|
||||
|
||||
// 3. DESIGN.md (case-insensitive)
|
||||
const designPath = firstExisting(cwd, DESIGN_NAMES);
|
||||
const designPath = firstExisting(contextDir, DESIGN_NAMES);
|
||||
|
||||
const product = productPath ? safeRead(productPath) : null;
|
||||
const design = designPath ? safeRead(designPath) : null;
|
||||
@@ -63,12 +110,13 @@ export function loadContext(cwd = process.cwd()) {
|
||||
design,
|
||||
designPath: designPath ? path.relative(cwd, designPath) : null,
|
||||
migrated,
|
||||
contextDir,
|
||||
};
|
||||
}
|
||||
|
||||
function firstExisting(cwd, names) {
|
||||
function firstExisting(dir, names) {
|
||||
for (const name of names) {
|
||||
const abs = path.join(cwd, name);
|
||||
const abs = path.join(dir, name);
|
||||
if (fs.existsSync(abs)) return abs;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{
|
||||
"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.2",
|
||||
"version": "3.0.5",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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.2",
|
||||
"version": "3.0.5",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -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.2
|
||||
version: 3.0.5
|
||||
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,11 +13,30 @@ 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
|
||||
|
||||
Two files at the project root, case-insensitive:
|
||||
Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd).
|
||||
|
||||
- **PRODUCT.md** — required. Users, brand, tone, anti-references, strategic principles.
|
||||
- **DESIGN.md** — optional, strongly recommended. Colors, typography, elevation, components.
|
||||
@@ -28,13 +47,13 @@ Load both in one call:
|
||||
node .claude/skills/impeccable/scripts/load-context.mjs
|
||||
```
|
||||
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`.
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from.
|
||||
|
||||
If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one.
|
||||
|
||||
`/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.
|
||||
|
||||
@@ -107,7 +126,10 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit
|
||||
|
||||
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
|
||||
|
||||
**Category-reflex check.** If someone could guess the theme and palette from the category name alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the training-data reflex. Rework the scene sentence and color strategy until the answer is no longer obvious from the domain.
|
||||
**Category-reflex check.** Run at two altitudes — the second one catches what the first one misses.
|
||||
|
||||
- **First-order:** if someone could guess the theme + palette from the category alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
|
||||
- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references — "AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode" — it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -147,6 +169,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
|
||||
|
||||
@@ -29,6 +29,16 @@ Training-data defaults. Ban list — look further:
|
||||
|
||||
Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
|
||||
|
||||
### Reflex-reject aesthetic lanes
|
||||
|
||||
Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex — the trap one tier deeper than picking a Fraunces font. Look further.
|
||||
|
||||
- **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
|
||||
|
||||
(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
|
||||
|
||||
The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins — variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in [live.md](live.md).
|
||||
|
||||
### Pairing and voice
|
||||
|
||||
Distinctive + refined is the goal — the specific shape depends on the brand:
|
||||
|
||||
@@ -1,24 +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.
|
||||
|
||||
## Real Example: Neo Mirai
|
||||
## Build Gate
|
||||
|
||||
Neo Mirai is the full craft loop in public. A retro-futurist AI design conference started with generated brand and hi-fi reference images, then shipped as a responsive static site in `public/neo-mirai`.
|
||||
Craft cannot build until all of these are true:
|
||||
|
||||
Repro command:
|
||||
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.
|
||||
|
||||
```bash
|
||||
/impeccable craft retro-futurist AI design conference website
|
||||
```
|
||||
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.
|
||||
|
||||
The important detail is the artifact chain: brand toolkit, north-star mock, semantic implementation, regenerated assets, browser iteration, responsive fixes. The mock was not treated as a screenshot to trace. It was used as direction for a real page.
|
||||
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.
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -52,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.
|
||||
|
||||
@@ -86,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.
|
||||
|
||||
@@ -29,7 +29,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
|
||||
node .claude/skills/impeccable/scripts/live.mjs
|
||||
```
|
||||
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent — extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
|
||||
`serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname).
|
||||
|
||||
@@ -74,7 +74,7 @@ Reading annotations precisely:
|
||||
### 2. Wrap the element
|
||||
|
||||
```bash
|
||||
node .claude/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
|
||||
node .claude/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
|
||||
```
|
||||
|
||||
Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
@@ -82,9 +82,12 @@ Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
- `--element-id` ← `event.element.id`
|
||||
- `--classes` ← `event.element.classes` joined with commas
|
||||
- `--tag` ← `event.element.tagName`
|
||||
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
|
||||
|
||||
The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only — do not use it for normal element lookups.
|
||||
|
||||
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"` — read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
|
||||
|
||||
Output on success: `{ file, insertLine, commentSyntax }`.
|
||||
|
||||
**Fallback errors.** Wrap only writes into files it judges to be source (tracked by git, not marked GENERATED, not listed in config's `generatedFiles`). If it can't land on a source file, it errors without writing — accepting a variant into a generated file is silent data loss. Three shapes:
|
||||
@@ -101,38 +104,81 @@ If `event.action` is `impeccable` (the default freeform action), use SKILL.md's
|
||||
|
||||
Any other `event.action` (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `adapt`, `animate`, `delight`, `overdrive`): Read `reference/<action>.md` before planning. Each sub-command encodes a specific discipline; skipping its reference produces generic output. Those files may require specific params; layer them on top of the §7 budget, not instead of it.
|
||||
|
||||
### 4. Plan three genuinely distinct directions
|
||||
### 4. Plan three variants — identity first, then mode, then axes
|
||||
|
||||
Before writing a single line of code, name each variant.
|
||||
The wrong frame for live mode is "show three different design directions." Live runs on an existing surface — the brand has already been chosen. The job is variation **within identity**, not selection between identities. Failure mode: three editorial-typographic variants on a brief that wasn't editorial. Bigger failure mode: three off-brand variants the user can't accept because they don't look like their product.
|
||||
|
||||
**For freeform (`action` is `impeccable`, or the user supplied a free prompt):** each variant must anchor to a different **archetype** — a real-world design analogue specific enough to be recognizable at a glance. Not "modern landing page." Not "minimal product hero." Examples:
|
||||
Four phases. Do them in order.
|
||||
|
||||
- *Broadsheet masthead with rule-divided columns* (think NYT print edition)
|
||||
- *Klim Type Foundry specimen page* (dense, technical, catalog-driven)
|
||||
- *Japanese print-poster minimalism with a single oversize glyph*
|
||||
- *Bloomberg Terminal status bar*
|
||||
- *Condé Nast Traveler feature layout*
|
||||
#### Phase A: Extract the identity (non-skippable)
|
||||
|
||||
Then commit each variant to a different **primary axis** of difference:
|
||||
The existing surface has an identity already. Read it before planning anything. Sources, in priority order:
|
||||
|
||||
1. **DESIGN.md** if loaded — read the visual system fields (palette, type pairing, motion, components). This is the authoritative answer.
|
||||
2. **CSS custom properties** in the page's stylesheets (`:root { --color-...; --font-...; ... }`) — these are de-facto tokens.
|
||||
3. **Computed styles** on the picked element and its parent — colors, fonts, spacing scales, corner radii.
|
||||
4. **Sibling components on the page** — what visual rhetoric do existing components use? (Asymmetric or centered? Dense or airy? Bold or quiet?)
|
||||
|
||||
Write down what you see in **one sentence**. The sentence describes the surface that's actually on screen — it is not aspirational, not opinionated, not edited toward what the brand "should" be. Capture, in roughly this order:
|
||||
|
||||
- The dominant surface color and accent color, by hex or token name (use the actual values, not categories like "warm" or "neutral").
|
||||
- The type pairing — the actual font names loaded, primary first.
|
||||
- The layout topology — how the dominant elements are arranged (stacked / side-by-side / grid / asymmetric / overlay).
|
||||
- The surface treatment — corners, borders, shadows, density of decoration.
|
||||
- The voice tone you read off the copy itself, not off the aesthetic feel.
|
||||
|
||||
Be specific. "Modern" is not a color, "elegant" is not a type pairing, "clean" is not a layout. If you can't extract a real value for an axis, skip it rather than fabricate. The point is to record what is, not to describe what you wish it were.
|
||||
|
||||
Do not include adjectives that name an aesthetic family ("editorial-leaning", "terminal-flavored", "brutalist") — those are conclusions, not data. They belong to Phase C lane selection in departure mode, not to identity description. Letting them sneak into Phase A is how the identity-lock collapses into a self-fulfilling prophecy.
|
||||
|
||||
This sentence is the **identity lock**. Every variant must be readable as the same brand if rendered side by side. Skipping this phase is the primary cause of off-brand variants. Absence of DESIGN.md is never an excuse — extract from CSS and computed styles instead.
|
||||
|
||||
#### Phase B: Pick mode (default vs departure)
|
||||
|
||||
**Default mode** — the existing identity is preserved. Variants vary expression axes within it. *This is the right mode for ~90% of live sessions.* The user picked an element on a real product they're shipping; they expect variants of *their* hero, not three different brands' heroes.
|
||||
|
||||
**Departure mode** — the existing identity is rejected. Variants propose alternatives consistent with PRODUCT.md voice. Trigger only when at least one is true:
|
||||
|
||||
- PRODUCT.md anti-references explicitly call out the current surface ("the current `index.html` is itself an example"; "diffuse away from this"; "the page on screen is the failure"). Generic anti-references that describe what to avoid in general do **not** trigger departure mode — only ones that point at *this* surface specifically.
|
||||
- The user's freeform prompt explicitly asks for departure ("rebuild this from scratch", "what if it weren't editorial at all", "show me something completely different").
|
||||
|
||||
If you're unsure, you're in default mode. The cost of being wrong about default is "three on-brand variants with similar feel" — recoverable, the user picks none. The cost of being wrong about departure is "three off-brand variants" — unrecoverable, the user is annoyed.
|
||||
|
||||
#### Phase C: Plan three variants
|
||||
|
||||
**Default mode.** Each variant commits to a different **primary axis** of difference, while preserving the identity sentence. The six axes:
|
||||
|
||||
1. **Hierarchy** — which element commands the eye?
|
||||
2. **Layout topology** — stacked / side-by-side / grid / asymmetric / overlay
|
||||
3. **Typographic system** — pairing, scale ratio, case/weight strategy
|
||||
4. **Color strategy** — Restrained / Committed / Full palette / Drenched
|
||||
3. **Typographic system** — pairing logic, scale ratio, case/weight strategy *within the available faces*
|
||||
4. **Color strategy** — which existing palette role carries the surface (Restrained / Committed / Full palette / Drenched). Use the brand's existing palette tokens, not new colors.
|
||||
5. **Density** — minimal / comfortable / dense
|
||||
6. **Structural decomposition** — merge, split, progressive disclosure
|
||||
|
||||
Three variants → three DIFFERENT primary axes, not three riffs on color.
|
||||
Three variants → three DIFFERENT axes. The trio reads as *the same brand at three angles*. Do not introduce new fonts, new palette hues, or new aesthetic-family signals — those belong to departure mode.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for one dark-neutral-accent, one light-drenched, one full-palette-saturated — three color worlds, not three shades of the same.
|
||||
**Departure mode.** Each variant anchors to a different **aesthetic lane**, drawn from non-monoculture options. Lanes (illustrative, not exhaustive):
|
||||
|
||||
**The squint test (before writing code).** Write the three one-sentence descriptions side by side:
|
||||
- Tech-minimal (Stripe / Linear / Vercel-restraint)
|
||||
- Brutalist-utility (system fonts, raw HTML defaults, Craigslist-energy)
|
||||
- Terminal-native (actual code surface, syntax-highlighted UI as the design)
|
||||
- Industrial-signage (dimensional type, arrow systems, ISO standards, wayfinding)
|
||||
- Technical-drawing (isometric line art, callout numbers, blueprint blue, exploded-view diagrams)
|
||||
- Drenched-saturated (Liquid Death / Mailchimp full-palette / single-hue commitment)
|
||||
- Swiss-grid-rigorous (visible grid as voice, tight type, accumulated systems)
|
||||
|
||||
> V1: Broadsheet masthead, ruled columns, 24px ink on cream.
|
||||
> V2: Enormous italic title, catalog spec rows, heavy monospace data.
|
||||
> V3: Card-framed poster with one oversize glyph, magenta veil.
|
||||
Avoid the **reflex-reject lanes** in [brand.md](brand.md). Don't trade one monoculture for another. *Editorial-typographic is currently a reflex-reject lane* — three variants that all read as "magazine cover" is the second-order training reflex, the trap one tier deeper than SaaS-cream.
|
||||
|
||||
If two of them rhyme ("both use big type" / "both are stacks of sections" / "both feature the CTA prominently"), rework the offender. Freeform variants failing the squint test is the primary failure mode of this flow — three-of-the-same with minor styling tweaks.
|
||||
#### Phase D: Squint test
|
||||
|
||||
**Default mode squint.** Read each variant's identity sentence and compare to the locked identity from Phase A. If any variant has drifted to a different palette, type voice, or visual rhetoric, it has crossed into departure mode by accident — rework. Then check that each variant commits to a different primary axis. Three "tighter density" variants is failure.
|
||||
|
||||
**Departure mode squint.** Two passes, family before sentence:
|
||||
|
||||
1. **Family pass.** Label each variant with one design-family word: *editorial / brutalist / terminal / signage / drenched / swiss / technical-drawing / tech-minimal*. If any two variants share a family label, rework. *This pass is non-negotiable in departure mode and catches the editorial-monoculture failure that the sentence pass misses.*
|
||||
2. **Sentence pass.** Write three one-sentence descriptions side by side. If two of them rhyme ("both feature big type" / "both are stacks of sections" / "both center the CTA"), rework the offender.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for three color worlds, not three shades of the same.
|
||||
|
||||
**For action-specific invocations**, each variant must vary along the dimension the action names:
|
||||
|
||||
@@ -150,7 +196,13 @@ If two of them rhyme ("both use big type" / "both are stacks of sections" / "bot
|
||||
|
||||
### 5. Apply the freeform prompt (if present)
|
||||
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. "Make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. The interpretations stay within whichever mode you picked in Phase B.
|
||||
|
||||
In **default mode**, the prompt narrows the axes you choose, not the identity. *"Make it feel more confident"* → variant 1 amplifies hierarchy (one element commands the eye), variant 2 commits the existing accent color (Committed strategy on the brand's hue), variant 3 tightens density and removes decorative slack. Three different axes, same brand.
|
||||
|
||||
In **departure mode**, the prompt narrows the lanes you draw from, not the families. *"Make it feel like a newspaper front page"* would itself be a departure-mode prompt; honor it but pick three meaningfully different newspaper-adjacent lanes (broadsheet vs. tabloid vs. trade journal), and run the family pass to confirm they don't collapse into one.
|
||||
|
||||
When the prompt and PRODUCT.md anti-references conflict (the prompt asks for X, the anti-references ban X), the anti-references win — they describe the brand's standing position, the prompt is one moment.
|
||||
|
||||
### 6. Write all variants in a single edit
|
||||
|
||||
@@ -181,6 +233,25 @@ The first variant has no `display: none` (visible by default). All others do. If
|
||||
|
||||
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
|
||||
|
||||
**Author every `:scope` rule with a descendant combinator.** The `@scope` boundary is the **variant wrapper `<div data-impeccable-variant="N">`**, not the element you're designing. A bare `:scope { background: cream; }` styles the wrapper, not the inner replacement, so the cream lands on a `display: contents` shell while the actual element keeps page defaults. Always step in: `:scope > .card`, `:scope > section`, `:scope .hero-title`, etc. The fake test agent's CSS in `tests/live-e2e/agent.mjs` is a faithful template — every rule starts `:scope > ...`.
|
||||
|
||||
**JSX / TSX target files.** Wrap `<style>` content in a template literal so the CSS `{` / `}` aren't parsed as JSX expressions, and use `className=` / `style={{…}}` on every variant element. Keep `data-impeccable-*` attributes as-is — they're plain strings:
|
||||
|
||||
```tsx
|
||||
<style data-impeccable-css="SESSION_ID">{`
|
||||
@scope ([data-impeccable-variant="1"]) { ... }
|
||||
@scope ([data-impeccable-variant="2"]) { ... }
|
||||
`}</style>
|
||||
<div data-impeccable-variant="1">
|
||||
{/* variant 1 */}
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style={{ display: 'none' }}>
|
||||
{/* variant 2 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
The wrap script already gives you a single-rooted JSX wrapper — a `<div data-impeccable-variants="…">` outer element with the marker comments tucked inside. Drop the variants block above into the "Variants: insert below this line" comment and the source stays valid TSX.
|
||||
|
||||
### 7. Parameters (composition-sized, 0–4 per variant)
|
||||
|
||||
Each variant can expose **coarse** knobs alongside the full HTML/CSS replacement. The browser docks a small panel to the right of the outline with one control per parameter. The user drags/clicks and sees instant feedback: there is zero regeneration cost because the knob toggles a CSS variable or data attribute that the variant's scoped CSS is already authored against.
|
||||
@@ -246,6 +317,16 @@ node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --fil
|
||||
|
||||
Then run `live-poll.mjs` again immediately.
|
||||
|
||||
### Aborting an in-flight session
|
||||
|
||||
If wrap or generation fails after the browser has flipped to GENERATING (e.g. wrap landed on the wrong source branch and you've already reverted it, or generation hit an unrecoverable error), tell the **browser** so its bar resets to PICKING:
|
||||
|
||||
```bash
|
||||
node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"
|
||||
```
|
||||
|
||||
Don't run `live-accept --discard` for this — that's a pure file mutator, the browser doesn't see it, and the bar gets stuck on the GENERATING dots forever (the user has to refresh). `--discard` is only correct when the **browser** initiated the discard (user clicked ✕ during CYCLING) and the agent is just running source-side cleanup the browser already triggered.
|
||||
|
||||
## Handle fallback
|
||||
|
||||
When wrap returns `fallback: "agent-driven"`, the deterministic flow doesn't apply. Pick up here.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -104,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`
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -105,15 +105,22 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...restored,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
return {};
|
||||
@@ -127,8 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -149,7 +162,6 @@ 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);
|
||||
// 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.
|
||||
@@ -177,7 +189,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
// 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) {
|
||||
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);
|
||||
@@ -187,9 +198,9 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
}
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
@@ -218,6 +229,72 @@ function findMarkerBlock(id, lines) {
|
||||
return (start !== -1 && end !== -1) ? { start, end } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the line range to REPLACE (vs. just the marker range to extract
|
||||
* from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE
|
||||
* the `<div data-impeccable-variants="ID">` outer wrapper so the picked
|
||||
* element's JSX slot keeps a single child — a Fragment `<></>` would have
|
||||
* solved the multi-sibling case but failed inside `asChild` / cloneElement
|
||||
* parents with "Invalid prop supplied to React.Fragment".
|
||||
*
|
||||
* That means the marker block is enclosed by the wrapper `<div>` opener
|
||||
* (with `data-impeccable-variants="ID"`) and its matching `</div>`. We
|
||||
* walk back to the opener and forward to the closer so accept/discard
|
||||
* remove the entire scaffold, not just the inner markers.
|
||||
*
|
||||
* Marker lines themselves stay where they were so extractOriginal /
|
||||
* extractVariant / extractCss continue to walk the same range.
|
||||
*/
|
||||
function expandReplaceRange(block, lines, isJsx) {
|
||||
if (!isJsx) return { start: block.start, end: block.end };
|
||||
|
||||
let { start, end } = block;
|
||||
|
||||
// Walk back for the wrapper `<div data-impeccable-variants="..."` opener.
|
||||
// The attr may sit on a continuation line of a multi-line opening tag, so
|
||||
// also walk to the line that actually contains `<div`.
|
||||
for (let i = start - 1; i >= Math.max(0, start - 12); i--) {
|
||||
if (/data-impeccable-variants=/.test(lines[i])) {
|
||||
let opener = i;
|
||||
while (opener > 0 && !/<div\b/.test(lines[opener])) opener--;
|
||||
start = opener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk forward to the matching `</div>` by div-depth tracking from the
|
||||
// wrapper opener. Operate on JOINED text instead of per-line: a
|
||||
// multi-line self-closing JSX `<div\n className="spacer"\n/>` would
|
||||
// fool per-line regex tracking (the `<div` line matches openRe but the
|
||||
// `/>` line never matches selfCloseRe since it needs `<div` on the same
|
||||
// line). That left depth permanently over-counted and the wrapper's
|
||||
// outer `</div>` orphaned after accept/discard. Single regex with
|
||||
// `[^>]*?` (which spans newlines in JS) handles either form correctly.
|
||||
const joined = lines.slice(start).join('\n');
|
||||
// Match either `<div … />` (self-close, group 1 is `/`), `<div … >`
|
||||
// (open, group 1 is empty), or `</div>`.
|
||||
const tagRe = /<div\b[^>]*?(\/?)>|<\/div\s*>/g;
|
||||
let depth = 0;
|
||||
let m;
|
||||
while ((m = tagRe.exec(joined)) !== null) {
|
||||
const isClose = m[0].startsWith('</');
|
||||
const isSelfClose = !isClose && m[1] === '/';
|
||||
if (isClose) depth--;
|
||||
else if (!isSelfClose) depth++;
|
||||
if (depth <= 0) {
|
||||
// m.index is offset within `joined`; convert back to a file line.
|
||||
const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
|
||||
const candidateEnd = start + linesBefore;
|
||||
if (candidateEnd >= end) {
|
||||
end = candidateEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wrapper lines into a single string with `<style>` elements removed so
|
||||
* marker matching and div-depth tracking aren't confused by:
|
||||
@@ -345,7 +422,7 @@ function extractCss(lines, block, id) {
|
||||
// Same-line open + close: extract inner text.
|
||||
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
||||
if (sameLine) {
|
||||
const inner = sameLine[1];
|
||||
const inner = stripJsxTemplateWrap(sameLine[1]);
|
||||
return inner.length > 0 ? inner.split('\n') : null;
|
||||
}
|
||||
inStyle = true;
|
||||
@@ -362,7 +439,60 @@ function extractCss(lines, block, id) {
|
||||
}
|
||||
}
|
||||
|
||||
return content.length > 0 ? content : null;
|
||||
if (content.length === 0) return null;
|
||||
return stripJsxTemplateLines(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a
|
||||
* `<style>` element in a JSX/TSX file. The agent may write the wrap with
|
||||
* `{` and `}` directly attached to the `<style>` tags, on their own lines,
|
||||
* or attached to the first/last CSS lines — all three are JSX-legal.
|
||||
*
|
||||
* Stripping is required because handleAccept re-wraps the CSS itself when
|
||||
* carbonizing. Without this, two consecutive accepts (or a previously-
|
||||
* accepted variants block being carbonized) would produce nested
|
||||
* `{` `{` … `}` `}`, which oxc rejects with "Expected `}` but found `@`".
|
||||
*/
|
||||
function stripJsxTemplateLines(content) {
|
||||
const out = content.slice();
|
||||
|
||||
// Drop any leading blank lines so we don't miss a `{` line buried below
|
||||
// them; same for trailing.
|
||||
while (out.length > 0 && out[0].trim() === '') out.shift();
|
||||
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Leading `{`: own line, or attached to the first CSS line.
|
||||
const firstTrim = out[0].trimStart();
|
||||
if (firstTrim === '{`') {
|
||||
out.shift();
|
||||
} else if (firstTrim.startsWith('{`')) {
|
||||
const idx = out[0].indexOf('{`');
|
||||
out[0] = out[0].slice(0, idx) + out[0].slice(idx + 2);
|
||||
if (out[0].trim() === '') out.shift();
|
||||
}
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Trailing `` ` `` `}`: own line, or attached to the last CSS line.
|
||||
const lastIdx = out.length - 1;
|
||||
const lastTrim = out[lastIdx].trimEnd();
|
||||
if (lastTrim === '`}') {
|
||||
out.pop();
|
||||
} else if (lastTrim.endsWith('`}')) {
|
||||
const text = out[lastIdx];
|
||||
const idx = text.lastIndexOf('`}');
|
||||
out[lastIdx] = text.slice(0, idx) + text.slice(idx + 2);
|
||||
if (out[lastIdx].trim() === '') out.pop();
|
||||
}
|
||||
|
||||
return out.length > 0 ? out : null;
|
||||
}
|
||||
|
||||
function stripJsxTemplateWrap(text) {
|
||||
const lines = text.split('\n');
|
||||
const stripped = stripJsxTemplateLines(lines);
|
||||
return stripped ? stripped.join('\n') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2622,11 +2622,15 @@
|
||||
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.body).backgroundColor ||
|
||||
getComputedStyle(document.documentElement).backgroundColor ||
|
||||
'#ffffff'
|
||||
);
|
||||
// The walk already passed through <body> and <html>; if they had been
|
||||
// opaque we would have returned. Falling through with the previous
|
||||
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
|
||||
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
|
||||
// never set its own bg, which is truthy and short-circuits the chain to
|
||||
// transparent-black — modern-screenshot then renders the capture on a
|
||||
// black canvas and the shader overlay flashes solid black during load.
|
||||
// The browser canvas defaults to white, so we do too.
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
// Capture the element (with current annotations baked in) and return a PNG
|
||||
|
||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
||||
|
||||
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;
|
||||
// The tagRe captures any whitespace between the last attribute and the
|
||||
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||
// a replace would land it BEFORE that trailing space, leaving a double
|
||||
// space inside attrs and clobbering the space before `/>`. Split off
|
||||
// the trailing whitespace, splice the marker into the attribute body,
|
||||
// and re-append the original trailing whitespace so a self-closing
|
||||
// `<meta … />` round-trips byte-for-byte.
|
||||
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||
const newTag = tag.full.replace(attrs, newAttrs);
|
||||
|
||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
@@ -146,13 +146,12 @@ Options:
|
||||
? ['--id', event.id, '--discard']
|
||||
: ['--id', event.id, '--variant', event.variantId];
|
||||
if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
|
||||
// Pass through a JSON blob; the shell-safe wrap uses single quotes because
|
||||
// values are finite {id, number|string|boolean} pairs from a validated payload.
|
||||
scriptArgs.push('--param-values', `'${JSON.stringify(event.paramValues).replace(/'/g, "'\\''")}'`);
|
||||
scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
`node "${acceptScript}" ${scriptArgs.join(' ')}`,
|
||||
const out = execFileSync(
|
||||
'node',
|
||||
[acceptScript, ...scriptArgs],
|
||||
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
|
||||
);
|
||||
event._acceptResult = JSON.parse(out.trim());
|
||||
|
||||
@@ -21,11 +21,16 @@ import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './load-context.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// PID file in the project root so both the server and agent can find it
|
||||
// predictably (os.tmpdir() varies across platforms).
|
||||
const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
|
||||
// PRODUCT.md / DESIGN.md / DESIGN.json live wherever load-context.mjs resolves.
|
||||
// Keeps live-server in sync with the loader when users keep the docs in
|
||||
// .agents/context/, docs/, or a path set via IMPECCABLE_CONTEXT_DIR.
|
||||
const CONTEXT_DIR = resolveContextDir(process.cwd());
|
||||
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
||||
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
||||
|
||||
@@ -113,7 +118,7 @@ function hasProjectContext() {
|
||||
// concern, surfaced by the design panel's own empty state. Legacy
|
||||
// .impeccable.md is auto-migrated to PRODUCT.md by load-context.mjs.
|
||||
try {
|
||||
fs.accessSync(path.join(process.cwd(), 'PRODUCT.md'), fs.constants.R_OK);
|
||||
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -131,11 +136,21 @@ const VISUAL_ACTIONS = [
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
|
||||
// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars)
|
||||
// and variantIds via String(small integer). Restrict to those shapes so
|
||||
// any value that reaches a downstream child_process or DOM selector is
|
||||
// inert by construction.
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
||||
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
||||
|
||||
function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
||||
switch (msg.type) {
|
||||
case 'generate':
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!isValidId(msg.id)) return 'generate: missing or malformed id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
|
||||
@@ -145,8 +160,8 @@ function validateEvent(msg) {
|
||||
if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array';
|
||||
return null;
|
||||
case 'accept':
|
||||
if (!msg.id) return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
if (!isValidId(msg.id)) return 'accept: missing or malformed id';
|
||||
if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
|
||||
if (msg.paramValues !== undefined) {
|
||||
if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
|
||||
return 'accept: paramValues must be an object';
|
||||
@@ -154,7 +169,7 @@ function validateEvent(msg) {
|
||||
}
|
||||
return null;
|
||||
case 'discard':
|
||||
return msg.id ? null : 'discard: missing id';
|
||||
return isValidId(msg.id) ? null : 'discard: missing or malformed id';
|
||||
case 'exit':
|
||||
return null;
|
||||
case 'prefetch':
|
||||
@@ -315,8 +330,8 @@ function createRequestHandler({ detectScript, livePath }) {
|
||||
const token = url.searchParams.get('token');
|
||||
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
||||
|
||||
const mdPath = path.join(process.cwd(), 'DESIGN.md');
|
||||
const jsonPath = path.join(process.cwd(), 'DESIGN.json');
|
||||
const mdPath = path.join(CONTEXT_DIR, 'DESIGN.md');
|
||||
const jsonPath = path.join(CONTEXT_DIR, 'DESIGN.json');
|
||||
const mdStat = statOrNull(mdPath);
|
||||
const jsonStat = statOrNull(jsonPath);
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ Element identification (at least one required):
|
||||
|
||||
Optional:
|
||||
--file PATH Source file to search in (skips auto-detection)
|
||||
--text TEXT Picked element's textContent. Used to disambiguate when
|
||||
classes/tag match multiple sibling elements (e.g. a list
|
||||
of <Card>s with the same className). Pass the first ~80
|
||||
chars of event.element.textContent.
|
||||
--help Show this help message
|
||||
|
||||
Output (JSON):
|
||||
@@ -53,6 +57,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const tag = argVal(args, '--tag');
|
||||
const query = argVal(args, '--query');
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -115,17 +120,67 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find the element, trying each query in priority order.
|
||||
// Pass tag hint so findElement can reject matches inside wrong element types
|
||||
// and walk backward to the real opener on multi-line JSX tags.
|
||||
// Find the element, trying each query in priority order. When `--text` is
|
||||
// supplied, collect every candidate the queries surface and disambiguate
|
||||
// by the picked element's textContent. Without `--text`, fall back to the
|
||||
// legacy first-match behavior so unmodified callers keep working.
|
||||
let match = null;
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
if (text) {
|
||||
const candidates = [];
|
||||
for (const q of queries) {
|
||||
const all = findAllElements(lines, q, tag);
|
||||
for (const c of all) {
|
||||
if (!candidates.some((x) => x.startLine === c.startLine)) {
|
||||
candidates.push(c);
|
||||
}
|
||||
}
|
||||
// Once a more-specific query (ID, full className combo) yielded a unique
|
||||
// result, stop — falling through to the loose tag+single-class query
|
||||
// would readmit the siblings we just disambiguated past.
|
||||
if (candidates.length === 1) break;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
if (candidates.length === 1) {
|
||||
match = candidates[0];
|
||||
} else {
|
||||
const filtered = filterByText(candidates, lines, text);
|
||||
if (filtered.length === 1) {
|
||||
match = filtered[0];
|
||||
} else if (filtered.length === 0) {
|
||||
// Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
|
||||
// browser-side textContent doesn't appear literally in source. Fall
|
||||
// back to first-match rather than refusing — this is the same
|
||||
// behavior unmodified callers see, just preserved.
|
||||
match = candidates[0];
|
||||
} else {
|
||||
// Multiple candidates ALSO match the text. Truly ambiguous — refuse
|
||||
// rather than pick wrong, and hand the agent the candidate locations
|
||||
// so it can disambiguate by reading the file.
|
||||
console.error(JSON.stringify({
|
||||
error: 'element_ambiguous',
|
||||
fallback: 'agent-driven',
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
candidates: filtered.map((c) => ({
|
||||
startLine: c.startLine + 1,
|
||||
endLine: c.endLine + 1,
|
||||
})),
|
||||
hint: 'Multiple source elements match both classes/tag and textContent. Pass --element-id, a more specific --text, or write the wrapper manually. See "Handle fallback" in live.md.',
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const { startLine, endLine } = match;
|
||||
@@ -133,17 +188,48 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
const indent = lines[startLine].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the original element
|
||||
// Extract the original element. Reindent under the wrapper while preserving
|
||||
// the relative depth between lines — `l.trimStart()` would strip ALL leading
|
||||
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
|
||||
// to a single uniform indent, so on accept/discard the round-trip restores
|
||||
// the inner element at its parent's depth instead of nested inside it.
|
||||
// Strip only the COMMON minimum leading whitespace across the picked lines;
|
||||
// `deindentContent` on the accept side already mirrors this convention.
|
||||
const originalLines = lines.slice(startLine, endLine + 1);
|
||||
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
|
||||
const originalBaseIndent = minLeadingSpaces(originalLines);
|
||||
const reindentOriginal = (extra) => originalLines
|
||||
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
|
||||
.join('\n');
|
||||
const originalIndented = reindentOriginal(' ');
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
// either type-errors or renders a literal CSS string).
|
||||
const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"';
|
||||
|
||||
// Build the wrapper
|
||||
const wrapperLines = [
|
||||
// JSX/TSX guard: the picked element occupies a single JSX child slot
|
||||
// (inside `return (...)`, an array `.map(...)`, an `asChild` branch, or
|
||||
// any other expression position). Replacing it with `comment + <div> +
|
||||
// comment` yields three adjacent siblings — invalid JSX. We can't use a
|
||||
// Fragment `<></>` either: parents that clone children (Radix `asChild`,
|
||||
// Headless UI, etc.) hit "Invalid prop supplied to React.Fragment" when
|
||||
// they try to pass an `id` through.
|
||||
//
|
||||
// Solution: keep the wrapper `<div>` as the single JSX-slot child and
|
||||
// tuck both marker comments INSIDE it. accept/discard then expands its
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
reindentOriginal(' '),
|
||||
indent + ' </div>',
|
||||
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
@@ -163,13 +249,24 @@ The agent should insert variant HTML at insertLine.`);
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
// Calculate insert line (the "insert below this line" comment)
|
||||
const insertLine = startLine + 6; // 0-indexed in the new file
|
||||
// Calculate insert line (the "insert below this line" comment).
|
||||
// 0-indexed file position. Both HTML and JSX wrappers have 6 lines above
|
||||
// the insert marker (HTML: start-comment + outer-div + Original-comment +
|
||||
// original-div + content + close-original-div; JSX: outer-div +
|
||||
// start-comment + Original-comment + original-div + content +
|
||||
// close-original-div). Multi-line originals push the marker by their
|
||||
// extra line count.
|
||||
const insertLine = startLine + 6 + (originalLines.length - 1);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
startLine: startLine + 1, // 1-indexed for the agent
|
||||
endLine: startLine + wrapperLines.length, // 1-indexed
|
||||
// wrapperLines is an array but one element (the original-content slot)
|
||||
// is a `\n`-joined multi-line string, so the actual file-row count is
|
||||
// wrapperLines.length + (originalLines.length - 1). Without the offset,
|
||||
// endLine pointed inside the wrapper for any picked element that
|
||||
// spanned more than one source line.
|
||||
endLine: startLine + wrapperLines.length + (originalLines.length - 1), // 1-indexed
|
||||
insertLine: insertLine + 1, // 1-indexed: where variants go
|
||||
commentSyntax: commentSyntax,
|
||||
originalLineCount: originalLines.length,
|
||||
@@ -310,6 +407,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
|
||||
* line to find the actual tag opener. When `tag` is provided, opener candidates
|
||||
* must match that tag name.
|
||||
*/
|
||||
/**
|
||||
* Return the smallest leading-whitespace count across a set of lines,
|
||||
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
|
||||
* the common base indent of a multi-line picked element so reindenting
|
||||
* under the wrapper preserves the relative depth between lines.
|
||||
*/
|
||||
function minLeadingSpaces(lines) {
|
||||
let min = Infinity;
|
||||
for (const l of lines) {
|
||||
if (l.trim() === '') continue;
|
||||
const m = l.match(/^(\s*)/);
|
||||
if (m && m[1].length < min) min = m[1].length;
|
||||
}
|
||||
return min === Infinity ? 0 : min;
|
||||
}
|
||||
|
||||
function findElement(lines, query, tag = null) {
|
||||
// Iterate all matches — the first substring hit isn't always the right one.
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -330,6 +443,69 @@ function findElement(lines, query, tag = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like findElement, but returns every match. Used for ambiguity detection
|
||||
* when the agent passes --text: when the same className appears on multiple
|
||||
* sibling elements (a list of cards, repeated section variants, etc.),
|
||||
* first-match silently lands on the wrong branch. Returning all matches lets
|
||||
* the caller narrow by textContent or fail with a structured ambiguity error.
|
||||
*/
|
||||
function findAllElements(lines, query, tag = null) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].includes(query)) continue;
|
||||
const stripped = lines[i].trim();
|
||||
if (stripped.startsWith('<!--') || stripped.startsWith('{/*') || stripped.startsWith('//')) continue;
|
||||
if (lines[i].includes('data-impeccable-variant')) continue;
|
||||
const openerLine = findOpenerLine(lines, i, tag);
|
||||
if (openerLine === -1) continue;
|
||||
if (seen.has(openerLine)) continue; // multiple matches inside the same element
|
||||
seen.add(openerLine);
|
||||
const endLine = findClosingLine(lines, openerLine);
|
||||
out.push({ startLine: openerLine, endLine });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a candidate set to those whose source body matches a meaningful
|
||||
* prefix of the picked element's textContent. The compare strips tags and
|
||||
* JSX expressions, then checks two whitespace normalizations side-by-side:
|
||||
*
|
||||
* - single-space ("hero two second card body")
|
||||
* - no-whitespace ("herotwosecondcardbody")
|
||||
*
|
||||
* Both are needed because `el.textContent` concatenates sibling text without
|
||||
* inserting whitespace (e.g. `<h1>Hero Two</h1><p>Second…</p>` reads as
|
||||
* `"Hero TwoSecond…"`), while the source has whitespace between tags. If
|
||||
* EITHER normalization matches, the candidate keeps. A snippet shorter than
|
||||
* 8 chars after stripping is too weak to disambiguate — the caller falls
|
||||
* back to first-match.
|
||||
*/
|
||||
function filterByText(candidates, lines, text) {
|
||||
const trimmed = text.replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 80);
|
||||
// Too short to disambiguate. Return [] so the caller's `filtered.length
|
||||
// === 0` branch fires (fall back to first-match) — the previous
|
||||
// `candidates.slice()` return forced `filtered.length > 1` and surfaced
|
||||
// a spurious `element_ambiguous` error on every short-text picker event
|
||||
// with multiple candidates.
|
||||
if (trimmed.length < 8) return [];
|
||||
const targetSpaced = trimmed;
|
||||
const targetCompact = trimmed.replace(/\s+/g, '');
|
||||
|
||||
return candidates.filter((c) => {
|
||||
const body = lines.slice(c.startLine, c.endLine + 1).join(' ');
|
||||
const inner = body
|
||||
.replace(/<[^>]*>/g, ' ') // strip HTML/JSX tags
|
||||
.replace(/\{[^}]*\}/g, ' ') // strip JSX expressions
|
||||
.toLowerCase();
|
||||
const sourceSpaced = inner.replace(/\s+/g, ' ').trim();
|
||||
const sourceCompact = inner.replace(/\s+/g, '');
|
||||
return sourceSpaced.includes(targetSpaced) || sourceCompact.includes(targetCompact);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a match line to the real tag opener. If the match line itself opens
|
||||
* a tag, return it. Otherwise walk up to 10 lines backward looking for the
|
||||
|
||||
@@ -13,11 +13,21 @@
|
||||
* design: string | null, // DESIGN.md contents
|
||||
* designPath: string | null,
|
||||
* migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md
|
||||
* contextDir: string, // absolute path of the directory the files were found in
|
||||
* }
|
||||
*
|
||||
* Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The
|
||||
* Google DESIGN.md convention is uppercase at repo root; Kiro-style and
|
||||
* lowercase variants are also matched so users don't get punished for case.
|
||||
*
|
||||
* Lookup directory resolution (first match wins):
|
||||
* 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
|
||||
* 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat)
|
||||
* 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/
|
||||
* 4. cwd as a default "no context found" location
|
||||
*
|
||||
* Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root;
|
||||
* fallback directories are read-only as far as auto-rename is concerned.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -26,15 +36,52 @@ import path from 'node:path';
|
||||
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
|
||||
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
|
||||
const LEGACY_NAMES = ['.impeccable.md'];
|
||||
const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
||||
|
||||
/**
|
||||
* Resolve the directory that holds PRODUCT.md / DESIGN.md / DESIGN.json for
|
||||
* this project. Exported so other scripts (e.g. live-server.mjs) can read the
|
||||
* design files from the same location the loader uses.
|
||||
*/
|
||||
export function resolveContextDir(cwd = process.cwd()) {
|
||||
// 1. Explicit override
|
||||
const envDir = process.env.IMPECCABLE_CONTEXT_DIR;
|
||||
if (envDir && envDir.trim()) {
|
||||
const trimmed = envDir.trim();
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
}
|
||||
|
||||
// 2. cwd wins if any canonical or legacy file is there. We check legacy too
|
||||
// so the auto-migration path in loadContext stays predictable.
|
||||
if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present;
|
||||
// legacy `.impeccable.md` does not pull the lookup into a fallback dir.
|
||||
for (const rel of FALLBACK_DIRS) {
|
||||
const candidate = path.resolve(cwd, rel);
|
||||
if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Nothing found — keep the historical "default to cwd" behaviour so the
|
||||
// caller's `hasProduct === false` branch still fires the same way.
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function loadContext(cwd = process.cwd()) {
|
||||
let migrated = false;
|
||||
const contextDir = resolveContextDir(cwd);
|
||||
|
||||
// 1. Look for PRODUCT.md (case-insensitive)
|
||||
let productPath = firstExisting(cwd, PRODUCT_NAMES);
|
||||
// 1. Look for PRODUCT.md (case-insensitive) in the resolved dir
|
||||
let productPath = firstExisting(contextDir, PRODUCT_NAMES);
|
||||
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists, rename in place
|
||||
if (!productPath) {
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename
|
||||
// it in place. We only migrate at the root — fallback dirs are read-only
|
||||
// so we don't surprise users by mutating files under docs/ or .agents/.
|
||||
if (!productPath && contextDir === cwd) {
|
||||
const legacyPath = firstExisting(cwd, LEGACY_NAMES);
|
||||
if (legacyPath) {
|
||||
const newPath = path.join(cwd, 'PRODUCT.md');
|
||||
@@ -50,7 +97,7 @@ export function loadContext(cwd = process.cwd()) {
|
||||
}
|
||||
|
||||
// 3. DESIGN.md (case-insensitive)
|
||||
const designPath = firstExisting(cwd, DESIGN_NAMES);
|
||||
const designPath = firstExisting(contextDir, DESIGN_NAMES);
|
||||
|
||||
const product = productPath ? safeRead(productPath) : null;
|
||||
const design = designPath ? safeRead(designPath) : null;
|
||||
@@ -63,12 +110,13 @@ export function loadContext(cwd = process.cwd()) {
|
||||
design,
|
||||
designPath: designPath ? path.relative(cwd, designPath) : null,
|
||||
migrated,
|
||||
contextDir,
|
||||
};
|
||||
}
|
||||
|
||||
function firstExisting(cwd, names) {
|
||||
function firstExisting(dir, names) {
|
||||
for (const name of names) {
|
||||
const abs = path.join(cwd, name);
|
||||
const abs = path.join(dir, name);
|
||||
if (fs.existsSync(abs)) return abs;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -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.2
|
||||
version: 3.0.5
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
|
||||
@@ -9,11 +9,30 @@ 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
|
||||
|
||||
Two files at the project root, case-insensitive:
|
||||
Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd).
|
||||
|
||||
- **PRODUCT.md** — required. Users, brand, tone, anti-references, strategic principles.
|
||||
- **DESIGN.md** — optional, strongly recommended. Colors, typography, elevation, components.
|
||||
@@ -24,13 +43,13 @@ Load both in one call:
|
||||
node .cursor/skills/impeccable/scripts/load-context.mjs
|
||||
```
|
||||
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`.
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from.
|
||||
|
||||
If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one.
|
||||
|
||||
`/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.
|
||||
|
||||
@@ -103,7 +122,10 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit
|
||||
|
||||
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
|
||||
|
||||
**Category-reflex check.** If someone could guess the theme and palette from the category name alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the training-data reflex. Rework the scene sentence and color strategy until the answer is no longer obvious from the domain.
|
||||
**Category-reflex check.** Run at two altitudes — the second one catches what the first one misses.
|
||||
|
||||
- **First-order:** if someone could guess the theme + palette from the category alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
|
||||
- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references — "AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode" — it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -143,6 +165,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
|
||||
|
||||
@@ -29,6 +29,16 @@ Training-data defaults. Ban list — look further:
|
||||
|
||||
Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
|
||||
|
||||
### Reflex-reject aesthetic lanes
|
||||
|
||||
Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex — the trap one tier deeper than picking a Fraunces font. Look further.
|
||||
|
||||
- **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
|
||||
|
||||
(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
|
||||
|
||||
The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins — variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in [live.md](live.md).
|
||||
|
||||
### Pairing and voice
|
||||
|
||||
Distinctive + refined is the goal — the specific shape depends on the brand:
|
||||
|
||||
@@ -1,24 +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.
|
||||
|
||||
## Real Example: Neo Mirai
|
||||
## Build Gate
|
||||
|
||||
Neo Mirai is the full craft loop in public. A retro-futurist AI design conference started with generated brand and hi-fi reference images, then shipped as a responsive static site in `public/neo-mirai`.
|
||||
Craft cannot build until all of these are true:
|
||||
|
||||
Repro command:
|
||||
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.
|
||||
|
||||
```bash
|
||||
/impeccable craft retro-futurist AI design conference website
|
||||
```
|
||||
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.
|
||||
|
||||
The important detail is the artifact chain: brand toolkit, north-star mock, semantic implementation, regenerated assets, browser iteration, responsive fixes. The mock was not treated as a screenshot to trace. It was used as direction for a real page.
|
||||
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.
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -52,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.
|
||||
|
||||
@@ -86,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.
|
||||
|
||||
@@ -29,7 +29,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
|
||||
node .cursor/skills/impeccable/scripts/live.mjs
|
||||
```
|
||||
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent — extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
|
||||
`serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname).
|
||||
|
||||
@@ -74,7 +74,7 @@ Reading annotations precisely:
|
||||
### 2. Wrap the element
|
||||
|
||||
```bash
|
||||
node .cursor/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
|
||||
node .cursor/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
|
||||
```
|
||||
|
||||
Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
@@ -82,9 +82,12 @@ Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
- `--element-id` ← `event.element.id`
|
||||
- `--classes` ← `event.element.classes` joined with commas
|
||||
- `--tag` ← `event.element.tagName`
|
||||
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
|
||||
|
||||
The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only — do not use it for normal element lookups.
|
||||
|
||||
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"` — read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
|
||||
|
||||
Output on success: `{ file, insertLine, commentSyntax }`.
|
||||
|
||||
**Fallback errors.** Wrap only writes into files it judges to be source (tracked by git, not marked GENERATED, not listed in config's `generatedFiles`). If it can't land on a source file, it errors without writing — accepting a variant into a generated file is silent data loss. Three shapes:
|
||||
@@ -101,38 +104,81 @@ If `event.action` is `impeccable` (the default freeform action), use SKILL.md's
|
||||
|
||||
Any other `event.action` (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `adapt`, `animate`, `delight`, `overdrive`): Read `reference/<action>.md` before planning. Each sub-command encodes a specific discipline; skipping its reference produces generic output. Those files may require specific params; layer them on top of the §7 budget, not instead of it.
|
||||
|
||||
### 4. Plan three genuinely distinct directions
|
||||
### 4. Plan three variants — identity first, then mode, then axes
|
||||
|
||||
Before writing a single line of code, name each variant.
|
||||
The wrong frame for live mode is "show three different design directions." Live runs on an existing surface — the brand has already been chosen. The job is variation **within identity**, not selection between identities. Failure mode: three editorial-typographic variants on a brief that wasn't editorial. Bigger failure mode: three off-brand variants the user can't accept because they don't look like their product.
|
||||
|
||||
**For freeform (`action` is `impeccable`, or the user supplied a free prompt):** each variant must anchor to a different **archetype** — a real-world design analogue specific enough to be recognizable at a glance. Not "modern landing page." Not "minimal product hero." Examples:
|
||||
Four phases. Do them in order.
|
||||
|
||||
- *Broadsheet masthead with rule-divided columns* (think NYT print edition)
|
||||
- *Klim Type Foundry specimen page* (dense, technical, catalog-driven)
|
||||
- *Japanese print-poster minimalism with a single oversize glyph*
|
||||
- *Bloomberg Terminal status bar*
|
||||
- *Condé Nast Traveler feature layout*
|
||||
#### Phase A: Extract the identity (non-skippable)
|
||||
|
||||
Then commit each variant to a different **primary axis** of difference:
|
||||
The existing surface has an identity already. Read it before planning anything. Sources, in priority order:
|
||||
|
||||
1. **DESIGN.md** if loaded — read the visual system fields (palette, type pairing, motion, components). This is the authoritative answer.
|
||||
2. **CSS custom properties** in the page's stylesheets (`:root { --color-...; --font-...; ... }`) — these are de-facto tokens.
|
||||
3. **Computed styles** on the picked element and its parent — colors, fonts, spacing scales, corner radii.
|
||||
4. **Sibling components on the page** — what visual rhetoric do existing components use? (Asymmetric or centered? Dense or airy? Bold or quiet?)
|
||||
|
||||
Write down what you see in **one sentence**. The sentence describes the surface that's actually on screen — it is not aspirational, not opinionated, not edited toward what the brand "should" be. Capture, in roughly this order:
|
||||
|
||||
- The dominant surface color and accent color, by hex or token name (use the actual values, not categories like "warm" or "neutral").
|
||||
- The type pairing — the actual font names loaded, primary first.
|
||||
- The layout topology — how the dominant elements are arranged (stacked / side-by-side / grid / asymmetric / overlay).
|
||||
- The surface treatment — corners, borders, shadows, density of decoration.
|
||||
- The voice tone you read off the copy itself, not off the aesthetic feel.
|
||||
|
||||
Be specific. "Modern" is not a color, "elegant" is not a type pairing, "clean" is not a layout. If you can't extract a real value for an axis, skip it rather than fabricate. The point is to record what is, not to describe what you wish it were.
|
||||
|
||||
Do not include adjectives that name an aesthetic family ("editorial-leaning", "terminal-flavored", "brutalist") — those are conclusions, not data. They belong to Phase C lane selection in departure mode, not to identity description. Letting them sneak into Phase A is how the identity-lock collapses into a self-fulfilling prophecy.
|
||||
|
||||
This sentence is the **identity lock**. Every variant must be readable as the same brand if rendered side by side. Skipping this phase is the primary cause of off-brand variants. Absence of DESIGN.md is never an excuse — extract from CSS and computed styles instead.
|
||||
|
||||
#### Phase B: Pick mode (default vs departure)
|
||||
|
||||
**Default mode** — the existing identity is preserved. Variants vary expression axes within it. *This is the right mode for ~90% of live sessions.* The user picked an element on a real product they're shipping; they expect variants of *their* hero, not three different brands' heroes.
|
||||
|
||||
**Departure mode** — the existing identity is rejected. Variants propose alternatives consistent with PRODUCT.md voice. Trigger only when at least one is true:
|
||||
|
||||
- PRODUCT.md anti-references explicitly call out the current surface ("the current `index.html` is itself an example"; "diffuse away from this"; "the page on screen is the failure"). Generic anti-references that describe what to avoid in general do **not** trigger departure mode — only ones that point at *this* surface specifically.
|
||||
- The user's freeform prompt explicitly asks for departure ("rebuild this from scratch", "what if it weren't editorial at all", "show me something completely different").
|
||||
|
||||
If you're unsure, you're in default mode. The cost of being wrong about default is "three on-brand variants with similar feel" — recoverable, the user picks none. The cost of being wrong about departure is "three off-brand variants" — unrecoverable, the user is annoyed.
|
||||
|
||||
#### Phase C: Plan three variants
|
||||
|
||||
**Default mode.** Each variant commits to a different **primary axis** of difference, while preserving the identity sentence. The six axes:
|
||||
|
||||
1. **Hierarchy** — which element commands the eye?
|
||||
2. **Layout topology** — stacked / side-by-side / grid / asymmetric / overlay
|
||||
3. **Typographic system** — pairing, scale ratio, case/weight strategy
|
||||
4. **Color strategy** — Restrained / Committed / Full palette / Drenched
|
||||
3. **Typographic system** — pairing logic, scale ratio, case/weight strategy *within the available faces*
|
||||
4. **Color strategy** — which existing palette role carries the surface (Restrained / Committed / Full palette / Drenched). Use the brand's existing palette tokens, not new colors.
|
||||
5. **Density** — minimal / comfortable / dense
|
||||
6. **Structural decomposition** — merge, split, progressive disclosure
|
||||
|
||||
Three variants → three DIFFERENT primary axes, not three riffs on color.
|
||||
Three variants → three DIFFERENT axes. The trio reads as *the same brand at three angles*. Do not introduce new fonts, new palette hues, or new aesthetic-family signals — those belong to departure mode.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for one dark-neutral-accent, one light-drenched, one full-palette-saturated — three color worlds, not three shades of the same.
|
||||
**Departure mode.** Each variant anchors to a different **aesthetic lane**, drawn from non-monoculture options. Lanes (illustrative, not exhaustive):
|
||||
|
||||
**The squint test (before writing code).** Write the three one-sentence descriptions side by side:
|
||||
- Tech-minimal (Stripe / Linear / Vercel-restraint)
|
||||
- Brutalist-utility (system fonts, raw HTML defaults, Craigslist-energy)
|
||||
- Terminal-native (actual code surface, syntax-highlighted UI as the design)
|
||||
- Industrial-signage (dimensional type, arrow systems, ISO standards, wayfinding)
|
||||
- Technical-drawing (isometric line art, callout numbers, blueprint blue, exploded-view diagrams)
|
||||
- Drenched-saturated (Liquid Death / Mailchimp full-palette / single-hue commitment)
|
||||
- Swiss-grid-rigorous (visible grid as voice, tight type, accumulated systems)
|
||||
|
||||
> V1: Broadsheet masthead, ruled columns, 24px ink on cream.
|
||||
> V2: Enormous italic title, catalog spec rows, heavy monospace data.
|
||||
> V3: Card-framed poster with one oversize glyph, magenta veil.
|
||||
Avoid the **reflex-reject lanes** in [brand.md](brand.md). Don't trade one monoculture for another. *Editorial-typographic is currently a reflex-reject lane* — three variants that all read as "magazine cover" is the second-order training reflex, the trap one tier deeper than SaaS-cream.
|
||||
|
||||
If two of them rhyme ("both use big type" / "both are stacks of sections" / "both feature the CTA prominently"), rework the offender. Freeform variants failing the squint test is the primary failure mode of this flow — three-of-the-same with minor styling tweaks.
|
||||
#### Phase D: Squint test
|
||||
|
||||
**Default mode squint.** Read each variant's identity sentence and compare to the locked identity from Phase A. If any variant has drifted to a different palette, type voice, or visual rhetoric, it has crossed into departure mode by accident — rework. Then check that each variant commits to a different primary axis. Three "tighter density" variants is failure.
|
||||
|
||||
**Departure mode squint.** Two passes, family before sentence:
|
||||
|
||||
1. **Family pass.** Label each variant with one design-family word: *editorial / brutalist / terminal / signage / drenched / swiss / technical-drawing / tech-minimal*. If any two variants share a family label, rework. *This pass is non-negotiable in departure mode and catches the editorial-monoculture failure that the sentence pass misses.*
|
||||
2. **Sentence pass.** Write three one-sentence descriptions side by side. If two of them rhyme ("both feature big type" / "both are stacks of sections" / "both center the CTA"), rework the offender.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for three color worlds, not three shades of the same.
|
||||
|
||||
**For action-specific invocations**, each variant must vary along the dimension the action names:
|
||||
|
||||
@@ -150,7 +196,13 @@ If two of them rhyme ("both use big type" / "both are stacks of sections" / "bot
|
||||
|
||||
### 5. Apply the freeform prompt (if present)
|
||||
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. "Make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. The interpretations stay within whichever mode you picked in Phase B.
|
||||
|
||||
In **default mode**, the prompt narrows the axes you choose, not the identity. *"Make it feel more confident"* → variant 1 amplifies hierarchy (one element commands the eye), variant 2 commits the existing accent color (Committed strategy on the brand's hue), variant 3 tightens density and removes decorative slack. Three different axes, same brand.
|
||||
|
||||
In **departure mode**, the prompt narrows the lanes you draw from, not the families. *"Make it feel like a newspaper front page"* would itself be a departure-mode prompt; honor it but pick three meaningfully different newspaper-adjacent lanes (broadsheet vs. tabloid vs. trade journal), and run the family pass to confirm they don't collapse into one.
|
||||
|
||||
When the prompt and PRODUCT.md anti-references conflict (the prompt asks for X, the anti-references ban X), the anti-references win — they describe the brand's standing position, the prompt is one moment.
|
||||
|
||||
### 6. Write all variants in a single edit
|
||||
|
||||
@@ -181,6 +233,25 @@ The first variant has no `display: none` (visible by default). All others do. If
|
||||
|
||||
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
|
||||
|
||||
**Author every `:scope` rule with a descendant combinator.** The `@scope` boundary is the **variant wrapper `<div data-impeccable-variant="N">`**, not the element you're designing. A bare `:scope { background: cream; }` styles the wrapper, not the inner replacement, so the cream lands on a `display: contents` shell while the actual element keeps page defaults. Always step in: `:scope > .card`, `:scope > section`, `:scope .hero-title`, etc. The fake test agent's CSS in `tests/live-e2e/agent.mjs` is a faithful template — every rule starts `:scope > ...`.
|
||||
|
||||
**JSX / TSX target files.** Wrap `<style>` content in a template literal so the CSS `{` / `}` aren't parsed as JSX expressions, and use `className=` / `style={{…}}` on every variant element. Keep `data-impeccable-*` attributes as-is — they're plain strings:
|
||||
|
||||
```tsx
|
||||
<style data-impeccable-css="SESSION_ID">{`
|
||||
@scope ([data-impeccable-variant="1"]) { ... }
|
||||
@scope ([data-impeccable-variant="2"]) { ... }
|
||||
`}</style>
|
||||
<div data-impeccable-variant="1">
|
||||
{/* variant 1 */}
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style={{ display: 'none' }}>
|
||||
{/* variant 2 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
The wrap script already gives you a single-rooted JSX wrapper — a `<div data-impeccable-variants="…">` outer element with the marker comments tucked inside. Drop the variants block above into the "Variants: insert below this line" comment and the source stays valid TSX.
|
||||
|
||||
### 7. Parameters (composition-sized, 0–4 per variant)
|
||||
|
||||
Each variant can expose **coarse** knobs alongside the full HTML/CSS replacement. The browser docks a small panel to the right of the outline with one control per parameter. The user drags/clicks and sees instant feedback: there is zero regeneration cost because the knob toggles a CSS variable or data attribute that the variant's scoped CSS is already authored against.
|
||||
@@ -246,6 +317,16 @@ node .cursor/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --fil
|
||||
|
||||
Then run `live-poll.mjs` again immediately.
|
||||
|
||||
### Aborting an in-flight session
|
||||
|
||||
If wrap or generation fails after the browser has flipped to GENERATING (e.g. wrap landed on the wrong source branch and you've already reverted it, or generation hit an unrecoverable error), tell the **browser** so its bar resets to PICKING:
|
||||
|
||||
```bash
|
||||
node .cursor/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"
|
||||
```
|
||||
|
||||
Don't run `live-accept --discard` for this — that's a pure file mutator, the browser doesn't see it, and the bar gets stuck on the GENERATING dots forever (the user has to refresh). `--discard` is only correct when the **browser** initiated the discard (user clicked ✕ during CYCLING) and the agent is just running source-side cleanup the browser already triggered.
|
||||
|
||||
## Handle fallback
|
||||
|
||||
When wrap returns `fallback: "agent-driven"`, the deterministic flow doesn't apply. Pick up here.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -104,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`
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -105,15 +105,22 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...restored,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
return {};
|
||||
@@ -127,8 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -149,7 +162,6 @@ 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);
|
||||
// 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.
|
||||
@@ -177,7 +189,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
// 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) {
|
||||
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);
|
||||
@@ -187,9 +198,9 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
}
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
@@ -218,6 +229,72 @@ function findMarkerBlock(id, lines) {
|
||||
return (start !== -1 && end !== -1) ? { start, end } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the line range to REPLACE (vs. just the marker range to extract
|
||||
* from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE
|
||||
* the `<div data-impeccable-variants="ID">` outer wrapper so the picked
|
||||
* element's JSX slot keeps a single child — a Fragment `<></>` would have
|
||||
* solved the multi-sibling case but failed inside `asChild` / cloneElement
|
||||
* parents with "Invalid prop supplied to React.Fragment".
|
||||
*
|
||||
* That means the marker block is enclosed by the wrapper `<div>` opener
|
||||
* (with `data-impeccable-variants="ID"`) and its matching `</div>`. We
|
||||
* walk back to the opener and forward to the closer so accept/discard
|
||||
* remove the entire scaffold, not just the inner markers.
|
||||
*
|
||||
* Marker lines themselves stay where they were so extractOriginal /
|
||||
* extractVariant / extractCss continue to walk the same range.
|
||||
*/
|
||||
function expandReplaceRange(block, lines, isJsx) {
|
||||
if (!isJsx) return { start: block.start, end: block.end };
|
||||
|
||||
let { start, end } = block;
|
||||
|
||||
// Walk back for the wrapper `<div data-impeccable-variants="..."` opener.
|
||||
// The attr may sit on a continuation line of a multi-line opening tag, so
|
||||
// also walk to the line that actually contains `<div`.
|
||||
for (let i = start - 1; i >= Math.max(0, start - 12); i--) {
|
||||
if (/data-impeccable-variants=/.test(lines[i])) {
|
||||
let opener = i;
|
||||
while (opener > 0 && !/<div\b/.test(lines[opener])) opener--;
|
||||
start = opener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk forward to the matching `</div>` by div-depth tracking from the
|
||||
// wrapper opener. Operate on JOINED text instead of per-line: a
|
||||
// multi-line self-closing JSX `<div\n className="spacer"\n/>` would
|
||||
// fool per-line regex tracking (the `<div` line matches openRe but the
|
||||
// `/>` line never matches selfCloseRe since it needs `<div` on the same
|
||||
// line). That left depth permanently over-counted and the wrapper's
|
||||
// outer `</div>` orphaned after accept/discard. Single regex with
|
||||
// `[^>]*?` (which spans newlines in JS) handles either form correctly.
|
||||
const joined = lines.slice(start).join('\n');
|
||||
// Match either `<div … />` (self-close, group 1 is `/`), `<div … >`
|
||||
// (open, group 1 is empty), or `</div>`.
|
||||
const tagRe = /<div\b[^>]*?(\/?)>|<\/div\s*>/g;
|
||||
let depth = 0;
|
||||
let m;
|
||||
while ((m = tagRe.exec(joined)) !== null) {
|
||||
const isClose = m[0].startsWith('</');
|
||||
const isSelfClose = !isClose && m[1] === '/';
|
||||
if (isClose) depth--;
|
||||
else if (!isSelfClose) depth++;
|
||||
if (depth <= 0) {
|
||||
// m.index is offset within `joined`; convert back to a file line.
|
||||
const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
|
||||
const candidateEnd = start + linesBefore;
|
||||
if (candidateEnd >= end) {
|
||||
end = candidateEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wrapper lines into a single string with `<style>` elements removed so
|
||||
* marker matching and div-depth tracking aren't confused by:
|
||||
@@ -345,7 +422,7 @@ function extractCss(lines, block, id) {
|
||||
// Same-line open + close: extract inner text.
|
||||
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
||||
if (sameLine) {
|
||||
const inner = sameLine[1];
|
||||
const inner = stripJsxTemplateWrap(sameLine[1]);
|
||||
return inner.length > 0 ? inner.split('\n') : null;
|
||||
}
|
||||
inStyle = true;
|
||||
@@ -362,7 +439,60 @@ function extractCss(lines, block, id) {
|
||||
}
|
||||
}
|
||||
|
||||
return content.length > 0 ? content : null;
|
||||
if (content.length === 0) return null;
|
||||
return stripJsxTemplateLines(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a
|
||||
* `<style>` element in a JSX/TSX file. The agent may write the wrap with
|
||||
* `{` and `}` directly attached to the `<style>` tags, on their own lines,
|
||||
* or attached to the first/last CSS lines — all three are JSX-legal.
|
||||
*
|
||||
* Stripping is required because handleAccept re-wraps the CSS itself when
|
||||
* carbonizing. Without this, two consecutive accepts (or a previously-
|
||||
* accepted variants block being carbonized) would produce nested
|
||||
* `{` `{` … `}` `}`, which oxc rejects with "Expected `}` but found `@`".
|
||||
*/
|
||||
function stripJsxTemplateLines(content) {
|
||||
const out = content.slice();
|
||||
|
||||
// Drop any leading blank lines so we don't miss a `{` line buried below
|
||||
// them; same for trailing.
|
||||
while (out.length > 0 && out[0].trim() === '') out.shift();
|
||||
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Leading `{`: own line, or attached to the first CSS line.
|
||||
const firstTrim = out[0].trimStart();
|
||||
if (firstTrim === '{`') {
|
||||
out.shift();
|
||||
} else if (firstTrim.startsWith('{`')) {
|
||||
const idx = out[0].indexOf('{`');
|
||||
out[0] = out[0].slice(0, idx) + out[0].slice(idx + 2);
|
||||
if (out[0].trim() === '') out.shift();
|
||||
}
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Trailing `` ` `` `}`: own line, or attached to the last CSS line.
|
||||
const lastIdx = out.length - 1;
|
||||
const lastTrim = out[lastIdx].trimEnd();
|
||||
if (lastTrim === '`}') {
|
||||
out.pop();
|
||||
} else if (lastTrim.endsWith('`}')) {
|
||||
const text = out[lastIdx];
|
||||
const idx = text.lastIndexOf('`}');
|
||||
out[lastIdx] = text.slice(0, idx) + text.slice(idx + 2);
|
||||
if (out[lastIdx].trim() === '') out.pop();
|
||||
}
|
||||
|
||||
return out.length > 0 ? out : null;
|
||||
}
|
||||
|
||||
function stripJsxTemplateWrap(text) {
|
||||
const lines = text.split('\n');
|
||||
const stripped = stripJsxTemplateLines(lines);
|
||||
return stripped ? stripped.join('\n') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2622,11 +2622,15 @@
|
||||
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.body).backgroundColor ||
|
||||
getComputedStyle(document.documentElement).backgroundColor ||
|
||||
'#ffffff'
|
||||
);
|
||||
// The walk already passed through <body> and <html>; if they had been
|
||||
// opaque we would have returned. Falling through with the previous
|
||||
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
|
||||
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
|
||||
// never set its own bg, which is truthy and short-circuits the chain to
|
||||
// transparent-black — modern-screenshot then renders the capture on a
|
||||
// black canvas and the shader overlay flashes solid black during load.
|
||||
// The browser canvas defaults to white, so we do too.
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
// Capture the element (with current annotations baked in) and return a PNG
|
||||
|
||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
||||
|
||||
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;
|
||||
// The tagRe captures any whitespace between the last attribute and the
|
||||
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||
// a replace would land it BEFORE that trailing space, leaving a double
|
||||
// space inside attrs and clobbering the space before `/>`. Split off
|
||||
// the trailing whitespace, splice the marker into the attribute body,
|
||||
// and re-append the original trailing whitespace so a self-closing
|
||||
// `<meta … />` round-trips byte-for-byte.
|
||||
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||
const newTag = tag.full.replace(attrs, newAttrs);
|
||||
|
||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
@@ -146,13 +146,12 @@ Options:
|
||||
? ['--id', event.id, '--discard']
|
||||
: ['--id', event.id, '--variant', event.variantId];
|
||||
if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
|
||||
// Pass through a JSON blob; the shell-safe wrap uses single quotes because
|
||||
// values are finite {id, number|string|boolean} pairs from a validated payload.
|
||||
scriptArgs.push('--param-values', `'${JSON.stringify(event.paramValues).replace(/'/g, "'\\''")}'`);
|
||||
scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
`node "${acceptScript}" ${scriptArgs.join(' ')}`,
|
||||
const out = execFileSync(
|
||||
'node',
|
||||
[acceptScript, ...scriptArgs],
|
||||
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
|
||||
);
|
||||
event._acceptResult = JSON.parse(out.trim());
|
||||
|
||||
@@ -21,11 +21,16 @@ import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './load-context.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// PID file in the project root so both the server and agent can find it
|
||||
// predictably (os.tmpdir() varies across platforms).
|
||||
const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
|
||||
// PRODUCT.md / DESIGN.md / DESIGN.json live wherever load-context.mjs resolves.
|
||||
// Keeps live-server in sync with the loader when users keep the docs in
|
||||
// .agents/context/, docs/, or a path set via IMPECCABLE_CONTEXT_DIR.
|
||||
const CONTEXT_DIR = resolveContextDir(process.cwd());
|
||||
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
||||
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
||||
|
||||
@@ -113,7 +118,7 @@ function hasProjectContext() {
|
||||
// concern, surfaced by the design panel's own empty state. Legacy
|
||||
// .impeccable.md is auto-migrated to PRODUCT.md by load-context.mjs.
|
||||
try {
|
||||
fs.accessSync(path.join(process.cwd(), 'PRODUCT.md'), fs.constants.R_OK);
|
||||
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -131,11 +136,21 @@ const VISUAL_ACTIONS = [
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
|
||||
// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars)
|
||||
// and variantIds via String(small integer). Restrict to those shapes so
|
||||
// any value that reaches a downstream child_process or DOM selector is
|
||||
// inert by construction.
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
||||
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
||||
|
||||
function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
||||
switch (msg.type) {
|
||||
case 'generate':
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!isValidId(msg.id)) return 'generate: missing or malformed id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
|
||||
@@ -145,8 +160,8 @@ function validateEvent(msg) {
|
||||
if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array';
|
||||
return null;
|
||||
case 'accept':
|
||||
if (!msg.id) return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
if (!isValidId(msg.id)) return 'accept: missing or malformed id';
|
||||
if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
|
||||
if (msg.paramValues !== undefined) {
|
||||
if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
|
||||
return 'accept: paramValues must be an object';
|
||||
@@ -154,7 +169,7 @@ function validateEvent(msg) {
|
||||
}
|
||||
return null;
|
||||
case 'discard':
|
||||
return msg.id ? null : 'discard: missing id';
|
||||
return isValidId(msg.id) ? null : 'discard: missing or malformed id';
|
||||
case 'exit':
|
||||
return null;
|
||||
case 'prefetch':
|
||||
@@ -315,8 +330,8 @@ function createRequestHandler({ detectScript, livePath }) {
|
||||
const token = url.searchParams.get('token');
|
||||
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
||||
|
||||
const mdPath = path.join(process.cwd(), 'DESIGN.md');
|
||||
const jsonPath = path.join(process.cwd(), 'DESIGN.json');
|
||||
const mdPath = path.join(CONTEXT_DIR, 'DESIGN.md');
|
||||
const jsonPath = path.join(CONTEXT_DIR, 'DESIGN.json');
|
||||
const mdStat = statOrNull(mdPath);
|
||||
const jsonStat = statOrNull(jsonPath);
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ Element identification (at least one required):
|
||||
|
||||
Optional:
|
||||
--file PATH Source file to search in (skips auto-detection)
|
||||
--text TEXT Picked element's textContent. Used to disambiguate when
|
||||
classes/tag match multiple sibling elements (e.g. a list
|
||||
of <Card>s with the same className). Pass the first ~80
|
||||
chars of event.element.textContent.
|
||||
--help Show this help message
|
||||
|
||||
Output (JSON):
|
||||
@@ -53,6 +57,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const tag = argVal(args, '--tag');
|
||||
const query = argVal(args, '--query');
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -115,17 +120,67 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find the element, trying each query in priority order.
|
||||
// Pass tag hint so findElement can reject matches inside wrong element types
|
||||
// and walk backward to the real opener on multi-line JSX tags.
|
||||
// Find the element, trying each query in priority order. When `--text` is
|
||||
// supplied, collect every candidate the queries surface and disambiguate
|
||||
// by the picked element's textContent. Without `--text`, fall back to the
|
||||
// legacy first-match behavior so unmodified callers keep working.
|
||||
let match = null;
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
if (text) {
|
||||
const candidates = [];
|
||||
for (const q of queries) {
|
||||
const all = findAllElements(lines, q, tag);
|
||||
for (const c of all) {
|
||||
if (!candidates.some((x) => x.startLine === c.startLine)) {
|
||||
candidates.push(c);
|
||||
}
|
||||
}
|
||||
// Once a more-specific query (ID, full className combo) yielded a unique
|
||||
// result, stop — falling through to the loose tag+single-class query
|
||||
// would readmit the siblings we just disambiguated past.
|
||||
if (candidates.length === 1) break;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
if (candidates.length === 1) {
|
||||
match = candidates[0];
|
||||
} else {
|
||||
const filtered = filterByText(candidates, lines, text);
|
||||
if (filtered.length === 1) {
|
||||
match = filtered[0];
|
||||
} else if (filtered.length === 0) {
|
||||
// Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
|
||||
// browser-side textContent doesn't appear literally in source. Fall
|
||||
// back to first-match rather than refusing — this is the same
|
||||
// behavior unmodified callers see, just preserved.
|
||||
match = candidates[0];
|
||||
} else {
|
||||
// Multiple candidates ALSO match the text. Truly ambiguous — refuse
|
||||
// rather than pick wrong, and hand the agent the candidate locations
|
||||
// so it can disambiguate by reading the file.
|
||||
console.error(JSON.stringify({
|
||||
error: 'element_ambiguous',
|
||||
fallback: 'agent-driven',
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
candidates: filtered.map((c) => ({
|
||||
startLine: c.startLine + 1,
|
||||
endLine: c.endLine + 1,
|
||||
})),
|
||||
hint: 'Multiple source elements match both classes/tag and textContent. Pass --element-id, a more specific --text, or write the wrapper manually. See "Handle fallback" in live.md.',
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const { startLine, endLine } = match;
|
||||
@@ -133,17 +188,48 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
const indent = lines[startLine].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the original element
|
||||
// Extract the original element. Reindent under the wrapper while preserving
|
||||
// the relative depth between lines — `l.trimStart()` would strip ALL leading
|
||||
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
|
||||
// to a single uniform indent, so on accept/discard the round-trip restores
|
||||
// the inner element at its parent's depth instead of nested inside it.
|
||||
// Strip only the COMMON minimum leading whitespace across the picked lines;
|
||||
// `deindentContent` on the accept side already mirrors this convention.
|
||||
const originalLines = lines.slice(startLine, endLine + 1);
|
||||
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
|
||||
const originalBaseIndent = minLeadingSpaces(originalLines);
|
||||
const reindentOriginal = (extra) => originalLines
|
||||
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
|
||||
.join('\n');
|
||||
const originalIndented = reindentOriginal(' ');
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
// either type-errors or renders a literal CSS string).
|
||||
const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"';
|
||||
|
||||
// Build the wrapper
|
||||
const wrapperLines = [
|
||||
// JSX/TSX guard: the picked element occupies a single JSX child slot
|
||||
// (inside `return (...)`, an array `.map(...)`, an `asChild` branch, or
|
||||
// any other expression position). Replacing it with `comment + <div> +
|
||||
// comment` yields three adjacent siblings — invalid JSX. We can't use a
|
||||
// Fragment `<></>` either: parents that clone children (Radix `asChild`,
|
||||
// Headless UI, etc.) hit "Invalid prop supplied to React.Fragment" when
|
||||
// they try to pass an `id` through.
|
||||
//
|
||||
// Solution: keep the wrapper `<div>` as the single JSX-slot child and
|
||||
// tuck both marker comments INSIDE it. accept/discard then expands its
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
reindentOriginal(' '),
|
||||
indent + ' </div>',
|
||||
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
@@ -163,13 +249,24 @@ The agent should insert variant HTML at insertLine.`);
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
// Calculate insert line (the "insert below this line" comment)
|
||||
const insertLine = startLine + 6; // 0-indexed in the new file
|
||||
// Calculate insert line (the "insert below this line" comment).
|
||||
// 0-indexed file position. Both HTML and JSX wrappers have 6 lines above
|
||||
// the insert marker (HTML: start-comment + outer-div + Original-comment +
|
||||
// original-div + content + close-original-div; JSX: outer-div +
|
||||
// start-comment + Original-comment + original-div + content +
|
||||
// close-original-div). Multi-line originals push the marker by their
|
||||
// extra line count.
|
||||
const insertLine = startLine + 6 + (originalLines.length - 1);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
startLine: startLine + 1, // 1-indexed for the agent
|
||||
endLine: startLine + wrapperLines.length, // 1-indexed
|
||||
// wrapperLines is an array but one element (the original-content slot)
|
||||
// is a `\n`-joined multi-line string, so the actual file-row count is
|
||||
// wrapperLines.length + (originalLines.length - 1). Without the offset,
|
||||
// endLine pointed inside the wrapper for any picked element that
|
||||
// spanned more than one source line.
|
||||
endLine: startLine + wrapperLines.length + (originalLines.length - 1), // 1-indexed
|
||||
insertLine: insertLine + 1, // 1-indexed: where variants go
|
||||
commentSyntax: commentSyntax,
|
||||
originalLineCount: originalLines.length,
|
||||
@@ -310,6 +407,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
|
||||
* line to find the actual tag opener. When `tag` is provided, opener candidates
|
||||
* must match that tag name.
|
||||
*/
|
||||
/**
|
||||
* Return the smallest leading-whitespace count across a set of lines,
|
||||
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
|
||||
* the common base indent of a multi-line picked element so reindenting
|
||||
* under the wrapper preserves the relative depth between lines.
|
||||
*/
|
||||
function minLeadingSpaces(lines) {
|
||||
let min = Infinity;
|
||||
for (const l of lines) {
|
||||
if (l.trim() === '') continue;
|
||||
const m = l.match(/^(\s*)/);
|
||||
if (m && m[1].length < min) min = m[1].length;
|
||||
}
|
||||
return min === Infinity ? 0 : min;
|
||||
}
|
||||
|
||||
function findElement(lines, query, tag = null) {
|
||||
// Iterate all matches — the first substring hit isn't always the right one.
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -330,6 +443,69 @@ function findElement(lines, query, tag = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like findElement, but returns every match. Used for ambiguity detection
|
||||
* when the agent passes --text: when the same className appears on multiple
|
||||
* sibling elements (a list of cards, repeated section variants, etc.),
|
||||
* first-match silently lands on the wrong branch. Returning all matches lets
|
||||
* the caller narrow by textContent or fail with a structured ambiguity error.
|
||||
*/
|
||||
function findAllElements(lines, query, tag = null) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].includes(query)) continue;
|
||||
const stripped = lines[i].trim();
|
||||
if (stripped.startsWith('<!--') || stripped.startsWith('{/*') || stripped.startsWith('//')) continue;
|
||||
if (lines[i].includes('data-impeccable-variant')) continue;
|
||||
const openerLine = findOpenerLine(lines, i, tag);
|
||||
if (openerLine === -1) continue;
|
||||
if (seen.has(openerLine)) continue; // multiple matches inside the same element
|
||||
seen.add(openerLine);
|
||||
const endLine = findClosingLine(lines, openerLine);
|
||||
out.push({ startLine: openerLine, endLine });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a candidate set to those whose source body matches a meaningful
|
||||
* prefix of the picked element's textContent. The compare strips tags and
|
||||
* JSX expressions, then checks two whitespace normalizations side-by-side:
|
||||
*
|
||||
* - single-space ("hero two second card body")
|
||||
* - no-whitespace ("herotwosecondcardbody")
|
||||
*
|
||||
* Both are needed because `el.textContent` concatenates sibling text without
|
||||
* inserting whitespace (e.g. `<h1>Hero Two</h1><p>Second…</p>` reads as
|
||||
* `"Hero TwoSecond…"`), while the source has whitespace between tags. If
|
||||
* EITHER normalization matches, the candidate keeps. A snippet shorter than
|
||||
* 8 chars after stripping is too weak to disambiguate — the caller falls
|
||||
* back to first-match.
|
||||
*/
|
||||
function filterByText(candidates, lines, text) {
|
||||
const trimmed = text.replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 80);
|
||||
// Too short to disambiguate. Return [] so the caller's `filtered.length
|
||||
// === 0` branch fires (fall back to first-match) — the previous
|
||||
// `candidates.slice()` return forced `filtered.length > 1` and surfaced
|
||||
// a spurious `element_ambiguous` error on every short-text picker event
|
||||
// with multiple candidates.
|
||||
if (trimmed.length < 8) return [];
|
||||
const targetSpaced = trimmed;
|
||||
const targetCompact = trimmed.replace(/\s+/g, '');
|
||||
|
||||
return candidates.filter((c) => {
|
||||
const body = lines.slice(c.startLine, c.endLine + 1).join(' ');
|
||||
const inner = body
|
||||
.replace(/<[^>]*>/g, ' ') // strip HTML/JSX tags
|
||||
.replace(/\{[^}]*\}/g, ' ') // strip JSX expressions
|
||||
.toLowerCase();
|
||||
const sourceSpaced = inner.replace(/\s+/g, ' ').trim();
|
||||
const sourceCompact = inner.replace(/\s+/g, '');
|
||||
return sourceSpaced.includes(targetSpaced) || sourceCompact.includes(targetCompact);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a match line to the real tag opener. If the match line itself opens
|
||||
* a tag, return it. Otherwise walk up to 10 lines backward looking for the
|
||||
|
||||
@@ -13,11 +13,21 @@
|
||||
* design: string | null, // DESIGN.md contents
|
||||
* designPath: string | null,
|
||||
* migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md
|
||||
* contextDir: string, // absolute path of the directory the files were found in
|
||||
* }
|
||||
*
|
||||
* Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The
|
||||
* Google DESIGN.md convention is uppercase at repo root; Kiro-style and
|
||||
* lowercase variants are also matched so users don't get punished for case.
|
||||
*
|
||||
* Lookup directory resolution (first match wins):
|
||||
* 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
|
||||
* 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat)
|
||||
* 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/
|
||||
* 4. cwd as a default "no context found" location
|
||||
*
|
||||
* Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root;
|
||||
* fallback directories are read-only as far as auto-rename is concerned.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -26,15 +36,52 @@ import path from 'node:path';
|
||||
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
|
||||
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
|
||||
const LEGACY_NAMES = ['.impeccable.md'];
|
||||
const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
||||
|
||||
/**
|
||||
* Resolve the directory that holds PRODUCT.md / DESIGN.md / DESIGN.json for
|
||||
* this project. Exported so other scripts (e.g. live-server.mjs) can read the
|
||||
* design files from the same location the loader uses.
|
||||
*/
|
||||
export function resolveContextDir(cwd = process.cwd()) {
|
||||
// 1. Explicit override
|
||||
const envDir = process.env.IMPECCABLE_CONTEXT_DIR;
|
||||
if (envDir && envDir.trim()) {
|
||||
const trimmed = envDir.trim();
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
}
|
||||
|
||||
// 2. cwd wins if any canonical or legacy file is there. We check legacy too
|
||||
// so the auto-migration path in loadContext stays predictable.
|
||||
if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present;
|
||||
// legacy `.impeccable.md` does not pull the lookup into a fallback dir.
|
||||
for (const rel of FALLBACK_DIRS) {
|
||||
const candidate = path.resolve(cwd, rel);
|
||||
if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Nothing found — keep the historical "default to cwd" behaviour so the
|
||||
// caller's `hasProduct === false` branch still fires the same way.
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function loadContext(cwd = process.cwd()) {
|
||||
let migrated = false;
|
||||
const contextDir = resolveContextDir(cwd);
|
||||
|
||||
// 1. Look for PRODUCT.md (case-insensitive)
|
||||
let productPath = firstExisting(cwd, PRODUCT_NAMES);
|
||||
// 1. Look for PRODUCT.md (case-insensitive) in the resolved dir
|
||||
let productPath = firstExisting(contextDir, PRODUCT_NAMES);
|
||||
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists, rename in place
|
||||
if (!productPath) {
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename
|
||||
// it in place. We only migrate at the root — fallback dirs are read-only
|
||||
// so we don't surprise users by mutating files under docs/ or .agents/.
|
||||
if (!productPath && contextDir === cwd) {
|
||||
const legacyPath = firstExisting(cwd, LEGACY_NAMES);
|
||||
if (legacyPath) {
|
||||
const newPath = path.join(cwd, 'PRODUCT.md');
|
||||
@@ -50,7 +97,7 @@ export function loadContext(cwd = process.cwd()) {
|
||||
}
|
||||
|
||||
// 3. DESIGN.md (case-insensitive)
|
||||
const designPath = firstExisting(cwd, DESIGN_NAMES);
|
||||
const designPath = firstExisting(contextDir, DESIGN_NAMES);
|
||||
|
||||
const product = productPath ? safeRead(productPath) : null;
|
||||
const design = designPath ? safeRead(designPath) : null;
|
||||
@@ -63,12 +110,13 @@ export function loadContext(cwd = process.cwd()) {
|
||||
design,
|
||||
designPath: designPath ? path.relative(cwd, designPath) : null,
|
||||
migrated,
|
||||
contextDir,
|
||||
};
|
||||
}
|
||||
|
||||
function firstExisting(cwd, names) {
|
||||
function firstExisting(dir, names) {
|
||||
for (const name of names) {
|
||||
const abs = path.join(cwd, name);
|
||||
const abs = path.join(dir, name);
|
||||
if (fs.existsSync(abs)) return abs;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
---
|
||||
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.2
|
||||
version: 3.0.5
|
||||
---
|
||||
|
||||
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
|
||||
|
||||
Two files at the project root, case-insensitive:
|
||||
Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd).
|
||||
|
||||
- **PRODUCT.md** — required. Users, brand, tone, anti-references, strategic principles.
|
||||
- **DESIGN.md** — optional, strongly recommended. Colors, typography, elevation, components.
|
||||
@@ -23,13 +42,13 @@ Load both in one call:
|
||||
node .gemini/skills/impeccable/scripts/load-context.mjs
|
||||
```
|
||||
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`.
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from.
|
||||
|
||||
If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one.
|
||||
|
||||
`/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.
|
||||
|
||||
@@ -102,7 +121,10 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit
|
||||
|
||||
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
|
||||
|
||||
**Category-reflex check.** If someone could guess the theme and palette from the category name alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the training-data reflex. Rework the scene sentence and color strategy until the answer is no longer obvious from the domain.
|
||||
**Category-reflex check.** Run at two altitudes — the second one catches what the first one misses.
|
||||
|
||||
- **First-order:** if someone could guess the theme + palette from the category alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
|
||||
- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references — "AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode" — it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -142,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,16 @@ Training-data defaults. Ban list — look further:
|
||||
|
||||
Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
|
||||
|
||||
### Reflex-reject aesthetic lanes
|
||||
|
||||
Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex — the trap one tier deeper than picking a Fraunces font. Look further.
|
||||
|
||||
- **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
|
||||
|
||||
(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
|
||||
|
||||
The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins — variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in [live.md](live.md).
|
||||
|
||||
### Pairing and voice
|
||||
|
||||
Distinctive + refined is the goal — the specific shape depends on the brand:
|
||||
|
||||
@@ -1,24 +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.
|
||||
|
||||
## Real Example: Neo Mirai
|
||||
## Build Gate
|
||||
|
||||
Neo Mirai is the full craft loop in public. A retro-futurist AI design conference started with generated brand and hi-fi reference images, then shipped as a responsive static site in `public/neo-mirai`.
|
||||
Craft cannot build until all of these are true:
|
||||
|
||||
Repro command:
|
||||
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.
|
||||
|
||||
```bash
|
||||
/impeccable craft retro-futurist AI design conference website
|
||||
```
|
||||
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.
|
||||
|
||||
The important detail is the artifact chain: brand toolkit, north-star mock, semantic implementation, regenerated assets, browser iteration, responsive fixes. The mock was not treated as a screenshot to trace. It was used as direction for a real page.
|
||||
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.
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -52,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.
|
||||
|
||||
@@ -86,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.
|
||||
|
||||
@@ -29,7 +29,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
|
||||
node .gemini/skills/impeccable/scripts/live.mjs
|
||||
```
|
||||
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent — extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
|
||||
`serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname).
|
||||
|
||||
@@ -74,7 +74,7 @@ Reading annotations precisely:
|
||||
### 2. Wrap the element
|
||||
|
||||
```bash
|
||||
node .gemini/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
|
||||
node .gemini/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
|
||||
```
|
||||
|
||||
Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
@@ -82,9 +82,12 @@ Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
- `--element-id` ← `event.element.id`
|
||||
- `--classes` ← `event.element.classes` joined with commas
|
||||
- `--tag` ← `event.element.tagName`
|
||||
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
|
||||
|
||||
The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only — do not use it for normal element lookups.
|
||||
|
||||
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"` — read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
|
||||
|
||||
Output on success: `{ file, insertLine, commentSyntax }`.
|
||||
|
||||
**Fallback errors.** Wrap only writes into files it judges to be source (tracked by git, not marked GENERATED, not listed in config's `generatedFiles`). If it can't land on a source file, it errors without writing — accepting a variant into a generated file is silent data loss. Three shapes:
|
||||
@@ -101,38 +104,81 @@ If `event.action` is `impeccable` (the default freeform action), use SKILL.md's
|
||||
|
||||
Any other `event.action` (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `adapt`, `animate`, `delight`, `overdrive`): Read `reference/<action>.md` before planning. Each sub-command encodes a specific discipline; skipping its reference produces generic output. Those files may require specific params; layer them on top of the §7 budget, not instead of it.
|
||||
|
||||
### 4. Plan three genuinely distinct directions
|
||||
### 4. Plan three variants — identity first, then mode, then axes
|
||||
|
||||
Before writing a single line of code, name each variant.
|
||||
The wrong frame for live mode is "show three different design directions." Live runs on an existing surface — the brand has already been chosen. The job is variation **within identity**, not selection between identities. Failure mode: three editorial-typographic variants on a brief that wasn't editorial. Bigger failure mode: three off-brand variants the user can't accept because they don't look like their product.
|
||||
|
||||
**For freeform (`action` is `impeccable`, or the user supplied a free prompt):** each variant must anchor to a different **archetype** — a real-world design analogue specific enough to be recognizable at a glance. Not "modern landing page." Not "minimal product hero." Examples:
|
||||
Four phases. Do them in order.
|
||||
|
||||
- *Broadsheet masthead with rule-divided columns* (think NYT print edition)
|
||||
- *Klim Type Foundry specimen page* (dense, technical, catalog-driven)
|
||||
- *Japanese print-poster minimalism with a single oversize glyph*
|
||||
- *Bloomberg Terminal status bar*
|
||||
- *Condé Nast Traveler feature layout*
|
||||
#### Phase A: Extract the identity (non-skippable)
|
||||
|
||||
Then commit each variant to a different **primary axis** of difference:
|
||||
The existing surface has an identity already. Read it before planning anything. Sources, in priority order:
|
||||
|
||||
1. **DESIGN.md** if loaded — read the visual system fields (palette, type pairing, motion, components). This is the authoritative answer.
|
||||
2. **CSS custom properties** in the page's stylesheets (`:root { --color-...; --font-...; ... }`) — these are de-facto tokens.
|
||||
3. **Computed styles** on the picked element and its parent — colors, fonts, spacing scales, corner radii.
|
||||
4. **Sibling components on the page** — what visual rhetoric do existing components use? (Asymmetric or centered? Dense or airy? Bold or quiet?)
|
||||
|
||||
Write down what you see in **one sentence**. The sentence describes the surface that's actually on screen — it is not aspirational, not opinionated, not edited toward what the brand "should" be. Capture, in roughly this order:
|
||||
|
||||
- The dominant surface color and accent color, by hex or token name (use the actual values, not categories like "warm" or "neutral").
|
||||
- The type pairing — the actual font names loaded, primary first.
|
||||
- The layout topology — how the dominant elements are arranged (stacked / side-by-side / grid / asymmetric / overlay).
|
||||
- The surface treatment — corners, borders, shadows, density of decoration.
|
||||
- The voice tone you read off the copy itself, not off the aesthetic feel.
|
||||
|
||||
Be specific. "Modern" is not a color, "elegant" is not a type pairing, "clean" is not a layout. If you can't extract a real value for an axis, skip it rather than fabricate. The point is to record what is, not to describe what you wish it were.
|
||||
|
||||
Do not include adjectives that name an aesthetic family ("editorial-leaning", "terminal-flavored", "brutalist") — those are conclusions, not data. They belong to Phase C lane selection in departure mode, not to identity description. Letting them sneak into Phase A is how the identity-lock collapses into a self-fulfilling prophecy.
|
||||
|
||||
This sentence is the **identity lock**. Every variant must be readable as the same brand if rendered side by side. Skipping this phase is the primary cause of off-brand variants. Absence of DESIGN.md is never an excuse — extract from CSS and computed styles instead.
|
||||
|
||||
#### Phase B: Pick mode (default vs departure)
|
||||
|
||||
**Default mode** — the existing identity is preserved. Variants vary expression axes within it. *This is the right mode for ~90% of live sessions.* The user picked an element on a real product they're shipping; they expect variants of *their* hero, not three different brands' heroes.
|
||||
|
||||
**Departure mode** — the existing identity is rejected. Variants propose alternatives consistent with PRODUCT.md voice. Trigger only when at least one is true:
|
||||
|
||||
- PRODUCT.md anti-references explicitly call out the current surface ("the current `index.html` is itself an example"; "diffuse away from this"; "the page on screen is the failure"). Generic anti-references that describe what to avoid in general do **not** trigger departure mode — only ones that point at *this* surface specifically.
|
||||
- The user's freeform prompt explicitly asks for departure ("rebuild this from scratch", "what if it weren't editorial at all", "show me something completely different").
|
||||
|
||||
If you're unsure, you're in default mode. The cost of being wrong about default is "three on-brand variants with similar feel" — recoverable, the user picks none. The cost of being wrong about departure is "three off-brand variants" — unrecoverable, the user is annoyed.
|
||||
|
||||
#### Phase C: Plan three variants
|
||||
|
||||
**Default mode.** Each variant commits to a different **primary axis** of difference, while preserving the identity sentence. The six axes:
|
||||
|
||||
1. **Hierarchy** — which element commands the eye?
|
||||
2. **Layout topology** — stacked / side-by-side / grid / asymmetric / overlay
|
||||
3. **Typographic system** — pairing, scale ratio, case/weight strategy
|
||||
4. **Color strategy** — Restrained / Committed / Full palette / Drenched
|
||||
3. **Typographic system** — pairing logic, scale ratio, case/weight strategy *within the available faces*
|
||||
4. **Color strategy** — which existing palette role carries the surface (Restrained / Committed / Full palette / Drenched). Use the brand's existing palette tokens, not new colors.
|
||||
5. **Density** — minimal / comfortable / dense
|
||||
6. **Structural decomposition** — merge, split, progressive disclosure
|
||||
|
||||
Three variants → three DIFFERENT primary axes, not three riffs on color.
|
||||
Three variants → three DIFFERENT axes. The trio reads as *the same brand at three angles*. Do not introduce new fonts, new palette hues, or new aesthetic-family signals — those belong to departure mode.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for one dark-neutral-accent, one light-drenched, one full-palette-saturated — three color worlds, not three shades of the same.
|
||||
**Departure mode.** Each variant anchors to a different **aesthetic lane**, drawn from non-monoculture options. Lanes (illustrative, not exhaustive):
|
||||
|
||||
**The squint test (before writing code).** Write the three one-sentence descriptions side by side:
|
||||
- Tech-minimal (Stripe / Linear / Vercel-restraint)
|
||||
- Brutalist-utility (system fonts, raw HTML defaults, Craigslist-energy)
|
||||
- Terminal-native (actual code surface, syntax-highlighted UI as the design)
|
||||
- Industrial-signage (dimensional type, arrow systems, ISO standards, wayfinding)
|
||||
- Technical-drawing (isometric line art, callout numbers, blueprint blue, exploded-view diagrams)
|
||||
- Drenched-saturated (Liquid Death / Mailchimp full-palette / single-hue commitment)
|
||||
- Swiss-grid-rigorous (visible grid as voice, tight type, accumulated systems)
|
||||
|
||||
> V1: Broadsheet masthead, ruled columns, 24px ink on cream.
|
||||
> V2: Enormous italic title, catalog spec rows, heavy monospace data.
|
||||
> V3: Card-framed poster with one oversize glyph, magenta veil.
|
||||
Avoid the **reflex-reject lanes** in [brand.md](brand.md). Don't trade one monoculture for another. *Editorial-typographic is currently a reflex-reject lane* — three variants that all read as "magazine cover" is the second-order training reflex, the trap one tier deeper than SaaS-cream.
|
||||
|
||||
If two of them rhyme ("both use big type" / "both are stacks of sections" / "both feature the CTA prominently"), rework the offender. Freeform variants failing the squint test is the primary failure mode of this flow — three-of-the-same with minor styling tweaks.
|
||||
#### Phase D: Squint test
|
||||
|
||||
**Default mode squint.** Read each variant's identity sentence and compare to the locked identity from Phase A. If any variant has drifted to a different palette, type voice, or visual rhetoric, it has crossed into departure mode by accident — rework. Then check that each variant commits to a different primary axis. Three "tighter density" variants is failure.
|
||||
|
||||
**Departure mode squint.** Two passes, family before sentence:
|
||||
|
||||
1. **Family pass.** Label each variant with one design-family word: *editorial / brutalist / terminal / signage / drenched / swiss / technical-drawing / tech-minimal*. If any two variants share a family label, rework. *This pass is non-negotiable in departure mode and catches the editorial-monoculture failure that the sentence pass misses.*
|
||||
2. **Sentence pass.** Write three one-sentence descriptions side by side. If two of them rhyme ("both feature big type" / "both are stacks of sections" / "both center the CTA"), rework the offender.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for three color worlds, not three shades of the same.
|
||||
|
||||
**For action-specific invocations**, each variant must vary along the dimension the action names:
|
||||
|
||||
@@ -150,7 +196,13 @@ If two of them rhyme ("both use big type" / "both are stacks of sections" / "bot
|
||||
|
||||
### 5. Apply the freeform prompt (if present)
|
||||
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. "Make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. The interpretations stay within whichever mode you picked in Phase B.
|
||||
|
||||
In **default mode**, the prompt narrows the axes you choose, not the identity. *"Make it feel more confident"* → variant 1 amplifies hierarchy (one element commands the eye), variant 2 commits the existing accent color (Committed strategy on the brand's hue), variant 3 tightens density and removes decorative slack. Three different axes, same brand.
|
||||
|
||||
In **departure mode**, the prompt narrows the lanes you draw from, not the families. *"Make it feel like a newspaper front page"* would itself be a departure-mode prompt; honor it but pick three meaningfully different newspaper-adjacent lanes (broadsheet vs. tabloid vs. trade journal), and run the family pass to confirm they don't collapse into one.
|
||||
|
||||
When the prompt and PRODUCT.md anti-references conflict (the prompt asks for X, the anti-references ban X), the anti-references win — they describe the brand's standing position, the prompt is one moment.
|
||||
|
||||
### 6. Write all variants in a single edit
|
||||
|
||||
@@ -181,6 +233,25 @@ The first variant has no `display: none` (visible by default). All others do. If
|
||||
|
||||
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
|
||||
|
||||
**Author every `:scope` rule with a descendant combinator.** The `@scope` boundary is the **variant wrapper `<div data-impeccable-variant="N">`**, not the element you're designing. A bare `:scope { background: cream; }` styles the wrapper, not the inner replacement, so the cream lands on a `display: contents` shell while the actual element keeps page defaults. Always step in: `:scope > .card`, `:scope > section`, `:scope .hero-title`, etc. The fake test agent's CSS in `tests/live-e2e/agent.mjs` is a faithful template — every rule starts `:scope > ...`.
|
||||
|
||||
**JSX / TSX target files.** Wrap `<style>` content in a template literal so the CSS `{` / `}` aren't parsed as JSX expressions, and use `className=` / `style={{…}}` on every variant element. Keep `data-impeccable-*` attributes as-is — they're plain strings:
|
||||
|
||||
```tsx
|
||||
<style data-impeccable-css="SESSION_ID">{`
|
||||
@scope ([data-impeccable-variant="1"]) { ... }
|
||||
@scope ([data-impeccable-variant="2"]) { ... }
|
||||
`}</style>
|
||||
<div data-impeccable-variant="1">
|
||||
{/* variant 1 */}
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style={{ display: 'none' }}>
|
||||
{/* variant 2 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
The wrap script already gives you a single-rooted JSX wrapper — a `<div data-impeccable-variants="…">` outer element with the marker comments tucked inside. Drop the variants block above into the "Variants: insert below this line" comment and the source stays valid TSX.
|
||||
|
||||
### 7. Parameters (composition-sized, 0–4 per variant)
|
||||
|
||||
Each variant can expose **coarse** knobs alongside the full HTML/CSS replacement. The browser docks a small panel to the right of the outline with one control per parameter. The user drags/clicks and sees instant feedback: there is zero regeneration cost because the knob toggles a CSS variable or data attribute that the variant's scoped CSS is already authored against.
|
||||
@@ -246,6 +317,16 @@ node .gemini/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --fil
|
||||
|
||||
Then run `live-poll.mjs` again immediately.
|
||||
|
||||
### Aborting an in-flight session
|
||||
|
||||
If wrap or generation fails after the browser has flipped to GENERATING (e.g. wrap landed on the wrong source branch and you've already reverted it, or generation hit an unrecoverable error), tell the **browser** so its bar resets to PICKING:
|
||||
|
||||
```bash
|
||||
node .gemini/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"
|
||||
```
|
||||
|
||||
Don't run `live-accept --discard` for this — that's a pure file mutator, the browser doesn't see it, and the bar gets stuck on the GENERATING dots forever (the user has to refresh). `--discard` is only correct when the **browser** initiated the discard (user clicked ✕ during CYCLING) and the agent is just running source-side cleanup the browser already triggered.
|
||||
|
||||
## Handle fallback
|
||||
|
||||
When wrap returns `fallback: "agent-driven"`, the deterministic flow doesn't apply. Pick up here.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -104,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`
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -105,15 +105,22 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...restored,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
return {};
|
||||
@@ -127,8 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -149,7 +162,6 @@ 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);
|
||||
// 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.
|
||||
@@ -177,7 +189,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
// 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) {
|
||||
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);
|
||||
@@ -187,9 +198,9 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
}
|
||||
|
||||
const newLines = [
|
||||
...lines.slice(0, block.start),
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
...lines.slice(block.end + 1),
|
||||
...lines.slice(replaceRange.end + 1),
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
@@ -218,6 +229,72 @@ function findMarkerBlock(id, lines) {
|
||||
return (start !== -1 && end !== -1) ? { start, end } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the line range to REPLACE (vs. just the marker range to extract
|
||||
* from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE
|
||||
* the `<div data-impeccable-variants="ID">` outer wrapper so the picked
|
||||
* element's JSX slot keeps a single child — a Fragment `<></>` would have
|
||||
* solved the multi-sibling case but failed inside `asChild` / cloneElement
|
||||
* parents with "Invalid prop supplied to React.Fragment".
|
||||
*
|
||||
* That means the marker block is enclosed by the wrapper `<div>` opener
|
||||
* (with `data-impeccable-variants="ID"`) and its matching `</div>`. We
|
||||
* walk back to the opener and forward to the closer so accept/discard
|
||||
* remove the entire scaffold, not just the inner markers.
|
||||
*
|
||||
* Marker lines themselves stay where they were so extractOriginal /
|
||||
* extractVariant / extractCss continue to walk the same range.
|
||||
*/
|
||||
function expandReplaceRange(block, lines, isJsx) {
|
||||
if (!isJsx) return { start: block.start, end: block.end };
|
||||
|
||||
let { start, end } = block;
|
||||
|
||||
// Walk back for the wrapper `<div data-impeccable-variants="..."` opener.
|
||||
// The attr may sit on a continuation line of a multi-line opening tag, so
|
||||
// also walk to the line that actually contains `<div`.
|
||||
for (let i = start - 1; i >= Math.max(0, start - 12); i--) {
|
||||
if (/data-impeccable-variants=/.test(lines[i])) {
|
||||
let opener = i;
|
||||
while (opener > 0 && !/<div\b/.test(lines[opener])) opener--;
|
||||
start = opener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk forward to the matching `</div>` by div-depth tracking from the
|
||||
// wrapper opener. Operate on JOINED text instead of per-line: a
|
||||
// multi-line self-closing JSX `<div\n className="spacer"\n/>` would
|
||||
// fool per-line regex tracking (the `<div` line matches openRe but the
|
||||
// `/>` line never matches selfCloseRe since it needs `<div` on the same
|
||||
// line). That left depth permanently over-counted and the wrapper's
|
||||
// outer `</div>` orphaned after accept/discard. Single regex with
|
||||
// `[^>]*?` (which spans newlines in JS) handles either form correctly.
|
||||
const joined = lines.slice(start).join('\n');
|
||||
// Match either `<div … />` (self-close, group 1 is `/`), `<div … >`
|
||||
// (open, group 1 is empty), or `</div>`.
|
||||
const tagRe = /<div\b[^>]*?(\/?)>|<\/div\s*>/g;
|
||||
let depth = 0;
|
||||
let m;
|
||||
while ((m = tagRe.exec(joined)) !== null) {
|
||||
const isClose = m[0].startsWith('</');
|
||||
const isSelfClose = !isClose && m[1] === '/';
|
||||
if (isClose) depth--;
|
||||
else if (!isSelfClose) depth++;
|
||||
if (depth <= 0) {
|
||||
// m.index is offset within `joined`; convert back to a file line.
|
||||
const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
|
||||
const candidateEnd = start + linesBefore;
|
||||
if (candidateEnd >= end) {
|
||||
end = candidateEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wrapper lines into a single string with `<style>` elements removed so
|
||||
* marker matching and div-depth tracking aren't confused by:
|
||||
@@ -345,7 +422,7 @@ function extractCss(lines, block, id) {
|
||||
// Same-line open + close: extract inner text.
|
||||
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
||||
if (sameLine) {
|
||||
const inner = sameLine[1];
|
||||
const inner = stripJsxTemplateWrap(sameLine[1]);
|
||||
return inner.length > 0 ? inner.split('\n') : null;
|
||||
}
|
||||
inStyle = true;
|
||||
@@ -362,7 +439,60 @@ function extractCss(lines, block, id) {
|
||||
}
|
||||
}
|
||||
|
||||
return content.length > 0 ? content : null;
|
||||
if (content.length === 0) return null;
|
||||
return stripJsxTemplateLines(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a
|
||||
* `<style>` element in a JSX/TSX file. The agent may write the wrap with
|
||||
* `{` and `}` directly attached to the `<style>` tags, on their own lines,
|
||||
* or attached to the first/last CSS lines — all three are JSX-legal.
|
||||
*
|
||||
* Stripping is required because handleAccept re-wraps the CSS itself when
|
||||
* carbonizing. Without this, two consecutive accepts (or a previously-
|
||||
* accepted variants block being carbonized) would produce nested
|
||||
* `{` `{` … `}` `}`, which oxc rejects with "Expected `}` but found `@`".
|
||||
*/
|
||||
function stripJsxTemplateLines(content) {
|
||||
const out = content.slice();
|
||||
|
||||
// Drop any leading blank lines so we don't miss a `{` line buried below
|
||||
// them; same for trailing.
|
||||
while (out.length > 0 && out[0].trim() === '') out.shift();
|
||||
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Leading `{`: own line, or attached to the first CSS line.
|
||||
const firstTrim = out[0].trimStart();
|
||||
if (firstTrim === '{`') {
|
||||
out.shift();
|
||||
} else if (firstTrim.startsWith('{`')) {
|
||||
const idx = out[0].indexOf('{`');
|
||||
out[0] = out[0].slice(0, idx) + out[0].slice(idx + 2);
|
||||
if (out[0].trim() === '') out.shift();
|
||||
}
|
||||
if (out.length === 0) return null;
|
||||
|
||||
// Trailing `` ` `` `}`: own line, or attached to the last CSS line.
|
||||
const lastIdx = out.length - 1;
|
||||
const lastTrim = out[lastIdx].trimEnd();
|
||||
if (lastTrim === '`}') {
|
||||
out.pop();
|
||||
} else if (lastTrim.endsWith('`}')) {
|
||||
const text = out[lastIdx];
|
||||
const idx = text.lastIndexOf('`}');
|
||||
out[lastIdx] = text.slice(0, idx) + text.slice(idx + 2);
|
||||
if (out[lastIdx].trim() === '') out.pop();
|
||||
}
|
||||
|
||||
return out.length > 0 ? out : null;
|
||||
}
|
||||
|
||||
function stripJsxTemplateWrap(text) {
|
||||
const lines = text.split('\n');
|
||||
const stripped = stripJsxTemplateLines(lines);
|
||||
return stripped ? stripped.join('\n') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2622,11 +2622,15 @@
|
||||
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.body).backgroundColor ||
|
||||
getComputedStyle(document.documentElement).backgroundColor ||
|
||||
'#ffffff'
|
||||
);
|
||||
// The walk already passed through <body> and <html>; if they had been
|
||||
// opaque we would have returned. Falling through with the previous
|
||||
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
|
||||
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
|
||||
// never set its own bg, which is truthy and short-circuits the chain to
|
||||
// transparent-black — modern-screenshot then renders the capture on a
|
||||
// black canvas and the shader overlay flashes solid black during load.
|
||||
// The browser canvas defaults to white, so we do too.
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
// Capture the element (with current annotations baked in) and return a PNG
|
||||
|
||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
||||
|
||||
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;
|
||||
// The tagRe captures any whitespace between the last attribute and the
|
||||
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||
// a replace would land it BEFORE that trailing space, leaving a double
|
||||
// space inside attrs and clobbering the space before `/>`. Split off
|
||||
// the trailing whitespace, splice the marker into the attribute body,
|
||||
// and re-append the original trailing whitespace so a self-closing
|
||||
// `<meta … />` round-trips byte-for-byte.
|
||||
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||
const newTag = tag.full.replace(attrs, newAttrs);
|
||||
|
||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
@@ -146,13 +146,12 @@ Options:
|
||||
? ['--id', event.id, '--discard']
|
||||
: ['--id', event.id, '--variant', event.variantId];
|
||||
if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
|
||||
// Pass through a JSON blob; the shell-safe wrap uses single quotes because
|
||||
// values are finite {id, number|string|boolean} pairs from a validated payload.
|
||||
scriptArgs.push('--param-values', `'${JSON.stringify(event.paramValues).replace(/'/g, "'\\''")}'`);
|
||||
scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
`node "${acceptScript}" ${scriptArgs.join(' ')}`,
|
||||
const out = execFileSync(
|
||||
'node',
|
||||
[acceptScript, ...scriptArgs],
|
||||
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
|
||||
);
|
||||
event._acceptResult = JSON.parse(out.trim());
|
||||
|
||||
@@ -21,11 +21,16 @@ import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './load-context.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// PID file in the project root so both the server and agent can find it
|
||||
// predictably (os.tmpdir() varies across platforms).
|
||||
const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
|
||||
// PRODUCT.md / DESIGN.md / DESIGN.json live wherever load-context.mjs resolves.
|
||||
// Keeps live-server in sync with the loader when users keep the docs in
|
||||
// .agents/context/, docs/, or a path set via IMPECCABLE_CONTEXT_DIR.
|
||||
const CONTEXT_DIR = resolveContextDir(process.cwd());
|
||||
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
||||
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
||||
|
||||
@@ -113,7 +118,7 @@ function hasProjectContext() {
|
||||
// concern, surfaced by the design panel's own empty state. Legacy
|
||||
// .impeccable.md is auto-migrated to PRODUCT.md by load-context.mjs.
|
||||
try {
|
||||
fs.accessSync(path.join(process.cwd(), 'PRODUCT.md'), fs.constants.R_OK);
|
||||
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -131,11 +136,21 @@ const VISUAL_ACTIONS = [
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
|
||||
// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars)
|
||||
// and variantIds via String(small integer). Restrict to those shapes so
|
||||
// any value that reaches a downstream child_process or DOM selector is
|
||||
// inert by construction.
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
||||
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
||||
|
||||
function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
||||
switch (msg.type) {
|
||||
case 'generate':
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!isValidId(msg.id)) return 'generate: missing or malformed id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
|
||||
@@ -145,8 +160,8 @@ function validateEvent(msg) {
|
||||
if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array';
|
||||
return null;
|
||||
case 'accept':
|
||||
if (!msg.id) return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
if (!isValidId(msg.id)) return 'accept: missing or malformed id';
|
||||
if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
|
||||
if (msg.paramValues !== undefined) {
|
||||
if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
|
||||
return 'accept: paramValues must be an object';
|
||||
@@ -154,7 +169,7 @@ function validateEvent(msg) {
|
||||
}
|
||||
return null;
|
||||
case 'discard':
|
||||
return msg.id ? null : 'discard: missing id';
|
||||
return isValidId(msg.id) ? null : 'discard: missing or malformed id';
|
||||
case 'exit':
|
||||
return null;
|
||||
case 'prefetch':
|
||||
@@ -315,8 +330,8 @@ function createRequestHandler({ detectScript, livePath }) {
|
||||
const token = url.searchParams.get('token');
|
||||
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
||||
|
||||
const mdPath = path.join(process.cwd(), 'DESIGN.md');
|
||||
const jsonPath = path.join(process.cwd(), 'DESIGN.json');
|
||||
const mdPath = path.join(CONTEXT_DIR, 'DESIGN.md');
|
||||
const jsonPath = path.join(CONTEXT_DIR, 'DESIGN.json');
|
||||
const mdStat = statOrNull(mdPath);
|
||||
const jsonStat = statOrNull(jsonPath);
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ Element identification (at least one required):
|
||||
|
||||
Optional:
|
||||
--file PATH Source file to search in (skips auto-detection)
|
||||
--text TEXT Picked element's textContent. Used to disambiguate when
|
||||
classes/tag match multiple sibling elements (e.g. a list
|
||||
of <Card>s with the same className). Pass the first ~80
|
||||
chars of event.element.textContent.
|
||||
--help Show this help message
|
||||
|
||||
Output (JSON):
|
||||
@@ -53,6 +57,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const tag = argVal(args, '--tag');
|
||||
const query = argVal(args, '--query');
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -115,17 +120,67 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find the element, trying each query in priority order.
|
||||
// Pass tag hint so findElement can reject matches inside wrong element types
|
||||
// and walk backward to the real opener on multi-line JSX tags.
|
||||
// Find the element, trying each query in priority order. When `--text` is
|
||||
// supplied, collect every candidate the queries surface and disambiguate
|
||||
// by the picked element's textContent. Without `--text`, fall back to the
|
||||
// legacy first-match behavior so unmodified callers keep working.
|
||||
let match = null;
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
if (text) {
|
||||
const candidates = [];
|
||||
for (const q of queries) {
|
||||
const all = findAllElements(lines, q, tag);
|
||||
for (const c of all) {
|
||||
if (!candidates.some((x) => x.startLine === c.startLine)) {
|
||||
candidates.push(c);
|
||||
}
|
||||
}
|
||||
// Once a more-specific query (ID, full className combo) yielded a unique
|
||||
// result, stop — falling through to the loose tag+single-class query
|
||||
// would readmit the siblings we just disambiguated past.
|
||||
if (candidates.length === 1) break;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
if (candidates.length === 1) {
|
||||
match = candidates[0];
|
||||
} else {
|
||||
const filtered = filterByText(candidates, lines, text);
|
||||
if (filtered.length === 1) {
|
||||
match = filtered[0];
|
||||
} else if (filtered.length === 0) {
|
||||
// Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
|
||||
// browser-side textContent doesn't appear literally in source. Fall
|
||||
// back to first-match rather than refusing — this is the same
|
||||
// behavior unmodified callers see, just preserved.
|
||||
match = candidates[0];
|
||||
} else {
|
||||
// Multiple candidates ALSO match the text. Truly ambiguous — refuse
|
||||
// rather than pick wrong, and hand the agent the candidate locations
|
||||
// so it can disambiguate by reading the file.
|
||||
console.error(JSON.stringify({
|
||||
error: 'element_ambiguous',
|
||||
fallback: 'agent-driven',
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
candidates: filtered.map((c) => ({
|
||||
startLine: c.startLine + 1,
|
||||
endLine: c.endLine + 1,
|
||||
})),
|
||||
hint: 'Multiple source elements match both classes/tag and textContent. Pass --element-id, a more specific --text, or write the wrapper manually. See "Handle fallback" in live.md.',
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const q of queries) {
|
||||
match = findElement(lines, q, tag);
|
||||
if (match) break;
|
||||
}
|
||||
if (!match) {
|
||||
console.error(JSON.stringify({ error: 'Found file but could not locate element in ' + targetFile + '. Searched for: ' + queries.join(', ') }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const { startLine, endLine } = match;
|
||||
@@ -133,17 +188,48 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
const indent = lines[startLine].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the original element
|
||||
// Extract the original element. Reindent under the wrapper while preserving
|
||||
// the relative depth between lines — `l.trimStart()` would strip ALL leading
|
||||
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
|
||||
// to a single uniform indent, so on accept/discard the round-trip restores
|
||||
// the inner element at its parent's depth instead of nested inside it.
|
||||
// Strip only the COMMON minimum leading whitespace across the picked lines;
|
||||
// `deindentContent` on the accept side already mirrors this convention.
|
||||
const originalLines = lines.slice(startLine, endLine + 1);
|
||||
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
|
||||
const originalBaseIndent = minLeadingSpaces(originalLines);
|
||||
const reindentOriginal = (extra) => originalLines
|
||||
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
|
||||
.join('\n');
|
||||
const originalIndented = reindentOriginal(' ');
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
// either type-errors or renders a literal CSS string).
|
||||
const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"';
|
||||
|
||||
// Build the wrapper
|
||||
const wrapperLines = [
|
||||
// JSX/TSX guard: the picked element occupies a single JSX child slot
|
||||
// (inside `return (...)`, an array `.map(...)`, an `asChild` branch, or
|
||||
// any other expression position). Replacing it with `comment + <div> +
|
||||
// comment` yields three adjacent siblings — invalid JSX. We can't use a
|
||||
// Fragment `<></>` either: parents that clone children (Radix `asChild`,
|
||||
// Headless UI, etc.) hit "Invalid prop supplied to React.Fragment" when
|
||||
// they try to pass an `id` through.
|
||||
//
|
||||
// Solution: keep the wrapper `<div>` as the single JSX-slot child and
|
||||
// tuck both marker comments INSIDE it. accept/discard then expands its
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
reindentOriginal(' '),
|
||||
indent + ' </div>',
|
||||
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
@@ -163,13 +249,24 @@ The agent should insert variant HTML at insertLine.`);
|
||||
];
|
||||
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
||||
|
||||
// Calculate insert line (the "insert below this line" comment)
|
||||
const insertLine = startLine + 6; // 0-indexed in the new file
|
||||
// Calculate insert line (the "insert below this line" comment).
|
||||
// 0-indexed file position. Both HTML and JSX wrappers have 6 lines above
|
||||
// the insert marker (HTML: start-comment + outer-div + Original-comment +
|
||||
// original-div + content + close-original-div; JSX: outer-div +
|
||||
// start-comment + Original-comment + original-div + content +
|
||||
// close-original-div). Multi-line originals push the marker by their
|
||||
// extra line count.
|
||||
const insertLine = startLine + 6 + (originalLines.length - 1);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
startLine: startLine + 1, // 1-indexed for the agent
|
||||
endLine: startLine + wrapperLines.length, // 1-indexed
|
||||
// wrapperLines is an array but one element (the original-content slot)
|
||||
// is a `\n`-joined multi-line string, so the actual file-row count is
|
||||
// wrapperLines.length + (originalLines.length - 1). Without the offset,
|
||||
// endLine pointed inside the wrapper for any picked element that
|
||||
// spanned more than one source line.
|
||||
endLine: startLine + wrapperLines.length + (originalLines.length - 1), // 1-indexed
|
||||
insertLine: insertLine + 1, // 1-indexed: where variants go
|
||||
commentSyntax: commentSyntax,
|
||||
originalLineCount: originalLines.length,
|
||||
@@ -310,6 +407,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
|
||||
* line to find the actual tag opener. When `tag` is provided, opener candidates
|
||||
* must match that tag name.
|
||||
*/
|
||||
/**
|
||||
* Return the smallest leading-whitespace count across a set of lines,
|
||||
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
|
||||
* the common base indent of a multi-line picked element so reindenting
|
||||
* under the wrapper preserves the relative depth between lines.
|
||||
*/
|
||||
function minLeadingSpaces(lines) {
|
||||
let min = Infinity;
|
||||
for (const l of lines) {
|
||||
if (l.trim() === '') continue;
|
||||
const m = l.match(/^(\s*)/);
|
||||
if (m && m[1].length < min) min = m[1].length;
|
||||
}
|
||||
return min === Infinity ? 0 : min;
|
||||
}
|
||||
|
||||
function findElement(lines, query, tag = null) {
|
||||
// Iterate all matches — the first substring hit isn't always the right one.
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -330,6 +443,69 @@ function findElement(lines, query, tag = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like findElement, but returns every match. Used for ambiguity detection
|
||||
* when the agent passes --text: when the same className appears on multiple
|
||||
* sibling elements (a list of cards, repeated section variants, etc.),
|
||||
* first-match silently lands on the wrong branch. Returning all matches lets
|
||||
* the caller narrow by textContent or fail with a structured ambiguity error.
|
||||
*/
|
||||
function findAllElements(lines, query, tag = null) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].includes(query)) continue;
|
||||
const stripped = lines[i].trim();
|
||||
if (stripped.startsWith('<!--') || stripped.startsWith('{/*') || stripped.startsWith('//')) continue;
|
||||
if (lines[i].includes('data-impeccable-variant')) continue;
|
||||
const openerLine = findOpenerLine(lines, i, tag);
|
||||
if (openerLine === -1) continue;
|
||||
if (seen.has(openerLine)) continue; // multiple matches inside the same element
|
||||
seen.add(openerLine);
|
||||
const endLine = findClosingLine(lines, openerLine);
|
||||
out.push({ startLine: openerLine, endLine });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a candidate set to those whose source body matches a meaningful
|
||||
* prefix of the picked element's textContent. The compare strips tags and
|
||||
* JSX expressions, then checks two whitespace normalizations side-by-side:
|
||||
*
|
||||
* - single-space ("hero two second card body")
|
||||
* - no-whitespace ("herotwosecondcardbody")
|
||||
*
|
||||
* Both are needed because `el.textContent` concatenates sibling text without
|
||||
* inserting whitespace (e.g. `<h1>Hero Two</h1><p>Second…</p>` reads as
|
||||
* `"Hero TwoSecond…"`), while the source has whitespace between tags. If
|
||||
* EITHER normalization matches, the candidate keeps. A snippet shorter than
|
||||
* 8 chars after stripping is too weak to disambiguate — the caller falls
|
||||
* back to first-match.
|
||||
*/
|
||||
function filterByText(candidates, lines, text) {
|
||||
const trimmed = text.replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 80);
|
||||
// Too short to disambiguate. Return [] so the caller's `filtered.length
|
||||
// === 0` branch fires (fall back to first-match) — the previous
|
||||
// `candidates.slice()` return forced `filtered.length > 1` and surfaced
|
||||
// a spurious `element_ambiguous` error on every short-text picker event
|
||||
// with multiple candidates.
|
||||
if (trimmed.length < 8) return [];
|
||||
const targetSpaced = trimmed;
|
||||
const targetCompact = trimmed.replace(/\s+/g, '');
|
||||
|
||||
return candidates.filter((c) => {
|
||||
const body = lines.slice(c.startLine, c.endLine + 1).join(' ');
|
||||
const inner = body
|
||||
.replace(/<[^>]*>/g, ' ') // strip HTML/JSX tags
|
||||
.replace(/\{[^}]*\}/g, ' ') // strip JSX expressions
|
||||
.toLowerCase();
|
||||
const sourceSpaced = inner.replace(/\s+/g, ' ').trim();
|
||||
const sourceCompact = inner.replace(/\s+/g, '');
|
||||
return sourceSpaced.includes(targetSpaced) || sourceCompact.includes(targetCompact);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a match line to the real tag opener. If the match line itself opens
|
||||
* a tag, return it. Otherwise walk up to 10 lines backward looking for the
|
||||
|
||||
@@ -13,11 +13,21 @@
|
||||
* design: string | null, // DESIGN.md contents
|
||||
* designPath: string | null,
|
||||
* migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md
|
||||
* contextDir: string, // absolute path of the directory the files were found in
|
||||
* }
|
||||
*
|
||||
* Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The
|
||||
* Google DESIGN.md convention is uppercase at repo root; Kiro-style and
|
||||
* lowercase variants are also matched so users don't get punished for case.
|
||||
*
|
||||
* Lookup directory resolution (first match wins):
|
||||
* 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
|
||||
* 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat)
|
||||
* 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/
|
||||
* 4. cwd as a default "no context found" location
|
||||
*
|
||||
* Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root;
|
||||
* fallback directories are read-only as far as auto-rename is concerned.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -26,15 +36,52 @@ import path from 'node:path';
|
||||
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
|
||||
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
|
||||
const LEGACY_NAMES = ['.impeccable.md'];
|
||||
const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
||||
|
||||
/**
|
||||
* Resolve the directory that holds PRODUCT.md / DESIGN.md / DESIGN.json for
|
||||
* this project. Exported so other scripts (e.g. live-server.mjs) can read the
|
||||
* design files from the same location the loader uses.
|
||||
*/
|
||||
export function resolveContextDir(cwd = process.cwd()) {
|
||||
// 1. Explicit override
|
||||
const envDir = process.env.IMPECCABLE_CONTEXT_DIR;
|
||||
if (envDir && envDir.trim()) {
|
||||
const trimmed = envDir.trim();
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
}
|
||||
|
||||
// 2. cwd wins if any canonical or legacy file is there. We check legacy too
|
||||
// so the auto-migration path in loadContext stays predictable.
|
||||
if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present;
|
||||
// legacy `.impeccable.md` does not pull the lookup into a fallback dir.
|
||||
for (const rel of FALLBACK_DIRS) {
|
||||
const candidate = path.resolve(cwd, rel);
|
||||
if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Nothing found — keep the historical "default to cwd" behaviour so the
|
||||
// caller's `hasProduct === false` branch still fires the same way.
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function loadContext(cwd = process.cwd()) {
|
||||
let migrated = false;
|
||||
const contextDir = resolveContextDir(cwd);
|
||||
|
||||
// 1. Look for PRODUCT.md (case-insensitive)
|
||||
let productPath = firstExisting(cwd, PRODUCT_NAMES);
|
||||
// 1. Look for PRODUCT.md (case-insensitive) in the resolved dir
|
||||
let productPath = firstExisting(contextDir, PRODUCT_NAMES);
|
||||
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists, rename in place
|
||||
if (!productPath) {
|
||||
// 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename
|
||||
// it in place. We only migrate at the root — fallback dirs are read-only
|
||||
// so we don't surprise users by mutating files under docs/ or .agents/.
|
||||
if (!productPath && contextDir === cwd) {
|
||||
const legacyPath = firstExisting(cwd, LEGACY_NAMES);
|
||||
if (legacyPath) {
|
||||
const newPath = path.join(cwd, 'PRODUCT.md');
|
||||
@@ -50,7 +97,7 @@ export function loadContext(cwd = process.cwd()) {
|
||||
}
|
||||
|
||||
// 3. DESIGN.md (case-insensitive)
|
||||
const designPath = firstExisting(cwd, DESIGN_NAMES);
|
||||
const designPath = firstExisting(contextDir, DESIGN_NAMES);
|
||||
|
||||
const product = productPath ? safeRead(productPath) : null;
|
||||
const design = designPath ? safeRead(designPath) : null;
|
||||
@@ -63,12 +110,13 @@ export function loadContext(cwd = process.cwd()) {
|
||||
design,
|
||||
designPath: designPath ? path.relative(cwd, designPath) : null,
|
||||
migrated,
|
||||
contextDir,
|
||||
};
|
||||
}
|
||||
|
||||
function firstExisting(cwd, names) {
|
||||
function firstExisting(dir, names) {
|
||||
for (const name of names) {
|
||||
const abs = path.join(cwd, name);
|
||||
const abs = path.join(dir, name);
|
||||
if (fs.existsSync(abs)) return abs;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -23,6 +23,7 @@ assignees: ''
|
||||
- [ ] VS Code Copilot
|
||||
- [ ] Kiro
|
||||
- [ ] OpenCode
|
||||
- [ ] Qoder
|
||||
- [ ] All providers
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
- [ ] Source files updated in `source/`
|
||||
- [ ] `bun run build` ran successfully
|
||||
- [ ] `bun test` passes
|
||||
- [ ] Tested with at least one provider (Cursor / Claude Code / Gemini CLI / Codex / Copilot / Kiro / OpenCode)
|
||||
- [ ] Tested with at least one provider (Cursor / Claude Code / Gemini CLI / Codex / Copilot / Kiro / OpenCode / Qoder)
|
||||
- [ ] README / DEVELOP.md updated if needed
|
||||
|
||||
@@ -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.2
|
||||
version: 3.0.5
|
||||
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,11 +11,30 @@ 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
|
||||
|
||||
Two files at the project root, case-insensitive:
|
||||
Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd).
|
||||
|
||||
- **PRODUCT.md** — required. Users, brand, tone, anti-references, strategic principles.
|
||||
- **DESIGN.md** — optional, strongly recommended. Colors, typography, elevation, components.
|
||||
@@ -26,13 +45,13 @@ Load both in one call:
|
||||
node .github/skills/impeccable/scripts/load-context.mjs
|
||||
```
|
||||
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`.
|
||||
Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from.
|
||||
|
||||
If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one.
|
||||
|
||||
`/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.
|
||||
|
||||
@@ -105,7 +124,10 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit
|
||||
|
||||
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
|
||||
|
||||
**Category-reflex check.** If someone could guess the theme and palette from the category name alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the training-data reflex. Rework the scene sentence and color strategy until the answer is no longer obvious from the domain.
|
||||
**Category-reflex check.** Run at two altitudes — the second one catches what the first one misses.
|
||||
|
||||
- **First-order:** if someone could guess the theme + palette from the category alone — "observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black" — it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
|
||||
- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references — "AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode" — it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -145,6 +167,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
|
||||
|
||||
@@ -29,6 +29,16 @@ Training-data defaults. Ban list — look further:
|
||||
|
||||
Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
|
||||
|
||||
### Reflex-reject aesthetic lanes
|
||||
|
||||
Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex — the trap one tier deeper than picking a Fraunces font. Look further.
|
||||
|
||||
- **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
|
||||
|
||||
(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
|
||||
|
||||
The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins — variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in [live.md](live.md).
|
||||
|
||||
### Pairing and voice
|
||||
|
||||
Distinctive + refined is the goal — the specific shape depends on the brand:
|
||||
|
||||
@@ -1,24 +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.
|
||||
|
||||
## Real Example: Neo Mirai
|
||||
## Build Gate
|
||||
|
||||
Neo Mirai is the full craft loop in public. A retro-futurist AI design conference started with generated brand and hi-fi reference images, then shipped as a responsive static site in `public/neo-mirai`.
|
||||
Craft cannot build until all of these are true:
|
||||
|
||||
Repro command:
|
||||
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.
|
||||
|
||||
```bash
|
||||
/impeccable craft retro-futurist AI design conference website
|
||||
```
|
||||
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.
|
||||
|
||||
The important detail is the artifact chain: brand toolkit, north-star mock, semantic implementation, regenerated assets, browser iteration, responsive fixes. The mock was not treated as a screenshot to trace. It was used as direction for a real page.
|
||||
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.
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -52,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.
|
||||
|
||||
@@ -86,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.
|
||||
|
||||
@@ -29,7 +29,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
|
||||
node .github/skills/impeccable/scripts/live.mjs
|
||||
```
|
||||
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent — extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md.
|
||||
|
||||
`serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname).
|
||||
|
||||
@@ -74,7 +74,7 @@ Reading annotations precisely:
|
||||
### 2. Wrap the element
|
||||
|
||||
```bash
|
||||
node .github/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
|
||||
node .github/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
|
||||
```
|
||||
|
||||
Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
@@ -82,9 +82,12 @@ Flag mapping — keep them separate, don't collapse into `--query`:
|
||||
- `--element-id` ← `event.element.id`
|
||||
- `--classes` ← `event.element.classes` joined with commas
|
||||
- `--tag` ← `event.element.tagName`
|
||||
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
|
||||
|
||||
The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only — do not use it for normal element lookups.
|
||||
|
||||
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"` — read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
|
||||
|
||||
Output on success: `{ file, insertLine, commentSyntax }`.
|
||||
|
||||
**Fallback errors.** Wrap only writes into files it judges to be source (tracked by git, not marked GENERATED, not listed in config's `generatedFiles`). If it can't land on a source file, it errors without writing — accepting a variant into a generated file is silent data loss. Three shapes:
|
||||
@@ -101,38 +104,81 @@ If `event.action` is `impeccable` (the default freeform action), use SKILL.md's
|
||||
|
||||
Any other `event.action` (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `adapt`, `animate`, `delight`, `overdrive`): Read `reference/<action>.md` before planning. Each sub-command encodes a specific discipline; skipping its reference produces generic output. Those files may require specific params; layer them on top of the §7 budget, not instead of it.
|
||||
|
||||
### 4. Plan three genuinely distinct directions
|
||||
### 4. Plan three variants — identity first, then mode, then axes
|
||||
|
||||
Before writing a single line of code, name each variant.
|
||||
The wrong frame for live mode is "show three different design directions." Live runs on an existing surface — the brand has already been chosen. The job is variation **within identity**, not selection between identities. Failure mode: three editorial-typographic variants on a brief that wasn't editorial. Bigger failure mode: three off-brand variants the user can't accept because they don't look like their product.
|
||||
|
||||
**For freeform (`action` is `impeccable`, or the user supplied a free prompt):** each variant must anchor to a different **archetype** — a real-world design analogue specific enough to be recognizable at a glance. Not "modern landing page." Not "minimal product hero." Examples:
|
||||
Four phases. Do them in order.
|
||||
|
||||
- *Broadsheet masthead with rule-divided columns* (think NYT print edition)
|
||||
- *Klim Type Foundry specimen page* (dense, technical, catalog-driven)
|
||||
- *Japanese print-poster minimalism with a single oversize glyph*
|
||||
- *Bloomberg Terminal status bar*
|
||||
- *Condé Nast Traveler feature layout*
|
||||
#### Phase A: Extract the identity (non-skippable)
|
||||
|
||||
Then commit each variant to a different **primary axis** of difference:
|
||||
The existing surface has an identity already. Read it before planning anything. Sources, in priority order:
|
||||
|
||||
1. **DESIGN.md** if loaded — read the visual system fields (palette, type pairing, motion, components). This is the authoritative answer.
|
||||
2. **CSS custom properties** in the page's stylesheets (`:root { --color-...; --font-...; ... }`) — these are de-facto tokens.
|
||||
3. **Computed styles** on the picked element and its parent — colors, fonts, spacing scales, corner radii.
|
||||
4. **Sibling components on the page** — what visual rhetoric do existing components use? (Asymmetric or centered? Dense or airy? Bold or quiet?)
|
||||
|
||||
Write down what you see in **one sentence**. The sentence describes the surface that's actually on screen — it is not aspirational, not opinionated, not edited toward what the brand "should" be. Capture, in roughly this order:
|
||||
|
||||
- The dominant surface color and accent color, by hex or token name (use the actual values, not categories like "warm" or "neutral").
|
||||
- The type pairing — the actual font names loaded, primary first.
|
||||
- The layout topology — how the dominant elements are arranged (stacked / side-by-side / grid / asymmetric / overlay).
|
||||
- The surface treatment — corners, borders, shadows, density of decoration.
|
||||
- The voice tone you read off the copy itself, not off the aesthetic feel.
|
||||
|
||||
Be specific. "Modern" is not a color, "elegant" is not a type pairing, "clean" is not a layout. If you can't extract a real value for an axis, skip it rather than fabricate. The point is to record what is, not to describe what you wish it were.
|
||||
|
||||
Do not include adjectives that name an aesthetic family ("editorial-leaning", "terminal-flavored", "brutalist") — those are conclusions, not data. They belong to Phase C lane selection in departure mode, not to identity description. Letting them sneak into Phase A is how the identity-lock collapses into a self-fulfilling prophecy.
|
||||
|
||||
This sentence is the **identity lock**. Every variant must be readable as the same brand if rendered side by side. Skipping this phase is the primary cause of off-brand variants. Absence of DESIGN.md is never an excuse — extract from CSS and computed styles instead.
|
||||
|
||||
#### Phase B: Pick mode (default vs departure)
|
||||
|
||||
**Default mode** — the existing identity is preserved. Variants vary expression axes within it. *This is the right mode for ~90% of live sessions.* The user picked an element on a real product they're shipping; they expect variants of *their* hero, not three different brands' heroes.
|
||||
|
||||
**Departure mode** — the existing identity is rejected. Variants propose alternatives consistent with PRODUCT.md voice. Trigger only when at least one is true:
|
||||
|
||||
- PRODUCT.md anti-references explicitly call out the current surface ("the current `index.html` is itself an example"; "diffuse away from this"; "the page on screen is the failure"). Generic anti-references that describe what to avoid in general do **not** trigger departure mode — only ones that point at *this* surface specifically.
|
||||
- The user's freeform prompt explicitly asks for departure ("rebuild this from scratch", "what if it weren't editorial at all", "show me something completely different").
|
||||
|
||||
If you're unsure, you're in default mode. The cost of being wrong about default is "three on-brand variants with similar feel" — recoverable, the user picks none. The cost of being wrong about departure is "three off-brand variants" — unrecoverable, the user is annoyed.
|
||||
|
||||
#### Phase C: Plan three variants
|
||||
|
||||
**Default mode.** Each variant commits to a different **primary axis** of difference, while preserving the identity sentence. The six axes:
|
||||
|
||||
1. **Hierarchy** — which element commands the eye?
|
||||
2. **Layout topology** — stacked / side-by-side / grid / asymmetric / overlay
|
||||
3. **Typographic system** — pairing, scale ratio, case/weight strategy
|
||||
4. **Color strategy** — Restrained / Committed / Full palette / Drenched
|
||||
3. **Typographic system** — pairing logic, scale ratio, case/weight strategy *within the available faces*
|
||||
4. **Color strategy** — which existing palette role carries the surface (Restrained / Committed / Full palette / Drenched). Use the brand's existing palette tokens, not new colors.
|
||||
5. **Density** — minimal / comfortable / dense
|
||||
6. **Structural decomposition** — merge, split, progressive disclosure
|
||||
|
||||
Three variants → three DIFFERENT primary axes, not three riffs on color.
|
||||
Three variants → three DIFFERENT axes. The trio reads as *the same brand at three angles*. Do not introduce new fonts, new palette hues, or new aesthetic-family signals — those belong to departure mode.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for one dark-neutral-accent, one light-drenched, one full-palette-saturated — three color worlds, not three shades of the same.
|
||||
**Departure mode.** Each variant anchors to a different **aesthetic lane**, drawn from non-monoculture options. Lanes (illustrative, not exhaustive):
|
||||
|
||||
**The squint test (before writing code).** Write the three one-sentence descriptions side by side:
|
||||
- Tech-minimal (Stripe / Linear / Vercel-restraint)
|
||||
- Brutalist-utility (system fonts, raw HTML defaults, Craigslist-energy)
|
||||
- Terminal-native (actual code surface, syntax-highlighted UI as the design)
|
||||
- Industrial-signage (dimensional type, arrow systems, ISO standards, wayfinding)
|
||||
- Technical-drawing (isometric line art, callout numbers, blueprint blue, exploded-view diagrams)
|
||||
- Drenched-saturated (Liquid Death / Mailchimp full-palette / single-hue commitment)
|
||||
- Swiss-grid-rigorous (visible grid as voice, tight type, accumulated systems)
|
||||
|
||||
> V1: Broadsheet masthead, ruled columns, 24px ink on cream.
|
||||
> V2: Enormous italic title, catalog spec rows, heavy monospace data.
|
||||
> V3: Card-framed poster with one oversize glyph, magenta veil.
|
||||
Avoid the **reflex-reject lanes** in [brand.md](brand.md). Don't trade one monoculture for another. *Editorial-typographic is currently a reflex-reject lane* — three variants that all read as "magazine cover" is the second-order training reflex, the trap one tier deeper than SaaS-cream.
|
||||
|
||||
If two of them rhyme ("both use big type" / "both are stacks of sections" / "both feature the CTA prominently"), rework the offender. Freeform variants failing the squint test is the primary failure mode of this flow — three-of-the-same with minor styling tweaks.
|
||||
#### Phase D: Squint test
|
||||
|
||||
**Default mode squint.** Read each variant's identity sentence and compare to the locked identity from Phase A. If any variant has drifted to a different palette, type voice, or visual rhetoric, it has crossed into departure mode by accident — rework. Then check that each variant commits to a different primary axis. Three "tighter density" variants is failure.
|
||||
|
||||
**Departure mode squint.** Two passes, family before sentence:
|
||||
|
||||
1. **Family pass.** Label each variant with one design-family word: *editorial / brutalist / terminal / signage / drenched / swiss / technical-drawing / tech-minimal*. If any two variants share a family label, rework. *This pass is non-negotiable in departure mode and catches the editorial-monoculture failure that the sentence pass misses.*
|
||||
2. **Sentence pass.** Write three one-sentence descriptions side by side. If two of them rhyme ("both feature big type" / "both are stacks of sections" / "both center the CTA"), rework the offender.
|
||||
|
||||
**When the primary axis is color or theme, forbid the trio from sharing theme + dominant hue.** Two dark-plus-one-dark is not distinct. Aim for three color worlds, not three shades of the same.
|
||||
|
||||
**For action-specific invocations**, each variant must vary along the dimension the action names:
|
||||
|
||||
@@ -150,7 +196,13 @@ If two of them rhyme ("both use big type" / "both are stacks of sections" / "bot
|
||||
|
||||
### 5. Apply the freeform prompt (if present)
|
||||
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. "Make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
|
||||
`event.freeformPrompt` is the user's ceiling on direction — all variants must honor it — but still explore meaningfully different *interpretations*. The interpretations stay within whichever mode you picked in Phase B.
|
||||
|
||||
In **default mode**, the prompt narrows the axes you choose, not the identity. *"Make it feel more confident"* → variant 1 amplifies hierarchy (one element commands the eye), variant 2 commits the existing accent color (Committed strategy on the brand's hue), variant 3 tightens density and removes decorative slack. Three different axes, same brand.
|
||||
|
||||
In **departure mode**, the prompt narrows the lanes you draw from, not the families. *"Make it feel like a newspaper front page"* would itself be a departure-mode prompt; honor it but pick three meaningfully different newspaper-adjacent lanes (broadsheet vs. tabloid vs. trade journal), and run the family pass to confirm they don't collapse into one.
|
||||
|
||||
When the prompt and PRODUCT.md anti-references conflict (the prompt asks for X, the anti-references ban X), the anti-references win — they describe the brand's standing position, the prompt is one moment.
|
||||
|
||||
### 6. Write all variants in a single edit
|
||||
|
||||
@@ -181,6 +233,25 @@ The first variant has no `display: none` (visible by default). All others do. If
|
||||
|
||||
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
|
||||
|
||||
**Author every `:scope` rule with a descendant combinator.** The `@scope` boundary is the **variant wrapper `<div data-impeccable-variant="N">`**, not the element you're designing. A bare `:scope { background: cream; }` styles the wrapper, not the inner replacement, so the cream lands on a `display: contents` shell while the actual element keeps page defaults. Always step in: `:scope > .card`, `:scope > section`, `:scope .hero-title`, etc. The fake test agent's CSS in `tests/live-e2e/agent.mjs` is a faithful template — every rule starts `:scope > ...`.
|
||||
|
||||
**JSX / TSX target files.** Wrap `<style>` content in a template literal so the CSS `{` / `}` aren't parsed as JSX expressions, and use `className=` / `style={{…}}` on every variant element. Keep `data-impeccable-*` attributes as-is — they're plain strings:
|
||||
|
||||
```tsx
|
||||
<style data-impeccable-css="SESSION_ID">{`
|
||||
@scope ([data-impeccable-variant="1"]) { ... }
|
||||
@scope ([data-impeccable-variant="2"]) { ... }
|
||||
`}</style>
|
||||
<div data-impeccable-variant="1">
|
||||
{/* variant 1 */}
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style={{ display: 'none' }}>
|
||||
{/* variant 2 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
The wrap script already gives you a single-rooted JSX wrapper — a `<div data-impeccable-variants="…">` outer element with the marker comments tucked inside. Drop the variants block above into the "Variants: insert below this line" comment and the source stays valid TSX.
|
||||
|
||||
### 7. Parameters (composition-sized, 0–4 per variant)
|
||||
|
||||
Each variant can expose **coarse** knobs alongside the full HTML/CSS replacement. The browser docks a small panel to the right of the outline with one control per parameter. The user drags/clicks and sees instant feedback: there is zero regeneration cost because the knob toggles a CSS variable or data attribute that the variant's scoped CSS is already authored against.
|
||||
@@ -246,6 +317,16 @@ node .github/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --fil
|
||||
|
||||
Then run `live-poll.mjs` again immediately.
|
||||
|
||||
### Aborting an in-flight session
|
||||
|
||||
If wrap or generation fails after the browser has flipped to GENERATING (e.g. wrap landed on the wrong source branch and you've already reverted it, or generation hit an unrecoverable error), tell the **browser** so its bar resets to PICKING:
|
||||
|
||||
```bash
|
||||
node .github/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"
|
||||
```
|
||||
|
||||
Don't run `live-accept --discard` for this — that's a pure file mutator, the browser doesn't see it, and the bar gets stuck on the GENERATING dots forever (the user has to refresh). `--discard` is only correct when the **browser** initiated the discard (user clicked ✕ during CYCLING) and the agent is just running source-side cleanup the browser already triggered.
|
||||
|
||||
## Handle fallback
|
||||
|
||||
When wrap returns `fallback: "agent-driven"`, the deterministic flow doesn't apply. Pick up here.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -104,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`
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user