mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Add live variant mode: element picker, action panel, poll/reply bridge (22 commands)
New feature: /impeccable live starts an interactive visual iteration server. Users select elements in the browser, pick a design action (bolder, quieter, etc.), and the agent generates HTML+CSS variants written directly to source. The dev server's HMR hot-swaps them in, and MutationObserver progressively reveals each variant in a cycler UI as it arrives. Architecture: - src/live/server.mjs: HTTP + WebSocket server with session token auth, long-poll /poll endpoint for the agent, WebSocket for the browser - src/live/poll.mjs: CLI client (npx impeccable poll / poll --reply) - src/live/browser.js: element picker with keyboard nav (arrows=siblings, shift+arrows=parent/child), action panel (12 commands, freeform input, variant count), variant cycler with progressive reveal via MutationObserver - src/live/protocol.mjs: shared message types and event validation - source/skills/impeccable/reference/live.md: agent loop instructions (inject script, poll loop, generate variants, accept/discard, cleanup) CLI changes: - bin/cli.js: added "poll" top-level command - src/detect-antipatterns.mjs: liveCli() now delegates to src/live/server.mjs - package.json: added ws dependency Registered /impeccable live as command #22 across all standard locations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e58cbc432f
commit
bb94dadda0
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -329,6 +329,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -362,6 +365,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "impeccable",
|
||||
"metadata": {
|
||||
"description": "Design fluency for AI harnesses. 1 skill, 21 commands, and curated anti-patterns for impeccable frontend design."
|
||||
"description": "Design fluency for AI harnesses. 1 skill, 22 commands, and curated anti-patterns for impeccable frontend design."
|
||||
},
|
||||
"owner": {
|
||||
"name": "Paul Bakaus",
|
||||
@@ -11,7 +11,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 21 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"description": "Design fluency for frontend development. 1 skill with 22 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.0.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 21 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"description": "Design fluency for frontend development. 1 skill with 22 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.0.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -331,6 +331,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -364,6 +367,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
argument-hint: "[command] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
@@ -328,6 +328,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `$impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `$impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `$impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `$impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates $audit)
|
||||
> `$impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -361,6 +364,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
@@ -327,6 +327,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -360,6 +363,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
---
|
||||
|
||||
@@ -326,6 +326,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -359,6 +362,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
@@ -327,6 +327,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -360,6 +363,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -331,6 +331,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -364,6 +367,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
allowed-tools:
|
||||
@@ -329,6 +329,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -362,6 +365,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -331,6 +331,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -364,6 +367,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -329,6 +329,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -362,6 +365,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
version: 3.0.0
|
||||
user-invocable: true
|
||||
argument-hint: "[command] [target]"
|
||||
@@ -329,6 +329,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `/impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `/impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `/impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `/impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates /audit)
|
||||
> `/impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -362,6 +365,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Architecture (v3.0+)
|
||||
|
||||
There is **one** user-invocable skill, `impeccable`, with **21 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `source/skills/impeccable/`:
|
||||
There is **one** user-invocable skill, `impeccable`, with **22 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `source/skills/impeccable/`:
|
||||
|
||||
- `SKILL.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design principles, and the **Command Router** section that dispatches sub-commands via argument matching.
|
||||
- `reference/` — one `<command>.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.) plus the domain reference files (`typography.md`, `color-and-contrast.md`, etc.). When a sub-command is matched, the router loads its reference file.
|
||||
|
||||
@@ -13,5 +13,5 @@ The `impeccable` skill in this project builds on Anthropic's original frontend-d
|
||||
|
||||
This project extends the original with:
|
||||
- 7 domain-specific reference files (typography, color-and-contrast, spatial-design, motion-design, interaction-design, responsive-design, ux-writing)
|
||||
- 21 commands
|
||||
- 22 commands
|
||||
- Expanded patterns and anti-patterns
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Impeccable
|
||||
|
||||
The vocabulary you didn't know you needed. 1 skill, 21 commands, and curated anti-patterns for impeccable frontend design.
|
||||
The vocabulary you didn't know you needed. 1 skill, 22 commands, and curated anti-patterns for impeccable frontend design.
|
||||
|
||||
> **Quick start:** Visit [impeccable.style](https://impeccable.style) to download ready-to-use bundles.
|
||||
|
||||
@@ -12,7 +12,7 @@ Every LLM learned from the same generic templates. Without guidance, you get the
|
||||
|
||||
Impeccable fights that bias with:
|
||||
- **An expanded skill** with 7 domain-specific reference files ([view source](source/skills/impeccable/))
|
||||
- **21 commands** to audit, review, polish, distill, animate, and more
|
||||
- **22 commands** to audit, review, polish, distill, animate, and more
|
||||
- **Curated anti-patterns** that explicitly tell the AI what NOT to do
|
||||
|
||||
## What's Included
|
||||
@@ -31,7 +31,7 @@ A comprehensive design skill with 7 domain-specific references ([view skill](sou
|
||||
| [responsive-design](source/skills/impeccable/reference/responsive-design.md) | Mobile-first, fluid design, container queries |
|
||||
| [ux-writing](source/skills/impeccable/reference/ux-writing.md) | Button labels, error messages, empty states |
|
||||
|
||||
### 21 Commands
|
||||
### 22 Commands
|
||||
|
||||
All commands are accessed through `/impeccable`:
|
||||
|
||||
@@ -58,6 +58,7 @@ All commands are accessed through `/impeccable`:
|
||||
| `/impeccable clarify` | Improve unclear UX copy |
|
||||
| `/impeccable adapt` | Adapt for different devices |
|
||||
| `/impeccable optimize` | Performance improvements |
|
||||
| `/impeccable live` | Visual variant mode: iterate on elements in the browser |
|
||||
|
||||
Use `/impeccable pin <command>` to create standalone shortcuts (e.g., `pin audit` creates `/audit`).
|
||||
|
||||
|
||||
+7
-1
@@ -24,8 +24,10 @@ if (!command || command === '--help' || command === '-h') {
|
||||
|
||||
Commands:
|
||||
detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues
|
||||
live [--port=PORT] Start browser detection overlay server
|
||||
live [--port=PORT] Start live variant server (element picker + variant cycling)
|
||||
live stop Stop a running live server
|
||||
poll Wait for a browser event from the live server
|
||||
poll --reply <id> <status> Reply to a pending event (done, error)
|
||||
skills help List all available skills and commands
|
||||
skills install Install impeccable skills into your project
|
||||
skills update Update skills to the latest version
|
||||
@@ -53,6 +55,10 @@ if (command === 'detect') {
|
||||
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
||||
const { liveCli } = await import('../src/detect-antipatterns.mjs');
|
||||
await liveCli();
|
||||
} else if (command === 'poll') {
|
||||
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
||||
const { pollCli } = await import('../src/live/poll.mjs');
|
||||
await pollCli();
|
||||
} else if (command === 'skills') {
|
||||
const { run } = await import('./commands/skills.mjs');
|
||||
await run(args.slice(1));
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"jsdom": "^29.0.0",
|
||||
"marked": "^16.1.0"
|
||||
"marked": "^16.1.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"puppeteer": "^24.39.1"
|
||||
|
||||
+11
-11
@@ -13,7 +13,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Impeccable: The missing upgrade to Anthropic's impeccable skill</title>
|
||||
<meta name="description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
|
||||
<meta name="description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
|
||||
<meta name="theme-color" content="#fafafa">
|
||||
<link rel="canonical" href="https://impeccable.style">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://impeccable.style">
|
||||
<meta property="og:title" content="Impeccable: Design skills for AI harnesses">
|
||||
<meta property="og:description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
|
||||
<meta property="og:description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
|
||||
<meta property="og:image" content="https://impeccable.style/og-image.jpg">
|
||||
|
||||
<!-- Twitter -->
|
||||
@@ -29,7 +29,7 @@
|
||||
<meta name="twitter:site" content="@pbakaus">
|
||||
<meta name="twitter:creator" content="@pbakaus">
|
||||
<meta name="twitter:title" content="Impeccable: Design skills for AI harnesses">
|
||||
<meta name="twitter:description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design.">
|
||||
<meta name="twitter:description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design.">
|
||||
<meta name="twitter:image" content="https://impeccable.style/og-image.jpg">
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
|
||||
@@ -88,13 +88,13 @@
|
||||
<h1 class="hero-title-combined">Impeccable</h1>
|
||||
<p class="hero-tagline-combined">Design fluency for AI harnesses</p>
|
||||
|
||||
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 21 commands to steer the result.</p>
|
||||
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 21 commands to steer the result.</p>
|
||||
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 22 commands to steer the result.</p>
|
||||
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 22 commands to steer the result.</p>
|
||||
|
||||
<div class="hero-included-box">
|
||||
<span class="hero-included-title">What's included</span>
|
||||
<div class="hero-included-items">
|
||||
<span><em>Impeccable</em> agent skill with 21 design commands</span>
|
||||
<span><em>Impeccable</em> agent skill with 22 design commands</span>
|
||||
<span class="hero-included-sep">·</span>
|
||||
<span>Optional CLI + Chrome extension</span>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hero-version-link"><a href="#changelog">v3.0: 1 skill, 21 commands</a></p>
|
||||
<p class="hero-version-link"><a href="#changelog">v3.0: 1 skill, 22 commands</a></p>
|
||||
</div>
|
||||
|
||||
<!-- Right: Before/After Demo -->
|
||||
@@ -203,7 +203,7 @@
|
||||
<h2 class="section-title">The Language</h2>
|
||||
</div>
|
||||
<div class="language-content">
|
||||
<p class="section-lead" data-reveal>21 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.</p>
|
||||
<p class="section-lead" data-reveal>22 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.</p>
|
||||
|
||||
<div class="solution-visual-interactive" id="framework-viz-container" data-reveal>
|
||||
<!-- Periodic table generated by JS -->
|
||||
@@ -332,7 +332,7 @@
|
||||
<!-- Left: Install the skill and CLI -->
|
||||
<div class="install-primary-main">
|
||||
<h3 class="install-path-title"><span class="install-path-step">1</span>Install the skill and CLI</h3>
|
||||
<p class="install-path-desc">One agent skill that teaches your AI to design, with 21 commands bundled inside. Plus the CLI that powers visual mode and scans files outside the skill.</p>
|
||||
<p class="install-path-desc">One agent skill that teaches your AI to design, with 22 commands bundled inside. Plus the CLI that powers visual mode and scans files outside the skill.</p>
|
||||
|
||||
<div class="install-path-terminal">
|
||||
<div class="glass-terminal">
|
||||
@@ -410,7 +410,7 @@
|
||||
<!-- Right: How to use -->
|
||||
<div class="install-primary-howto">
|
||||
<h3 class="install-path-title"><span class="install-path-step">2</span>Use it</h3>
|
||||
<p class="install-path-desc">Impeccable gives you a shared design vocabulary with your AI. 21 commands (<em>polish</em>, <em>audit</em>, <em>critique</em>, <em>typeset</em>, and more) that each encode a specific design discipline, so you can steer with precision.</p>
|
||||
<p class="install-path-desc">Impeccable gives you a shared design vocabulary with your AI. 22 commands (<em>polish</em>, <em>audit</em>, <em>critique</em>, <em>typeset</em>, and more) that each encode a specific design discipline, so you can steer with precision.</p>
|
||||
|
||||
<ol class="install-howto-steps">
|
||||
<li>
|
||||
@@ -509,7 +509,7 @@
|
||||
<span class="changelog-date">April 10, 2026</span>
|
||||
</div>
|
||||
<ul class="changelog-items">
|
||||
<li><strong>18 skills became 1 skill with 21 commands.</strong> Every command now lives under <code>/impeccable</code>: <code>/impeccable audit</code>, <code>/impeccable polish</code>, <code>/impeccable critique</code>, and the rest. One entry in your <code>/</code> menu instead of 18, a shared design vocabulary between you and your AI, and far less namespace pollution as the plugin ecosystem grows. The autocomplete shows the full list the moment you type <code>/impeccable</code>.</li>
|
||||
<li><strong>18 skills became 1 skill with 22 commands.</strong> Every command now lives under <code>/impeccable</code>: <code>/impeccable audit</code>, <code>/impeccable polish</code>, <code>/impeccable critique</code>, and the rest. One entry in your <code>/</code> menu instead of 18, a shared design vocabulary between you and your AI, and far less namespace pollution as the plugin ecosystem grows. The autocomplete shows the full list the moment you type <code>/impeccable</code>.</li>
|
||||
<li><strong>Pin your favorites back as shortcuts.</strong> Run <code>/impeccable pin audit</code> and <code>/audit</code> becomes a standalone command again, without reversing the consolidation. Under the hood it writes a lightweight redirect skill that delegates to <code>/impeccable audit</code>, so updates to the parent skill flow through automatically. <code>/impeccable unpin audit</code> removes it.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,8 @@ const commandSymbols = {
|
||||
'harden': 'Ha',
|
||||
'onboard': 'On',
|
||||
'teach': 'Te',
|
||||
'extract': 'Ex'
|
||||
'extract': 'Ex',
|
||||
'live': 'Li'
|
||||
};
|
||||
|
||||
const commandNumbers = {
|
||||
@@ -56,7 +57,7 @@ const commandNumbers = {
|
||||
'delight': 10, 'bolder': 11, 'quieter': 12, 'overdrive': 13,
|
||||
'distill': 14, 'clarify': 15, 'adapt': 16,
|
||||
'polish': 17, 'optimize': 18, 'harden': 19, 'onboard': 20,
|
||||
'teach': 21, 'extract': 22
|
||||
'teach': 21, 'extract': 22, 'live': 23
|
||||
};
|
||||
|
||||
// After the v3.0 consolidation, all commands except the root "impeccable" are
|
||||
|
||||
+6
-3
@@ -74,7 +74,8 @@ export const commandProcessSteps = {
|
||||
'harden': ['Assess', 'Implement', 'Test', 'Verify'],
|
||||
'onboard': ['Identify', 'Design', 'Guide', 'Measure'],
|
||||
'teach': ['Explore', 'Interview', 'Synthesize', 'Save'],
|
||||
'extract': ['Identify', 'Abstract', 'Migrate', 'Document']
|
||||
'extract': ['Identify', 'Abstract', 'Migrate', 'Document'],
|
||||
'live': ['Start', 'Select', 'Generate', 'Accept']
|
||||
};
|
||||
|
||||
export const commandCategories = {
|
||||
@@ -105,7 +106,8 @@ export const commandCategories = {
|
||||
'onboard': 'harden',
|
||||
// SYSTEM - setup and tooling
|
||||
'teach': 'system',
|
||||
'extract': 'system'
|
||||
'extract': 'system',
|
||||
'live': 'system'
|
||||
};
|
||||
|
||||
// Skill relationships - now consolidated into impeccable skill
|
||||
@@ -139,5 +141,6 @@ export const commandRelationships = {
|
||||
'harden': { combinesWith: ['optimize'], flow: 'Harden: Edge cases, error handling, and i18n' },
|
||||
'onboard': { combinesWith: ['clarify', 'delight'], flow: 'Harden: First-run experiences and empty states' },
|
||||
'teach': { flow: 'System: One-time project design context setup' },
|
||||
'extract': { flow: 'System: Extract design system components and tokens' }
|
||||
'extract': { flow: 'System: Extract design system components and tokens' },
|
||||
'live': { flow: 'System: Visual variant mode in the browser' }
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// GENERATED by build.js — do not edit
|
||||
export const COMMAND_COUNT = 21;
|
||||
export const COMMAND_COUNT = 22;
|
||||
export const DETECTION_COUNT = 25;
|
||||
|
||||
+2
-1
@@ -40,7 +40,7 @@
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
|
||||
<!-- Command detail pages (the impeccable skill + 21 commands) -->
|
||||
<!-- Command detail pages (the impeccable skill + 22 commands) -->
|
||||
<url><loc>https://impeccable.style/docs/impeccable</loc><lastmod>2026-04-11</lastmod><priority>0.9</priority></url>
|
||||
<url><loc>https://impeccable.style/docs/craft</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
<url><loc>https://impeccable.style/docs/teach</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
@@ -63,6 +63,7 @@
|
||||
<url><loc>https://impeccable.style/docs/clarify</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
<url><loc>https://impeccable.style/docs/adapt</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
<url><loc>https://impeccable.style/docs/optimize</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
<url><loc>https://impeccable.style/docs/live</loc><lastmod>2026-04-12</lastmod><priority>0.8</priority></url>
|
||||
|
||||
<!-- Tutorial detail pages -->
|
||||
<url><loc>https://impeccable.style/tutorials/getting-started</loc><lastmod>2026-04-10</lastmod><priority>0.8</priority></url>
|
||||
|
||||
@@ -695,7 +695,7 @@ export async function generateSubPages(rootDir) {
|
||||
const html = renderPage({
|
||||
title: 'Docs | Impeccable',
|
||||
description:
|
||||
'21 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden.',
|
||||
'22 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden.',
|
||||
bodyHtml: wrapInDocsLayout(sidebar, main),
|
||||
activeNav: 'docs',
|
||||
canonicalPath: '/docs',
|
||||
|
||||
@@ -72,6 +72,7 @@ const SKILL_CATEGORIES = {
|
||||
// SYSTEM - setup and tooling
|
||||
teach: 'system',
|
||||
extract: 'system',
|
||||
live: 'system',
|
||||
};
|
||||
|
||||
export const CATEGORY_ORDER = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
|
||||
@@ -130,6 +131,7 @@ export const COMMAND_RELATIONSHIPS = {
|
||||
// System
|
||||
teach: {},
|
||||
extract: {},
|
||||
live: {},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, or push boundaries with shaders/physics/scroll effects. Commands: craft, teach, extract, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
description: "Design fluency for frontend interfaces. Build distinctive, production-grade web components, pages, artifacts, posters, and applications with high design quality. Also handles: critique/review/evaluate designs, audit accessibility/performance/responsive, polish finishing touches, improve typography/fonts/readability, fix layout/spacing/hierarchy, add animation/transitions/motion, adapt for mobile/tablet/responsive, simplify/declutter/distill, amplify bland/generic/safe designs, tone down loud/overwhelming designs, add color to gray/monochromatic interfaces, improve UX copy/labels/error messages, harden for production with edge cases/i18n/errors, design onboarding/first-run/empty states/activation flows, optimize slow/laggy performance, plan UX before coding, extract design tokens, push boundaries with shaders/physics/scroll effects, or visually iterate on elements in the browser with live variant mode. Commands: craft, teach, extract, live, pin, audit, critique, polish, shape, adapt, animate, bolder, quieter, colorize, clarify, delight, distill, harden, onboard, layout, optimize, overdrive, typeset."
|
||||
argument-hint: "[command] [target]"
|
||||
user-invocable: true
|
||||
allowed-tools:
|
||||
@@ -330,6 +330,9 @@ This skill supports sub-commands. Parse the first word of the argument string to
|
||||
> `{{command_prefix}}impeccable adapt [target]` - Adapt for different devices and screen sizes
|
||||
> `{{command_prefix}}impeccable optimize [target]` - Diagnose and fix UI performance
|
||||
>
|
||||
> **Iterate**
|
||||
> `{{command_prefix}}impeccable live` - Visual variant mode: pick elements in the browser, generate alternatives
|
||||
>
|
||||
> **Manage**
|
||||
> `{{command_prefix}}impeccable pin <command>` - Create a standalone shortcut (e.g., pin audit creates {{command_prefix}}audit)
|
||||
> `{{command_prefix}}impeccable unpin <command>` - Remove a pinned shortcut
|
||||
@@ -363,6 +366,7 @@ When a sub-command is matched, load the linked reference and follow its instruct
|
||||
| `clarify` | [clarify](reference/clarify.md) | Improve UX copy, labels, error messages, and microcopy |
|
||||
| `adapt` | [adapt](reference/adapt.md) | Adapt designs across screen sizes, devices, and platforms |
|
||||
| `optimize` | [optimize](reference/optimize.md) | Diagnose and fix UI performance issues |
|
||||
| `live` | [live](reference/live.md) | Interactive visual variant mode: pick elements, generate alternatives in the browser |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
|
||||
- The impeccable CLI installed (`npm i -g impeccable`)
|
||||
|
||||
## Start the Server
|
||||
|
||||
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
|
||||
2. Start the live variant server:
|
||||
```bash
|
||||
npx impeccable live &
|
||||
```
|
||||
3. Note the **port** and **token** printed to stdout.
|
||||
|
||||
## Inject the Browser Script
|
||||
|
||||
Find the project's main HTML entry point. This varies by framework:
|
||||
|
||||
| Framework | Typical file |
|
||||
|-----------|-------------|
|
||||
| Plain HTML | `index.html` |
|
||||
| Vite / React | `index.html` (project root) |
|
||||
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
|
||||
| Next.js (Pages) | `pages/_document.tsx` |
|
||||
| Nuxt | `app.vue` or `nuxt.config.ts` |
|
||||
| Svelte / SvelteKit | `src/app.html` |
|
||||
|
||||
Add the script tag between comment markers (replace PORT with the actual port):
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-live-start -->
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
<!-- impeccable-live-end -->
|
||||
```
|
||||
|
||||
**JSX / TSX (React, Next.js):**
|
||||
```jsx
|
||||
{/* impeccable-live-start */}
|
||||
<script src="http://localhost:PORT/live.js"></script>
|
||||
{/* impeccable-live-end */}
|
||||
```
|
||||
|
||||
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
|
||||
|
||||
If browser automation tools are available, also navigate to the page so the user can see it.
|
||||
|
||||
## Enter the Poll Loop
|
||||
|
||||
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Run: npx impeccable poll
|
||||
Read the JSON output. Dispatch based on the "type" field:
|
||||
|
||||
TYPE "generate":
|
||||
→ See "Handle Generate" below
|
||||
|
||||
TYPE "accept":
|
||||
→ See "Handle Accept" below
|
||||
|
||||
TYPE "discard":
|
||||
→ See "Handle Discard" below
|
||||
|
||||
TYPE "exit":
|
||||
→ Break the loop
|
||||
|
||||
TYPE "timeout":
|
||||
→ Continue (re-poll)
|
||||
|
||||
END LOOP
|
||||
```
|
||||
|
||||
## Handle Generate
|
||||
|
||||
The event contains: `{id, action, freeformPrompt, count, element}`.
|
||||
|
||||
### Step 1: Find the source file
|
||||
|
||||
Use `element.tagName`, `element.id`, `element.classes`, `element.textContent`, and `element.outerHTML` to locate the element in the project source. Search for matching markup across the codebase.
|
||||
|
||||
### Step 2: Create the variant wrapper
|
||||
|
||||
Wrap the original element in a variant container. Use the comment syntax appropriate for the framework:
|
||||
|
||||
**HTML / Vue / Svelte:**
|
||||
```html
|
||||
<!-- impeccable-variants-start SESSION_ID -->
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count="COUNT" style="display: contents">
|
||||
<div data-impeccable-variant="original" style="display: none">
|
||||
<!-- move the original element here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SESSION_ID -->
|
||||
```
|
||||
|
||||
**JSX / TSX:**
|
||||
```jsx
|
||||
{/* impeccable-variants-start SESSION_ID */}
|
||||
<div data-impeccable-variants="SESSION_ID" data-impeccable-variant-count={COUNT} style={{display: 'contents'}}>
|
||||
<div data-impeccable-variant="original" style={{display: 'none'}}>
|
||||
{/* move the original element here */}
|
||||
</div>
|
||||
</div>
|
||||
{/* impeccable-variants-end SESSION_ID */}
|
||||
```
|
||||
|
||||
Replace SESSION_ID with `event.id` and COUNT with `event.count`.
|
||||
|
||||
`display: contents` makes the wrapper layout-transparent, preserving the original element's relationship with its parent (flex/grid child, etc.).
|
||||
|
||||
### Step 3: Generate variants one by one
|
||||
|
||||
For each variant (1 through COUNT):
|
||||
|
||||
1. **Load the design command's reference file.** If `event.action` is "bolder", load `reference/bolder.md`. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference.
|
||||
|
||||
2. **Generate a complete replacement** for the original element. Each variant is a full HTML+CSS rewrite, not a patch. Consider the element's context (computed styles, parent structure, CSS custom properties from `event.element`).
|
||||
|
||||
3. **Diversify across variants.** Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate 4 variations on the same idea.
|
||||
|
||||
4. **If a freeform prompt was provided** (`event.freeformPrompt`), use it as additional guidance for all variants.
|
||||
|
||||
5. **Write the variant** into the wrapper in the source file:
|
||||
```html
|
||||
<div data-impeccable-variant="N" style="display: none">
|
||||
<!-- variant N content -->
|
||||
</div>
|
||||
```
|
||||
The first variant should NOT have `style="display: none"` (it should be visible by default).
|
||||
|
||||
6. **Write scoped CSS** if the variant needs styles beyond inline:
|
||||
```css
|
||||
/* impeccable-variants-css-start SESSION_ID */
|
||||
@scope ([data-impeccable-variant="N"]) {
|
||||
:scope { /* styles for the variant root */ }
|
||||
.child-class { /* styles for children */ }
|
||||
}
|
||||
/* impeccable-variants-css-end SESSION_ID */
|
||||
```
|
||||
Place the CSS in a `<style>` block in the same file, or in the component's CSS file.
|
||||
|
||||
7. **Save the file** after each variant. The dev server's HMR will update the browser, and the live script's MutationObserver will detect the new variant and activate it in the cycler UI.
|
||||
|
||||
### Step 4: Signal completion
|
||||
|
||||
After all variants are written:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Accept
|
||||
|
||||
The event contains: `{id, variantId}`.
|
||||
|
||||
The user accepted a specific variant. For v1 (inspection mode):
|
||||
1. Read the accepted variant's HTML from the source (the content inside `[data-impeccable-variant="VARIANT_ID"]`).
|
||||
2. Present the variant code to the user in the conversation.
|
||||
3. Clean up the source: remove the entire variant wrapper (everything between `impeccable-variants-start` and `impeccable-variants-end` markers), and restore the original element.
|
||||
4. Remove any scoped CSS blocks (between `impeccable-variants-css-start` and `impeccable-variants-css-end` markers).
|
||||
5. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Handle Discard
|
||||
|
||||
The event contains: `{id}`.
|
||||
|
||||
1. Remove the variant wrapper from the source file.
|
||||
2. Restore the original element (the content inside `[data-impeccable-variant="original"]`).
|
||||
3. Remove any scoped CSS blocks for this session.
|
||||
4. Reply:
|
||||
```bash
|
||||
npx impeccable poll --reply SESSION_ID done
|
||||
```
|
||||
|
||||
## Cleanup (on exit)
|
||||
|
||||
When the loop ends:
|
||||
|
||||
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
|
||||
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
|
||||
3. **Stop the server**:
|
||||
```bash
|
||||
npx impeccable live stop
|
||||
```
|
||||
|
||||
## Variant Generation Guidelines
|
||||
|
||||
- Each variant must be a **complete element replacement**, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
|
||||
- Use **`@scope`** for CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers.
|
||||
- Follow the design principles from this skill (typography, color, spatial design, etc.) and the `.impeccable.md` project context if available.
|
||||
- If no `.impeccable.md` exists, generate brand-agnostic variants. The live UI will show a warning to the user.
|
||||
- **Non-interactive mode**: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.
|
||||
@@ -11,6 +11,10 @@
|
||||
"description": "Pull reusable patterns, components, and design tokens into the design system. Identifies repeated patterns and consolidates them. Use when you have drift across the codebase and want to bring things back to a consistent system.",
|
||||
"argumentHint": "[target]"
|
||||
},
|
||||
"live": {
|
||||
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
|
||||
"argumentHint": ""
|
||||
},
|
||||
"adapt": {
|
||||
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
|
||||
"argumentHint": "[target] [context (mobile, tablet, print...)]"
|
||||
|
||||
@@ -29,7 +29,7 @@ const HARNESS_DIRS = [
|
||||
const VALID_COMMANDS = [
|
||||
'craft', 'teach', 'extract', 'shape',
|
||||
'critique', 'audit',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard',
|
||||
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
||||
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
||||
'clarify', 'adapt', 'optimize',
|
||||
];
|
||||
|
||||
+2
-111
@@ -3446,118 +3446,9 @@ async function main() {
|
||||
// Live detection server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function findOpenPort(start = 8400) {
|
||||
const net = await import('node:net');
|
||||
return new Promise((resolve) => {
|
||||
const server = net.default.createServer();
|
||||
server.listen(start, '127.0.0.1', () => {
|
||||
const port = server.address().port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.on('error', () => resolve(findOpenPort(start + 1)));
|
||||
});
|
||||
}
|
||||
|
||||
const LIVE_PID_FILE = path.join((await import('node:os')).default.tmpdir(), 'impeccable-live.json');
|
||||
|
||||
async function liveCli() {
|
||||
const args = process.argv.slice(2);
|
||||
const helpMode = args.includes('--help');
|
||||
const stopMode = args.includes('stop');
|
||||
const portArg = args.find(a => a.startsWith('--port='));
|
||||
const requestedPort = portArg ? parseInt(portArg.split('=')[1], 10) : null;
|
||||
|
||||
if (helpMode) {
|
||||
console.log(`Usage: impeccable live [options]
|
||||
|
||||
Start a local server that serves the browser detection overlay script.
|
||||
Inject the script into any page to scan for anti-patterns in real time.
|
||||
|
||||
Commands:
|
||||
live Start the server (default)
|
||||
live stop Stop a running live server
|
||||
|
||||
Options:
|
||||
--port=PORT Use a specific port (default: auto-detect unused port)
|
||||
--help Show this help message
|
||||
|
||||
The server provides:
|
||||
/detect.js The detection overlay script (inject via <script> tag)
|
||||
/health Health check endpoint
|
||||
/stop Stop the server remotely`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Stop a running server
|
||||
if (stopMode) {
|
||||
try {
|
||||
const info = JSON.parse(fs.readFileSync(LIVE_PID_FILE, 'utf-8'));
|
||||
const res = await fetch(`http://localhost:${info.port}/stop`);
|
||||
if (res.ok) {
|
||||
console.log(`Stopped live server on port ${info.port}.`);
|
||||
}
|
||||
} catch {
|
||||
console.log('No running live server found.');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const http = await import('node:http');
|
||||
const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'detect-antipatterns-browser.js');
|
||||
|
||||
let browserScript;
|
||||
try {
|
||||
browserScript = fs.readFileSync(scriptPath, 'utf-8');
|
||||
} catch {
|
||||
process.stderr.write('Error: Browser script not found. Run `npm run build:browser` first.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const port = requestedPort || await findOpenPort();
|
||||
|
||||
const shutdown = () => {
|
||||
try { fs.unlinkSync(LIVE_PID_FILE); } catch { /* ignore */ }
|
||||
server.close();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const server = http.default.createServer((req, res) => {
|
||||
// CORS headers for cross-origin injection
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
||||
|
||||
if (req.url === '/detect.js' || req.url === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(browserScript);
|
||||
} else if (req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', port }));
|
||||
} else if (req.url === '/stop') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('stopping');
|
||||
shutdown();
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
// Write PID file so `live stop` can find us
|
||||
fs.writeFileSync(LIVE_PID_FILE, JSON.stringify({ pid: process.pid, port }));
|
||||
|
||||
const url = `http://localhost:${port}`;
|
||||
console.log(`Impeccable live detection server running on ${url}\n`);
|
||||
console.log(`Inject into any page:`);
|
||||
console.log(` const s = document.createElement('script');`);
|
||||
console.log(` s.src = '${url}/detect.js';`);
|
||||
console.log(` document.head.appendChild(s);\n`);
|
||||
console.log(`Stop: npx impeccable live stop`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
const { startLiveServer } = await import('./live/server.mjs');
|
||||
await startLiveServer();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,931 @@
|
||||
/**
|
||||
* Impeccable Live Variant Mode — Browser Script
|
||||
*
|
||||
* Injected into the user's page via <script src="http://localhost:PORT/live.js">.
|
||||
* The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__
|
||||
* before this code.
|
||||
*
|
||||
* Features:
|
||||
* - Element picker with hover highlight and keyboard navigation
|
||||
* - Action panel (command dropdown, freeform input, variant count, go button)
|
||||
* - Variant cycler with MutationObserver progressive reveal
|
||||
* - WebSocket connection to the live server
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const TOKEN = window.__IMPECCABLE_TOKEN__;
|
||||
const PORT = window.__IMPECCABLE_PORT__;
|
||||
if (!TOKEN || !PORT) {
|
||||
console.warn('[impeccable] Live script loaded without token/port. Aborting.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent double-init
|
||||
if (window.__IMPECCABLE_LIVE_INIT__) return;
|
||||
window.__IMPECCABLE_LIVE_INIT__ = true;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BRAND = 'oklch(55% 0.25 350)';
|
||||
const BRAND_HOVER = 'oklch(45% 0.25 350)';
|
||||
const BRAND_LIGHT = 'oklch(92% 0.06 350)';
|
||||
const INK = 'oklch(15% 0.01 350)';
|
||||
const ASH = 'oklch(55% 0 0)';
|
||||
const PAPER = 'oklch(98% 0.005 350)';
|
||||
const MIST = 'oklch(90% 0.01 350)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
const Z_HIGHLIGHT = 99990;
|
||||
const Z_PANEL = 99995;
|
||||
const Z_CYCLER = 99995;
|
||||
const PREFIX = 'impeccable-live';
|
||||
|
||||
const SKIP_TAGS = new Set([
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta',
|
||||
'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
const VISUAL_ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Auto (freeform)' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let state = 'IDLE'; // IDLE | PICKING | CONFIGURING | GENERATING | CYCLING
|
||||
let ws = null;
|
||||
let hoveredElement = null;
|
||||
let selectedElement = null;
|
||||
let currentSessionId = null;
|
||||
let expectedVariants = 0;
|
||||
let arrivedVariants = 0;
|
||||
let visibleVariant = 0;
|
||||
let variantObserver = null;
|
||||
let hasProjectContext = false;
|
||||
|
||||
// UI elements
|
||||
let highlightOverlay = null;
|
||||
let infoTooltip = null;
|
||||
let panelEl = null;
|
||||
let cyclerEl = null;
|
||||
let toastEl = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isOwnElement(el) {
|
||||
if (!el || !el.id) return false;
|
||||
return el.id.startsWith(PREFIX) || el.closest('[id^="' + PREFIX + '"]');
|
||||
}
|
||||
|
||||
function isPickable(el) {
|
||||
if (!el || el.nodeType !== 1) return false;
|
||||
if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false;
|
||||
if (isOwnElement(el)) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width < 20 || rect.height < 20) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function elDescriptor(el) {
|
||||
if (!el) return '';
|
||||
let s = el.tagName.toLowerCase();
|
||||
if (el.id) s += '#' + el.id;
|
||||
if (el.classList.length) s += '.' + [...el.classList].slice(0, 3).join('.');
|
||||
return s;
|
||||
}
|
||||
|
||||
function genId() {
|
||||
return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
||||
}
|
||||
|
||||
function setState(newState) {
|
||||
state = newState;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Highlight overlay
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createHighlight() {
|
||||
const el = document.createElement('div');
|
||||
el.id = PREFIX + '-highlight';
|
||||
Object.assign(el.style, {
|
||||
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
|
||||
border: '2px solid ' + BRAND,
|
||||
borderRadius: '3px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: Z_HIGHLIGHT,
|
||||
transition: 'top 0.08s ease, left 0.08s ease, width 0.08s ease, height 0.08s ease',
|
||||
display: 'none',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function createInfoTooltip() {
|
||||
const el = document.createElement('div');
|
||||
el.id = PREFIX + '-info';
|
||||
Object.assign(el.style, {
|
||||
position: 'fixed', top: '0', left: '0',
|
||||
background: INK, color: PAPER,
|
||||
fontFamily: MONO, fontSize: '11px',
|
||||
padding: '2px 6px', borderRadius: '3px',
|
||||
zIndex: Z_HIGHLIGHT + 1,
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'none',
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function positionHighlight(el) {
|
||||
if (!el || !highlightOverlay) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
Object.assign(highlightOverlay.style, {
|
||||
top: (rect.top - 2) + 'px',
|
||||
left: (rect.left - 2) + 'px',
|
||||
width: (rect.width + 4) + 'px',
|
||||
height: (rect.height + 4) + 'px',
|
||||
display: 'block',
|
||||
});
|
||||
if (infoTooltip) {
|
||||
infoTooltip.textContent = elDescriptor(el);
|
||||
const tipTop = rect.top - 22;
|
||||
Object.assign(infoTooltip.style, {
|
||||
top: (tipTop < 4 ? rect.bottom + 4 : tipTop) + 'px',
|
||||
left: Math.max(4, rect.left) + 'px',
|
||||
display: 'block',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideHighlight() {
|
||||
if (highlightOverlay) highlightOverlay.style.display = 'none';
|
||||
if (infoTooltip) infoTooltip.style.display = 'none';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Element context extraction (sent to agent)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractElementContext(el) {
|
||||
const computed = getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// Collect CSS custom properties from stylesheets
|
||||
const customProps = {};
|
||||
for (const sheet of document.styleSheets) {
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
if (rule.style) {
|
||||
for (let i = 0; i < rule.style.length; i++) {
|
||||
const prop = rule.style[i];
|
||||
if (prop.startsWith('--') && !customProps[prop]) {
|
||||
const val = computed.getPropertyValue(prop).trim();
|
||||
if (val) customProps[prop] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* cross-origin sheet */ }
|
||||
}
|
||||
|
||||
return {
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
classes: [...el.classList],
|
||||
textContent: (el.textContent || '').slice(0, 500),
|
||||
outerHTML: el.outerHTML.slice(0, 10000),
|
||||
computedStyles: {
|
||||
'font-family': computed.fontFamily,
|
||||
'font-size': computed.fontSize,
|
||||
'font-weight': computed.fontWeight,
|
||||
'line-height': computed.lineHeight,
|
||||
'color': computed.color,
|
||||
'background': computed.background,
|
||||
'background-color': computed.backgroundColor,
|
||||
'padding': computed.padding,
|
||||
'margin': computed.margin,
|
||||
'display': computed.display,
|
||||
'position': computed.position,
|
||||
'gap': computed.gap,
|
||||
'border-radius': computed.borderRadius,
|
||||
'box-shadow': computed.boxShadow,
|
||||
},
|
||||
cssCustomProperties: customProps,
|
||||
parentContext: el.parentElement ?
|
||||
'<' + el.parentElement.tagName.toLowerCase() +
|
||||
(el.parentElement.id ? ' id="' + el.parentElement.id + '"' : '') +
|
||||
(el.parentElement.className ? ' class="' + el.parentElement.className + '"' : '') +
|
||||
'>' : null,
|
||||
boundingRect: {
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createActionPanel() {
|
||||
const panel = document.createElement('div');
|
||||
panel.id = PREFIX + '-panel';
|
||||
Object.assign(panel.style, {
|
||||
position: 'fixed', zIndex: Z_PANEL,
|
||||
background: PAPER,
|
||||
border: '1px solid ' + MIST,
|
||||
borderRadius: '10px',
|
||||
padding: '14px 16px',
|
||||
boxShadow: '0 4px 24px oklch(0% 0 0 / 0.12)',
|
||||
fontFamily: FONT,
|
||||
fontSize: '13px',
|
||||
color: INK,
|
||||
minWidth: '260px',
|
||||
maxWidth: '320px',
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
Object.assign(header.style, {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
marginBottom: '10px', fontSize: '12px', color: ASH,
|
||||
});
|
||||
const brand = document.createElement('span');
|
||||
brand.textContent = 'impeccable';
|
||||
Object.assign(brand.style, { fontWeight: '600', color: BRAND, fontFamily: MONO, fontSize: '11px' });
|
||||
header.appendChild(brand);
|
||||
const elLabel = document.createElement('span');
|
||||
elLabel.id = PREFIX + '-panel-label';
|
||||
Object.assign(elLabel.style, { fontFamily: MONO, fontSize: '11px' });
|
||||
header.appendChild(elLabel);
|
||||
panel.appendChild(header);
|
||||
|
||||
// Action select
|
||||
const actionRow = document.createElement('div');
|
||||
Object.assign(actionRow.style, { marginBottom: '8px' });
|
||||
const actionLabel = document.createElement('label');
|
||||
actionLabel.textContent = 'Action';
|
||||
Object.assign(actionLabel.style, {
|
||||
display: 'block', fontSize: '11px', fontWeight: '500',
|
||||
color: ASH, marginBottom: '3px', textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
});
|
||||
actionRow.appendChild(actionLabel);
|
||||
const actionSelect = document.createElement('select');
|
||||
actionSelect.id = PREFIX + '-action';
|
||||
Object.assign(actionSelect.style, {
|
||||
width: '100%', padding: '6px 8px', borderRadius: '5px',
|
||||
border: '1px solid ' + MIST, fontFamily: FONT, fontSize: '13px',
|
||||
background: '#fff', color: INK, cursor: 'pointer',
|
||||
});
|
||||
VISUAL_ACTIONS.forEach(a => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = a.value;
|
||||
opt.textContent = a.label;
|
||||
actionSelect.appendChild(opt);
|
||||
});
|
||||
actionRow.appendChild(actionSelect);
|
||||
panel.appendChild(actionRow);
|
||||
|
||||
// Freeform input
|
||||
const freeformRow = document.createElement('div');
|
||||
Object.assign(freeformRow.style, { marginBottom: '8px' });
|
||||
const freeformLabel = document.createElement('label');
|
||||
freeformLabel.textContent = 'Instructions (optional)';
|
||||
Object.assign(freeformLabel.style, {
|
||||
display: 'block', fontSize: '11px', fontWeight: '500',
|
||||
color: ASH, marginBottom: '3px', textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
});
|
||||
freeformRow.appendChild(freeformLabel);
|
||||
const freeformInput = document.createElement('input');
|
||||
freeformInput.id = PREFIX + '-freeform';
|
||||
freeformInput.type = 'text';
|
||||
freeformInput.placeholder = 'e.g. make it feel more premium';
|
||||
Object.assign(freeformInput.style, {
|
||||
width: '100%', padding: '6px 8px', borderRadius: '5px',
|
||||
border: '1px solid ' + MIST, fontFamily: FONT, fontSize: '13px',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
freeformRow.appendChild(freeformInput);
|
||||
panel.appendChild(freeformRow);
|
||||
|
||||
// Variant count
|
||||
const countRow = document.createElement('div');
|
||||
Object.assign(countRow.style, {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
marginBottom: '12px',
|
||||
});
|
||||
const countLabel = document.createElement('span');
|
||||
countLabel.textContent = 'Variants';
|
||||
Object.assign(countLabel.style, {
|
||||
fontSize: '11px', fontWeight: '500', color: ASH,
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||||
});
|
||||
countRow.appendChild(countLabel);
|
||||
[2, 3, 4].forEach(n => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = n;
|
||||
btn.dataset.count = n;
|
||||
btn.className = PREFIX + '-count-btn';
|
||||
Object.assign(btn.style, {
|
||||
width: '32px', height: '28px', borderRadius: '5px',
|
||||
border: '1px solid ' + MIST, background: n === 3 ? BRAND : '#fff',
|
||||
color: n === 3 ? '#fff' : INK, fontFamily: FONT, fontSize: '13px',
|
||||
fontWeight: '500', cursor: 'pointer', transition: 'all 0.1s ease',
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.' + PREFIX + '-count-btn').forEach(b => {
|
||||
Object.assign(b.style, { background: '#fff', color: INK });
|
||||
});
|
||||
Object.assign(btn.style, { background: BRAND, color: '#fff' });
|
||||
});
|
||||
countRow.appendChild(btn);
|
||||
});
|
||||
panel.appendChild(countRow);
|
||||
|
||||
// Go + Cancel row
|
||||
const btnRow = document.createElement('div');
|
||||
Object.assign(btnRow.style, { display: 'flex', alignItems: 'center', gap: '8px' });
|
||||
const goBtn = document.createElement('button');
|
||||
goBtn.id = PREFIX + '-go';
|
||||
goBtn.textContent = 'Generate';
|
||||
Object.assign(goBtn.style, {
|
||||
flex: '1', padding: '8px 16px', borderRadius: '5px',
|
||||
border: 'none', background: INK, color: '#fff',
|
||||
fontFamily: FONT, fontSize: '13px', fontWeight: '600',
|
||||
cursor: 'pointer', transition: 'background 0.15s ease',
|
||||
});
|
||||
goBtn.addEventListener('mouseenter', () => { goBtn.style.background = BRAND; });
|
||||
goBtn.addEventListener('mouseleave', () => { goBtn.style.background = INK; });
|
||||
goBtn.addEventListener('click', handleGo);
|
||||
btnRow.appendChild(goBtn);
|
||||
const cancelLink = document.createElement('button');
|
||||
cancelLink.textContent = 'Cancel';
|
||||
Object.assign(cancelLink.style, {
|
||||
background: 'none', border: 'none', color: ASH,
|
||||
fontFamily: FONT, fontSize: '12px', cursor: 'pointer',
|
||||
padding: '4px',
|
||||
});
|
||||
cancelLink.addEventListener('click', () => {
|
||||
hidePanel();
|
||||
setState('PICKING');
|
||||
});
|
||||
btnRow.appendChild(cancelLink);
|
||||
panel.appendChild(btnRow);
|
||||
|
||||
document.body.appendChild(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
function showPanel(el) {
|
||||
if (!panelEl) panelEl = createActionPanel();
|
||||
const label = panelEl.querySelector('#' + PREFIX + '-panel-label');
|
||||
if (label) label.textContent = elDescriptor(el);
|
||||
|
||||
// Position near element
|
||||
const rect = el.getBoundingClientRect();
|
||||
const panelWidth = 300;
|
||||
const panelHeight = 280;
|
||||
let top = rect.bottom + 8;
|
||||
let left = rect.right - panelWidth;
|
||||
if (left < 8) left = 8;
|
||||
if (top + panelHeight > window.innerHeight) top = rect.top - panelHeight - 8;
|
||||
if (top < 8) top = 8;
|
||||
if (left + panelWidth > window.innerWidth) left = window.innerWidth - panelWidth - 8;
|
||||
|
||||
Object.assign(panelEl.style, {
|
||||
top: top + 'px', left: left + 'px', display: 'block',
|
||||
});
|
||||
|
||||
// Focus the freeform input
|
||||
const input = panelEl.querySelector('#' + PREFIX + '-freeform');
|
||||
if (input) setTimeout(() => input.focus(), 50);
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
if (panelEl) panelEl.style.display = 'none';
|
||||
}
|
||||
|
||||
function getSelectedAction() {
|
||||
const select = document.querySelector('#' + PREFIX + '-action');
|
||||
return select ? select.value : 'impeccable';
|
||||
}
|
||||
|
||||
function getSelectedCount() {
|
||||
const active = document.querySelector('.' + PREFIX + '-count-btn[style*="' + BRAND + '"]');
|
||||
return active ? parseInt(active.dataset.count) : 3;
|
||||
}
|
||||
|
||||
function getFreeformPrompt() {
|
||||
const input = document.querySelector('#' + PREFIX + '-freeform');
|
||||
return input ? input.value.trim() : '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variant cycler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createCycler() {
|
||||
const el = document.createElement('div');
|
||||
el.id = PREFIX + '-cycler';
|
||||
Object.assign(el.style, {
|
||||
position: 'fixed', zIndex: Z_CYCLER,
|
||||
background: PAPER,
|
||||
border: '1px solid ' + MIST,
|
||||
borderRadius: '10px',
|
||||
padding: '10px 14px',
|
||||
boxShadow: '0 4px 24px oklch(0% 0 0 / 0.12)',
|
||||
fontFamily: FONT, fontSize: '13px', color: INK,
|
||||
display: 'none',
|
||||
minWidth: '280px',
|
||||
});
|
||||
|
||||
// Status line
|
||||
const statusRow = document.createElement('div');
|
||||
statusRow.id = PREFIX + '-cycler-status';
|
||||
Object.assign(statusRow.style, {
|
||||
textAlign: 'center', marginBottom: '8px',
|
||||
fontSize: '12px', color: ASH,
|
||||
});
|
||||
el.appendChild(statusRow);
|
||||
|
||||
// Slot indicators
|
||||
const slotsRow = document.createElement('div');
|
||||
slotsRow.id = PREFIX + '-cycler-slots';
|
||||
Object.assign(slotsRow.style, {
|
||||
display: 'flex', justifyContent: 'center', gap: '4px',
|
||||
marginBottom: '10px',
|
||||
});
|
||||
el.appendChild(slotsRow);
|
||||
|
||||
// Navigation
|
||||
const navRow = document.createElement('div');
|
||||
Object.assign(navRow.style, {
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
|
||||
});
|
||||
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.id = PREFIX + '-cycler-prev';
|
||||
prevBtn.textContent = '\u2190';
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.id = PREFIX + '-cycler-next';
|
||||
nextBtn.textContent = '\u2192';
|
||||
[prevBtn, nextBtn].forEach(b => {
|
||||
Object.assign(b.style, {
|
||||
width: '32px', height: '28px', borderRadius: '5px',
|
||||
border: '1px solid ' + MIST, background: '#fff', color: INK,
|
||||
fontFamily: FONT, fontSize: '14px', cursor: 'pointer',
|
||||
});
|
||||
});
|
||||
|
||||
const acceptBtn = document.createElement('button');
|
||||
acceptBtn.id = PREFIX + '-cycler-accept';
|
||||
acceptBtn.textContent = 'Accept';
|
||||
Object.assign(acceptBtn.style, {
|
||||
padding: '6px 16px', borderRadius: '5px',
|
||||
border: 'none', background: INK, color: '#fff',
|
||||
fontFamily: FONT, fontSize: '12px', fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const discardBtn = document.createElement('button');
|
||||
discardBtn.id = PREFIX + '-cycler-discard';
|
||||
discardBtn.textContent = 'Discard';
|
||||
Object.assign(discardBtn.style, {
|
||||
padding: '6px 12px', borderRadius: '5px',
|
||||
border: '1px solid ' + MIST, background: '#fff', color: ASH,
|
||||
fontFamily: FONT, fontSize: '12px', cursor: 'pointer',
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', () => cycleVariant(-1));
|
||||
nextBtn.addEventListener('click', () => cycleVariant(1));
|
||||
acceptBtn.addEventListener('click', handleAccept);
|
||||
discardBtn.addEventListener('click', handleDiscard);
|
||||
|
||||
navRow.appendChild(prevBtn);
|
||||
navRow.appendChild(acceptBtn);
|
||||
navRow.appendChild(discardBtn);
|
||||
navRow.appendChild(nextBtn);
|
||||
el.appendChild(navRow);
|
||||
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function showCycler() {
|
||||
if (!cyclerEl) cyclerEl = createCycler();
|
||||
|
||||
// Position near the selected element (or its variant wrapper)
|
||||
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
|
||||
const target = wrapper || selectedElement;
|
||||
if (target) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
let top = rect.bottom + 8;
|
||||
if (top + 80 > window.innerHeight) top = rect.top - 88;
|
||||
if (top < 8) top = 8;
|
||||
const left = Math.max(8, Math.min(rect.left, window.innerWidth - 300));
|
||||
Object.assign(cyclerEl.style, { top: top + 'px', left: left + 'px' });
|
||||
}
|
||||
|
||||
cyclerEl.style.display = 'block';
|
||||
updateCyclerUI();
|
||||
}
|
||||
|
||||
function hideCycler() {
|
||||
if (cyclerEl) cyclerEl.style.display = 'none';
|
||||
if (variantObserver) {
|
||||
variantObserver.disconnect();
|
||||
variantObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCyclerUI() {
|
||||
if (!cyclerEl) return;
|
||||
|
||||
const statusEl = cyclerEl.querySelector('#' + PREFIX + '-cycler-status');
|
||||
const slotsEl = cyclerEl.querySelector('#' + PREFIX + '-cycler-slots');
|
||||
|
||||
// Update slots
|
||||
slotsEl.innerHTML = '';
|
||||
for (let i = 1; i <= expectedVariants; i++) {
|
||||
const dot = document.createElement('div');
|
||||
const arrived = i <= arrivedVariants;
|
||||
const active = i === visibleVariant;
|
||||
Object.assign(dot.style, {
|
||||
width: '8px', height: '8px', borderRadius: '50%',
|
||||
background: active ? BRAND : (arrived ? MIST : 'transparent'),
|
||||
border: '1.5px solid ' + (arrived ? BRAND : MIST),
|
||||
cursor: arrived ? 'pointer' : 'default',
|
||||
transition: 'all 0.15s ease',
|
||||
});
|
||||
if (arrived) {
|
||||
dot.addEventListener('click', () => {
|
||||
visibleVariant = i;
|
||||
showVariantInDOM(currentSessionId, i);
|
||||
updateCyclerUI();
|
||||
});
|
||||
}
|
||||
slotsEl.appendChild(dot);
|
||||
}
|
||||
|
||||
// Update status text
|
||||
if (arrivedVariants < expectedVariants) {
|
||||
statusEl.textContent = 'Generating variant ' + (arrivedVariants + 1) + ' of ' + expectedVariants + '...';
|
||||
} else {
|
||||
statusEl.textContent = 'Variant ' + visibleVariant + ' of ' + expectedVariants;
|
||||
}
|
||||
|
||||
// Enable/disable nav buttons
|
||||
const prev = cyclerEl.querySelector('#' + PREFIX + '-cycler-prev');
|
||||
const next = cyclerEl.querySelector('#' + PREFIX + '-cycler-next');
|
||||
const accept = cyclerEl.querySelector('#' + PREFIX + '-cycler-accept');
|
||||
if (prev) prev.disabled = visibleVariant <= 1;
|
||||
if (next) next.disabled = visibleVariant >= arrivedVariants;
|
||||
if (accept) accept.disabled = arrivedVariants === 0;
|
||||
}
|
||||
|
||||
function cycleVariant(dir) {
|
||||
const newV = visibleVariant + dir;
|
||||
if (newV < 1 || newV > arrivedVariants) return;
|
||||
visibleVariant = newV;
|
||||
showVariantInDOM(currentSessionId, newV);
|
||||
updateCyclerUI();
|
||||
}
|
||||
|
||||
function showVariantInDOM(sessionId, num) {
|
||||
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
||||
if (!wrapper) return;
|
||||
for (const child of wrapper.children) {
|
||||
const v = child.dataset ? child.dataset.impeccableVariant : null;
|
||||
if (!v) continue;
|
||||
child.style.display = (v === String(num)) ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MutationObserver for progressive variant reveal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startVariantObserver(sessionId) {
|
||||
// Watch the entire body for [data-impeccable-variants] appearing (after HMR)
|
||||
const bodyObserver = new MutationObserver(() => {
|
||||
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
||||
if (!wrapper) return;
|
||||
|
||||
// Count arrived variants
|
||||
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
||||
const newCount = variants.length;
|
||||
|
||||
if (newCount > arrivedVariants) {
|
||||
arrivedVariants = newCount;
|
||||
// Auto-show the first variant
|
||||
if (visibleVariant === 0 && arrivedVariants > 0) {
|
||||
visibleVariant = 1;
|
||||
showVariantInDOM(sessionId, 1);
|
||||
}
|
||||
updateCyclerUI();
|
||||
}
|
||||
|
||||
// Read expected count from the wrapper attribute
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0 && expected !== expectedVariants) {
|
||||
expectedVariants = expected;
|
||||
updateCyclerUI();
|
||||
}
|
||||
|
||||
// If all variants have arrived, transition to CYCLING
|
||||
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
|
||||
setState('CYCLING');
|
||||
}
|
||||
});
|
||||
|
||||
bodyObserver.observe(document.body, { childList: true, subtree: true });
|
||||
return bodyObserver;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocket
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function connectWS() {
|
||||
ws = new WebSocket('ws://localhost:' + PORT + '/ws');
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: 'auth', token: TOKEN }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(event.data); } catch { return; }
|
||||
|
||||
switch (msg.type) {
|
||||
case 'auth_ok':
|
||||
hasProjectContext = !!msg.hasProjectContext;
|
||||
if (!hasProjectContext) showContextWarning();
|
||||
console.log('[impeccable] Live mode connected.');
|
||||
setState('PICKING');
|
||||
break;
|
||||
case 'auth_fail':
|
||||
console.error('[impeccable] Auth failed:', msg.reason);
|
||||
break;
|
||||
case 'generating':
|
||||
// Agent acknowledged the generate request
|
||||
break;
|
||||
case 'done':
|
||||
// Agent finished writing all variants
|
||||
setState('CYCLING');
|
||||
updateCyclerUI();
|
||||
break;
|
||||
case 'error':
|
||||
console.error('[impeccable] Error:', msg.message);
|
||||
showError(msg.message);
|
||||
hideCycler();
|
||||
setState('PICKING');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[impeccable] WebSocket closed. Reconnecting in 3s...');
|
||||
setTimeout(connectWS, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
// onclose will fire after this
|
||||
};
|
||||
}
|
||||
|
||||
function sendWS(msg) {
|
||||
if (ws && ws.readyState === 1) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function handleMouseMove(e) {
|
||||
if (state !== 'PICKING') return;
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || !isPickable(el) || el === hoveredElement) return;
|
||||
hoveredElement = el;
|
||||
positionHighlight(el);
|
||||
}
|
||||
|
||||
function handleClick(e) {
|
||||
if (state !== 'PICKING') return;
|
||||
if (isOwnElement(e.target)) return;
|
||||
const el = hoveredElement;
|
||||
if (!el || !isPickable(el)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedElement = el;
|
||||
setState('CONFIGURING');
|
||||
positionHighlight(el);
|
||||
showPanel(el);
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
// ESC: cancel current state
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (state === 'CONFIGURING') {
|
||||
hidePanel();
|
||||
setState('PICKING');
|
||||
} else if (state === 'CYCLING') {
|
||||
handleDiscard();
|
||||
} else if (state === 'PICKING') {
|
||||
hideHighlight();
|
||||
hidePanel();
|
||||
hideCycler();
|
||||
setState('IDLE');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys: navigate elements
|
||||
if (state === 'PICKING' && hoveredElement) {
|
||||
let next = null;
|
||||
if (e.key === 'ArrowDown' && !e.shiftKey) {
|
||||
next = hoveredElement.nextElementSibling;
|
||||
while (next && !isPickable(next)) next = next.nextElementSibling;
|
||||
} else if (e.key === 'ArrowUp' && !e.shiftKey) {
|
||||
next = hoveredElement.previousElementSibling;
|
||||
while (next && !isPickable(next)) next = next.previousElementSibling;
|
||||
} else if (e.key === 'ArrowUp' && e.shiftKey) {
|
||||
next = hoveredElement.parentElement;
|
||||
if (next && !isPickable(next)) next = null;
|
||||
} else if (e.key === 'ArrowDown' && e.shiftKey) {
|
||||
next = hoveredElement.firstElementChild;
|
||||
while (next && !isPickable(next)) next = next.nextElementSibling;
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
selectedElement = hoveredElement;
|
||||
setState('CONFIGURING');
|
||||
positionHighlight(hoveredElement);
|
||||
showPanel(hoveredElement);
|
||||
return;
|
||||
}
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
hoveredElement = next;
|
||||
positionHighlight(next);
|
||||
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Variant cycling keys
|
||||
if (state === 'CYCLING') {
|
||||
if (e.key === 'ArrowLeft') { e.preventDefault(); cycleVariant(-1); }
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); cycleVariant(1); }
|
||||
if (e.key === 'Enter') { e.preventDefault(); handleAccept(); }
|
||||
}
|
||||
}
|
||||
|
||||
function handleGo() {
|
||||
if (!selectedElement || state !== 'CONFIGURING') return;
|
||||
|
||||
const action = getSelectedAction();
|
||||
const count = getSelectedCount();
|
||||
const freeformPrompt = getFreeformPrompt();
|
||||
|
||||
currentSessionId = genId();
|
||||
expectedVariants = count;
|
||||
arrivedVariants = 0;
|
||||
visibleVariant = 0;
|
||||
|
||||
// Send generate event to server
|
||||
sendWS({
|
||||
type: 'generate',
|
||||
id: currentSessionId,
|
||||
action: action,
|
||||
freeformPrompt: freeformPrompt || undefined,
|
||||
count: count,
|
||||
element: extractElementContext(selectedElement),
|
||||
});
|
||||
|
||||
hidePanel();
|
||||
setState('GENERATING');
|
||||
|
||||
// Start observing for variants in the DOM
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
|
||||
// Show cycler in "generating" state
|
||||
showCycler();
|
||||
}
|
||||
|
||||
function handleAccept() {
|
||||
if (!currentSessionId || arrivedVariants === 0) return;
|
||||
sendWS({
|
||||
type: 'accept',
|
||||
id: currentSessionId,
|
||||
variantId: String(visibleVariant),
|
||||
});
|
||||
hideCycler();
|
||||
hideHighlight();
|
||||
selectedElement = null;
|
||||
currentSessionId = null;
|
||||
setState('PICKING');
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
if (!currentSessionId) return;
|
||||
sendWS({
|
||||
type: 'discard',
|
||||
id: currentSessionId,
|
||||
});
|
||||
hideCycler();
|
||||
hideHighlight();
|
||||
selectedElement = null;
|
||||
currentSessionId = null;
|
||||
setState('PICKING');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Toast / warnings / errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function showContextWarning() {
|
||||
showToast('Running without project context. Run /impeccable teach for better variants.', 8000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showToast('Error: ' + message, 6000);
|
||||
}
|
||||
|
||||
function showToast(message, duration) {
|
||||
if (toastEl) toastEl.remove();
|
||||
toastEl = document.createElement('div');
|
||||
toastEl.id = PREFIX + '-toast';
|
||||
toastEl.textContent = message;
|
||||
Object.assign(toastEl.style, {
|
||||
position: 'fixed', bottom: '16px', left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: INK, color: PAPER,
|
||||
fontFamily: FONT, fontSize: '12px',
|
||||
padding: '8px 16px', borderRadius: '6px',
|
||||
zIndex: Z_PANEL + 10,
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.3s ease',
|
||||
pointerEvents: 'none',
|
||||
maxWidth: '500px', textAlign: 'center',
|
||||
});
|
||||
document.body.appendChild(toastEl);
|
||||
requestAnimationFrame(() => { toastEl.style.opacity = '1'; });
|
||||
setTimeout(() => {
|
||||
if (toastEl) {
|
||||
toastEl.style.opacity = '0';
|
||||
setTimeout(() => { if (toastEl) toastEl.remove(); toastEl = null; }, 300);
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Init
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function init() {
|
||||
highlightOverlay = createHighlight();
|
||||
infoTooltip = createInfoTooltip();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove, true);
|
||||
document.addEventListener('click', handleClick, true);
|
||||
document.addEventListener('keydown', handleKeyDown, true);
|
||||
|
||||
connectWS();
|
||||
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* CLI client for the live variant mode poll/reply protocol.
|
||||
*
|
||||
* Usage:
|
||||
* npx impeccable poll # Block until browser event, print JSON
|
||||
* npx impeccable poll --timeout=60000 # Custom timeout (ms)
|
||||
* npx impeccable poll --reply <id> done # Reply "done" to event <id>
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
const LIVE_PID_FILE = path.join(os.tmpdir(), 'impeccable-live.json');
|
||||
|
||||
function readServerInfo() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(LIVE_PID_FILE, 'utf-8'));
|
||||
} catch {
|
||||
console.error('No running live server found. Start one with: npx impeccable live');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pollCli() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`Usage: impeccable poll [options]
|
||||
|
||||
Wait for a browser event from the live variant server, or reply to one.
|
||||
|
||||
Modes:
|
||||
poll Block until a browser event arrives, print JSON
|
||||
poll --reply <id> done Reply "done" to event <id>
|
||||
poll --reply <id> error "msg" Reply with an error message
|
||||
|
||||
Options:
|
||||
--timeout=MS Poll timeout in milliseconds (default: 120000)
|
||||
--help Show this help message`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const info = readServerInfo();
|
||||
const base = `http://localhost:${info.port}`;
|
||||
|
||||
// Reply mode: npx impeccable poll --reply <id> <status> [message]
|
||||
const replyIdx = args.indexOf('--reply');
|
||||
if (replyIdx !== -1) {
|
||||
const id = args[replyIdx + 1];
|
||||
const status = args[replyIdx + 2] || 'done';
|
||||
const message = args[replyIdx + 3] || undefined;
|
||||
|
||||
if (!id) {
|
||||
console.error('Usage: npx impeccable poll --reply <id> <status> [message]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${base}/poll`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: info.token,
|
||||
id,
|
||||
type: status,
|
||||
message,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
console.error(`Reply failed (${res.status}):`, body.error || res.statusText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Success — silent exit (agent doesn't need output for replies)
|
||||
} catch (err) {
|
||||
if (err.cause?.code === 'ECONNREFUSED') {
|
||||
console.error('Live server not running. Start one with: npx impeccable live');
|
||||
} else {
|
||||
console.error('Reply failed:', err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll mode: block until browser event
|
||||
const timeoutArg = args.find(a => a.startsWith('--timeout='));
|
||||
const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 120000;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`);
|
||||
|
||||
if (res.status === 401) {
|
||||
console.error('Authentication failed. The server token may have changed.');
|
||||
console.error('Try restarting: npx impeccable live stop && npx impeccable live');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`Poll failed: ${res.status} ${res.statusText}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const event = await res.json();
|
||||
// Print the event as JSON — the agent reads this from stdout
|
||||
console.log(JSON.stringify(event));
|
||||
} catch (err) {
|
||||
if (err.cause?.code === 'ECONNREFUSED') {
|
||||
console.error('Live server not running. Start one with: npx impeccable live');
|
||||
} else {
|
||||
console.error('Poll failed:', err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Shared protocol constants and validation for the live variant mode.
|
||||
* Imported by both server.mjs and poll.mjs.
|
||||
*/
|
||||
|
||||
// Browser → Server event types
|
||||
export const EVENT = Object.freeze({
|
||||
GENERATE: 'generate',
|
||||
ACCEPT: 'accept',
|
||||
DISCARD: 'discard',
|
||||
EXIT: 'exit',
|
||||
});
|
||||
|
||||
// Server → Browser message types
|
||||
export const MSG = Object.freeze({
|
||||
AUTH_OK: 'auth_ok',
|
||||
AUTH_FAIL: 'auth_fail',
|
||||
GENERATING: 'generating',
|
||||
DONE: 'done',
|
||||
ERROR: 'error',
|
||||
});
|
||||
|
||||
// Poll return types (superset of EVENT — adds timeout)
|
||||
export const POLL = Object.freeze({
|
||||
...EVENT,
|
||||
TIMEOUT: 'timeout',
|
||||
});
|
||||
|
||||
// Commands that make sense for visual variant generation.
|
||||
// Shown in the browser action panel dropdown.
|
||||
export const VISUAL_ACTIONS = Object.freeze([
|
||||
'impeccable', // default: freeform design pass
|
||||
'bolder',
|
||||
'quieter',
|
||||
'distill',
|
||||
'polish',
|
||||
'typeset',
|
||||
'colorize',
|
||||
'layout',
|
||||
'adapt',
|
||||
'animate',
|
||||
'delight',
|
||||
'overdrive',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Validate a browser event before queuing it for the agent.
|
||||
* Returns null if valid, or an error string if not.
|
||||
*/
|
||||
export function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) {
|
||||
return 'Missing or invalid message';
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case EVENT.GENERATE:
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return `generate: invalid action "${msg.action}"`;
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || typeof msg.element !== 'object') return 'generate: missing element context';
|
||||
if (!msg.element.outerHTML) return 'generate: element must include outerHTML';
|
||||
return null;
|
||||
|
||||
case EVENT.ACCEPT:
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
return null;
|
||||
|
||||
case EVENT.DISCARD:
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'discard: missing id';
|
||||
return null;
|
||||
|
||||
case EVENT.EXIT:
|
||||
return null;
|
||||
|
||||
default:
|
||||
return `Unknown event type: "${msg.type}"`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* Live variant mode server.
|
||||
*
|
||||
* Serves the browser script (/live.js), the detection overlay (/detect.js),
|
||||
* manages a WebSocket connection to the browser, and exposes HTTP long-poll
|
||||
* endpoints so the agent CLI can receive events and send replies.
|
||||
*
|
||||
* Start: npx impeccable live
|
||||
* Stop: npx impeccable live stop
|
||||
* Health: curl http://localhost:PORT/health
|
||||
*/
|
||||
|
||||
import http from 'node:http';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { validateEvent } from './protocol.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const LIVE_PID_FILE = path.join(os.tmpdir(), 'impeccable-live.json');
|
||||
const DEFAULT_POLL_TIMEOUT = 120_000; // 2 minutes
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function findOpenPort(start = 8400) {
|
||||
return new Promise((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(start, '127.0.0.1', () => {
|
||||
const port = srv.address().port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
srv.on('error', () => resolve(findOpenPort(start + 1)));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const state = {
|
||||
token: null,
|
||||
port: null,
|
||||
wsClients: new Set(),
|
||||
// Queue: browser events waiting for the agent to poll
|
||||
pendingEvents: [],
|
||||
// Queue: agent poll response callbacks waiting for browser events
|
||||
pendingPolls: [],
|
||||
};
|
||||
|
||||
/** Push an event from the browser into the queue or resolve a waiting poll. */
|
||||
function enqueueEvent(event) {
|
||||
if (state.pendingPolls.length > 0) {
|
||||
const resolve = state.pendingPolls.shift();
|
||||
resolve(event);
|
||||
} else {
|
||||
state.pendingEvents.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Broadcast a message to all authenticated WS clients. */
|
||||
function broadcast(msg) {
|
||||
const data = JSON.stringify(msg);
|
||||
for (const ws of state.wsClients) {
|
||||
if (ws.readyState === 1 /* OPEN */) {
|
||||
ws.send(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load scripts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadBrowserScripts() {
|
||||
const detectPath = path.join(__dirname, '..', 'detect-antipatterns-browser.js');
|
||||
const livePath = path.join(__dirname, 'browser.js');
|
||||
|
||||
let detectScript = '';
|
||||
try {
|
||||
detectScript = fs.readFileSync(detectPath, 'utf-8');
|
||||
} catch {
|
||||
// Detection script is optional for the live variant server
|
||||
}
|
||||
|
||||
let liveScript = '';
|
||||
try {
|
||||
liveScript = fs.readFileSync(livePath, 'utf-8');
|
||||
} catch {
|
||||
process.stderr.write('Error: Browser live script not found at ' + livePath + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { detectScript, liveScript };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check for .impeccable.md
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function hasProjectContext() {
|
||||
try {
|
||||
fs.accessSync(path.join(process.cwd(), '.impeccable.md'), fs.constants.R_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP request handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createRequestHandler({ detectScript, liveScriptWithToken }) {
|
||||
return (req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${state.port}`);
|
||||
|
||||
// CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
||||
|
||||
const pathname = url.pathname;
|
||||
|
||||
// --- Public endpoints (no auth) ---
|
||||
|
||||
if (pathname === '/live.js') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(liveScriptWithToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/detect.js' || pathname === '/') {
|
||||
if (!detectScript) {
|
||||
res.writeHead(404);
|
||||
res.end('Detection script not available. Run npm run build:browser first.');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(detectScript);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
port: state.port,
|
||||
mode: 'variant',
|
||||
hasProjectContext: hasProjectContext(),
|
||||
connectedClients: state.wsClients.size,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Authenticated endpoints ---
|
||||
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
if (pathname === '/stop') {
|
||||
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('stopping');
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/poll') {
|
||||
if (req.method === 'GET') {
|
||||
handlePollGet(req, res, url);
|
||||
} else if (req.method === 'POST') {
|
||||
handlePollPost(req, res);
|
||||
} else {
|
||||
res.writeHead(405);
|
||||
res.end('Method not allowed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Poll endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** GET /poll — agent blocks here until a browser event arrives. */
|
||||
function handlePollGet(req, res, url) {
|
||||
const token = url.searchParams.get('token');
|
||||
if (token !== state.token) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = parseInt(url.searchParams.get('timeout') || DEFAULT_POLL_TIMEOUT, 10);
|
||||
|
||||
// If there's already an event queued, return it immediately
|
||||
if (state.pendingEvents.length > 0) {
|
||||
const event = state.pendingEvents.shift();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(event));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, wait for one
|
||||
const timer = setTimeout(() => {
|
||||
// Remove this callback from pendingPolls
|
||||
const idx = state.pendingPolls.indexOf(resolve);
|
||||
if (idx !== -1) state.pendingPolls.splice(idx, 1);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ type: 'timeout' }));
|
||||
}, timeout);
|
||||
|
||||
function resolve(event) {
|
||||
clearTimeout(timer);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(event));
|
||||
}
|
||||
|
||||
state.pendingPolls.push(resolve);
|
||||
|
||||
// Clean up if the agent disconnects before we respond
|
||||
req.on('close', () => {
|
||||
clearTimeout(timer);
|
||||
const idx = state.pendingPolls.indexOf(resolve);
|
||||
if (idx !== -1) state.pendingPolls.splice(idx, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/** POST /poll — agent replies to a pending browser event. */
|
||||
function handlePollPost(req, res) {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(body);
|
||||
} catch {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.token !== state.token) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward the reply to the browser
|
||||
broadcast({
|
||||
type: msg.type || 'done',
|
||||
id: msg.id,
|
||||
message: msg.message,
|
||||
data: msg.data,
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocket handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function setupWebSocket(server) {
|
||||
const wss = new WebSocketServer({ server, path: '/ws' });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
let authenticated = false;
|
||||
|
||||
ws.on('message', (raw) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// First message must be auth
|
||||
if (!authenticated) {
|
||||
if (msg.type === 'auth' && msg.token === state.token) {
|
||||
authenticated = true;
|
||||
state.wsClients.add(ws);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth_ok',
|
||||
hasProjectContext: hasProjectContext(),
|
||||
}));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'auth_fail', reason: 'Invalid token' }));
|
||||
ws.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Validated browser events go to the agent poll queue
|
||||
const error = validateEvent(msg);
|
||||
if (error) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: error }));
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueEvent(msg);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
state.wsClients.delete(ws);
|
||||
// If all browser clients disconnected, signal exit to agent
|
||||
if (authenticated && state.wsClients.size === 0) {
|
||||
enqueueEvent({ type: 'exit' });
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
state.wsClients.delete(ws);
|
||||
});
|
||||
});
|
||||
|
||||
return wss;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let httpServer = null;
|
||||
let wss = null;
|
||||
|
||||
function shutdown() {
|
||||
try { fs.unlinkSync(LIVE_PID_FILE); } catch { /* ignore */ }
|
||||
// Close all WebSocket connections
|
||||
for (const ws of state.wsClients) {
|
||||
try { ws.close(); } catch { /* ignore */ }
|
||||
}
|
||||
state.wsClients.clear();
|
||||
// Resolve any pending polls with exit
|
||||
for (const resolve of state.pendingPolls) {
|
||||
resolve({ type: 'exit' });
|
||||
}
|
||||
state.pendingPolls.length = 0;
|
||||
if (wss) { try { wss.close(); } catch { /* ignore */ } }
|
||||
if (httpServer) { httpServer.close(); }
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the live variant server.
|
||||
* Called from liveCli() in detect-antipatterns.mjs.
|
||||
*/
|
||||
export async function startLiveServer({ port: requestedPort } = {}) {
|
||||
const args = process.argv.slice(2);
|
||||
const helpMode = args.includes('--help');
|
||||
const stopMode = args.includes('stop');
|
||||
const portArg = args.find(a => a.startsWith('--port='));
|
||||
const parsedPort = portArg ? parseInt(portArg.split('=')[1], 10) : null;
|
||||
|
||||
if (helpMode) {
|
||||
console.log(`Usage: impeccable live [options]
|
||||
|
||||
Start the live variant mode server. Serves the browser overlay script and
|
||||
bridges WebSocket connections from the browser to the agent poll CLI.
|
||||
|
||||
Commands:
|
||||
live Start the server (default)
|
||||
live stop Stop a running live server
|
||||
|
||||
Options:
|
||||
--port=PORT Use a specific port (default: auto-detect starting at 8400)
|
||||
--help Show this help message
|
||||
|
||||
Endpoints:
|
||||
/live.js Browser script for element picker + variant cycling
|
||||
/detect.js Detection overlay script (backwards compatible)
|
||||
/health Health check
|
||||
/ws WebSocket endpoint for browser
|
||||
/poll Long-poll endpoint for agent CLI`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Stop mode
|
||||
if (stopMode) {
|
||||
try {
|
||||
const info = JSON.parse(fs.readFileSync(LIVE_PID_FILE, 'utf-8'));
|
||||
const res = await fetch(`http://localhost:${info.port}/stop?token=${info.token}`);
|
||||
if (res.ok) {
|
||||
console.log(`Stopped live server on port ${info.port}.`);
|
||||
}
|
||||
} catch {
|
||||
console.log('No running live server found.');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check for existing session
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(LIVE_PID_FILE, 'utf-8'));
|
||||
// Check if the process is actually running
|
||||
try {
|
||||
process.kill(existing.pid, 0);
|
||||
console.error(`Live server already running on port ${existing.port} (pid ${existing.pid}).`);
|
||||
console.error('Stop it first: npx impeccable live stop');
|
||||
process.exit(1);
|
||||
} catch {
|
||||
// Process is dead, clean up stale PID file
|
||||
fs.unlinkSync(LIVE_PID_FILE);
|
||||
}
|
||||
} catch {
|
||||
// No PID file — good
|
||||
}
|
||||
|
||||
// Generate session token
|
||||
state.token = randomUUID();
|
||||
state.port = requestedPort || parsedPort || await findOpenPort();
|
||||
|
||||
// Load scripts
|
||||
const { detectScript, liveScript } = loadBrowserScripts();
|
||||
|
||||
// Inject token and port into the live browser script
|
||||
const liveScriptWithToken =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
liveScript;
|
||||
|
||||
// Create HTTP server
|
||||
httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptWithToken }));
|
||||
|
||||
// Attach WebSocket
|
||||
wss = setupWebSocket(httpServer);
|
||||
|
||||
// Start listening
|
||||
httpServer.listen(state.port, '127.0.0.1', () => {
|
||||
// Write PID file with token so poll CLI can authenticate
|
||||
fs.writeFileSync(LIVE_PID_FILE, JSON.stringify({
|
||||
pid: process.pid,
|
||||
port: state.port,
|
||||
token: state.token,
|
||||
}));
|
||||
|
||||
const url = `http://localhost:${state.port}`;
|
||||
console.log(`\nImpeccable live variant server running on ${url}`);
|
||||
console.log(`Token: ${state.token}\n`);
|
||||
console.log(`Inject into your page source:`);
|
||||
console.log(` <script src="${url}/live.js"><\/script>\n`);
|
||||
console.log(`Or inject via browser console:`);
|
||||
console.log(` const s = document.createElement('script');`);
|
||||
console.log(` s.src = '${url}/live.js';`);
|
||||
console.log(` document.head.appendChild(s);\n`);
|
||||
console.log(`Agent poll: npx impeccable poll`);
|
||||
console.log(`Stop: npx impeccable live stop`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
Reference in New Issue
Block a user