flight-rules 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,20 @@ export declare function copyCommandsWithConflictHandling(sourceDir: string, dest
5
5
  copied: string[];
6
6
  skipped: string[];
7
7
  }>;
8
+ /**
9
+ * Copy skill files to a destination directory with conflict handling
10
+ */
11
+ export declare function copySkillsWithConflictHandling(sourceDir: string, destDir: string, skipPrompts?: boolean): Promise<{
12
+ copied: string[];
13
+ skipped: string[];
14
+ }>;
15
+ /**
16
+ * Setup skills for a given adapter directory
17
+ */
18
+ export declare function setupSkills(cwd: string, sourceSkillsDir: string, adapterSkillsDir: string, skipPrompts?: boolean): Promise<{
19
+ copied: string[];
20
+ skipped: string[];
21
+ }>;
8
22
  /**
9
23
  * Setup Cursor-specific directories and commands
10
24
  */
@@ -140,6 +140,68 @@ async function promptForConflict(filename, showBatchOptions) {
140
140
  }
141
141
  return action;
142
142
  }
143
+ /**
144
+ * Copy skill files to a destination directory with conflict handling
145
+ */
146
+ export async function copySkillsWithConflictHandling(sourceDir, destDir, skipPrompts = false) {
147
+ const copied = [];
148
+ const skipped = [];
149
+ if (!existsSync(sourceDir)) {
150
+ return { copied, skipped };
151
+ }
152
+ const files = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
153
+ let batchAction = null;
154
+ for (const file of files) {
155
+ const srcPath = join(sourceDir, file);
156
+ const destPath = join(destDir, file);
157
+ if (existsSync(destPath)) {
158
+ if (skipPrompts) {
159
+ cpSync(srcPath, destPath);
160
+ copied.push(file);
161
+ continue;
162
+ }
163
+ if (batchAction === 'replace_all') {
164
+ cpSync(srcPath, destPath);
165
+ copied.push(file);
166
+ continue;
167
+ }
168
+ else if (batchAction === 'skip_all') {
169
+ skipped.push(file);
170
+ continue;
171
+ }
172
+ const action = await promptForConflict(file, files.length > 1);
173
+ if (action === 'replace_all') {
174
+ batchAction = 'replace_all';
175
+ cpSync(srcPath, destPath);
176
+ copied.push(file);
177
+ }
178
+ else if (action === 'skip_all') {
179
+ batchAction = 'skip_all';
180
+ skipped.push(file);
181
+ }
182
+ else if (action === 'replace') {
183
+ cpSync(srcPath, destPath);
184
+ copied.push(file);
185
+ }
186
+ else {
187
+ skipped.push(file);
188
+ }
189
+ }
190
+ else {
191
+ cpSync(srcPath, destPath);
192
+ copied.push(file);
193
+ }
194
+ }
195
+ return { copied, skipped };
196
+ }
197
+ /**
198
+ * Setup skills for a given adapter directory
199
+ */
200
+ export async function setupSkills(cwd, sourceSkillsDir, adapterSkillsDir, skipPrompts = false) {
201
+ ensureDir(join(cwd, adapterSkillsDir.split('/')[0])); // e.g., .claude
202
+ ensureDir(join(cwd, adapterSkillsDir));
203
+ return copySkillsWithConflictHandling(sourceSkillsDir, join(cwd, adapterSkillsDir), skipPrompts);
204
+ }
143
205
  /**
144
206
  * Setup Cursor-specific directories and commands
145
207
  */
@@ -240,8 +302,10 @@ export async function adapter(args) {
240
302
  }
241
303
  export async function generateAdapters(adapterNames, sourceCommandsDir, interactive = true) {
242
304
  const cwd = process.cwd();
305
+ const flightRulesDir = getFlightRulesDir(cwd);
243
306
  // Default to .flight-rules/commands if no source specified
244
- const commandsDir = sourceCommandsDir ?? join(getFlightRulesDir(cwd), 'commands');
307
+ const commandsDir = sourceCommandsDir ?? join(flightRulesDir, 'commands');
308
+ const skillsDir = join(flightRulesDir, 'skills');
245
309
  for (const name of adapterNames) {
246
310
  const config = ADAPTERS[name];
247
311
  if (!config)
@@ -278,7 +342,7 @@ export async function generateAdapters(adapterNames, sourceCommandsDir, interact
278
342
  p.log.success(`Created ${pc.cyan(config.filename)} for ${config.name}`);
279
343
  adapterFileWritten = true;
280
344
  }
281
- // For Cursor, also set up .cursor/commands/
345
+ // For Cursor, also set up .cursor/commands/ and .cursor/skills/
282
346
  if (name === 'cursor') {
283
347
  const skipPrompts = !interactive;
284
348
  const commandsDirExists = isCursorAdapterInstalled(cwd);
@@ -290,8 +354,18 @@ export async function generateAdapters(adapterNames, sourceCommandsDir, interact
290
354
  if (result.skipped.length > 0) {
291
355
  p.log.info(`Skipped ${result.skipped.length} existing command(s) in .cursor/commands/`);
292
356
  }
357
+ // Copy skills
358
+ if (existsSync(skillsDir)) {
359
+ const skillResult = await setupSkills(cwd, skillsDir, '.cursor/skills', skipPrompts);
360
+ if (skillResult.copied.length > 0) {
361
+ p.log.success(`${commandsDirExists ? 'Updated' : 'Created'} ${pc.cyan('.cursor/skills/')} with ${skillResult.copied.length} skill(s)`);
362
+ }
363
+ if (skillResult.skipped.length > 0) {
364
+ p.log.info(`Skipped ${skillResult.skipped.length} existing skill(s) in .cursor/skills/`);
365
+ }
366
+ }
293
367
  }
294
- // For Claude, also set up .claude/commands/
368
+ // For Claude, also set up .claude/commands/ and .claude/skills/
295
369
  if (name === 'claude') {
296
370
  const skipPrompts = !interactive;
297
371
  const commandsDirExists = isClaudeAdapterInstalled(cwd);
@@ -303,6 +377,16 @@ export async function generateAdapters(adapterNames, sourceCommandsDir, interact
303
377
  if (result.skipped.length > 0) {
304
378
  p.log.info(`Skipped ${result.skipped.length} existing command(s) in .claude/commands/`);
305
379
  }
380
+ // Copy skills
381
+ if (existsSync(skillsDir)) {
382
+ const skillResult = await setupSkills(cwd, skillsDir, '.claude/skills', skipPrompts);
383
+ if (skillResult.copied.length > 0) {
384
+ p.log.success(`${commandsDirExists ? 'Updated' : 'Created'} ${pc.cyan('.claude/skills/')} with ${skillResult.copied.length} skill(s)`);
385
+ }
386
+ if (skillResult.skipped.length > 0) {
387
+ p.log.info(`Skipped ${skillResult.skipped.length} existing skill(s) in .claude/skills/`);
388
+ }
389
+ }
306
390
  }
307
391
  }
308
392
  }
@@ -123,28 +123,29 @@ async function runClaudeWithPrompt(promptContent, verbose) {
123
123
  ], {
124
124
  stdio: ['pipe', 'pipe', 'pipe'],
125
125
  });
126
- let output = '';
126
+ let reassembledText = ''; // Plain text reassembled from stream-json fragments
127
+ let resultText = ''; // Full text from the final result event (fallback)
127
128
  let errorOutput = '';
128
129
  let lineBuffer = ''; // Buffer for incomplete JSON lines
129
130
  let needsTimestamp = true; // Track whether next text output needs a timestamp
130
131
  claude.stdout?.on('data', (data) => {
131
132
  const text = data.toString();
132
- output += text;
133
- if (verbose) {
134
- // Prepend any buffered partial line from previous chunk
135
- const fullText = lineBuffer + text;
136
- const lines = fullText.split('\n');
137
- // Last element might be incomplete - save it for next chunk
138
- lineBuffer = lines.pop() || '';
139
- for (const line of lines) {
140
- if (!line.trim())
141
- continue;
142
- try {
143
- const parsed = JSON.parse(line);
144
- // Handle different message types in stream-json format
145
- if (parsed.type === 'assistant' && parsed.message?.content) {
146
- for (const block of parsed.message.content) {
147
- if (block.type === 'text' && block.text) {
133
+ // Parse stream-json lines to reassemble plain text output
134
+ const fullText = lineBuffer + text;
135
+ const lines = fullText.split('\n');
136
+ // Last element might be incomplete - save it for next chunk
137
+ lineBuffer = lines.pop() || '';
138
+ for (const line of lines) {
139
+ if (!line.trim())
140
+ continue;
141
+ try {
142
+ const parsed = JSON.parse(line);
143
+ // Handle different message types in stream-json format
144
+ if (parsed.type === 'assistant' && parsed.message?.content) {
145
+ for (const block of parsed.message.content) {
146
+ if (block.type === 'text' && block.text) {
147
+ reassembledText += block.text;
148
+ if (verbose) {
148
149
  if (needsTimestamp) {
149
150
  process.stdout.write(`${formatTimestamp()} `);
150
151
  needsTimestamp = false;
@@ -153,23 +154,31 @@ async function runClaudeWithPrompt(promptContent, verbose) {
153
154
  }
154
155
  }
155
156
  }
156
- else if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
157
+ }
158
+ else if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
159
+ reassembledText += parsed.delta.text;
160
+ if (verbose) {
157
161
  if (needsTimestamp) {
158
162
  process.stdout.write(`${formatTimestamp()} `);
159
163
  needsTimestamp = false;
160
164
  }
161
165
  process.stdout.write(parsed.delta.text);
162
166
  }
163
- else if (parsed.type === 'content_block_stop') {
167
+ }
168
+ else if (parsed.type === 'content_block_stop') {
169
+ if (verbose) {
164
170
  // Add newline + blank line after each content block ends
165
171
  process.stdout.write('\n\n');
166
172
  needsTimestamp = true;
167
173
  }
168
174
  }
169
- catch {
170
- // Not valid JSON, skip
175
+ else if (parsed.type === 'result' && typeof parsed.result === 'string') {
176
+ resultText = parsed.result;
171
177
  }
172
178
  }
179
+ catch {
180
+ // Not valid JSON, skip
181
+ }
173
182
  }
174
183
  });
175
184
  claude.stderr?.on('data', (data) => {
@@ -180,6 +189,30 @@ async function runClaudeWithPrompt(promptContent, verbose) {
180
189
  }
181
190
  });
182
191
  claude.on('close', (code) => {
192
+ // Flush any remaining data in lineBuffer
193
+ if (lineBuffer.trim()) {
194
+ try {
195
+ const parsed = JSON.parse(lineBuffer);
196
+ if (parsed.type === 'assistant' && parsed.message?.content) {
197
+ for (const block of parsed.message.content) {
198
+ if (block.type === 'text' && block.text) {
199
+ reassembledText += block.text;
200
+ }
201
+ }
202
+ }
203
+ else if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
204
+ reassembledText += parsed.delta.text;
205
+ }
206
+ else if (parsed.type === 'result' && typeof parsed.result === 'string') {
207
+ resultText = parsed.result;
208
+ }
209
+ }
210
+ catch {
211
+ // Not valid JSON, skip
212
+ }
213
+ }
214
+ // Prefer reassembled streaming text; fall back to result event text
215
+ const output = reassembledText || resultText;
183
216
  resolve({ output, exitCode: code ?? 0 });
184
217
  });
185
218
  claude.on('error', (err) => {
@@ -4,7 +4,7 @@ import { existsSync, cpSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { isFlightRulesInstalled, fetchPayloadFromGitHub, copyFrameworkFilesFrom, ensureDir, getInstalledVersion, writeManifest, getCliVersion } from '../utils/files.js';
6
6
  import { isInteractive } from '../utils/interactive.js';
7
- import { isCursorAdapterInstalled, isClaudeAdapterInstalled, setupCursorCommands, setupClaudeCommands, } from './adapter.js';
7
+ import { isCursorAdapterInstalled, isClaudeAdapterInstalled, setupCursorCommands, setupClaudeCommands, setupSkills, } from './adapter.js';
8
8
  const DOC_FILES = [
9
9
  { src: 'prd.md', dest: 'prd.md' },
10
10
  { src: 'progress.md', dest: 'progress.md' },
@@ -148,21 +148,34 @@ export async function upgrade(version) {
148
148
  spinner.start('Upgrading adapters...');
149
149
  try {
150
150
  const sourceCommandsDir = join(fetched.payloadPath, 'commands');
151
- // Upgrade Cursor commands if installed
151
+ const sourceSkillsDir = join(fetched.payloadPath, 'skills');
152
+ // Upgrade Cursor commands and skills if installed
152
153
  if (cursorAdapterInstalled) {
153
154
  const result = await setupCursorCommands(cwd, sourceCommandsDir, true); // skipPrompts = true for upgrade
154
155
  if (result.copied.length > 0) {
155
156
  p.log.success(`Updated ${result.copied.length} command(s) in .cursor/commands/`);
156
157
  }
158
+ if (existsSync(sourceSkillsDir)) {
159
+ const skillResult = await setupSkills(cwd, sourceSkillsDir, '.cursor/skills', true);
160
+ if (skillResult.copied.length > 0) {
161
+ p.log.success(`Updated ${skillResult.copied.length} skill(s) in .cursor/skills/`);
162
+ }
163
+ }
157
164
  }
158
- // Upgrade Claude commands if installed, or create them if CLAUDE.md exists but .claude/commands/ doesn't
159
- // (handles upgrade from pre-0.5.4 where Claude didn't have native commands)
165
+ // Upgrade Claude commands and skills if installed
160
166
  if (claudeAdapterInstalled || claudeMdExists) {
161
167
  const result = await setupClaudeCommands(cwd, sourceCommandsDir, true); // skipPrompts = true for upgrade
162
168
  if (result.copied.length > 0) {
163
169
  const action = claudeAdapterInstalled ? 'Updated' : 'Created';
164
170
  p.log.success(`${action} ${result.copied.length} command(s) in .claude/commands/`);
165
171
  }
172
+ if (existsSync(sourceSkillsDir)) {
173
+ const skillResult = await setupSkills(cwd, sourceSkillsDir, '.claude/skills', true);
174
+ if (skillResult.copied.length > 0) {
175
+ const action = claudeAdapterInstalled ? 'Updated' : 'Created';
176
+ p.log.success(`${action} ${skillResult.copied.length} skill(s) in .claude/skills/`);
177
+ }
178
+ }
166
179
  }
167
180
  // Regenerate adapter files
168
181
  const adaptersToRegenerate = [];
@@ -67,7 +67,8 @@ export function copyFrameworkFiles(targetDir) {
67
67
  'AGENTS.md',
68
68
  'doc-templates',
69
69
  'commands',
70
- 'prompts'
70
+ 'prompts',
71
+ 'skills'
71
72
  ];
72
73
  for (const item of frameworkItems) {
73
74
  const srcItem = join(payloadPath, item);
@@ -175,7 +176,8 @@ export function copyFrameworkFilesFrom(sourcePayloadPath, targetDir) {
175
176
  'AGENTS.md',
176
177
  'doc-templates',
177
178
  'commands',
178
- 'prompts'
179
+ 'prompts',
180
+ 'skills'
179
181
  ];
180
182
  for (const item of frameworkItems) {
181
183
  const srcItem = join(sourcePayloadPath, item);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flight-rules",
3
- "version": "0.15.2",
3
+ "version": "0.15.4",
4
4
  "description": "An opinionated framework for AI-assisted software development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/payload/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Flight Rules – Agent Guidelines
2
2
 
3
- flight_rules_version: 0.15.2
3
+ flight_rules_version: 0.15.4
4
4
 
5
5
  This file defines how agents (Claude Code, Cursor, etc.) should work on software projects using the Flight Rules system.
6
6
 
@@ -1,20 +1,25 @@
1
1
  # Flight Rules Discovery Agent
2
2
 
3
- You are a discovery agent for Flight Rules. Your ONLY job is to scan implementation docs and report which task groups have incomplete tasks. Do NOT implement anything.
3
+ You are a discovery agent for Flight Rules. Your ONLY job is to scan implementation docs and report which task groups have **incomplete** (not-yet-done) tasks. Do NOT implement anything.
4
4
 
5
5
  ## Instructions
6
6
 
7
7
  1. Read `docs/implementation/overview.md` to understand the area/task-group structure
8
8
  2. Scan each Area directory in `docs/implementation/`
9
- 3. For each Task Group file (.md), check every task's status
10
- 4. Report all task groups that contain any task with status other than ✅ Complete (i.e., 🔵 Planned, 🟡 In Progress, or ⏸️ Blocked)
9
+ 3. For each Task Group file (.md), check every task's `**Status**:` field
10
+ 4. A task is **incomplete** if its status is 🔵 Planned, 🟡 In Progress, or ⏸️ Blocked
11
+ 5. A task is **complete** only if its status is ✅ Complete
12
+ 6. Report all task groups that contain any incomplete task
11
13
 
12
14
  ## Response Format
13
15
 
14
- Respond with a pipe-delimited report inside `<ralph-discovery>` tags. Use EXACTLY this format:
16
+ Output ONLY the `<ralph-discovery>` block below. No commentary, no summary, no interpretation before or after the tags. Your entire response must be the block and nothing else.
17
+
18
+ When incomplete tasks exist (the common case), use this format:
15
19
 
16
20
  ```
17
21
  <ralph-discovery>
22
+ INCOMPLETE|{totalIncompleteTaskCount}
18
23
  TASK_GROUP|{id}|{title}|{filePath}|{areaDir}
19
24
  TASK|{taskId}|{taskTitle}|{status}
20
25
  TASK|{taskId}|{taskTitle}|{status}
@@ -23,16 +28,17 @@ TASK|{taskId}|{taskTitle}|{status}
23
28
  </ralph-discovery>
24
29
  ```
25
30
 
26
- - Each `TASK_GROUP` line is followed by its `TASK` lines (only incomplete tasks)
31
+ - First line is `INCOMPLETE|{N}` where N = total number of incomplete tasks across all task groups
32
+ - Each `TASK_GROUP` line is followed by its incomplete `TASK` lines
27
33
  - `{id}` = task group ID as written in the file (e.g., "1.1", "2.3")
28
34
  - `{title}` = task group title
29
35
  - `{filePath}` = relative path from project root to the task group file
30
36
  - `{areaDir}` = area directory name (e.g., "1-project-setup", "2-cli-core")
31
37
  - `{taskId}` = individual task ID (e.g., "1.1.1", "2.3.2")
32
38
  - `{taskTitle}` = individual task title
33
- - `{status}` = one of: planned, in_progress, blocked
39
+ - `{status}` = one of: `planned`, `in_progress`, `blocked` — all three mean the task is NOT done
34
40
 
35
- If ALL tasks in ALL task groups are ✅ Complete, respond with:
41
+ Only if every single task in every single task group has ✅ Complete status (this is rare during active development), respond with:
36
42
 
37
43
  ```
38
44
  <ralph-discovery>
@@ -40,11 +46,21 @@ ALL_COMPLETE
40
46
  </ralph-discovery>
41
47
  ```
42
48
 
49
+ ## Field Reference
50
+
51
+ | Status in doc | Meaning | Output value |
52
+ |---|---|---|
53
+ | 🔵 Planned | Not started, work remains | `planned` |
54
+ | 🟡 In Progress | Started but not finished | `in_progress` |
55
+ | ⏸️ Blocked | Cannot proceed, work remains | `blocked` |
56
+ | ✅ Complete | Done, do NOT include in output | (omit) |
57
+
43
58
  ## Rules
44
59
 
45
60
  - Do NOT implement or modify any code
46
61
  - Do NOT create or modify any files
47
62
  - Do NOT run any scripts or quality checks
48
63
  - ONLY read implementation files and report status
49
- - Always include the `<ralph-discovery>` tags in your response
50
- - List task groups in the order they should be worked on (by area number, then task group number)
64
+ - Your response must contain ONLY the `<ralph-discovery>` block no other text
65
+ - List task groups in order by area number, then task group number
66
+ - Never output `ALL_COMPLETE` if any task has a status of planned, in_progress, or blocked
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: web-prototype
3
+ description: >
4
+ Create multiple distinct design variations of a web page as HTML/CSS/JS prototypes.
5
+ Use this skill whenever the user wants to create web prototypes, page mockups, design
6
+ variations, HTML page concepts, landing page options, or explore different visual
7
+ directions for a page. Also trigger when the user asks to "try different designs",
8
+ "show me options for a page", "create page variations", or anything involving generating
9
+ multiple visual alternatives of a web page. Even if the user just says "prototype this
10
+ page" or "mock up a landing page", this skill applies.
11
+ ---
12
+
13
+ # Web Prototype Generator
14
+
15
+ Create multiple visually distinct variations of a web page so the user can compare
16
+ design directions side by side. Each variation is a standalone HTML file with inline
17
+ CSS and JavaScript — no frameworks, no build steps, just open in a browser.
18
+
19
+ ## How it works
20
+
21
+ 1. Understand what the user wants the page to do and look like
22
+ 2. Check or create the project config (`.web-prototype.json`)
23
+ 3. Generate N variations (default 5), each as different as possible
24
+ 4. Inject a shared navigation bar so the user can flip between variants
25
+
26
+ ## Step 1: Understand the request
27
+
28
+ Ask the user (if not already clear):
29
+ - What is this page? (landing page, dashboard, portfolio, form, etc.)
30
+ - What content should it include?
31
+ - Any specific features? (see Feature Library below)
32
+ - Any brand constraints? (colors, fonts, tone)
33
+ - How many variations? (default: 5)
34
+
35
+ Give the page a short kebab-case name for the folder (e.g., `landing-page`, `pricing`,
36
+ `signup-flow`). Confirm the name with the user if it's ambiguous.
37
+
38
+ ## Step 2: Project configuration
39
+
40
+ Look for `.web-prototype.json` in the project root. If it doesn't exist, create it:
41
+
42
+ ```json
43
+ {
44
+ "outputRoot": "prototypes",
45
+ "pages": {}
46
+ }
47
+ ```
48
+
49
+ Ask the user if `prototypes/` is the right output directory. If they want something
50
+ different, use their preference.
51
+
52
+ When generating a new page, add an entry to `pages`:
53
+
54
+ ```json
55
+ {
56
+ "outputRoot": "prototypes",
57
+ "pages": {
58
+ "landing-page": {
59
+ "variants": 5,
60
+ "created": "2026-03-07",
61
+ "description": "Main marketing landing page"
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ The output structure is:
68
+ ```
69
+ {outputRoot}/
70
+ {page-name}/
71
+ variant-1.html
72
+ variant-2.html
73
+ ...
74
+ variant-N.html
75
+ ```
76
+
77
+ ## Step 3: Generate variations
78
+
79
+ For each variation, use the `frontend-design` skill to produce a distinctive,
80
+ production-grade design. The critical goal is **maximum visual diversity** — each
81
+ variant should feel like it came from a different designer with a different aesthetic
82
+ philosophy.
83
+
84
+ ### Diversity strategy
85
+
86
+ Before generating, plan N distinct design directions. Vary these dimensions across
87
+ the set so that no two variants share the same combination:
88
+
89
+ - **Layout**: single-column, split-screen, asymmetric grid, full-bleed sections,
90
+ card-based, editorial/magazine, bento grid
91
+ - **Visual tone**: minimal/stark, bold/maximalist, organic/soft, geometric/precise,
92
+ editorial/typographic, dark/moody, light/airy, retro/vintage, futuristic/glassmorphic
93
+ - **Color approach**: monochromatic, complementary, analogous, high-contrast,
94
+ muted/earthy, vibrant/saturated, dark mode, light mode
95
+ - **Typography**: large display type, classic serif, geometric sans, mixed type scales,
96
+ monospace accents, handwritten touches
97
+ - **Motion/interaction**: static, subtle hover effects, scroll-triggered animations,
98
+ parallax, micro-interactions
99
+ - **Spacing/density**: generous whitespace, dense/information-rich, mixed rhythm
100
+
101
+ Write out your plan (which direction each variant will take) before generating code.
102
+ Present it to the user for approval. Then generate each variant.
103
+
104
+ ### Per-variant requirements
105
+
106
+ Each variant must be:
107
+ - A single, self-contained HTML file with all CSS and JS inline
108
+ - Responsive (works on mobile and desktop)
109
+ - Production-grade visual quality — not a wireframe, not a generic template
110
+ - Populated with realistic placeholder content (not lorem ipsum — write real-sounding
111
+ headlines, descriptions, and data)
112
+
113
+ ### Using the frontend-design skill
114
+
115
+ For each variant, invoke the frontend-design skill with clear direction about the
116
+ specific aesthetic you're targeting for that variant. Tell it the design direction
117
+ (e.g., "Create a bold maximalist landing page with large typography and vibrant
118
+ complementary colors") so that each invocation produces a genuinely different result.
119
+
120
+ ## Step 4: Inject the variant navigation bar
121
+
122
+ Every generated HTML file must include the variant navigation bar. This is a floating
123
+ pill-shaped bar at the bottom-right of the viewport that lets the user click between
124
+ variants.
125
+
126
+ ### Navigation bar HTML and CSS
127
+
128
+ Insert this exactly at the end of `<body>`, before `</body>`. Replace `{N}` with the
129
+ total variant count and set the correct `href` values and `active` class.
130
+
131
+ ```html
132
+ <!-- Variant Navigation -->
133
+ <nav class="variant-nav" aria-label="Design variants">
134
+ <a href="variant-1.html" class="active" aria-current="page">1</a>
135
+ <a href="variant-2.html">2</a>
136
+ <a href="variant-3.html">3</a>
137
+ <!-- ... up to N -->
138
+ </nav>
139
+ <style>
140
+ .variant-nav {
141
+ position: fixed;
142
+ bottom: 24px;
143
+ right: 24px;
144
+ display: flex;
145
+ gap: 4px;
146
+ padding: 6px;
147
+ background: rgba(20, 20, 20, 0.55);
148
+ backdrop-filter: blur(24px) saturate(180%);
149
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
150
+ border-radius: 20px;
151
+ border: 1px solid rgba(255, 255, 255, 0.15);
152
+ box-shadow:
153
+ 0 8px 32px rgba(0, 0, 0, 0.18),
154
+ 0 2px 8px rgba(0, 0, 0, 0.1),
155
+ inset 0 0.5px 0 rgba(255, 255, 255, 0.2);
156
+ z-index: 2147483647;
157
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
158
+ transition: opacity 0.3s ease;
159
+ }
160
+ .variant-nav:hover {
161
+ background: rgba(20, 20, 20, 0.7);
162
+ }
163
+ .variant-nav a {
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ width: 36px;
168
+ height: 36px;
169
+ border-radius: 12px;
170
+ text-decoration: none;
171
+ font-size: 14px;
172
+ font-weight: 500;
173
+ letter-spacing: -0.01em;
174
+ color: rgba(255, 255, 255, 0.7);
175
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
176
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
177
+ background: transparent;
178
+ border: 1px solid transparent;
179
+ }
180
+ .variant-nav a:hover {
181
+ background: rgba(255, 255, 255, 0.12);
182
+ color: rgba(255, 255, 255, 0.95);
183
+ border-color: rgba(255, 255, 255, 0.08);
184
+ }
185
+ .variant-nav a.active {
186
+ background: rgba(255, 255, 255, 0.2);
187
+ color: #fff;
188
+ font-weight: 600;
189
+ border-color: rgba(255, 255, 255, 0.15);
190
+ box-shadow:
191
+ 0 2px 8px rgba(0, 0, 0, 0.15),
192
+ inset 0 0.5px 0 rgba(255, 255, 255, 0.25);
193
+ }
194
+ </style>
195
+ ```
196
+
197
+ This dark-tinted glass style is intentionally designed to be legible on any background —
198
+ light pages, dark pages, images, gradients, anything. Do not modify the nav bar styles
199
+ to match the page design. The nav bar should look identical across all variants.
200
+
201
+ Set `aria-current="page"` and the `active` class on whichever variant number matches
202
+ the current file (variant-1.html gets `active` on the "1" link, etc.).
203
+
204
+ ## Feature Library
205
+
206
+ When the user mentions any of these features by name, you already know what they mean.
207
+ Apply the implementation details described here without requiring further explanation
208
+ from the user.
209
+
210
+ ### Scrolling Parallax
211
+
212
+ **Trigger phrases**: "parallax", "scrolling parallax", "web parallax", "parallax scrolling",
213
+ "depth scrolling"
214
+
215
+ Multi-layer parallax scrolling where background elements move at different speeds than
216
+ foreground content during scroll, creating an illusion of depth.
217
+
218
+ **Implementation approach** (choose based on complexity needs):
219
+
220
+ **CSS-only (simpler, good for 2-3 layers):**
221
+ ```css
222
+ .parallax-container {
223
+ perspective: 1px;
224
+ height: 100vh;
225
+ overflow-x: hidden;
226
+ overflow-y: auto;
227
+ }
228
+ .parallax-layer-back {
229
+ transform: translateZ(-2px) scale(3);
230
+ }
231
+ .parallax-layer-mid {
232
+ transform: translateZ(-1px) scale(2);
233
+ }
234
+ .parallax-layer-front {
235
+ transform: translateZ(0);
236
+ }
237
+ ```
238
+
239
+ **JavaScript (smoother, more control, good for complex effects):**
240
+ - Use `requestAnimationFrame` for smooth 60fps updates
241
+ - Track scroll position with a passive scroll listener
242
+ - Apply `transform: translate3d(0, Ypx, 0)` to layers at different rates (e.g.,
243
+ background at 0.3x scroll speed, midground at 0.6x, foreground at 1x)
244
+ - Use `will-change: transform` on animated elements for GPU acceleration
245
+ - Consider `IntersectionObserver` to only animate visible sections
246
+
247
+ **Design considerations:**
248
+ - Parallax works best with distinct visual sections/bands
249
+ - Use high-quality background images or gradient layers
250
+ - Ensure content remains readable — parallax is decoration, not the content itself
251
+ - Disable or reduce parallax on mobile (`prefers-reduced-motion` media query)
252
+ - Add `@media (prefers-reduced-motion: reduce)` to disable parallax for accessibility
253
+
254
+ ---
255
+
256
+ ## Notes
257
+
258
+ - If the user asks for more variants of an existing page, read the config to find the
259
+ current count, generate additional variants, and update the nav bar in ALL files to
260
+ include the new total.
261
+ - If the user wants to iterate on a specific variant, make edits to that file directly
262
+ rather than regenerating from scratch.
263
+ - Always confirm the output path with the user before writing files.