mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-09-16 17:16:16 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15de38fb70 | ||
|
|
7643fd1501 | ||
|
|
975cba327e | ||
|
|
d8ce090f82 | ||
|
|
7f69fed6a2 | ||
|
|
9cf21aef9b | ||
|
|
e7f7850dc7 | ||
|
|
53b8e6913e | ||
|
|
4aad0584d9 | ||
|
|
ce1586f774 | ||
|
|
314307f156 | ||
|
|
b2ac9b2aa1 | ||
|
|
f3ac195224 | ||
|
|
91c193ac05 | ||
|
|
d9062e3bc2 | ||
|
|
08b2e54ce0 | ||
|
|
58c220ff9d | ||
|
|
e2effd5775 | ||
|
|
40d8b6facf | ||
|
|
c21a7b095f | ||
|
|
26f79bdfb6 | ||
|
|
36c879d974 | ||
|
|
f23267105a | ||
|
|
bd19ab9070 | ||
|
|
c7d413e7c3 | ||
|
|
d95bdf5669 | ||
|
|
d279284fb1 | ||
|
|
e9bd28180f | ||
|
|
5f2b160e3b | ||
|
|
e7e873dada | ||
|
|
0720614e6e | ||
|
|
dfe8765f10 | ||
|
|
8bd29e7754 | ||
|
|
e4f4547369 | ||
|
|
3e6be6d538 | ||
|
|
e353a50876 | ||
|
|
a8f4e7e1ec | ||
|
|
9f1824aa7b | ||
|
|
e084ceb4ee | ||
|
|
c87cdc226f | ||
|
|
13179471f9 | ||
|
|
2872e4cc23 | ||
|
|
bc826e2267 | ||
|
|
8a1a6d8573 | ||
|
|
7b1db583ee |
@ -5,7 +5,7 @@
|
||||
"name": "nextlevelbuilder"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "UI/UX design intelligence skill with 84 styles, 192 palettes, 74 font pairings, 25 charts, and 22 stack guidelines",
|
||||
"description": "UI/UX design intelligence skill with 79 styles, 192 palettes, 74 font pairings, 25 charts, and 22 stack guidelines",
|
||||
"version": "2.13.0"
|
||||
},
|
||||
"plugins": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ui-ux-pro-max",
|
||||
"description": "UI/UX design intelligence. Searchable local database with 84 styles, 192 palettes, 74 font pairings, 25 charts, and 22 stacks (React, Next.js, Vue, Nuxt.js, Nuxt UI, Svelte, Astro, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, Jetpack Compose, Angular, Laravel, JavaFX, WPF, WinUI, Avalonia, Uno Platform, UWP, Three.js). Use when designing, building, or reviewing UI: pages, components, color schemes, typography, layout, accessibility, animation, or data visualization.",
|
||||
"description": "UI/UX design intelligence. Searchable local database with 79 styles, 192 palettes, 74 font pairings, 25 charts, and 22 stacks (React, Next.js, Vue, Nuxt.js, Nuxt UI, Svelte, Astro, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, Jetpack Compose, Angular, Laravel, JavaFX, WPF, WinUI, Avalonia, Uno Platform, UWP, Three.js). Use when designing, building, or reviewing UI: pages, components, color schemes, typography, layout, accessibility, animation, or data visualization.",
|
||||
"version": "2.13.0",
|
||||
"author": {
|
||||
"name": "nextlevelbuilder"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: banner-design
|
||||
description: "Design banners for social media, ads, website heroes, creative assets, and print. Multiple art direction options with AI-generated visuals. Actions: design, create, generate banner. Platforms: Facebook, Twitter/X, LinkedIn, YouTube, Instagram, Google Display, website hero, print. Styles: minimalist, gradient, bold typography, photo-based, illustrated, geometric, retro, glassmorphism, 3D, neon, duotone, editorial, collage. Uses ui-ux-pro-max, frontend-design, ai-artist, ai-multimodal skills."
|
||||
description: "Design banners for social media, ads, website heroes, creative assets, and print. Multiple art direction options with optional generated or supplied visuals. Actions: design, create, generate banner. Platforms: Facebook, Twitter/X, LinkedIn, YouTube, Instagram, Google Display, website hero, print. Styles: minimalist, gradient, bold typography, photo-based, illustrated, geometric, retro, glassmorphism, 3D, neon, duotone, editorial, collage."
|
||||
argument-hint: "[platform] [style] [dimensions]"
|
||||
license: MIT
|
||||
metadata:
|
||||
@ -10,7 +10,7 @@ metadata:
|
||||
|
||||
# Banner Design - Multi-Format Creative Banner System
|
||||
|
||||
Design banners across social, ads, web, and print formats. Generates multiple art direction options per request with AI-powered visual elements. This skill handles banner design only. Does NOT handle video editing, full website design, or print production.
|
||||
Design banners across social, ads, web, and print formats. Generate multiple art direction options with CSS-built, user-supplied, or optionally generated visual elements. This skill handles banner design only. It does not handle video editing, full website design, or print production.
|
||||
|
||||
## When to Activate
|
||||
|
||||
@ -21,9 +21,9 @@ Design banners across social, ads, web, and print formats. Generates multiple ar
|
||||
- Event/print banner design
|
||||
- Creative asset generation for campaigns
|
||||
|
||||
## Prerequisites
|
||||
## Available Resources
|
||||
|
||||
**Python:** This skill uses Python scripts. On Windows, use `python` instead of `python3` (e.g., `python scripts/search.py` instead of `python3 scripts/search.py`).
|
||||
This workflow is self-contained: it requires no sibling skills or skill-relative scripts. Use `references/banner-sizes-and-styles.md` for the bundled size, safe-zone, and art-direction guidance. Browser research, image generation, and screenshot tooling are optional capabilities; when unavailable, use supplied assets, CSS-built visuals, and the runtime's standard preview or capture workflow.
|
||||
|
||||
## Workflow
|
||||
|
||||
@ -33,95 +33,44 @@ Collect via AskUserQuestion:
|
||||
1. **Purpose** — social cover, ad banner, website hero, print, or creative asset?
|
||||
2. **Platform/size** — which platform or custom dimensions?
|
||||
3. **Content** — headline, subtext, CTA, logo placement?
|
||||
4. **Brand** — existing brand guidelines? (check `docs/brand-guidelines.md`)
|
||||
4. **Brand** — existing brand guidelines, logo files, colors, or typography?
|
||||
5. **Style preference** — any art direction? (show style options if unsure)
|
||||
6. **Quantity** — how many options to generate? (default: 3)
|
||||
|
||||
### Step 2: Research & Art Direction
|
||||
|
||||
1. Activate `ui-ux-pro-max` skill for design intelligence
|
||||
2. Use Chrome browser to research Pinterest for design references:
|
||||
```
|
||||
Navigate to pinterest.com → search "[purpose] banner design [style]"
|
||||
Screenshot 3-5 reference pins for art direction inspiration
|
||||
```
|
||||
3. Select 2-3 complementary art direction styles from references:
|
||||
`references/banner-sizes-and-styles.md`
|
||||
1. Read `references/banner-sizes-and-styles.md` for the target format, safe zone, and suitable styles.
|
||||
2. If browser research is available and permitted, collect 3–5 references for composition and art-direction inspiration. Otherwise, work from the bundled reference and any examples supplied by the user.
|
||||
3. Select 2–3 complementary art directions and state how each supports the banner's purpose.
|
||||
|
||||
### Step 3: Design & Generate Options
|
||||
|
||||
For each art direction option:
|
||||
|
||||
1. **Create HTML/CSS banner** using `frontend-design` skill
|
||||
- Use exact platform dimensions from size reference
|
||||
- Apply safe zone rules (critical content in central 70-80%)
|
||||
- Max 2 typefaces, single CTA, 4.5:1 contrast ratio
|
||||
- Inject brand context via `inject-brand-context.cjs`
|
||||
1. **Create the banner in HTML/CSS**
|
||||
- Use the exact platform dimensions from the size reference
|
||||
- Apply safe-zone rules (critical content in the central 70–80%)
|
||||
- Use at most 2 typefaces, a single CTA, and text contrast of at least 4.5:1
|
||||
- Apply the user's supplied logo, colors, typography, and imagery; do not invent brand rules
|
||||
|
||||
2. **Generate visual elements** with `ai-artist` + `ai-multimodal` skills
|
||||
2. **Choose a visual source**
|
||||
- Prefer user-supplied or appropriately licensed assets when provided
|
||||
- Use gradients, geometric forms, type, and other CSS-built visuals for a dependency-free result
|
||||
- If the runtime provides an authorized image-generation capability, it may generate a background or illustration at the target aspect ratio
|
||||
- Keep generated visual prompts free of text, letters, and words so final copy remains editable and accessible in HTML
|
||||
|
||||
**a) Search prompt inspiration** (6000+ examples in ai-artist):
|
||||
```bash
|
||||
python3 .claude/skills/ai-artist/scripts/search.py "<banner style keywords>"
|
||||
```
|
||||
|
||||
**b) Generate with Standard model** (fast, good for backgrounds/patterns):
|
||||
```bash
|
||||
.claude/skills/.venv/bin/python3 .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \
|
||||
--task generate --model gemini-2.5-flash-image \
|
||||
--prompt "<banner visual prompt>" --aspect-ratio <platform-ratio> \
|
||||
--size 2K --output assets/banners/
|
||||
```
|
||||
|
||||
**c) Generate with Pro model** (4K, complex illustrations/hero visuals):
|
||||
```bash
|
||||
.claude/skills/.venv/bin/python3 .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \
|
||||
--task generate --model gemini-3-pro-image-preview \
|
||||
--prompt "<creative banner prompt>" --aspect-ratio <platform-ratio> \
|
||||
--size 4K --output assets/banners/
|
||||
```
|
||||
|
||||
**When to use which model:**
|
||||
| Use Case | Model | Quality |
|
||||
|----------|-------|---------|
|
||||
| Backgrounds, gradients, patterns | Standard (Flash) | 2K, fast |
|
||||
| Hero illustrations, product shots | Pro | 4K, detailed |
|
||||
| Photorealistic scenes, complex art | Pro | 4K, best quality |
|
||||
| Quick iterations, A/B variants | Standard (Flash) | 2K, fast |
|
||||
|
||||
**Aspect ratios:** `1:1`, `16:9`, `9:16`, `3:4`, `4:3`, `2:3`, `3:2`
|
||||
Match to platform - e.g., Twitter header = `3:1` (use `3:2` closest), Instagram story = `9:16`
|
||||
|
||||
**Pro model prompt tips** (see `ai-artist` references/nano-banana-pro-examples.md):
|
||||
- Be descriptive: style, lighting, mood, composition, color palette
|
||||
- Include art direction: "minimalist flat design", "cyberpunk neon", "editorial photography"
|
||||
- Specify no-text: "no text, no letters, no words" (text overlaid in HTML step)
|
||||
|
||||
3. **Compose final banner** — overlay text, CTA, logo on generated visual in HTML/CSS
|
||||
3. **Compose the final banner** — overlay the headline, supporting copy, CTA, and logo in HTML/CSS, then verify hierarchy, safe zones, contrast, and crop behavior at the exact target size
|
||||
|
||||
### Step 4: Export Banners to Images
|
||||
|
||||
After designing HTML banners, export each to PNG using `chrome-devtools` skill:
|
||||
After designing the HTML banners:
|
||||
|
||||
1. **Serve HTML files** via local server (python http.server or similar)
|
||||
2. **Screenshot each banner** at exact platform dimensions:
|
||||
```bash
|
||||
# Export banner to PNG at exact dimensions
|
||||
node .claude/skills/chrome-devtools/scripts/screenshot.js \
|
||||
--url "http://localhost:8765/banner-01-minimalist.html" \
|
||||
--width 1500 --height 500 \
|
||||
--output "assets/banners/{campaign}/{variant}-{size}.png"
|
||||
```
|
||||
3. **Auto-compress** if >5MB (Sharp compression built-in):
|
||||
```bash
|
||||
# With custom max size threshold
|
||||
node .claude/skills/chrome-devtools/scripts/screenshot.js \
|
||||
--url "http://localhost:8765/banner-02-gradient.html" \
|
||||
--width 1500 --height 500 --max-size 3 \
|
||||
--output "assets/banners/{campaign}/{variant}-{size}.png"
|
||||
```
|
||||
1. Preview each banner in an available browser at the exact target viewport.
|
||||
2. Capture the banner element as PNG with the runtime's standard browser or screenshot capability. If capture is unavailable, deliver the HTML/CSS source and clearly mark PNG export as pending rather than naming an uninstalled tool.
|
||||
3. Verify the exported pixel dimensions, safe-zone crop, font loading, and image quality.
|
||||
4. If an exported file exceeds the platform limit, use an available image optimizer or reduce image quality and dimensions within the platform specification.
|
||||
|
||||
**Output path convention** (per `assets-organizing` skill):
|
||||
**Output path convention:**
|
||||
```
|
||||
assets/banners/{campaign}/
|
||||
├── minimalist-1500x500.png
|
||||
@ -139,7 +88,7 @@ assets/banners/{campaign}/
|
||||
|
||||
Present all exported images side-by-side. For each option show:
|
||||
- Art direction style name
|
||||
- Exported PNG preview (use `ai-multimodal` skill to display if needed)
|
||||
- Exported PNG preview, or an HTML/CSS preview when image capture is unavailable
|
||||
- Key design rationale
|
||||
- File path & dimensions
|
||||
|
||||
@ -185,7 +134,7 @@ Full 22 styles: `references/banner-sizes-and-styles.md`
|
||||
- **Typography**: max 2 fonts, min 16px body, ≥32px headline
|
||||
- **Text ratio**: under 20% for ads (Meta penalizes heavy text)
|
||||
- **Print**: 300 DPI, CMYK, 3-5mm bleed
|
||||
- **Brand**: always inject via `inject-brand-context.cjs`
|
||||
- **Brand**: apply only supplied, verified brand guidance and assets
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
@ -20,6 +20,10 @@ Brand identity, voice, messaging, asset management, and consistency frameworks.
|
||||
- Asset organization, naming, and approval
|
||||
- Color palette management and typography specs
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Inject brand context into prompts:**
|
||||
|
||||
@ -157,7 +157,7 @@ The `validate-asset.cjs` script can auto-check:
|
||||
- Naming convention
|
||||
- Basic metadata
|
||||
|
||||
Run: `node .claude/skills/brand/scripts/validate-asset.cjs <asset-path>`
|
||||
Run: `node scripts/validate-asset.cjs <asset-path>`
|
||||
|
||||
## Archival
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Edit `docs/brand-guidelines.md`:
|
||||
|
||||
Run the sync script:
|
||||
```bash
|
||||
node .claude/skills/brand/scripts/sync-brand-to-tokens.cjs
|
||||
node scripts/sync-brand-to-tokens.cjs
|
||||
```
|
||||
|
||||
This will:
|
||||
@ -58,7 +58,7 @@ This will:
|
||||
Confirm all files are updated:
|
||||
```bash
|
||||
# Check brand context extraction
|
||||
node .claude/skills/brand/scripts/inject-brand-context.cjs --json | head -30
|
||||
node scripts/inject-brand-context.cjs --json | head -30
|
||||
|
||||
# Check CSS variables
|
||||
grep "primary" assets/design-tokens.css | head -5
|
||||
|
||||
@ -287,11 +287,7 @@ function main() {
|
||||
"1. Run the ImageMagick command to extract colors:",
|
||||
` ${generateImageMagickCommand(resolvedPath)}`,
|
||||
"",
|
||||
"2. Or use the ai-multimodal skill:",
|
||||
` python .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \\`,
|
||||
` --files "${resolvedPath}" \\`,
|
||||
` --task analyze \\`,
|
||||
` --prompt "Extract the 10 most dominant colors as hex values"`,
|
||||
"2. Or use an image-analysis skill (e.g. ai-multimodal, if installed) to extract the 10 most dominant colors as hex values",
|
||||
"",
|
||||
"3. Then compare extracted colors against brand palette",
|
||||
],
|
||||
|
||||
@ -17,7 +17,10 @@ const { execFileSync } = require('child_process');
|
||||
const BRAND_GUIDELINES = 'docs/brand-guidelines.md';
|
||||
const DESIGN_TOKENS_JSON = 'assets/design-tokens.json';
|
||||
const DESIGN_TOKENS_CSS = 'assets/design-tokens.css';
|
||||
const GENERATE_TOKENS_SCRIPT = '.claude/skills/design-system/scripts/generate-tokens.cjs';
|
||||
// Sibling sub-skill, resolved from this file's location so it works in every
|
||||
// install context (plugin cache, project or --global CLI install), not only
|
||||
// when the process runs from a project root that contains .claude/skills/.
|
||||
const GENERATE_TOKENS_SCRIPT = path.resolve(__dirname, '..', '..', 'design-system', 'scripts', 'generate-tokens.cjs');
|
||||
|
||||
/**
|
||||
* Extract color info from brand guidelines markdown
|
||||
@ -96,15 +99,34 @@ function generateColorScale(baseHex, darkHex, lightHex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust hex color brightness
|
||||
* Adjust hex color brightness.
|
||||
*
|
||||
* Blends each channel proportionally toward white (percent > 0) or toward
|
||||
* black (percent < 0) instead of adding/subtracting a flat 255*percent to
|
||||
* every channel. The flat-shift approach clamped all three channels to 0
|
||||
* (or 255) whenever the base color's channels were already low (or high)
|
||||
* relative to the shift — e.g. darkening a dark brand color like #4A3228
|
||||
* by -0.3/-0.45/-0.6 produced #000000 for all three, collapsing shades
|
||||
* 700/800/900 into an identical, useless black.
|
||||
*/
|
||||
function adjustBrightness(hex, percent) {
|
||||
if (typeof hex !== 'string') return '#000000';
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent)));
|
||||
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent)));
|
||||
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + Math.round(255 * percent)));
|
||||
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
const r = (num >> 16) & 0xFF;
|
||||
const g = (num >> 8) & 0xFF;
|
||||
const b = num & 0xFF;
|
||||
|
||||
const adjustChannel = (channel) => {
|
||||
const adjusted = percent >= 0
|
||||
? channel + (255 - channel) * percent
|
||||
: channel * (1 + percent);
|
||||
return Math.min(255, Math.max(0, Math.round(adjusted)));
|
||||
};
|
||||
|
||||
const newR = adjustChannel(r);
|
||||
const newG = adjustChannel(g);
|
||||
const newB = adjustChannel(b);
|
||||
return `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,7 +251,7 @@ function main() {
|
||||
console.log(`✅ Updated: ${DESIGN_TOKENS_JSON}`);
|
||||
|
||||
// Regenerate CSS
|
||||
const generateScript = path.resolve(process.cwd(), GENERATE_TOKENS_SCRIPT);
|
||||
const generateScript = GENERATE_TOKENS_SCRIPT;
|
||||
if (fs.existsSync(generateScript)) {
|
||||
try {
|
||||
execFileSync('node', [generateScript, '--config', DESIGN_TOKENS_JSON, '-o', DESIGN_TOKENS_CSS], {
|
||||
@ -240,6 +262,8 @@ function main() {
|
||||
} catch (e) {
|
||||
console.error('⚠️ Failed to regenerate CSS:', e.message);
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ design-system sub-skill not found at ${generateScript}; ${DESIGN_TOKENS_CSS} not regenerated`);
|
||||
}
|
||||
|
||||
console.log('\n✨ Brand sync complete!');
|
||||
|
||||
@ -24,22 +24,33 @@ TOKENS_STARTER = (
|
||||
)
|
||||
|
||||
|
||||
def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
def _run(tmp_path: Path) -> subprocess.CompletedProcess:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node not available")
|
||||
return subprocess.run(
|
||||
[node, str(SCRIPT)],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# sync-brand-to-tokens.cjs prints emoji. Without an explicit encoding,
|
||||
# `text=True` decodes the pipe with the locale codec, and several of
|
||||
# those emoji have UTF-8 bytes that cp1252 has no character for
|
||||
# (0x8F in the warning, 0x9D in the error, 0x8F in the dry-run notice).
|
||||
# Decoding then raises inside subprocess's reader thread, the stream
|
||||
# comes back as None, and assertions against it fail with a TypeError
|
||||
# that hides the real result.
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "assets").mkdir()
|
||||
shutil.copy(BRAND_STARTER, tmp_path / "docs" / "brand-guidelines.md")
|
||||
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
||||
|
||||
result = subprocess.run(
|
||||
[node, str(SCRIPT)],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
result = _run(tmp_path)
|
||||
|
||||
# Must not crash (the bug raised an unhandled TypeError).
|
||||
assert "TypeError" not in result.stderr, result.stderr
|
||||
@ -50,3 +61,62 @@ def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
assert primitive["primary"]["500"]["$value"] == "#2563EB"
|
||||
assert primitive["secondary"]["500"]["$value"] == "#8B5CF6"
|
||||
assert primitive["accent"]["500"]["$value"] == "#10B981"
|
||||
|
||||
# #474: the sibling design-system script is resolved from this skill's own
|
||||
# location, so the CSS regeneration must run even though tmp_path has no
|
||||
# .claude/skills/ tree. Before the fix it was resolved from the working
|
||||
# directory and silently skipped in every layout but a project install.
|
||||
assert "Regenerated" in result.stdout, result.stdout
|
||||
css = tmp_path / "assets" / "design-tokens.css"
|
||||
assert css.exists() and css.stat().st_size > 0
|
||||
|
||||
|
||||
def test_dark_base_color_does_not_collapse_shades_to_black(tmp_path):
|
||||
"""adjustBrightness() used to add/subtract a flat 255*percent per channel.
|
||||
|
||||
For a dark base color (channels already close to 0), darkening by
|
||||
-0.3/-0.45/-0.6 clamped every channel to 0, so shades 700, 800, and 900
|
||||
all came back as the identical, useless #000000 instead of a graded dark
|
||||
scale. This runs the sync against a dark, coffee-roastery-style brand
|
||||
color and asserts the three shades stay distinct and non-black.
|
||||
"""
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "assets").mkdir()
|
||||
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
||||
(tmp_path / "docs" / "brand-guidelines.md").write_text(
|
||||
"## Quick Reference\n\n"
|
||||
"| Element | Value |\n"
|
||||
"|---------|-------|\n"
|
||||
"| Primary Color | #4A3228 |\n"
|
||||
"| Secondary Color | #C08A3E |\n"
|
||||
"| Accent Color | #6B8F71 |\n"
|
||||
)
|
||||
|
||||
result = _run(tmp_path)
|
||||
assert result.returncode == 0, result.stderr + result.stdout
|
||||
|
||||
tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text())
|
||||
primary = tokens["primitive"]["color"]["primary"]
|
||||
dark_shades = [primary[shade]["$value"] for shade in ("700", "800", "900")]
|
||||
|
||||
assert len(set(dark_shades)) == 3, (
|
||||
f"expected three distinct dark shades, got {dark_shades}"
|
||||
)
|
||||
assert "#000000" not in dark_shades, dark_shades
|
||||
|
||||
|
||||
def test_reports_missing_guidelines_without_breaking_the_harness(tmp_path):
|
||||
"""The missing-guidelines path is the one that breaks a locale-decoded pipe.
|
||||
|
||||
It is also the default state of any project that has not run the brand skill
|
||||
yet, so it is the path a contributor hits first. The script prints its error
|
||||
with a leading emoji whose UTF-8 encoding contains 0x9D; cp1252 has no
|
||||
character there, so on Windows this test fails with
|
||||
``TypeError: argument of type 'NoneType' is not a container`` unless the
|
||||
subprocess pipe is pinned to UTF-8.
|
||||
"""
|
||||
result = _run(tmp_path)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert result.stderr is not None
|
||||
assert "Brand guidelines not found" in result.stderr
|
||||
|
||||
@ -48,6 +48,10 @@ Component (component-specific)
|
||||
--button-bg: var(--color-primary);
|
||||
```
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Generate tokens:**
|
||||
|
||||
@ -15,12 +15,16 @@ const path = require('path');
|
||||
|
||||
// Find project root (look for assets/design-tokens.css)
|
||||
function findProjectRoot(startDir) {
|
||||
// Walk up until dirname stops changing: on Windows the root is 'C:\', so a
|
||||
// `dir !== '/'` guard never terminates.
|
||||
let dir = startDir;
|
||||
while (dir !== '/') {
|
||||
for (;;) {
|
||||
if (fs.existsSync(path.join(dir, 'assets', 'design-tokens.css'))) {
|
||||
return dir;
|
||||
}
|
||||
dir = path.dirname(dir);
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -9,10 +9,32 @@ import json
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Project root relative to this script
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
||||
# The skill can be installed outside the project it operates on (user-level
|
||||
# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from
|
||||
# this file's location. Resolve it from the working directory instead -- the same
|
||||
# convention generate-tokens.cjs and validate-tokens.cjs already use via
|
||||
# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly.
|
||||
def _find_project_root():
|
||||
override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT')
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
start = Path.cwd().resolve()
|
||||
markers = (
|
||||
Path('assets') / 'design-tokens.json',
|
||||
Path('assets') / 'design-tokens.css',
|
||||
Path('package.json'),
|
||||
Path('.git'),
|
||||
)
|
||||
for candidate in (start, *start.parents):
|
||||
if any((candidate / marker).exists() for marker in markers):
|
||||
return candidate
|
||||
return start
|
||||
|
||||
|
||||
PROJECT_ROOT = _find_project_root()
|
||||
TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv'
|
||||
|
||||
|
||||
@ -15,11 +15,43 @@ Usage:
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
# Project root relative to this script
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
||||
# The skill can be installed outside the project it operates on (user-level
|
||||
# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from
|
||||
# this file's location. Resolve it from the working directory instead -- the same
|
||||
# convention generate-tokens.cjs and validate-tokens.cjs already use via
|
||||
# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly.
|
||||
def _find_project_root():
|
||||
override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT')
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
start = Path.cwd().resolve()
|
||||
markers = (
|
||||
Path('assets') / 'design-tokens.json',
|
||||
Path('assets') / 'design-tokens.css',
|
||||
Path('package.json'),
|
||||
Path('.git'),
|
||||
)
|
||||
for candidate in (start, *start.parents):
|
||||
if any((candidate / marker).exists() for marker in markers):
|
||||
return candidate
|
||||
return start
|
||||
|
||||
|
||||
PROJECT_ROOT = _find_project_root()
|
||||
|
||||
# Force UTF-8 on stdout/stderr: this script prints emoji, which raises
|
||||
# UnicodeEncodeError on a Windows console (cp1252). Same guard as
|
||||
# src/ui-ux-pro-max/scripts/search.py.
|
||||
import io
|
||||
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css'
|
||||
|
||||
|
||||
@ -13,6 +13,16 @@ from slide_search_core import (
|
||||
get_color_for_emotion, get_background_config
|
||||
)
|
||||
|
||||
# Force UTF-8 on stdout/stderr: this script prints emoji, which raises
|
||||
# UnicodeEncodeError on a Windows console (cp1252). Same guard as
|
||||
# src/ui-ux-pro-max/scripts/search.py.
|
||||
import io
|
||||
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
|
||||
|
||||
def format_result(result, domain):
|
||||
"""Format a single search result for display"""
|
||||
|
||||
@ -20,11 +20,15 @@ def _run(tmp_path: Path, css: str) -> subprocess.CompletedProcess:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node not available")
|
||||
(tmp_path / "sample.css").write_text(css)
|
||||
(tmp_path / "sample.css").write_text(css, encoding="utf-8")
|
||||
return subprocess.run(
|
||||
[node, str(SCRIPT), "--dir", str(tmp_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# validate-tokens.cjs prints emoji; without an explicit encoding Python
|
||||
# decodes the pipe with the locale codec (cp1252 on Windows), which
|
||||
# raises in the reader thread and leaves result.stdout set to None.
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: design
|
||||
description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads."
|
||||
description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads."
|
||||
argument-hint: "[design-type] [context]"
|
||||
license: MIT
|
||||
metadata:
|
||||
@ -27,9 +27,9 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot
|
||||
|
||||
| Task | Sub-skill | Details |
|
||||
|------|-----------|---------|
|
||||
| Brand identity, voice, assets | `brand` | External skill |
|
||||
| Tokens, specs, CSS vars | `design-system` | External skill |
|
||||
| shadcn/ui, Tailwind, code | `ui-styling` | External skill |
|
||||
| Brand identity, voice, assets | `brand` | Bundled sibling skill |
|
||||
| Tokens, specs, CSS vars | `design-system` | Bundled sibling skill |
|
||||
| shadcn/ui, Tailwind, code | `ui-styling` | Bundled sibling skill |
|
||||
| Logo creation, AI generation | Logo (built-in) | `references/logo-design.md` |
|
||||
| CIP mockups, deliverables | CIP (built-in) | `references/cip-design.md` |
|
||||
| Presentations, pitch decks | Slides (built-in) | `references/slides.md` |
|
||||
@ -37,22 +37,27 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot
|
||||
| Social media images/photos | Social Photos (built-in) | `references/social-photos-design.md` |
|
||||
| SVG icons, icon sets | Icon (built-in) | `references/icon-design.md` |
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Logo Design (Built-in)
|
||||
|
||||
55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana models.
|
||||
55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana, Atlas
|
||||
Cloud, and MuAPI image generation.
|
||||
|
||||
### Logo: Generate Design Brief
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
python3 scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
```
|
||||
|
||||
### Logo: Search Styles/Colors/Industries
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech professional" --domain color
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --domain industry
|
||||
python3 scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 scripts/logo/search.py "tech professional" --domain color
|
||||
python3 scripts/logo/search.py "healthcare medical" --domain industry
|
||||
```
|
||||
|
||||
### Logo: Generate with AI
|
||||
@ -60,13 +65,16 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do
|
||||
**ALWAYS** generate output logo images with white background.
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
```
|
||||
|
||||
**IMPORTANT:** When scripts fail, try to fix them directly.
|
||||
|
||||
After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`. If yes, invoke `/ui-ux-pro-max` for gallery.
|
||||
After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`. If yes, use the bundled `ui-ux-pro-max` skill for the gallery.
|
||||
|
||||
## CIP Design (Built-in)
|
||||
|
||||
@ -75,32 +83,32 @@ After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`.
|
||||
### CIP: Generate Brief
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
python3 scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
```
|
||||
|
||||
### CIP: Search Domains
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "office reception" --domain mockup
|
||||
python3 scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 scripts/cip/search.py "office reception" --domain mockup
|
||||
```
|
||||
|
||||
### CIP: Generate Mockups
|
||||
|
||||
```bash
|
||||
# With logo (RECOMMENDED)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
|
||||
# Full CIP set
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
|
||||
# Pro model (4K text)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
|
||||
# Without logo
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
python3 scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
```
|
||||
|
||||
Models: `flash` (default, `gemini-2.5-flash-image`), `pro` (`gemini-3-pro-image-preview`)
|
||||
@ -108,7 +116,7 @@ Models: `flash` (default, `gemini-2.5-flash-image`), `pro` (`gemini-3-pro-image-
|
||||
### CIP: Render HTML Presentation
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
```
|
||||
|
||||
**Tip:** If no logo exists, use Logo Design section above first.
|
||||
@ -131,16 +139,16 @@ Load `references/slides-create.md` for the creation workflow.
|
||||
|
||||
## Banner Design (Built-in)
|
||||
|
||||
22 art direction styles across social, ads, web, print. Uses `frontend-design`, `ai-artist`, `ai-multimodal`, `chrome-devtools` skills.
|
||||
22 art direction styles across social, ads, web, print. This workflow needs nothing outside the bundle: `references/banner-sizes-and-styles.md` and the bundled `ui-ux-pro-max` skill for style and palette guidance. Browser research, image generation, and screenshot capture are optional runtime capabilities; when unavailable, use supplied assets, CSS-built visuals, and the runtime's standard preview or capture workflow.
|
||||
|
||||
Load `references/banner-sizes-and-styles.md` for complete sizes and styles reference.
|
||||
|
||||
### Banner: Workflow
|
||||
|
||||
1. **Gather requirements** via `AskUserQuestion` — purpose, platform, content, brand, style, quantity
|
||||
2. **Research** — Activate `ui-ux-pro-max`, browse Pinterest for references
|
||||
3. **Design** — Create HTML/CSS banner with `frontend-design`, generate visuals with `ai-artist`/`ai-multimodal`
|
||||
4. **Export** — Screenshot to PNG at exact dimensions via `chrome-devtools`
|
||||
2. **Research** — Read `references/banner-sizes-and-styles.md` and use the bundled `ui-ux-pro-max` skill for style and palette guidance; if browser research is available and permitted, collect 3–5 references
|
||||
3. **Design** — Create the HTML/CSS banner at exact platform dimensions; use supplied assets or CSS-built visuals, or an authorized image-generation capability if the runtime provides one
|
||||
4. **Export** — Capture PNG at exact dimensions with the runtime's browser or screenshot capability; if unavailable, deliver the HTML/CSS source and mark PNG export as pending
|
||||
5. **Present** — Show all options side-by-side, iterate on feedback
|
||||
|
||||
### Banner: Quick Size Reference
|
||||
@ -183,21 +191,21 @@ Load `references/banner-sizes-and-styles.md` for complete sizes and styles refer
|
||||
### Icon: Generate Single Icon
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
python3 scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
```
|
||||
|
||||
### Icon: Generate Batch Variations
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
```
|
||||
|
||||
### Icon: Multi-size Export
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
```
|
||||
|
||||
### Icon: Top Styles
|
||||
@ -216,20 +224,20 @@ python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile"
|
||||
|
||||
## Social Photos (Built-in)
|
||||
|
||||
Multi-platform social image design: HTML/CSS → screenshot export. Uses `ui-ux-pro-max`, `brand`, `design-system`, `chrome-devtools` skills.
|
||||
Multi-platform social image design: HTML/CSS → screenshot export. Uses the bundled `ui-ux-pro-max`, `brand`, and `design-system` skills; screenshot export runs through Chrome headless, Playwright, or Puppeteer (see the reference).
|
||||
|
||||
Load `references/social-photos-design.md` for sizes, templates, best practices.
|
||||
|
||||
### Social Photos: Workflow
|
||||
|
||||
1. **Orchestrate** — `project-management` skill for TODO tasks; parallel subagents for independent work
|
||||
1. **Orchestrate** — Track the steps below with the runtime's native task list; parallel subagents for independent work
|
||||
2. **Analyze** — Parse prompt: subject, platforms, style, brand context, content elements
|
||||
3. **Ideate** — 3-5 concepts, present via `AskUserQuestion`
|
||||
4. **Design** — `/ckm:brand` → `/ckm:design-system` → randomly invoke `/ck:ui-ux-pro-max` OR `/ck:frontend-design`; HTML per idea × size
|
||||
5. **Export** — `chrome-devtools` or Playwright screenshot at exact px (2x deviceScaleFactor)
|
||||
6. **Verify** — Use Chrome MCP or `chrome-devtools` skill to visually inspect exported designs; fix layout/styling issues and re-export
|
||||
4. **Design** — bundled `brand` → `design-system` → `ui-ux-pro-max` skills; HTML per idea × size
|
||||
5. **Export** — Chrome headless, Playwright, or Puppeteer screenshot at exact px (2x device scale factor where the tool supports it; see the reference)
|
||||
6. **Verify** — Open the exported PNGs in an available browser or image viewer and inspect them; fix layout/styling issues and re-export
|
||||
7. **Report** — Summary to `plans/reports/` with design decisions
|
||||
8. **Organize** — Invoke `assets-organizing` skill to sort output files and reports
|
||||
8. **Organize** — Sort output files and reports into the project's asset directories
|
||||
|
||||
### Social Photos: Key Sizes
|
||||
|
||||
@ -303,11 +311,23 @@ python3 --version || python --version
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-key" # https://aistudio.google.com/apikey
|
||||
pip install google-genai pillow
|
||||
|
||||
# Optional MuAPI provider (no extra Python package required)
|
||||
export MUAPI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
MuAPI uses the asynchronous model endpoint and prediction result API. See the
|
||||
[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication
|
||||
and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana)
|
||||
or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro)
|
||||
for the current model-specific schemas. The logo generator supports both documented
|
||||
model slugs and sends their shared required `prompt` plus optional `aspect_ratio`
|
||||
fields; the Pro model also accepts an optional `resolution` field that this focused
|
||||
logo workflow leaves at the provider default.
|
||||
|
||||
> **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`).
|
||||
|
||||
## Integration
|
||||
|
||||
**External sub-skills:** brand, design-system, ui-styling
|
||||
**Related Skills:** frontend-design, ui-ux-pro-max, ai-multimodal, chrome-devtools
|
||||
**Bundled sub-skills:** brand, design-system, ui-styling
|
||||
**Related Skills:** ui-ux-pro-max
|
||||
|
||||
@ -16,49 +16,49 @@ Corporate Identity Program design with 50+ deliverables, 20 styles, 20 industrie
|
||||
### CIP Brief (Start Here)
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
python3 scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
```
|
||||
|
||||
### Search Domains
|
||||
|
||||
```bash
|
||||
# Deliverables
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
|
||||
# Design styles
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
|
||||
# Industry guidelines
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
|
||||
# Mockup contexts
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "office reception" --domain mockup
|
||||
python3 scripts/cip/search.py "office reception" --domain mockup
|
||||
```
|
||||
|
||||
### Generate Mockups
|
||||
|
||||
```bash
|
||||
# With logo (RECOMMENDED - uses image editing)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
|
||||
# Full CIP set with logo
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
|
||||
# Pro model for 4K text rendering
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
|
||||
# Custom deliverables with aspect ratio
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "GreenLeaf" --logo logo.png --industry "organic food" --deliverables "letterhead,packaging,vehicle" --ratio 16:9
|
||||
python3 scripts/cip/generate.py --brand "GreenLeaf" --logo logo.png --industry "organic food" --deliverables "letterhead,packaging,vehicle" --ratio 16:9
|
||||
|
||||
# Without logo (AI generates interpretation)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
python3 scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
```
|
||||
|
||||
### Render HTML Presentation
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip --output presentation.html
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip --output presentation.html
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
@ -164,14 +164,14 @@ Application Code
|
||||
|
||||
**Brand:**
|
||||
```bash
|
||||
node .claude/skills/brand/scripts/inject-brand-context.cjs
|
||||
node .claude/skills/brand/scripts/validate-asset.cjs <path>
|
||||
node ../brand/scripts/inject-brand-context.cjs
|
||||
node ../brand/scripts/validate-asset.cjs <path>
|
||||
```
|
||||
|
||||
**Tokens:**
|
||||
```bash
|
||||
node .claude/skills/design-system/scripts/generate-tokens.cjs -c tokens.json
|
||||
node .claude/skills/design-system/scripts/validate-tokens.cjs -d src/
|
||||
node ../design-system/scripts/generate-tokens.cjs -c tokens.json
|
||||
node ../design-system/scripts/validate-tokens.cjs -d src/
|
||||
```
|
||||
|
||||
**Components:**
|
||||
|
||||
@ -13,29 +13,29 @@ AI-powered SVG icon generation using Gemini 3.1 Pro Preview. 15 styles, 12 categ
|
||||
### Generate Single Icon
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
python3 scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
```
|
||||
|
||||
### Generate Batch Variations
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "notification bell" --batch 6 --style outlined --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "notification bell" --batch 6 --style outlined --output-dir ./icons
|
||||
```
|
||||
|
||||
### Generate Multiple Sizes
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
```
|
||||
|
||||
### List Styles/Categories
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --list-styles
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --list-categories
|
||||
python3 scripts/icon/generate.py --list-styles
|
||||
python3 scripts/icon/generate.py --list-categories
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# Logo Design Reference
|
||||
|
||||
AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Uses Gemini Nano Banana models.
|
||||
AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud and MuAPI are also available as explicit opt-in providers.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs |
|
||||
| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana |
|
||||
| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana, Atlas Cloud, or MuAPI |
|
||||
| `scripts/logo/core.py` | BM25 search engine for logo data |
|
||||
|
||||
## Commands
|
||||
@ -15,20 +15,20 @@ AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. U
|
||||
### Design Brief (Start Here)
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
python3 scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
```
|
||||
|
||||
### Search Domains
|
||||
|
||||
```bash
|
||||
# Styles
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 scripts/logo/search.py "minimalist clean" --domain style
|
||||
|
||||
# Color palettes
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech professional" --domain color
|
||||
python3 scripts/logo/search.py "tech professional" --domain color
|
||||
|
||||
# Industry guidelines
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --domain industry
|
||||
python3 scripts/logo/search.py "healthcare medical" --domain industry
|
||||
```
|
||||
|
||||
### Generate Logo
|
||||
@ -36,11 +36,14 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do
|
||||
**ALWAYS** use white background for output logos.
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
```
|
||||
|
||||
Options: `--style`, `--industry`, `--prompt`
|
||||
Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model`, `--muapi-model`
|
||||
|
||||
## Available Styles
|
||||
|
||||
@ -76,7 +79,7 @@ Options: `--style`, `--industry`, `--prompt`
|
||||
1. Generate design brief → `scripts/logo/search.py --design-brief`
|
||||
2. Generate logo variations → `scripts/logo/generate.py --brand --style --industry`
|
||||
3. Ask user about HTML preview → `AskUserQuestion` tool
|
||||
4. If yes, invoke `/ui-ux-pro-max` for HTML gallery
|
||||
4. If yes, use the bundled `ui-ux-pro-max` skill for the HTML gallery
|
||||
|
||||
## Detailed References
|
||||
|
||||
@ -89,4 +92,19 @@ Options: `--style`, `--industry`, `--prompt`
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-key"
|
||||
pip install google-genai
|
||||
|
||||
# Optional Atlas Cloud provider (no extra Python package required)
|
||||
export ATLASCLOUD_API_KEY="your-key"
|
||||
|
||||
# Optional MuAPI provider (no extra Python package required)
|
||||
export MUAPI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
MuAPI uses the asynchronous model endpoint and prediction result API. See the
|
||||
[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication
|
||||
and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana)
|
||||
or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro)
|
||||
for the current model-specific schemas. The logo generator supports both documented
|
||||
model slugs and sends their shared required `prompt` plus optional `aspect_ratio`
|
||||
fields; the Pro model also accepts an optional `resolution` field that this focused
|
||||
logo workflow leaves at the provider default.
|
||||
|
||||
@ -66,10 +66,10 @@
|
||||
|
||||
```bash
|
||||
# Find formula for slide type
|
||||
python .claude/skills/design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
python ../design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
|
||||
# Get emotion-appropriate formula
|
||||
python .claude/skills/design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
python ../design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@ -113,10 +113,10 @@
|
||||
|
||||
```bash
|
||||
# Find layout for specific use
|
||||
python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
python ../design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
|
||||
# Contextual recommendation
|
||||
python .claude/skills/design-system/scripts/search-slides.py "traction slide" \
|
||||
python ../design-system/scripts/search-slides.py "traction slide" \
|
||||
--context --position 4 --total 10
|
||||
```
|
||||
|
||||
|
||||
@ -76,10 +76,10 @@ Pattern breaks at 1/3 and 2/3 positions create engagement peaks.
|
||||
|
||||
```bash
|
||||
# Find strategy by goal
|
||||
python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
python ../design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
|
||||
# Get emotion arc
|
||||
python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
python ../design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
```
|
||||
|
||||
## Matching Strategy to Context
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Social Photos Design Guide
|
||||
|
||||
Design social media images via HTML/CSS rendering + screenshot export. Orchestrates `ui-ux-pro-max`, `brand`, `design-system`, and `chrome-devtools` skills.
|
||||
Design social media images via HTML/CSS rendering + screenshot export. Orchestrates the bundled `ui-ux-pro-max`, `brand`, and `design-system` skills; screenshot export runs through Chrome headless, Playwright, or Puppeteer.
|
||||
|
||||
## Platform Sizes
|
||||
|
||||
@ -22,9 +22,9 @@ Design social media images via HTML/CSS rendering + screenshot export. Orchestra
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Activate Project Management
|
||||
### Step 1: Plan the Work
|
||||
|
||||
Invoke `project-management` skill to create persistent TODO tasks via Claude's native task orchestration. Break down into:
|
||||
Create TODO tasks with the runtime's native task list. Break down into:
|
||||
- Requirement analysis task
|
||||
- Idea generation task(s)
|
||||
- HTML design task(s) — can parallelize per size/variant
|
||||
@ -55,11 +55,11 @@ Present ideas to user via `AskUserQuestion` for approval before designing.
|
||||
|
||||
### Step 4: Design HTML Files
|
||||
|
||||
Activate these skills in sequence:
|
||||
Use these bundled skills in sequence:
|
||||
|
||||
1. **`/ckm:brand`** — Extract brand colors, fonts, voice from user's project
|
||||
2. **`/ckm:design-system`** — Get design tokens (spacing, typography scale, color palette)
|
||||
3. **Randomly invoke ONE of:** `/ck:ui-ux-pro-max` OR `/ck:frontend-design` — for layout, hierarchy, visual balance. Pick one at random each run for design variety.
|
||||
1. **`brand`** — Extract brand colors, fonts, voice from user's project
|
||||
2. **`design-system`** — Get design tokens (spacing, typography scale, color palette)
|
||||
3. **`ui-ux-pro-max`** — Layout, hierarchy, visual balance; search a different style, palette, or font pairing per concept for design variety.
|
||||
|
||||
For each approved idea + each target size, create an HTML file:
|
||||
|
||||
@ -119,7 +119,7 @@ output/social-photos/
|
||||
|
||||
### Step 5: Screenshot Export
|
||||
|
||||
Use Chrome headless, `chrome-devtools` skill, or Playwright/Puppeteer to capture exact-size screenshots.
|
||||
Use Chrome headless, Playwright, or Puppeteer to capture exact-size screenshots.
|
||||
|
||||
**IMPORTANT:** Always add a delay (3-5s) after page load for fonts/images to fully render before capture.
|
||||
|
||||
@ -145,9 +145,9 @@ Key flags:
|
||||
- `--hide-scrollbars` — prevents scrollbar artifacts in screenshots
|
||||
- `--window-size=WxH` — sets exact pixel dimensions
|
||||
|
||||
#### Option B: chrome-devtools skill
|
||||
#### Option B: Browser automation provided by the runtime
|
||||
|
||||
Invoke `/chrome-devtools` with instructions to:
|
||||
If the runtime offers a browser-automation or screenshot capability (for example a browser MCP server), use it to:
|
||||
1. Open each HTML file in browser
|
||||
2. Set viewport to exact target dimensions
|
||||
3. Wait 3-5s for fonts/images to fully load
|
||||
@ -210,7 +210,7 @@ async function captureScreenshots(htmlFiles) {
|
||||
|
||||
### Step 6: Verify & Fix Designs
|
||||
|
||||
Use Chrome MCP or `chrome-devtools` skill to visually inspect each exported PNG:
|
||||
Open each exported PNG in an available browser or image viewer and inspect it:
|
||||
|
||||
1. Open exported screenshots and check for layout/styling issues
|
||||
2. Verify: fonts rendered correctly, colors match brand, text readable at thumbnail size
|
||||
@ -227,7 +227,7 @@ Use Chrome MCP or `chrome-devtools` skill to visually inspect each exported PNG:
|
||||
|
||||
### Step 7: Generate Summary Report
|
||||
|
||||
Save report to `plans/reports/` with naming pattern from session hooks.
|
||||
Save the report as `plans/reports/{YYMMDD}-social-photos-{topic}.md`.
|
||||
|
||||
Report structure:
|
||||
|
||||
@ -269,9 +269,9 @@ Report structure:
|
||||
|
||||
### Step 8: Organize Output
|
||||
|
||||
Invoke `assets-organizing` skill to organize all output files and reports:
|
||||
Organize all output files and reports:
|
||||
- Move/copy exported PNGs to proper asset directories
|
||||
- Ensure reports are in `plans/reports/` with correct naming
|
||||
- Ensure reports are in `plans/reports/` under the name from Step 7
|
||||
- Clean up intermediate HTML files if requested
|
||||
- Tag outputs with metadata (platform, size, concept name)
|
||||
|
||||
@ -326,4 +326,4 @@ This sub-skill handles social media image design only. Does NOT handle:
|
||||
- Animation/motion graphics
|
||||
- Print production files (CMYK, bleed)
|
||||
- Direct social media posting/scheduling
|
||||
- AI image generation (use `ai-artist` skill for that)
|
||||
- AI image generation (supply images, or generate them with a separate authorized capability)
|
||||
|
||||
@ -427,7 +427,10 @@ Image Editing Mode:
|
||||
action = check_logo_required(args.brand, skip_prompt=args.no_logo_prompt)
|
||||
if action == 'generate':
|
||||
print("\n💡 To generate a logo, use the logo-design skill:")
|
||||
print(f" python ~/.claude/skills/design/scripts/logo/generate.py --brand \"{args.brand}\" --industry \"{args.industry}\"")
|
||||
# Resolved from this file so the hint is correct from any cwd and in
|
||||
# every install layout (plugin cache, project or --global install).
|
||||
logo_script = Path(__file__).resolve().parents[1] / "logo" / "generate.py"
|
||||
print(f" python \"{logo_script}\" --brand \"{args.brand}\" --industry \"{args.industry}\"")
|
||||
print("\n Then re-run this command with --logo <generated_logo.png>")
|
||||
sys.exit(0)
|
||||
elif action == 'exit':
|
||||
|
||||
@ -1,29 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Logo Generation Script using Gemini Nano Banana API
|
||||
Uses Gemini 2.5 Flash Image and Gemini 3 Pro Image Preview models
|
||||
"""Logo generation with Gemini, Atlas Cloud, or MuAPI.
|
||||
|
||||
Gemini remains the default provider. Atlas Cloud is opt-in with
|
||||
``--provider atlas`` and uses its asynchronous image generation API. MuAPI is
|
||||
opt-in with ``--provider muapi`` and uses its asynchronous image generation API
|
||||
with the selected model's prompt/aspect-ratio contract.
|
||||
|
||||
Models:
|
||||
- Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency
|
||||
- Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning
|
||||
- MuAPI Nano Banana (--provider muapi): nano-banana - hosted asynchronous image generation
|
||||
|
||||
Usage:
|
||||
python generate.py --prompt "tech startup logo minimalist blue"
|
||||
python generate.py --prompt "coffee shop vintage badge" --style vintage --output logo.png
|
||||
python generate.py --brand "TechFlow" --industry tech --style minimalist
|
||||
python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model
|
||||
python generate.py --brand "TechFlow" --provider atlas
|
||||
python generate.py --brand "TechFlow" --provider muapi
|
||||
python generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
|
||||
Batch mode (generates multiple variants):
|
||||
python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
||||
|
||||
|
||||
# Load environment variables
|
||||
def load_env():
|
||||
@ -31,7 +42,7 @@ def load_env():
|
||||
env_paths = [
|
||||
Path(__file__).parent.parent.parent / ".env",
|
||||
Path.home() / ".claude" / "skills" / ".env",
|
||||
Path.home() / ".claude" / ".env"
|
||||
Path.home() / ".claude" / ".env",
|
||||
]
|
||||
|
||||
for env_path in env_paths:
|
||||
@ -39,29 +50,36 @@ def load_env():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value.strip('"\'')
|
||||
os.environ[key] = value.strip("\"'")
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed.")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
ATLASCLOUD_API_KEY = os.environ.get("ATLASCLOUD_API_KEY")
|
||||
MUAPI_API_KEY = os.environ.get("MUAPI_API_KEY")
|
||||
|
||||
# Gemini "Nano Banana" model configurations for image generation
|
||||
GEMINI_FLASH = "gemini-2.5-flash-image" # Nano Banana: fast, high-volume, low-latency
|
||||
GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quality, advanced reasoning
|
||||
|
||||
# Atlas Cloud model validated against the live model catalog and schema.
|
||||
ATLAS_MODEL = "google/nano-banana-2-lite/text-to-image"
|
||||
ATLAS_API_BASE = "https://api.atlascloud.ai/api/v1"
|
||||
MUAPI_MODEL = "nano-banana"
|
||||
MUAPI_MODELS = ("nano-banana", "nano-banana-pro")
|
||||
MUAPI_API_BASE = "https://api.muapi.ai/api/v1"
|
||||
HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (logo generation)"
|
||||
ATLAS_POLL_INTERVAL = 2
|
||||
ATLAS_MAX_POLLS = 90
|
||||
MUAPI_POLL_INTERVAL = 2
|
||||
MUAPI_MAX_POLLS = 90
|
||||
|
||||
# Supported aspect ratios
|
||||
ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
||||
DEFAULT_ASPECT_RATIO = "1:1" # Square is ideal for logos
|
||||
@ -99,7 +117,7 @@ STYLE_MODIFIERS = {
|
||||
"mascot": "mascot, character, friendly face, personified, memorable figure",
|
||||
"gradient": "gradient, color transition, vibrant, modern digital feel, smooth color flow",
|
||||
"lineart": "line art, single stroke, continuous line, elegant simplicity, wire-frame style",
|
||||
"negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion"
|
||||
"negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion",
|
||||
}
|
||||
|
||||
INDUSTRY_PROMPTS = {
|
||||
@ -112,7 +130,7 @@ INDUSTRY_PROMPTS = {
|
||||
"eco": "eco-friendly, sustainable, natural, green, leaf or earth elements",
|
||||
"education": "education, knowledge, growth, learning, book or cap symbol",
|
||||
"real-estate": "real estate, property, home, roof or building silhouette",
|
||||
"creative": "creative agency, artistic, unique, expressive, colorful"
|
||||
"creative": "creative agency, artistic, unique, expressive, colorful",
|
||||
}
|
||||
|
||||
|
||||
@ -133,101 +151,425 @@ def enhance_prompt(base_prompt, style=None, industry=None, brand_name=None):
|
||||
return LOGO_PROMPT_TEMPLATE.format(prompt=combined)
|
||||
|
||||
|
||||
def generate_logo(prompt, style=None, industry=None, brand_name=None,
|
||||
output_path=None, use_pro=False, aspect_ratio=None):
|
||||
"""Generate a logo using Gemini models with image generation
|
||||
class _SafeRedirectHandler(HTTPRedirectHandler):
|
||||
"""Reject redirects to non-public or non-HTTPS destinations."""
|
||||
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
_validate_public_https_url(newurl)
|
||||
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
||||
|
||||
|
||||
def _validate_public_https_url(url):
|
||||
parsed = urlparse(url)
|
||||
if (
|
||||
parsed.scheme != "https"
|
||||
or not parsed.hostname
|
||||
or parsed.username
|
||||
or parsed.password
|
||||
):
|
||||
raise ValueError("Provider returned an invalid media URL")
|
||||
|
||||
hostname = parsed.hostname.lower().rstrip(".")
|
||||
if hostname == "localhost" or hostname.endswith(
|
||||
(".localhost", ".local", ".internal")
|
||||
):
|
||||
raise ValueError("Provider media URL used a local hostname")
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
return
|
||||
else:
|
||||
if not ip.is_global:
|
||||
raise ValueError("Provider media URL used a non-public address")
|
||||
|
||||
|
||||
def _json_request(
|
||||
url, api_key, method="GET", payload=None, api_key_header="Authorization"
|
||||
):
|
||||
if api_key_header == "Authorization":
|
||||
auth_value = f"Bearer {api_key}"
|
||||
elif api_key_header == "x-api-key":
|
||||
auth_value = api_key
|
||||
else:
|
||||
raise ValueError("Unsupported API key header")
|
||||
|
||||
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
||||
request = Request(
|
||||
url,
|
||||
data=body,
|
||||
method=method,
|
||||
headers={
|
||||
api_key_header: auth_value,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": HTTP_USER_AGENT,
|
||||
**({"Content-Type": "application/json"} if body is not None else {}),
|
||||
},
|
||||
)
|
||||
try:
|
||||
with build_opener(_SafeRedirectHandler()).open(request, timeout=60) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(
|
||||
f"Provider request failed ({exc.code}): {detail[:300]}"
|
||||
) from exc
|
||||
except (URLError, TimeoutError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError(f"Provider request failed: {exc}") from exc
|
||||
|
||||
|
||||
def _atlas_prediction_data(response):
|
||||
if not isinstance(response, dict):
|
||||
raise TypeError("Atlas Cloud returned an invalid response")
|
||||
if response.get("code") not in (None, 0, 200):
|
||||
raise RuntimeError(response.get("message") or "Atlas Cloud request failed")
|
||||
data = response.get("data")
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("Atlas Cloud response did not include prediction data")
|
||||
return data
|
||||
|
||||
|
||||
def _download_atlas_image(url, output_path):
|
||||
_download_image(url, output_path, "image provider")
|
||||
|
||||
|
||||
def _download_image(url, output_path, provider_name):
|
||||
_validate_public_https_url(url)
|
||||
request = Request(
|
||||
url,
|
||||
headers={"Accept": "image/*", "User-Agent": HTTP_USER_AGENT},
|
||||
)
|
||||
try:
|
||||
with build_opener(_SafeRedirectHandler()).open(
|
||||
request, timeout=120
|
||||
) as response:
|
||||
content_type = response.headers.get_content_type()
|
||||
if not content_type.startswith("image/"):
|
||||
raise RuntimeError(
|
||||
f"{provider_name} output is not an image ({content_type})"
|
||||
)
|
||||
image_data = response.read()
|
||||
except (HTTPError, URLError, TimeoutError) as exc:
|
||||
raise RuntimeError(f"Unable to download {provider_name} image: {exc}") from exc
|
||||
|
||||
if not image_data:
|
||||
raise RuntimeError(f"{provider_name} returned an empty image")
|
||||
with open(output_path, "wb") as output_file:
|
||||
output_file.write(image_data)
|
||||
|
||||
|
||||
def _generate_with_atlas(prompt, output_path, aspect_ratio, api_key, model):
|
||||
if not api_key:
|
||||
raise RuntimeError("ATLASCLOUD_API_KEY not set")
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
}
|
||||
response = _json_request(
|
||||
f"{ATLAS_API_BASE}/model/generateImage",
|
||||
api_key,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
)
|
||||
data = _atlas_prediction_data(response)
|
||||
prediction_id = data.get("id")
|
||||
if not prediction_id:
|
||||
raise RuntimeError("Atlas Cloud did not return a prediction ID")
|
||||
|
||||
for poll_number in range(ATLAS_MAX_POLLS + 1):
|
||||
status = str(data.get("status", "")).lower()
|
||||
if status == "completed":
|
||||
outputs = data.get("outputs")
|
||||
if (
|
||||
not isinstance(outputs, list)
|
||||
or not outputs
|
||||
or not isinstance(outputs[0], str)
|
||||
):
|
||||
raise RuntimeError("Atlas Cloud completed without an image URL")
|
||||
_download_atlas_image(outputs[0], output_path)
|
||||
return
|
||||
if status in {"failed", "timeout", "canceled", "cancelled"}:
|
||||
raise RuntimeError(data.get("error") or f"Atlas Cloud prediction {status}")
|
||||
if poll_number == ATLAS_MAX_POLLS:
|
||||
break
|
||||
time.sleep(ATLAS_POLL_INTERVAL)
|
||||
data = _atlas_prediction_data(
|
||||
_json_request(
|
||||
f"{ATLAS_API_BASE}/model/prediction/{prediction_id}",
|
||||
api_key,
|
||||
)
|
||||
)
|
||||
|
||||
raise RuntimeError("Atlas Cloud prediction timed out while polling")
|
||||
|
||||
|
||||
def _muapi_response_objects(response):
|
||||
"""Return the response and common MuAPI envelopes without guessing fields."""
|
||||
if not isinstance(response, dict):
|
||||
raise TypeError("MuAPI returned an invalid response")
|
||||
|
||||
objects = [response]
|
||||
for key in ("data", "output", "result"):
|
||||
value = response.get(key)
|
||||
if isinstance(value, dict) and value not in objects:
|
||||
objects.append(value)
|
||||
return objects
|
||||
|
||||
|
||||
def _muapi_response_value(response, keys):
|
||||
for item in _muapi_response_objects(response):
|
||||
for key in keys:
|
||||
value = item.get(key)
|
||||
if value not in (None, ""):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _muapi_error(response):
|
||||
value = _muapi_response_value(response, ("error", "message", "detail"))
|
||||
if isinstance(value, str):
|
||||
return value[:300]
|
||||
return "MuAPI request failed"
|
||||
|
||||
|
||||
def _muapi_result_url(response):
|
||||
"""Return the documented result URL from the creation response."""
|
||||
for item in _muapi_response_objects(response):
|
||||
urls = item.get("urls")
|
||||
if not isinstance(urls, dict) or "get" not in urls:
|
||||
continue
|
||||
|
||||
result_url = urls.get("get")
|
||||
if not isinstance(result_url, str) or not result_url:
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
)
|
||||
try:
|
||||
_validate_public_https_url(result_url)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
) from exc
|
||||
return result_url
|
||||
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
)
|
||||
|
||||
|
||||
def _muapi_output_url(response):
|
||||
for item in _muapi_response_objects(response):
|
||||
outputs = item.get("outputs")
|
||||
if isinstance(outputs, list):
|
||||
for output in outputs:
|
||||
if isinstance(output, str) and output.startswith("https://"):
|
||||
return output
|
||||
if isinstance(output, dict):
|
||||
for key in ("url", "image_url"):
|
||||
value = output.get(key)
|
||||
if isinstance(value, str) and value.startswith("https://"):
|
||||
return value
|
||||
raise RuntimeError("MuAPI completed without an HTTPS image URL")
|
||||
|
||||
|
||||
def _download_muapi_image(url, output_path):
|
||||
_download_image(url, output_path, "MuAPI")
|
||||
|
||||
|
||||
def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model):
|
||||
if not api_key:
|
||||
raise RuntimeError("MUAPI_API_KEY not set")
|
||||
if model not in MUAPI_MODELS:
|
||||
raise RuntimeError(
|
||||
f"Unsupported MuAPI logo model: {model}. "
|
||||
f"Choose one of: {', '.join(MUAPI_MODELS)}"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
}
|
||||
response = _json_request(
|
||||
f"{MUAPI_API_BASE}/{model}",
|
||||
api_key,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
request_id = _muapi_response_value(response, ("request_id", "id"))
|
||||
if not isinstance(request_id, str) or not request_id:
|
||||
raise RuntimeError("MuAPI did not return a request ID")
|
||||
result_url = _muapi_result_url(response)
|
||||
|
||||
data = response
|
||||
for poll_number in range(MUAPI_MAX_POLLS + 1):
|
||||
status = _muapi_response_value(data, ("status",))
|
||||
normalized_status = str(status or "").lower()
|
||||
if normalized_status in {"completed", "succeeded", "success"}:
|
||||
_download_muapi_image(_muapi_output_url(data), output_path)
|
||||
return
|
||||
if normalized_status in {
|
||||
"failed",
|
||||
"error",
|
||||
"timeout",
|
||||
"canceled",
|
||||
"cancelled",
|
||||
}:
|
||||
raise RuntimeError(f"MuAPI generation {normalized_status}: {_muapi_error(data)}")
|
||||
if poll_number == MUAPI_MAX_POLLS:
|
||||
break
|
||||
|
||||
time.sleep(MUAPI_POLL_INTERVAL)
|
||||
data = _json_request(
|
||||
result_url,
|
||||
api_key,
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
|
||||
raise RuntimeError("MuAPI prediction timed out while polling")
|
||||
|
||||
|
||||
def _generate_with_gemini(prompt, output_path, aspect_ratio, use_pro):
|
||||
if not GEMINI_API_KEY:
|
||||
raise RuntimeError("GEMINI_API_KEY not set")
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"google-genai package not installed; run: pip install google-genai"
|
||||
) from exc
|
||||
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
model = GEMINI_PRO if use_pro else GEMINI_FLASH
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
image_config=types.ImageConfig(aspect_ratio=aspect_ratio),
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HARASSMENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
for part in response.candidates[0].content.parts:
|
||||
if (
|
||||
hasattr(part, "inline_data")
|
||||
and part.inline_data
|
||||
and part.inline_data.mime_type.startswith("image/")
|
||||
):
|
||||
with open(output_path, "wb") as output_file:
|
||||
output_file.write(part.inline_data.data)
|
||||
return
|
||||
raise RuntimeError("Gemini did not return an image")
|
||||
|
||||
|
||||
def generate_logo(
|
||||
prompt,
|
||||
style=None,
|
||||
industry=None,
|
||||
brand_name=None,
|
||||
output_path=None,
|
||||
use_pro=False,
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate a logo using Gemini, Atlas Cloud, or MuAPI image generation.
|
||||
|
||||
Args:
|
||||
aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4"
|
||||
Default is "1:1" (square) for logos.
|
||||
"""
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
print("Error: GEMINI_API_KEY not set")
|
||||
print("Set it with: export GEMINI_API_KEY='your-key'")
|
||||
return None
|
||||
|
||||
# Initialize client
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
|
||||
# Enhance the prompt
|
||||
full_prompt = enhance_prompt(prompt, style, industry, brand_name)
|
||||
|
||||
# Select model
|
||||
model = GEMINI_PRO if use_pro else GEMINI_FLASH
|
||||
model_label = "Nano Banana Pro (gemini-3-pro-image-preview)" if use_pro else "Nano Banana (gemini-2.5-flash-image)"
|
||||
|
||||
# Set aspect ratio (default to 1:1 for logos)
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # noqa: DTZ005
|
||||
brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo"
|
||||
output_path = f"{brand_slug}_{timestamp}.png"
|
||||
|
||||
if provider == "atlas":
|
||||
model_label = f"Atlas Cloud ({atlas_model})"
|
||||
elif provider == "muapi":
|
||||
model_label = f"MuAPI ({muapi_model})"
|
||||
else:
|
||||
model_label = (
|
||||
"Nano Banana Pro (gemini-3-pro-image-preview)"
|
||||
if use_pro
|
||||
else "Nano Banana (gemini-2.5-flash-image)"
|
||||
)
|
||||
|
||||
print(f"Generating logo with {model_label}...")
|
||||
print(f"Aspect ratio: {ratio}")
|
||||
print(f"Prompt: {full_prompt[:150]}...")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Generate image using Gemini with image generation capability
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
image_config=types.ImageConfig(
|
||||
aspect_ratio=ratio
|
||||
),
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HARASSMENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
]
|
||||
if provider == "atlas":
|
||||
_generate_with_atlas(
|
||||
full_prompt,
|
||||
output_path,
|
||||
ratio,
|
||||
ATLASCLOUD_API_KEY,
|
||||
atlas_model,
|
||||
)
|
||||
)
|
||||
|
||||
# Extract image from response
|
||||
image_data = None
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'inline_data') and part.inline_data:
|
||||
if part.inline_data.mime_type.startswith('image/'):
|
||||
image_data = part.inline_data.data
|
||||
break
|
||||
|
||||
if not image_data:
|
||||
print("No image generated. The model may not have produced an image.")
|
||||
print("Try a different prompt or check if the model supports image generation.")
|
||||
return None
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo"
|
||||
output_path = f"{brand_slug}_{timestamp}.png"
|
||||
|
||||
# Save image
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
elif provider == "muapi":
|
||||
_generate_with_muapi(
|
||||
full_prompt,
|
||||
output_path,
|
||||
ratio,
|
||||
MUAPI_API_KEY,
|
||||
muapi_model,
|
||||
)
|
||||
else:
|
||||
_generate_with_gemini(full_prompt, output_path, ratio, use_pro)
|
||||
|
||||
print(f"Logo saved to: {output_path}")
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating logo: {e}")
|
||||
except Exception as exc: # noqa: BLE001 - provider SDK errors are not standardized
|
||||
print(f"Error generating logo: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_context=None, aspect_ratio=None):
|
||||
def generate_batch(
|
||||
prompt,
|
||||
brand_name,
|
||||
count,
|
||||
output_dir,
|
||||
use_pro=False,
|
||||
brand_context=None,
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate multiple logo variants with different styles"""
|
||||
|
||||
# Select appropriate styles for batch generation
|
||||
@ -247,16 +589,22 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
results = []
|
||||
model_label = "Pro" if use_pro else "Flash"
|
||||
model_label = (
|
||||
f"Atlas Cloud ({atlas_model})"
|
||||
if provider == "atlas"
|
||||
else f"MuAPI ({muapi_model})"
|
||||
if provider == "muapi"
|
||||
else f"Nano Banana {'Pro' if use_pro else 'Flash'}"
|
||||
)
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" BATCH LOGO GENERATION: {brand_name}")
|
||||
print(f" Model: Nano Banana {model_label}")
|
||||
print(f" Model: {model_label}")
|
||||
print(f" Aspect Ratio: {ratio}")
|
||||
print(f" Variants: {count}")
|
||||
print(f" Output: {output_dir}")
|
||||
print(f"{'='*60}\n")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
for i in range(min(count, len(batch_styles))):
|
||||
style_key, style_desc = batch_styles[i]
|
||||
@ -267,10 +615,10 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
enhanced_prompt = f"{brand_context}, {enhanced_prompt}"
|
||||
|
||||
# Generate filename
|
||||
filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i+1:02d}.png"
|
||||
filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i + 1:02d}.png"
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
|
||||
print(f"[{i+1}/{count}] Generating {style_key} variant...")
|
||||
print(f"[{i + 1}/{count}] Generating {style_key} variant...")
|
||||
|
||||
result = generate_logo(
|
||||
prompt=enhanced_prompt,
|
||||
@ -279,7 +627,10 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
brand_name=brand_name,
|
||||
output_path=output_path,
|
||||
use_pro=use_pro,
|
||||
aspect_ratio=aspect_ratio
|
||||
aspect_ratio=aspect_ratio,
|
||||
provider=provider,
|
||||
atlas_model=atlas_model,
|
||||
muapi_model=muapi_model,
|
||||
)
|
||||
|
||||
if result:
|
||||
@ -292,31 +643,79 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
if i < count - 1:
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" BATCH COMPLETE: {len(results)}/{count} logos generated")
|
||||
print(f"{'='*60}\n")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate logos using Gemini Nano Banana models")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate logos using Gemini, Atlas Cloud, or MuAPI"
|
||||
)
|
||||
parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt")
|
||||
parser.add_argument("--brand", "-b", type=str, help="Brand name")
|
||||
parser.add_argument("--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style")
|
||||
parser.add_argument("--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type")
|
||||
parser.add_argument(
|
||||
"--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type"
|
||||
)
|
||||
parser.add_argument("--output", "-o", type=str, help="Output file path")
|
||||
parser.add_argument("--output-dir", type=str, help="Output directory for batch generation")
|
||||
parser.add_argument("--batch", type=int, help="Number of logo variants to generate (batch mode)")
|
||||
parser.add_argument("--brand-context", type=str, help="Additional brand context for prompts")
|
||||
parser.add_argument("--pro", action="store_true", help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality")
|
||||
parser.add_argument("--aspect-ratio", "-r", choices=ASPECT_RATIOS, default=DEFAULT_ASPECT_RATIO,
|
||||
help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)")
|
||||
parser.add_argument("--list-styles", action="store_true", help="List available styles")
|
||||
parser.add_argument("--list-industries", action="store_true", help="List available industries")
|
||||
parser.add_argument(
|
||||
"--output-dir", type=str, help="Output directory for batch generation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch", type=int, help="Number of logo variants to generate (batch mode)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brand-context", type=str, help="Additional brand context for prompts"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pro",
|
||||
action="store_true",
|
||||
help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provider",
|
||||
choices=["gemini", "atlas", "muapi"],
|
||||
default="gemini",
|
||||
help="Image provider (default: gemini)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--atlas-model",
|
||||
default=ATLAS_MODEL,
|
||||
help=f"Atlas Cloud image model (default: {ATLAS_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--muapi-model",
|
||||
choices=MUAPI_MODELS,
|
||||
default=MUAPI_MODEL,
|
||||
help=f"MuAPI image model (default: {MUAPI_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--aspect-ratio",
|
||||
"-r",
|
||||
choices=ASPECT_RATIOS,
|
||||
default=DEFAULT_ASPECT_RATIO,
|
||||
help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-styles", action="store_true", help="List available styles"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-industries", action="store_true", help="List available industries"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.provider != "gemini" and args.pro:
|
||||
parser.error(
|
||||
"--pro is only available with --provider gemini; "
|
||||
"use --muapi-model nano-banana-pro for MuAPI"
|
||||
)
|
||||
|
||||
if args.list_styles:
|
||||
print("Available styles:")
|
||||
for style, desc in STYLE_MODIFIERS.items():
|
||||
@ -336,7 +735,9 @@ def main():
|
||||
|
||||
# Batch mode
|
||||
if args.batch:
|
||||
output_dir = args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos"
|
||||
output_dir = (
|
||||
args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos"
|
||||
)
|
||||
generate_batch(
|
||||
prompt=prompt,
|
||||
brand_name=args.brand or "Logo",
|
||||
@ -344,7 +745,10 @@ def main():
|
||||
output_dir=output_dir,
|
||||
use_pro=args.pro,
|
||||
brand_context=args.brand_context,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
else:
|
||||
generate_logo(
|
||||
@ -354,7 +758,10 @@ def main():
|
||||
brand_name=args.brand,
|
||||
output_path=args.output,
|
||||
use_pro=args.pro,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
|
||||
|
||||
|
||||
288
.claude/skills/design/scripts/logo/tests/test_generate.py
Normal file
288
.claude/skills/design/scripts/logo/tests/test_generate.py
Normal file
@ -0,0 +1,288 @@
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import call, patch
|
||||
|
||||
MODULE_PATH = Path(__file__).parents[1] / "generate.py"
|
||||
SPEC = importlib.util.spec_from_file_location("logo_generate", MODULE_PATH)
|
||||
logo_generate = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(logo_generate)
|
||||
|
||||
|
||||
class AtlasGenerationTests(unittest.TestCase):
|
||||
@patch.object(logo_generate, "_download_atlas_image")
|
||||
@patch.object(logo_generate.time, "sleep")
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_atlas_submits_once_and_polls_until_completed(
|
||||
self, json_request, sleep, download
|
||||
):
|
||||
json_request.side_effect = [
|
||||
{"code": 200, "data": {"id": "pred-123", "status": "created"}},
|
||||
{"code": 200, "data": {"id": "pred-123", "status": "processing"}},
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"id": "pred-123",
|
||||
"status": "completed",
|
||||
"outputs": ["https://media.example.com/logo.png"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", "atlas-key", "atlas/model"
|
||||
)
|
||||
|
||||
self.assertEqual(json_request.call_count, 3)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[0],
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/generateImage",
|
||||
"atlas-key",
|
||||
method="POST",
|
||||
payload={
|
||||
"model": "atlas/model",
|
||||
"prompt": "logo prompt",
|
||||
"aspect_ratio": "1:1",
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[1:],
|
||||
[
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/prediction/pred-123",
|
||||
"atlas-key",
|
||||
),
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/prediction/pred-123",
|
||||
"atlas-key",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(sleep.call_count, 2)
|
||||
download.assert_called_once_with(
|
||||
"https://media.example.com/logo.png", "logo.png"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_atlas_does_not_retry_generation_post(self, json_request):
|
||||
json_request.side_effect = RuntimeError("network error")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "network error"):
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", "atlas-key", "atlas/model"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
@patch.object(logo_generate, "_validate_public_https_url")
|
||||
@patch.object(logo_generate, "build_opener")
|
||||
def test_media_download_never_forwards_api_key(self, build_opener, validate):
|
||||
class Headers:
|
||||
@staticmethod
|
||||
def get_content_type():
|
||||
return "image/png"
|
||||
|
||||
class Response:
|
||||
headers = Headers()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
return b"png-bytes"
|
||||
|
||||
build_opener.return_value.open.return_value = Response()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir) / "logo.png"
|
||||
logo_generate._download_atlas_image(
|
||||
"https://media.example.com/logo.png", output
|
||||
)
|
||||
self.assertEqual(output.read_bytes(), b"png-bytes")
|
||||
|
||||
request = build_opener.return_value.open.call_args.args[0]
|
||||
headers = {key.lower(): value for key, value in request.header_items()}
|
||||
self.assertNotIn("authorization", headers)
|
||||
self.assertEqual(headers["accept"], "image/*")
|
||||
self.assertEqual(headers["user-agent"], logo_generate.HTTP_USER_AGENT)
|
||||
validate.assert_called_once_with("https://media.example.com/logo.png")
|
||||
|
||||
def test_atlas_requires_api_key(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "ATLASCLOUD_API_KEY not set"):
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", None, "atlas/model"
|
||||
)
|
||||
|
||||
def test_media_url_rejects_private_addresses(self):
|
||||
with self.assertRaisesRegex(ValueError, "non-public address"):
|
||||
logo_generate._validate_public_https_url("https://127.0.0.1/logo.png")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "local hostname"):
|
||||
logo_generate._validate_public_https_url("https://assets.local/logo.png")
|
||||
|
||||
|
||||
class MuapiGenerationTests(unittest.TestCase):
|
||||
@patch.object(logo_generate, "_download_muapi_image")
|
||||
@patch.object(logo_generate.time, "sleep")
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_submits_once_and_polls_until_completed(
|
||||
self, json_request, sleep, download
|
||||
):
|
||||
json_request.side_effect = [
|
||||
{
|
||||
"id": "req-123",
|
||||
"status": "created",
|
||||
"output": {
|
||||
"urls": {
|
||||
"get": "https://api.muapi.ai/api/v1/results/req-123"
|
||||
}
|
||||
},
|
||||
},
|
||||
{"id": "req-123", "status": "processing"},
|
||||
{
|
||||
"id": "req-123",
|
||||
"status": "completed",
|
||||
"output": {"outputs": ["https://media.example.com/logo.png"]},
|
||||
},
|
||||
]
|
||||
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
self.assertEqual(json_request.call_count, 3)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[0],
|
||||
call(
|
||||
f"{logo_generate.MUAPI_API_BASE}/nano-banana",
|
||||
"muapi-key",
|
||||
method="POST",
|
||||
payload={"prompt": "logo prompt", "aspect_ratio": "1:1"},
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[1:],
|
||||
[
|
||||
call(
|
||||
"https://api.muapi.ai/api/v1/results/req-123",
|
||||
"muapi-key",
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
call(
|
||||
"https://api.muapi.ai/api/v1/results/req-123",
|
||||
"muapi-key",
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(sleep.call_count, 2)
|
||||
download.assert_called_once_with(
|
||||
"https://media.example.com/logo.png", "logo.png"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_does_not_retry_generation_post(self, json_request):
|
||||
json_request.side_effect = RuntimeError("network error")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "network error"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
def test_muapi_requires_key_and_known_model(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "MUAPI_API_KEY not set"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", None, "nano-banana"
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "Unsupported MuAPI logo model"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "unknown-model"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "build_opener")
|
||||
def test_muapi_uses_x_api_key_header(self, build_opener):
|
||||
class Response:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
return b"{}"
|
||||
|
||||
build_opener.return_value.open.return_value = Response()
|
||||
|
||||
logo_generate._json_request(
|
||||
"https://api.muapi.ai/api/v1/nano-banana",
|
||||
"muapi-key",
|
||||
method="POST",
|
||||
payload={"prompt": "logo"},
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
|
||||
request = build_opener.return_value.open.call_args.args[0]
|
||||
headers = {key.lower(): value for key, value in request.header_items()}
|
||||
self.assertEqual(headers["x-api-key"], "muapi-key")
|
||||
self.assertNotIn("authorization", headers)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_reports_failed_prediction(self, json_request):
|
||||
json_request.side_effect = [
|
||||
{
|
||||
"request_id": "req-123",
|
||||
"output": {
|
||||
"urls": {
|
||||
"get": "https://api.muapi.ai/api/v1/results/req-123"
|
||||
}
|
||||
},
|
||||
},
|
||||
{"status": "failed", "error": "invalid prompt"},
|
||||
]
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "invalid prompt"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_requires_creation_result_url(self, json_request):
|
||||
json_request.return_value = {"request_id": "req-123", "status": "created"}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_rejects_invalid_creation_result_url(self, json_request):
|
||||
json_request.return_value = {
|
||||
"request_id": "req-123",
|
||||
"status": "created",
|
||||
"output": {"urls": {"get": "http://api.muapi.ai/results/req-123"}},
|
||||
}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -24,6 +24,10 @@ Strategic HTML presentation design with data visualization.
|
||||
|------------|-------------|-----------|
|
||||
| `create` | Create strategic presentation slides | `references/create.md` |
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## References (Knowledge Base)
|
||||
|
||||
| Topic | File |
|
||||
|
||||
@ -66,10 +66,10 @@
|
||||
|
||||
```bash
|
||||
# Find formula for slide type
|
||||
python .claude/skills/design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
python ../design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
|
||||
# Get emotion-appropriate formula
|
||||
python .claude/skills/design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
python ../design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@ -113,10 +113,10 @@
|
||||
|
||||
```bash
|
||||
# Find layout for specific use
|
||||
python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
python ../design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
|
||||
# Contextual recommendation
|
||||
python .claude/skills/design-system/scripts/search-slides.py "traction slide" \
|
||||
python ../design-system/scripts/search-slides.py "traction slide" \
|
||||
--context --position 4 --total 10
|
||||
```
|
||||
|
||||
|
||||
@ -76,10 +76,10 @@ Pattern breaks at 1/3 and 2/3 positions create engagement peaks.
|
||||
|
||||
```bash
|
||||
# Find strategy by goal
|
||||
python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
python ../design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
|
||||
# Get emotion arc
|
||||
python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
python ../design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
```
|
||||
|
||||
## Matching Strategy to Context
|
||||
|
||||
@ -53,6 +53,10 @@ Use when:
|
||||
- Minimal text, maximum visual impact
|
||||
- Systematic patterns and refined aesthetics
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Component + Styling Setup
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"verifiedAt": "2026-08-13",
|
||||
"verifiedAt": "2026-08-26",
|
||||
"counts": {
|
||||
"styles": {
|
||||
"total": 88,
|
||||
|
||||
@ -120,6 +120,12 @@ if __name__ == "__main__":
|
||||
|
||||
# Design system takes priority
|
||||
if args.design_system:
|
||||
if args.stack:
|
||||
print(
|
||||
f"note: --stack {args.stack} is ignored in --design-system mode; "
|
||||
"run a separate --stack query for stack-specific guidelines",
|
||||
file=sys.stderr,
|
||||
)
|
||||
result = generate_design_system(
|
||||
args.query,
|
||||
args.project_name,
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The catalog snapshot must not depend on the checkout's line endings.
|
||||
|
||||
Regression test for bd19ab9 (#462), where catalog-summary.json was regenerated
|
||||
on a CRLF checkout. Every recorded sha256 was the CRLF hash of the source file,
|
||||
so `verify:data` failed on every LF platform, including CI.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
DATA = REPO / "src/ui-ux-pro-max/data"
|
||||
SNAPSHOT_FILES = (
|
||||
"google-fonts.csv",
|
||||
"google-font-licenses.json",
|
||||
"icons.csv",
|
||||
"phosphor-icons-upstream.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_generator():
|
||||
path = REPO / "scripts" / "generate-catalog-summary.py"
|
||||
spec = importlib.util.spec_from_file_location("generate_catalog_summary", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class CatalogSummaryLineEndingsTest(unittest.TestCase):
|
||||
def test_digest_is_identical_for_lf_and_crlf(self):
|
||||
digest = _load_generator().digest
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lf = Path(tmp) / "lf.csv"
|
||||
crlf = Path(tmp) / "crlf.csv"
|
||||
lf.write_bytes(b"id,name\n1,alpha\n2,beta\n")
|
||||
crlf.write_bytes(b"id,name\r\n1,alpha\r\n2,beta\r\n")
|
||||
self.assertEqual(
|
||||
digest(lf), digest(crlf),
|
||||
"snapshot hashes must not change with the checkout's line endings",
|
||||
)
|
||||
|
||||
def test_committed_snapshot_matches_normalized_sources(self):
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
for name in SNAPSHOT_FILES:
|
||||
expected = hashlib.sha256(
|
||||
(DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
self.assertEqual(
|
||||
summary["snapshots"][name]["sha256"], expected,
|
||||
f"{name}: committed snapshot hash does not match the LF-normalized source",
|
||||
)
|
||||
|
||||
def test_crlf_checkout_produces_the_committed_hashes(self):
|
||||
"""Simulate a Windows checkout: the recorded hashes must still validate."""
|
||||
digest = _load_generator().digest
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name in SNAPSHOT_FILES:
|
||||
crlf_copy = Path(tmp) / name
|
||||
raw = (DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
crlf_copy.write_bytes(raw.replace(b"\n", b"\r\n"))
|
||||
self.assertEqual(
|
||||
digest(crlf_copy), summary["snapshots"][name]["sha256"],
|
||||
f"{name}: a CRLF checkout would record a different hash",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Regression tests for the dropped --stack flag in --design-system mode (issue #484).
|
||||
|
||||
`search.py "<query>" --design-system --stack nextjs` used to exit successfully
|
||||
without any indication that the stack was never applied, so a caller following
|
||||
SKILL.md's "never assume a stack" guidance could believe stack guidance was
|
||||
part of the generated design system. The combination must stay valid, but it
|
||||
must say that --stack was ignored.
|
||||
|
||||
Stdlib-only (unittest, not pytest) to match test_core.py -- this project ships
|
||||
with zero external dependencies.
|
||||
|
||||
Run with:
|
||||
python -m unittest discover -s scripts/tests -v
|
||||
or directly:
|
||||
python scripts/tests/test_design_system_stack.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
||||
SEARCH = SCRIPTS_DIR / "search.py"
|
||||
|
||||
|
||||
class TestStackFlagWithDesignSystem(unittest.TestCase):
|
||||
def run_search(self, *args):
|
||||
# The child forces UTF-8 on its streams (search.py), so decode as UTF-8
|
||||
# regardless of the parent's locale on Windows.
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SEARCH), *map(str, args)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_design_system_with_stack_succeeds_and_says_stack_is_ignored(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("--stack", proc.stderr)
|
||||
self.assertIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_design_system_without_stack_stays_quiet(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_stack_search_alone_never_warns(self):
|
||||
proc = self.run_search("dashboard table density", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@ -0,0 +1,82 @@
|
||||
"""Every script invocation in the shipped skill markdown resolves from the skill directory.
|
||||
|
||||
Regression test for #474. The sub-skills ship in two copies (.claude/skills/<skill>/
|
||||
for the plugin, cli/assets/skills/<skill>/ for CLI installs) and land in layouts where
|
||||
neither the project root nor ~/.claude/skills/ is a valid anchor: the plugin cache, a
|
||||
project's .claude/skills/, ~/.claude/skills/ (--global), or a manual copy. The one anchor
|
||||
that exists in all of them is the skill's own directory, so documented commands use
|
||||
`scripts/<file>` for the skill's own scripts and `../<skill>/scripts/<file>` for a
|
||||
sibling sub-skill (the sub-skills are always installed side by side).
|
||||
|
||||
This test extracts every `python|python3|node|bash <path>` invocation from every
|
||||
markdown file under both trees and asserts that the path is skill-relative and names a
|
||||
file that ships. The core skill's `${CLAUDE_PLUGIN_ROOT}/.claude/skills/...` form is
|
||||
resolved against the repository root, which is what that variable denotes under a
|
||||
plugin install - and accepted only in that file, because the sub-skills also ship
|
||||
through the CLI, where the variable does not exist. The grep-based path contract in check-asset-sync.yml is the negative
|
||||
side (no home-, project- or variable-rooted paths anywhere, code included); this is
|
||||
the positive side (every documented invocation points at a real file).
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
SKILL_TREES = ("cli/assets/skills", ".claude/skills")
|
||||
# The only file that may use the plugin-root form: hand-authored for the plugin install
|
||||
# and not shipped by the CLI (sync-assets.mjs mirrors data/ and scripts/, never SKILL.md).
|
||||
# (Built from segments: the path contract in check-asset-sync.yml scans this file too.)
|
||||
PLUGIN_ONLY_FILE = Path(".claude") / "skills" / "ui-ux-pro-max" / "SKILL.md"
|
||||
INVOCATION = re.compile(r'(?<![\w/.-])(?:python3?|node|bash)\s+"?([^\s"`\']+\.(?:py|cjs|js|mjs|sh))')
|
||||
PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}/"
|
||||
|
||||
|
||||
def shipped_invocations():
|
||||
for tree in SKILL_TREES:
|
||||
for skill_dir in sorted((REPO / tree).iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
for md in sorted(skill_dir.rglob("*.md")):
|
||||
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
|
||||
for match in INVOCATION.finditer(line):
|
||||
yield skill_dir, md, lineno, match.group(1)
|
||||
|
||||
|
||||
def resolve(skill_dir, md, path):
|
||||
"""Return (target, None) for a skill-relative path, or (None, reason)."""
|
||||
if path.startswith(PLUGIN_ROOT):
|
||||
if md.relative_to(REPO) != PLUGIN_ONLY_FILE:
|
||||
return None, "the ${CLAUDE_PLUGIN_ROOT} form is only valid in the plugin-only core SKILL.md"
|
||||
return REPO / path[len(PLUGIN_ROOT):], None
|
||||
if path.startswith("scripts/"):
|
||||
return skill_dir / path, None
|
||||
if path.startswith("../"):
|
||||
parts = path.split("/")
|
||||
if len(parts) > 3 and parts[2] == "scripts" and (skill_dir.parent / parts[1]).is_dir():
|
||||
return skill_dir.parent / parts[1] / "/".join(parts[2:]), None
|
||||
return None, "a sibling invocation must be ../<skill>/scripts/<file> and the sibling must ship"
|
||||
return None, "not skill-relative (expected scripts/<file> or ../<skill>/scripts/<file>)"
|
||||
|
||||
|
||||
class SkillScriptPathsTest(unittest.TestCase):
|
||||
def test_every_shipped_markdown_invocation_resolves_from_the_skill_directory(self):
|
||||
problems, seen = [], 0
|
||||
for skill_dir, md, lineno, path in shipped_invocations():
|
||||
seen += 1
|
||||
target, reason = resolve(skill_dir, md, path)
|
||||
if reason is None and not target.is_file():
|
||||
reason = f"no such file: {target}"
|
||||
if reason:
|
||||
problems.append(f"{md.relative_to(REPO)}:{lineno}: {path} -- {reason}")
|
||||
# Guard against a silently broken extractor: the two trees carry well over
|
||||
# a hundred documented invocations between them.
|
||||
self.assertGreater(seen, 100, f"extractor found only {seen} invocations")
|
||||
self.assertEqual(problems, [], "\n" + "\n".join(problems))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -663,7 +663,11 @@ def _check_catalog_summary(summary, licenses, phosphor, problems):
|
||||
problems.append(f"[catalog:summary] stale count for {key}")
|
||||
snapshots = summary.get("snapshots") if isinstance(summary.get("snapshots"), dict) else {}
|
||||
for name in ("google-fonts.csv", "google-font-licenses.json", "icons.csv", "phosphor-icons-upstream.json"):
|
||||
digest = hashlib.sha256((DATA_DIR / name).read_bytes()).hexdigest()
|
||||
# Line endings are normalized so the check matches
|
||||
# generate-catalog-summary.py on CRLF checkouts too.
|
||||
digest = hashlib.sha256(
|
||||
(DATA_DIR / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
if snapshots.get(name) != {"sha256": digest}:
|
||||
problems.append(f"[catalog:summary] stale snapshot for {name}")
|
||||
policy = summary.get("promotionPolicy")
|
||||
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# catalog-summary.json records sha256 hashes of the files below, computed over
|
||||
# their bytes. A CRLF checkout therefore produces different hashes than an LF
|
||||
# one, which makes `verify:data` fail on every other platform (see #462/#478).
|
||||
# Pin these files to LF so the snapshot is reproducible everywhere.
|
||||
src/ui-ux-pro-max/data/*.csv text eol=lf
|
||||
src/ui-ux-pro-max/data/*.json text eol=lf
|
||||
124
.github/workflows/check-asset-sync.yml
vendored
124
.github/workflows/check-asset-sync.yml
vendored
@ -5,8 +5,7 @@ on:
|
||||
paths:
|
||||
- "src/ui-ux-pro-max/**"
|
||||
- "cli/assets/**"
|
||||
- ".claude/skills/ui-ux-pro-max/data/**"
|
||||
- ".claude/skills/ui-ux-pro-max/scripts/**"
|
||||
- ".claude/skills/**"
|
||||
- "cli/scripts/sync-assets.mjs"
|
||||
- "cli/package.json"
|
||||
- "scripts/evaluate-relevance.py"
|
||||
@ -18,8 +17,9 @@ on:
|
||||
- "src/ui-ux-pro-max/**"
|
||||
- "cli/assets/**"
|
||||
- "cli/package.json"
|
||||
- ".claude/skills/ui-ux-pro-max/data/**"
|
||||
- ".claude/skills/ui-ux-pro-max/scripts/**"
|
||||
- ".claude/skills/**"
|
||||
- "cli/scripts/sync-assets.mjs"
|
||||
- ".github/workflows/check-asset-sync.yml"
|
||||
|
||||
jobs:
|
||||
check-assets:
|
||||
@ -38,3 +38,119 @@ jobs:
|
||||
# installed as a plugin, and previously had no sync check at all.
|
||||
- name: Check assets are in sync with source of truth
|
||||
run: npm --prefix cli run check:assets
|
||||
# Path contract (#474): skill instructions and scripts reach their scripts
|
||||
# via skill-relative paths ("scripts/<file>" for the skill's own, "../<skill>/scripts/<file>"
|
||||
# for a sibling sub-skill) so they resolve in every install context: marketplace/
|
||||
# plugin cache, project-level CLI install, CLI --global install, manual copy.
|
||||
# Home-rooted ("~/.claude/skills/<skill>"), project-rooted (".claude/skills/<skill>")
|
||||
# and variable-rooted ("$HOME/...", "${PWD}/...") forms each work in only one of them.
|
||||
# Every file under both skill trees is checked, not just SKILL.md - the first
|
||||
# version of this step looked only at SKILL.md and missed 27 home-rooted paths
|
||||
# one directory down in references/. The one allowed absolute form is
|
||||
# "${CLAUDE_PLUGIN_ROOT}/.claude/skills/ui-ux-pro-max/..." (braced or bare variable,
|
||||
# directly followed by "/"): the core skill's SKILL.md is hand-authored for the
|
||||
# plugin install only and that variable anchors it there. The same variable into a
|
||||
# sub-skill is flagged, because sub-skills are also installed by the CLI where it is
|
||||
# unset - and a third check pins the token itself to that one file (plus the checker
|
||||
# that names it), so a sub-skill cannot borrow the core form either: sub-skills ship
|
||||
# through the CLI too, where the variable does not exist. Both patterns require a path INTO a named skill ("skills/<name>"), so a bare
|
||||
# mention of the directory in prose or a code comment ("~/.claude/skills/, or ...")
|
||||
# is not a hit - naming a skill after "skills/" in prose is.
|
||||
# LC_ALL=C so that only NUL-containing files count as binary and an offending line
|
||||
# with a stray non-UTF-8 byte is printed instead of suppressed as improperly encoded
|
||||
# (the verdict is the same in both locales; the diagnostic is not); -I then skips
|
||||
# .claude/skills/ui-styling/scripts/.coverage, a tracked SQLite database whose
|
||||
# recorded absolute paths contain "/.claude/skills/ui-styling/".
|
||||
# Not covered: backslash-separated Windows spellings and the platform-root-relative
|
||||
# "skills/<skill>/..." form - the docs are bash-fenced and skill-relative, so neither
|
||||
# appears; the positive side (every documented invocation names a file that ships)
|
||||
# is src/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py.
|
||||
# grep exit codes: 0 = hits (violation), 1 = clean, 2 = error - only 1 passes, so an
|
||||
# unreadable file can never turn into a green run (a missing tree is caught above).
|
||||
- name: Path contract - no install-specific skill paths
|
||||
run: |
|
||||
for d in .claude/skills cli/assets/skills; do
|
||||
[ -d "$d" ] || { echo "::error::$d is missing - the path contract has nothing to scan"; exit 1; }
|
||||
done
|
||||
status=0
|
||||
rc=0; LC_ALL=C grep -rnIP '~/\.claude/skills/[A-Za-z0-9_-]+\b' .claude/skills cli/assets/skills || rc=$?
|
||||
if [ "$rc" -ne 1 ]; then
|
||||
echo "::error::home-rooted skill paths (~/.claude/skills/<skill>) only resolve for one install layout - use skill-relative paths (see #474); grep rc=$rc"
|
||||
status=1
|
||||
fi
|
||||
rc=0; LC_ALL=C grep -rnIP '(?<![/\w])\.claude/skills/[A-Za-z0-9_-]+\b|(?<!~)(?<!\$CLAUDE_PLUGIN_ROOT)(?<!\$\{CLAUDE_PLUGIN_ROOT\})/\.claude/skills/[A-Za-z0-9_-]+\b|\$\{?CLAUDE_PLUGIN_ROOT\}?/\.claude/skills/(?!ui-ux-pro-max\b)[A-Za-z0-9_-]+\b' .claude/skills cli/assets/skills || rc=$?
|
||||
if [ "$rc" -ne 1 ]; then
|
||||
echo "::error::project- or variable-rooted skill paths (.claude/skills/<skill>, \$HOME/..., \${CLAUDE_PLUGIN_ROOT}/... outside the core skill) only resolve for one install layout - use skill-relative paths (see #474); grep rc=$rc"
|
||||
status=1
|
||||
fi
|
||||
rc=0; found=$(LC_ALL=C grep -rlIF 'CLAUDE_PLUGIN_ROOT' .claude/skills cli/assets/skills) || rc=$?
|
||||
if [ "$rc" -eq 2 ]; then echo "::error::grep failed while scanning for CLAUDE_PLUGIN_ROOT (rc=2)"; status=1; fi
|
||||
offenders=$(printf '%s\n' "$found" | grep -vxF -e '.claude/skills/ui-ux-pro-max/SKILL.md' -e '.claude/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py' | grep -v '^$' || true)
|
||||
if [ -n "$offenders" ]; then
|
||||
printf '%s\n' "$offenders"
|
||||
echo "::error::CLAUDE_PLUGIN_ROOT is only defined under a plugin install; only the plugin-only core SKILL.md may use it - sub-skills ship through the CLI too (see #474)"
|
||||
status=1
|
||||
fi
|
||||
if [ "$status" -eq 0 ]; then echo "OK: all skill paths are skill-relative"; fi
|
||||
exit "$status"
|
||||
# Bundled-skill contract (#474 finding 2): the skill directories under .claude/skills are
|
||||
# everything a plugin or CLI install ships, so a workflow step that names a skill outside
|
||||
# that set ("frontend-design", "chrome-devtools", ...) or a claudekit command namespace
|
||||
# ("/ckm:brand", "/ck:frontend-design") fails silently or leaves the agent improvising under
|
||||
# either install. #473 made banner-design self-contained; this step keeps every shipped
|
||||
# skill that way. Three greps over every shipped Markdown file in both trees:
|
||||
# 1. no claudekit command namespace - "/ck:" or "/ckm:" not preceded by a word character
|
||||
# or "/", so a URL or file path containing the letters is not a hit;
|
||||
# 2. every backticked name followed by "skill", "skills" or "sub-skill" (the form these docs
|
||||
# use to name a skill) must be a directory under .claude/skills - an allowlist, so a new
|
||||
# unbundled name fails without editing this file. The lookahead walks a list ("`a`, `b`,
|
||||
# and `c` skills", separators ", ", " and ", " or ", "/", " -> ") so every member is
|
||||
# checked, not only the last one, and tolerates "**" around the name. The file is read as
|
||||
# one record (-z, separators as \s+) so a list wrapped over several lines is still a
|
||||
# list; a hit therefore names the file and the name, not the line;
|
||||
# 3. none of the claudekit-only skill names the shipped docs used to reference, matched
|
||||
# case-insensitively in their hyphenated spelling - "Related Skills: frontend-design, ..."
|
||||
# carries neither backticks nor a "skill" suffix, so 2. cannot see it. "project-management"
|
||||
# is also ordinary English: a hit in prose is a reword, not a workflow step.
|
||||
# Not covered: a spaced or underscored spelling ("frontend design"); a bare name with no
|
||||
# backticks and no "skill" suffix that is not on the list in 3.; mentions inside scripts
|
||||
# (brand/scripts/extract-colors.cjs names ai-multimodal three times as a hint, one of them
|
||||
# "if installed" - not a workflow step); cli/assets/templates, the CLI's rendered core skill,
|
||||
# which neither this step nor the path contract scans (clean at the time of writing); and a
|
||||
# bundled-set change - a skill directory added or removed under .claude/skills moves the
|
||||
# allowlist with it, which is the intent.
|
||||
# grep exit codes as above: for 1. and 3. only 1 (no hits) passes; for 2. hits are fine as
|
||||
# long as every name is bundled, and 2 (error) never passes.
|
||||
- name: Bundled-skill contract - no unbundled skill or claudekit command references
|
||||
run: |
|
||||
[ -d .claude/skills ] || { echo "::error::.claude/skills is missing - the bundled set cannot be derived"; exit 1; }
|
||||
bundled=$(find .claude/skills -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | LC_ALL=C sort)
|
||||
[ -n "$bundled" ] || { echo "::error::.claude/skills has no skill directories - the bundled set cannot be derived"; exit 1; }
|
||||
status=0
|
||||
rc=0; LC_ALL=C grep -rnIP --include='*.md' '(?<![\w/])/ckm?:' .claude/skills cli/assets/skills || rc=$?
|
||||
if [ "$rc" -ne 1 ]; then
|
||||
echo "::error::claudekit command namespaces (/ck:, /ckm:) are not shipped by this plugin - name the bundled skill instead (see #474 finding 2); grep rc=$rc"
|
||||
status=1
|
||||
fi
|
||||
rc=0; hits=$(LC_ALL=C grep -rzoIP --include='*.md' '`[a-z0-9-]+`(?:\*\*)?(?=(?:(?:,\s+|\s+and\s+|,\s+and\s+|\s+or\s+|,\s+or\s+|/|\s+→\s+|\s+->\s+)(?:\*\*)?`[a-z0-9-]+`(?:\*\*)?)*\s+(?:sub-)?[Ss]kills?\b)' .claude/skills cli/assets/skills | tr '\0' '\n'; exit "${PIPESTATUS[0]}") || rc=$?
|
||||
if [ "$rc" -eq 2 ]; then echo "::error::grep failed while scanning for skill references (rc=2)"; status=1; fi
|
||||
unbundled=0
|
||||
while IFS= read -r hit; do
|
||||
[ -n "$hit" ] || continue
|
||||
name=$(printf '%s\n' "$hit" | sed 's/.*`\([^`]*\)`.*/\1/')
|
||||
if ! printf '%s\n' "$bundled" | grep -qxF -- "$name"; then
|
||||
printf '%s\n' "$hit"
|
||||
unbundled=1
|
||||
fi
|
||||
done <<< "$hits"
|
||||
if [ "$unbundled" -ne 0 ]; then
|
||||
echo "::error::skill references outside the bundled set ($(printf '%s' "$bundled" | tr '\n' ' ')) fail under a plugin or CLI install - implement the step inline, name a bundled skill, or drop the step (see #474 finding 2)"
|
||||
status=1
|
||||
fi
|
||||
rc=0; LC_ALL=C grep -rniIP --include='*.md' '\b(frontend-design|ai-artist|ai-multimodal|chrome-devtools|assets-organizing|project-management)\b' .claude/skills cli/assets/skills || rc=$?
|
||||
if [ "$rc" -ne 1 ]; then
|
||||
echo "::error::claudekit-only skill names are not shipped by this plugin - implement the step inline, name a bundled skill, or drop the step (see #474 finding 2); grep rc=$rc"
|
||||
status=1
|
||||
fi
|
||||
if [ "$status" -eq 0 ]; then echo "OK: shipped skill docs reference only bundled skills"; fi
|
||||
exit "$status"
|
||||
|
||||
@ -99,10 +99,8 @@ git checkout -b feat/your-feature-name
|
||||
|
||||
# 2. Make your changes in src/ui-ux-pro-max/
|
||||
|
||||
# 3. Sync changes to CLI assets
|
||||
cp -r src/ui-ux-pro-max/data/* cli/assets/data/
|
||||
cp -r src/ui-ux-pro-max/scripts/* cli/assets/scripts/
|
||||
cp -r src/ui-ux-pro-max/templates/* cli/assets/templates/
|
||||
# 3. Sync changes to CLI assets and the Claude Code skill
|
||||
cd cli && npm run sync:assets && cd ..
|
||||
|
||||
# 4. Build and test the CLI locally
|
||||
cd cli && bun run build
|
||||
|
||||
663
README.id.md
Normal file
663
README.id.md
Normal file
@ -0,0 +1,663 @@
|
||||
# [UI UX Pro Max](https://uupm.cc)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.id.md">🇮🇩 Bahasa Indonesia</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.ko.md">🇰🇷 한국어</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.vi.md">🇻🇳 Tiếng Việt</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/releases"><img src="https://img.shields.io/github/v/release/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=blue" alt="Rilis GitHub"></a>
|
||||
<img src="https://img.shields.io/badge/reasoning_rules-192-green?style=for-the-badge" alt="192 aturan penalaran">
|
||||
<img src="https://img.shields.io/badge/UI_styles-79_searchable-purple?style=for-the-badge" alt="79 gaya UI yang dapat dicari">
|
||||
<img src="https://img.shields.io/badge/python-3.x-yellow?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.x">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/LICENSE"><img src="https://img.shields.io/github/license/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=green" alt="Lisensi"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/v/ui-ux-pro-max-cli?style=flat-square&logo=npm&label=CLI" alt="npm"></a>
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/dm/ui-ux-pro-max-cli?style=flat-square&label=downloads" alt="unduhan npm"></a>
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/stargazers"><img src="https://img.shields.io/github/stars/nextlevelbuilder/ui-ux-pro-max-skill?style=flat-square&logo=github" alt="bintang GitHub"></a>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Support%20Development-00457C?style=flat-square&logo=paypal&logoColor=white" alt="PayPal"></a>
|
||||
</p>
|
||||
|
||||
Skill AI yang menyediakan kecerdasan desain untuk membangun UI/UX profesional di berbagai platform dan framework.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://uupm.cc">
|
||||
<img src="screenshots/website.png" alt="UI UX Pro Max" width="800">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>Jika proyek ini bermanfaat bagi Anda, pertimbangkan untuk mendukungnya:</b><br><br>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Donate-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="Donasi PayPal"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<i>Proyek lainnya</i><br>
|
||||
<a href="https://nextlevelbuilder.io">NextLevelBuilder.io</a> | <a href="https://goclaw.sh">GoClaw.sh</a> | <a href="https://claudekit.cc">ClaudeKit.cc</a> | <a href="https://tose.sh">TOSE.sh</a>
|
||||
</p>
|
||||
|
||||
## Yang Baru di v2.0
|
||||
|
||||
### Pembuatan Design System Cerdas
|
||||
|
||||
Fitur unggulan v2.0 adalah **Design System Generator** — mesin penalaran berbasis AI yang menganalisis kebutuhan proyek Anda dan menghasilkan design system yang lengkap serta disesuaikan dalam hitungan detik.
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| TARGET: Serenity Spa - DESIGN SYSTEM YANG DIREKOMENDASIKAN |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| POLA: Hero-Centric + Social Proof |
|
||||
| Konversi: Didorong emosi dengan elemen kepercayaan |
|
||||
| CTA: Di atas fold, diulang setelah testimonial |
|
||||
| Bagian: |
|
||||
| 1. Hero |
|
||||
| 2. Layanan |
|
||||
| 3. Testimonial |
|
||||
| 4. Booking |
|
||||
| 5. Kontak |
|
||||
| |
|
||||
| GAYA: Soft UI Evolution |
|
||||
| Kata kunci: Bayangan lembut, kedalaman halus, tenang, premium, bentuk organik |
|
||||
| Cocok untuk: Wellness, kecantikan, brand lifestyle, layanan premium |
|
||||
| Performa: cost:low | Aksesibilitas: risk:conditional; verifikasi kebutuhan |
|
||||
| |
|
||||
| WARNA: |
|
||||
| Primer: #E8B4B8 (Soft Pink) |
|
||||
| Sekunder: #A8D5BA (Sage Green) |
|
||||
| CTA: #D4AF37 (Gold) |
|
||||
| Background: #FFF5F5 (Warm White) |
|
||||
| Teks: #2D3436 (Charcoal) |
|
||||
| Catatan: Palet menenangkan dengan aksen emas untuk nuansa mewah |
|
||||
| |
|
||||
| TIPOGRAFI: Cormorant Garamond / Montserrat |
|
||||
| Nuansa: Elegan, menenangkan, sophisticated |
|
||||
| Cocok untuk: Brand mewah, wellness, kecantikan, editorial |
|
||||
| Google Fonts: https://fonts.google.com/share?selection.family=... |
|
||||
| |
|
||||
| EFEK UTAMA: |
|
||||
| Bayangan lembut + Transisi sesuai konteks + Hover state yang halus |
|
||||
| |
|
||||
| HINDARI (Anti-pattern): |
|
||||
| Warna neon terang + Animasi keras + Dark mode + Gradien ungu/merah muda ala AI |
|
||||
| |
|
||||
| CHECKLIST PRA-DELIVERY: |
|
||||
| [ ] Tidak menggunakan emoji sebagai ikon (gunakan SVG: Heroicons/Lucide) |
|
||||
| [ ] cursor-pointer pada semua elemen yang dapat diklik |
|
||||
| [ ] Timing interaksi mengikuti platform, komponen, dan preferensi pengguna |
|
||||
| [ ] Light mode: kontras teks minimum 4.5:1 |
|
||||
| [ ] Focus state terlihat untuk navigasi keyboard |
|
||||
| [ ] Menghormati prefers-reduced-motion |
|
||||
| [ ] Teks, chip, dan badge reflow tanpa terpotong atau merusak label |
|
||||
| [ ] Responsive: 375px, 768px, 1024px, 1440px |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Cara Kerja Pembuatan Design System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. PERMINTAAN PENGGUNA │
|
||||
│ "Buat landing page untuk spa kecantikan saya" │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. PENCARIAN MULTI-DOMAIN (5 pencarian paralel) │
|
||||
│ • Pencocokan jenis produk (192 kategori) │
|
||||
│ • Rekomendasi gaya (79 dapat dicari; 50 aktif) │
|
||||
│ • Pemilihan palet warna (192 palet) │
|
||||
│ • Pola landing page (34 pola) │
|
||||
│ • Pasangan tipografi (74 kombinasi font) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. MESIN PENALARAN │
|
||||
│ • Cocokkan produk → aturan kategori UI │
|
||||
│ • Terapkan prioritas gaya (ranking BM25) │
|
||||
│ • Filter anti-pattern untuk industri │
|
||||
│ • Proses aturan keputusan (kondisi JSON) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. OUTPUT DESIGN SYSTEM LENGKAP │
|
||||
│ Pola + Gaya + Warna + Tipografi + Efek │
|
||||
│ + Anti-pattern yang dihindari + Checklist pra-delivery │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 192 Aturan Penalaran Khusus Industri
|
||||
|
||||
Mesin penalaran ini mencakup aturan khusus untuk:
|
||||
|
||||
| Kategori | Contoh |
|
||||
|----------|--------|
|
||||
| **Teknologi & SaaS** | SaaS, Micro SaaS, layanan B2B, Developer Tool / IDE, platform AI/chatbot, platform keamanan siber |
|
||||
| **Keuangan** | Fintech/Crypto, perbankan, asuransi, pelacak keuangan pribadi, alat invoice & billing |
|
||||
| **Kesehatan** | Klinik medis, apotek, kedokteran gigi, veteriner, kesehatan mental, pengingat obat |
|
||||
| **E-commerce** | Umum, mewah, marketplace (P2P), subscription box, pengantaran makanan |
|
||||
| **Layanan** | Kecantikan/spa, restoran, hotel, hukum, layanan rumah, booking & appointment |
|
||||
| **Kreatif** | Portofolio, agensi, fotografi, gaming, streaming musik, editor foto/video |
|
||||
| **Gaya Hidup** | Pelacak kebiasaan, resep & memasak, meditasi, cuaca, diari, pelacak suasana hati |
|
||||
| **Teknologi Baru** | Web3/NFT, Spatial Computing, Quantum Computing, armada drone otonom |
|
||||
|
||||
Setiap aturan mencakup:
|
||||
- **Pola yang Direkomendasikan** - Struktur landing page
|
||||
- **Prioritas Gaya** - Gaya UI yang paling cocok
|
||||
- **Nuansa Warna** - Palet yang sesuai dengan industri
|
||||
- **Nuansa Tipografi** - Pencocokan karakter font
|
||||
- **Efek Utama** - Animasi dan interaksi
|
||||
- **Anti-Pattern** - Hal yang TIDAK boleh dilakukan (misalnya, "AI purple/pink gradients" untuk perbankan)
|
||||
|
||||
## Fitur
|
||||
|
||||
- **79 Gaya UI yang Dapat Dicari (50 aktif)** - Glassmorphism, Claymorphism, Minimalism, Brutalism, Neumorphism, Bento Grid, Dark Mode, AI-Native UI, dan lainnya
|
||||
- **192 Palet Warna** - Palet khusus industri yang selaras 1:1 dengan 192 jenis produk
|
||||
- **74 Pasangan Font** - Kombinasi tipografi pilihan dengan import Google Fonts
|
||||
- **25 Jenis Chart** - Rekomendasi untuk dashboard dan analitik
|
||||
- **22 Tech Stack** - React, Next.js, Astro, Vue, Nuxt.js, Nuxt UI, Svelte, SwiftUI, React Native, Flutter, HTML+Tailwind, shadcn/ui, Jetpack Compose, Angular, Laravel, Three.js, JavaFX, WPF, WinUI 3, UWP, Avalonia, Uno Platform
|
||||
- **119 Panduan UX** - Best practice, anti-pattern, aturan aksesibilitas, layout teks yang tangguh, label ringkas, dan interaksi yang dapat dibatalkan
|
||||
- **192 Aturan Penalaran** - Pembuatan design system khusus industri (BARU di v2.0)
|
||||
|
||||
### Teks Tangguh dan UI Ringkas
|
||||
|
||||
Panduan ini kini mencakup kegagalan umum di lingkungan production terkait heading, token panjang,
|
||||
chip, badge, dan micro-interaction yang terinterupsi:
|
||||
|
||||
- Pembungkusan heading yang seimbang adalah progressive enhancement, bukan jaminan bahwa
|
||||
kata tertentu akan tetap berada pada baris terakhir. Desain tetap harus berfungsi dengan
|
||||
pembungkusan alami di berbagai lebar, font, dan locale.
|
||||
- Teks penting harus dapat mengalir ulang tanpa terpotong pada lebar sempit, zoom browser, text
|
||||
scaling, dan override spasi pengguna. URL dan identifier panjang boleh dibungkus dengan aman.
|
||||
- Kumpulan chip dan tag sebaiknya membungkus atau menggunakan disclosure `+n` yang dapat dioperasikan. Label ringkas
|
||||
sebaiknya tetap utuh jika memungkinkan; pemotongan yang tidak dapat dihindari memerlukan jalur
|
||||
nilai penuh yang aksesibel bagi pengguna keyboard, pointer, dan sentuhan.
|
||||
- Makna badge tidak boleh bergantung pada warna saja. Chip interaktif memerlukan semantik native,
|
||||
focus yang terlihat, dan state yang dapat diprogram; live count memerlukan konteks yang bermakna.
|
||||
- Interaksi cepat dapat membatalkan animasi, tetapi state semantik akhir, focus, dan
|
||||
konten harus tetap benar. Timing dipilih sesuai platform dan komponen,
|
||||
dengan tetap menghormati preferensi reduced-motion.
|
||||
|
||||
### Taksonomi Gaya
|
||||
|
||||
Katalog berisi **79 gaya yang dapat dicari** dengan dukungan ID dan alias yang stabil:
|
||||
|
||||
| Status | Jumlah | Perilaku pencarian |
|
||||
|--------|-------:|--------------------|
|
||||
| Aktif | 50 | Disertakan dalam rekomendasi normal dan ditampilkan secara default di gallery |
|
||||
| Tambahan | 29 | Dikembalikan untuk intent varian/sistem yang eksplisit atau exact; tersedia melalui filter status gallery |
|
||||
| Deprecated | 9 | Dikecualikan dari ranking normal; nama legacy dialihkan ke gaya canonical atau landing pattern |
|
||||
|
||||
Set aktif mencakup 43 keluarga visual umum, 2 gaya khusus mobile, 3 platform/design system resmi, 1 material platform, dan 1 gaya analitik inti. Sistem resmi saat ini mencakup Fluent 2, Shopify Polaris, dan Adobe Spectrum; Liquid Glass dikategorikan sebagai material platform Apple, Material 3 Expressive tetap menjadi varian Material mobile, dan Spectrum 2 bersifat tambahan. Struktur landing page berada dalam dataset terpisah yang berisi 34 pola landing, sehingga tidak bersaing dengan gaya visual dalam ranking BM25.
|
||||
|
||||
Lihat [`styles.csv`](src/ui-ux-pro-max/data/styles.csv) untuk taksonomi lengkap dan metadata yang menyertakan provenance.
|
||||
|
||||
## 💎 Perbandingan Versi Basic vs. Premium
|
||||
|
||||
Banyak pengguna bertanya mengenai perbedaan antara versi open-source dan premium. Berikut rincian untuk membantu Anda memilih yang paling sesuai dengan workflow Anda.
|
||||
|
||||
### 🟢 Versi Basic (Repository Ini)
|
||||
* **Sepenuhnya Open Source:** Cocok untuk developer individu, hobbyist, dan proyek standar.
|
||||
* **Kecerdasan UI/UX Inti:** Akses penuh ke 79 gaya UI yang dapat dicari (50 aktif), 192 jenis produk, palet warna, dan pasangan font pilihan.
|
||||
* **Rekomendasi Cerdas:** Mesin pencarian BM25 bawaan untuk pencocokan desain yang sangat akurat.
|
||||
* **Dukungan Cross-Platform:** Panduan khusus stack yang mendukung 22 framework utama (React, Vue, Tailwind, iOS, Android, dll.).
|
||||
* **Pembuatan Design System:** Buat aturan UI, pola, dan logika yang disesuaikan secara instan melalui CLI.
|
||||
|
||||
### 🟡 Versi Premium
|
||||
* **Skill Brand Design yang Diperluas:** Melampaui UI/UX dengan mencakup pembuatan Brand Identity, Logo Design, Corporate Identity Programs (CIP), Banner, Presentation Slides, dan Iconography kustom.
|
||||
* **Pembuatan Aset Tingkat Lanjut:** Integrasi mendalam dengan image generation berbasis AI untuk membuat aset visual nyata, bukan sekadar placeholder.
|
||||
* **Arsitektur Enterprise:** Arsitektur Design Token yang lebih komprehensif dan scalable untuk deployment tim skala besar.
|
||||
* **Priority Support:** Dukungan teknis khusus untuk tim dan profesional yang membutuhkan workflow desain lengkap tanpa gangguan.
|
||||
|
||||
👉 *Untuk detail lebih lanjut tentang upgrade ke tier Premium, kunjungi [uupm.cc](https://uupm.cc).*
|
||||
|
||||
## Instalasi
|
||||
|
||||
### Menggunakan Claude Marketplace (Claude Code)
|
||||
|
||||
Instal langsung di Claude Code dengan dua perintah:
|
||||
|
||||
```
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
### Menggunakan CLI (Direkomendasikan)
|
||||
|
||||
```bash
|
||||
# Install CLI globally
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
|
||||
# Go to your project
|
||||
cd /path/to/your/project
|
||||
|
||||
# Install for your AI assistant
|
||||
uipro init --ai claude # Claude Code
|
||||
uipro init --ai cursor # Cursor
|
||||
uipro init --ai windsurf # Windsurf
|
||||
uipro init --ai antigravity # Antigravity
|
||||
uipro init --ai copilot # GitHub Copilot
|
||||
uipro init --ai kiro # Kiro
|
||||
uipro init --ai codex # Codex CLI
|
||||
uipro init --ai qoder # Qoder
|
||||
uipro init --ai roocode # Roo Code
|
||||
uipro init --ai gemini # Gemini CLI
|
||||
uipro init --ai trae # Trae
|
||||
uipro init --ai opencode # OpenCode
|
||||
uipro init --ai continue # Continue
|
||||
uipro init --ai codebuddy # CodeBuddy
|
||||
uipro init --ai droid # Droid (Factory)
|
||||
uipro init --ai kilocode # KiloCode
|
||||
uipro init --ai warp # Warp
|
||||
uipro init --ai augment # Augment
|
||||
uipro init --ai codewhale # CodeWhale
|
||||
uipro init --ai openclaw # OpenClaw
|
||||
uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
|
||||
uipro init --ai all # All assistants
|
||||
```
|
||||
|
||||
Paket npm-nya adalah `ui-ux-pro-max-cli`; paket tersebut tetap menginstal command `uipro`. Rilis lama `uipro-cli` sudah usang dan tidak boleh digunakan untuk aset saat ini.
|
||||
|
||||
### Instalasi Global (Tersedia untuk Semua Proyek)
|
||||
|
||||
```bash
|
||||
uipro init --ai claude --global # Install to ~/.claude/skills/
|
||||
uipro init --ai cursor --global # Install to ~/.cursor/skills/
|
||||
uipro init --ai universal --global # Install to ~/.agents/skills/
|
||||
```
|
||||
|
||||
### Command CLI Lainnya
|
||||
|
||||
```bash
|
||||
uipro versions # List available versions
|
||||
uipro update # Refresh skill files from installed CLI package
|
||||
uipro update --global # Refresh global skill files from installed CLI package
|
||||
uipro init --offline # Compatibility flag; installs bundled templates
|
||||
uipro uninstall # Remove skill (auto-detect platform)
|
||||
uipro uninstall --ai claude # Remove specific platform
|
||||
uipro uninstall --global # Remove from global install
|
||||
```
|
||||
|
||||
## Prasyarat
|
||||
|
||||
Python 3.x diperlukan untuk script pencarian (hanya standard library — script tidak menginstal apa pun dan tidak melakukan network call).
|
||||
|
||||
Periksa apakah Python sudah terinstal:
|
||||
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
Jika belum ada, instal sendiri dari [python.org](https://www.python.org/downloads/) atau melalui package manager OS Anda (Homebrew, apt, winget). Langkah instalasi ini ditujukan untuk **Anda sebagai pengguna manusia** — agent AI yang menggunakan skill ini tidak boleh menginstal software di mesin Anda; agent diperintahkan untuk meminta Anda melakukannya.
|
||||
|
||||
## Penggunaan
|
||||
|
||||
### Mode Skill (Aktif Otomatis)
|
||||
|
||||
**Didukung:** Claude Code, Cursor, Windsurf, Antigravity, Codex CLI, Continue, Gemini CLI, OpenCode, Qoder, CodeBuddy, Droid (Factory), KiloCode, Warp, Augment, CodeWhale
|
||||
|
||||
Skill akan aktif otomatis saat Anda meminta pekerjaan UI/UX. Cukup gunakan percakapan secara natural:
|
||||
|
||||
```
|
||||
Build a landing page for my SaaS product
|
||||
```
|
||||
|
||||
> **Trae**: Beralihlah ke mode **SOLO** terlebih dahulu. Skill akan aktif untuk permintaan UI/UX.
|
||||
|
||||
### Mode Workflow (Slash Command)
|
||||
|
||||
**Didukung:** Kiro, GitHub Copilot, Roo Code, KiloCode
|
||||
|
||||
Gunakan slash command untuk menjalankan skill:
|
||||
|
||||
```
|
||||
/ui-ux-pro-max Build a landing page for my SaaS product
|
||||
```
|
||||
|
||||
### Contoh Prompt
|
||||
|
||||
```
|
||||
Build a landing page for my SaaS product
|
||||
|
||||
Create a dashboard for healthcare analytics
|
||||
|
||||
Design a portfolio website with dark mode
|
||||
|
||||
Make a mobile app UI for e-commerce
|
||||
|
||||
Build a fintech banking app with dark theme
|
||||
```
|
||||
|
||||
### Cara Kerjanya
|
||||
|
||||
1. **Anda meminta** - Minta tugas UI/UX apa pun (build, design, create, implement, review, fix, improve)
|
||||
2. **Design System Dibuat** - AI secara otomatis menghasilkan design system lengkap menggunakan mesin penalaran
|
||||
3. **Rekomendasi cerdas** - Berdasarkan jenis produk dan kebutuhan Anda, AI menemukan gaya, warna, dan tipografi yang paling sesuai
|
||||
4. **Pembuatan kode** - Menerapkan UI dengan warna, font, spacing, dan best practice yang tepat
|
||||
5. **Pemeriksaan sebelum delivery** - Memvalidasi hasil terhadap anti-pattern UI/UX umum
|
||||
|
||||
### Stack yang Didukung
|
||||
|
||||
Skill menyediakan panduan khusus stack untuk:
|
||||
|
||||
| Kategori | Stack |
|
||||
|----------|-------|
|
||||
| **Web (HTML)** | HTML + Tailwind (default) |
|
||||
| **Ekosistem React** | React, Next.js, shadcn/ui |
|
||||
| **Ekosistem Vue** | Vue, Nuxt.js, Nuxt UI |
|
||||
| **Angular** | Angular |
|
||||
| **PHP** | Laravel (Blade, Livewire, Inertia.js) |
|
||||
| **Web Lainnya** | Svelte, Astro, Three.js |
|
||||
| **Desktop** | JavaFX, WPF, WinUI 3, Avalonia, Uno Platform, UWP |
|
||||
| **iOS** | SwiftUI |
|
||||
| **Android** | Jetpack Compose |
|
||||
| **Cross-Platform** | React Native, Flutter |
|
||||
|
||||
Cukup sebutkan stack pilihan Anda di prompt, atau biarkan default ke HTML + Tailwind.
|
||||
|
||||
## Command Design System (Lanjutan)
|
||||
|
||||
Untuk mengakses design system generator secara langsung:
|
||||
|
||||
> Catatan: Jika Anda menginstal melalui Continue, ganti `.claude/skills/` dengan `.continue/skills/` pada command di bawah. Untuk Droid (Factory), gunakan `.factory/skills/`.
|
||||
|
||||
```bash
|
||||
# Generate design system with ASCII output
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --design-system -p "Serenity Spa"
|
||||
|
||||
# Generate with Markdown output
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "fintech banking" --design-system -f markdown
|
||||
|
||||
# Domain-specific search
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "glassmorphism" --domain style
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant serif" --domain typography
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "dashboard" --domain chart
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "error summary validation" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "decorative icon aria hidden" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "icon button accessible label" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "orphan heading line balance" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "badge chip label wraps to second line" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "rapid chip animation interrupted" --domain ux
|
||||
|
||||
# Stack-specific guidelines
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "form validation" --stack react
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "responsive layout" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "chip badge overflow nowrap" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "tableview binding" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "atlantafx primer enterprise theme" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "enterprise tableview density permission" --stack javafx
|
||||
```
|
||||
|
||||
Pencarian web-stack memahami versi. Query tanpa major version lama akan mengembalikan panduan yang aktif dan terkini. Istilah legacy atau major version lama yang eksplisit (misalnya, `Svelte 4` atau `Next.js 15`) hanya mengembalikan row legacy yang sudah dikurasi, dengan label `Status` dan `Applies To`; jika tidak ada panduan legacy yang sesuai, pencarian tidak mengembalikan hasil alih-alih mencampur generasi framework.
|
||||
|
||||
### Menyimpan Design System (Pola Master + Overrides)
|
||||
|
||||
Simpan design system ke file untuk **hierarchical retrieval lintas sesi**:
|
||||
|
||||
```bash
|
||||
# Generate and persist to design-system/MASTER.md
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp"
|
||||
|
||||
# Also create a page-specific override file
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp" --page "dashboard"
|
||||
```
|
||||
|
||||
Ini membuat struktur folder `design-system/`:
|
||||
|
||||
```
|
||||
design-system/
|
||||
├── MASTER.md # Global Source of Truth (colors, typography, spacing, components)
|
||||
└── pages/
|
||||
└── dashboard.md # Page-specific overrides (only deviations from Master)
|
||||
```
|
||||
|
||||
**Cara kerja hierarchical retrieval:**
|
||||
1. Saat membuat halaman tertentu (misalnya, "Checkout"), periksa `design-system/pages/checkout.md` terlebih dahulu
|
||||
2. Jika file halaman ada, aturannya **menimpa** file Master
|
||||
3. Jika tidak, gunakan `design-system/MASTER.md` saja
|
||||
|
||||
**Prompt context-aware retrieval:**
|
||||
```
|
||||
I am building the [Page Name] page. Please read design-system/MASTER.md.
|
||||
Also check if design-system/pages/[page-name].md exists.
|
||||
If the page file exists, prioritize its rules.
|
||||
If not, use the Master rules exclusively.
|
||||
Now, generate the code...
|
||||
```
|
||||
|
||||
## Arsitektur & Kontribusi
|
||||
|
||||
### Untuk Pengguna
|
||||
|
||||
Codebase telah direstrukturisasi menggunakan **sistem pembuatan berbasis template**. Semua file khusus platform (`.cursor/`, `.windsurf/`, `.kiro/`, `.factory/`, dll.) sekarang dibuat secara dinamis oleh CLI.
|
||||
|
||||
**Selalu gunakan CLI untuk menginstal:**
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai <platform>
|
||||
```
|
||||
|
||||
Ini memastikan Anda mendapatkan template terbaru yang dibundel bersama package CLI yang terinstal serta struktur file yang tepat untuk agent AI Anda. Update package npm terlebih dahulu ketika rilis baru diterbitkan.
|
||||
|
||||
### Untuk Kontributor
|
||||
|
||||
Jika Anda ingin berkontribusi pada proyek ini:
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
|
||||
cd ui-ux-pro-max-skill
|
||||
|
||||
# 2. Understand the structure
|
||||
src/ui-ux-pro-max/ # Source of truth (data, scripts, templates)
|
||||
cli/ # CLI installer (generates files from templates)
|
||||
.claude/ # Local dev/test for Claude Code skill
|
||||
.factory/ # Local dev/test for Droid (Factory) skill
|
||||
|
||||
# 3. Make changes in src/ui-ux-pro-max/
|
||||
# - data/*.csv → Database files
|
||||
# - scripts/*.py → Search engine & design system
|
||||
# - templates/ → Platform-specific templates
|
||||
|
||||
# 4. Sync to CLI and test locally
|
||||
cd cli
|
||||
npm run sync:assets
|
||||
npm run check:assets
|
||||
npm run verify:data
|
||||
npm run typecheck
|
||||
|
||||
# 5. Build and test CLI
|
||||
# `npm run build` uses Bun when available and falls back to TypeScript compiler output after `npm ci`.
|
||||
npm run build
|
||||
node dist/index.js init --ai claude --offline # Test in a temp folder
|
||||
|
||||
# 6. Create PR (never push directly to main)
|
||||
git checkout -b feat/your-feature
|
||||
git commit -m "feat: description"
|
||||
git push -u origin feat/your-feature
|
||||
gh pr create
|
||||
```
|
||||
|
||||
Lihat [CLAUDE.md](CLAUDE.md) untuk panduan development yang lebih rinci.
|
||||
|
||||
### Provenance dan Refresh Katalog
|
||||
|
||||
Ringkasan katalog yang di-commit saat ini mencatat **1.934 Google Fonts yang disetujui**
|
||||
serta **8 pengecualian review** yang tidak dipromosikan tanpa metadata lisensi resmi yang sesuai. Panduan ikon tetap berisi **105 row terkurasi** (100 import web
|
||||
Phosphor langsung ditambah panduan React Native/fallback); sedangkan **manifest upstream Phosphor dengan 1.512 ikon** memvalidasi nama,
|
||||
weight, dan import React/SSR tanpa membanjiri hasil pencarian dengan seluruh
|
||||
package upstream.
|
||||
|
||||
Development biasa dan CI pull request tidak bergantung pada jaringan. Jalankan gate
|
||||
offline lengkap, termasuk hash snapshot dan validasi jumlah hasil generate, dengan:
|
||||
|
||||
```bash
|
||||
npm --prefix cli run verify:data
|
||||
# Or check only the generated catalog summary:
|
||||
npm --prefix cli run validate:catalog-summary
|
||||
```
|
||||
|
||||
Normalisasi refresh juga dapat diuji sepenuhnya secara offline menggunakan
|
||||
fixture yang sudah di-commit. Output disimpan ke direktori kandidat sementara dan tidak pernah
|
||||
menggantikan data canonical:
|
||||
|
||||
```bash
|
||||
candidate_dir="$(mktemp -d)"
|
||||
python3 scripts/refresh-google-fonts.py \
|
||||
--api-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json \
|
||||
--metadata-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json \
|
||||
--existing-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv \
|
||||
--overrides src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json \
|
||||
--output-csv "$candidate_dir/google-fonts.csv" \
|
||||
--license-output "$candidate_dir/google-font-licenses.json" \
|
||||
--metadata-revision fixture-catalogs-v1 \
|
||||
--verified-at 2026-08-13 --expected-count 2 --approve-changes
|
||||
python3 scripts/refresh-icon-catalog.py \
|
||||
--input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json \
|
||||
--package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json \
|
||||
--react-package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json \
|
||||
--react-exports-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json \
|
||||
--curated-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv \
|
||||
--output "$candidate_dir/phosphor-icons-upstream.json" \
|
||||
--verified-at 2026-08-13 --expected-count 2
|
||||
```
|
||||
|
||||
Refresh upstream live sengaja diisolasi dalam workflow `refresh-catalogs.yml`,
|
||||
dijadwalkan setiap Senin pukul 03:17 UTC dan juga tersedia untuk dijalankan secara manual.
|
||||
Konfigurasikan `GOOGLE_FONTS_API_KEY` sebagai secret GitHub Actions, lalu jalankan dan
|
||||
unduh artifact review-nya:
|
||||
|
||||
```bash
|
||||
gh workflow run refresh-catalogs.yml
|
||||
run_id="$(gh run list --workflow refresh-catalogs.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
||||
gh run watch "$run_id"
|
||||
gh run download "$run_id" --name "catalog-refresh-review-$run_id"
|
||||
```
|
||||
|
||||
Workflow membaca Google Fonts Developer API dan package resmi Phosphor yang di-pin,
|
||||
menulis kandidat serta unified diff ke artifact, dan hanya memiliki izin read-only
|
||||
pada repository. Workflow tidak pernah melakukan commit, push, membuka PR, atau merge. Tinjau
|
||||
laporan perubahan, pengecualian, lisensi, relevance metrics, dan offline gate
|
||||
sebelum secara manual mempromosikan file kandidat ke `src/ui-ux-pro-max/data/`.
|
||||
|
||||
|
||||
## Rilis Otomatis
|
||||
|
||||
Repository ini menggunakan semantic-release dengan Conventional Commits untuk membuat rilis GitHub secara otomatis:
|
||||
|
||||
- Branch `dev` membuat GitHub prerelease beta seperti `2.6.0-beta.1`.
|
||||
- Branch `main` membuat rilis GitHub stable resmi seperti `2.6.0`.
|
||||
|
||||
Release notes dan `CHANGELOG.md` dibuat dari pesan Conventional Commit. Nomor versi disinkronkan di `skill.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `cli/package.json`, dan `cli/package-lock.json` selama persiapan rilis.
|
||||
|
||||
Gunakan tipe commit berikut agar version bump benar:
|
||||
|
||||
- `fix:` -> patch release
|
||||
- `feat:` -> minor release
|
||||
- `feat!:` atau `BREAKING CHANGE:` -> major release
|
||||
|
||||
Workflow rilis menggunakan `GITHUB_TOKEN` default untuk rilis GitHub dan secret repository `NPM_TOKEN` untuk memublikasikan `ui-ux-pro-max-cli` ke npm.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `uipro: unknown command 'uninstall'` atau `unknown command 'update'`
|
||||
|
||||
Versi `ui-ux-pro-max-cli` yang terinstal sudah usang. Perbarui lalu coba lagi:
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli@latest
|
||||
uipro uninstall
|
||||
```
|
||||
|
||||
### `uipro uninstall` menampilkan "No installed AI skill directories detected"
|
||||
|
||||
Skill terinstal di direktori yang berbeda dari tempat Anda menjalankan command. Pilih salah satu:
|
||||
|
||||
```bash
|
||||
# Option A — run from the project root where you originally installed it
|
||||
cd /path/to/your/project
|
||||
uipro uninstall
|
||||
|
||||
# Option B — remove the global install
|
||||
uipro uninstall --global
|
||||
|
||||
# Option C — remove manually
|
||||
rm -rf .claude/skills/ui-ux-pro-max # Claude Code
|
||||
rm -rf .cursor/skills/ui-ux-pro-max # Cursor
|
||||
rm -rf .windsurf/skills/ui-ux-pro-max # Windsurf
|
||||
rm -rf .agents/skills/ui-ux-pro-max # Antigravity / Codex
|
||||
```
|
||||
|
||||
### Dialog "Upload a skill" Claude.ai menampilkan "Zip contains too many files (maximum 200)"
|
||||
|
||||
Jangan upload ZIP repository GitHub secara penuh. Itu adalah checkout development yang berisi source code, aset CLI, dokumentasi, preview, dan beberapa skill yang dibundel, sehingga melebihi batas upload 200 file milik Claude. ZIP tersebut bukan artifact upload skill Claude, dan proyek ini saat ini tidak menerbitkan ZIP manual terpisah untuk upload ke Claude.ai.
|
||||
|
||||
Untuk Claude Code, instal melalui Marketplace:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
Atau gunakan installer CLI:
|
||||
|
||||
```bash
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### Instalasi Claude Marketplace gagal dengan "Zip file contains a symbolic link"
|
||||
|
||||
Ini adalah masalah yang diketahui pada versi sebelum v2.5.1. Repository sebelumnya menggunakan symlink secara internal yang tidak dapat ditangani oleh sebagian tool instalasi. **Perbaikan:** gunakan installer CLI sebagai gantinya:
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai claude
|
||||
```
|
||||
|
||||
Atau tunggu rilis berikutnya yang menyelesaikan masalah ini.
|
||||
|
||||
### `npm install -g ui-ux-pro-max-cli` gagal karena permission error
|
||||
|
||||
Gunakan Node version manager (direkomendasikan), atau lewati instalasi global sepenuhnya:
|
||||
|
||||
```bash
|
||||
# npx without installing globally
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### Python tidak ditemukan saat menjalankan command design system
|
||||
|
||||
Script pencarian memerlukan Python 3.x. Instal secara manual dari [python.org](https://www.python.org/downloads/) atau melalui package manager OS Anda (Homebrew, apt, winget). Agent AI tidak boleh menginstalnya untuk Anda — agent diperintahkan untuk meminta Anda melakukannya.
|
||||
|
||||
### Output design system terpotong / field ter-truncate
|
||||
|
||||
Output yang mudah dibaca manusia memotong field panjang pada 300 karakter. Gunakan `--json` untuk mendapatkan data lengkap tanpa pemotongan:
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS" --domain style --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Riwayat Star
|
||||
|
||||
[](https://star-history.dera.page/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
|
||||
## Lisensi
|
||||
|
||||
Proyek ini dilisensikan di bawah [MIT License](LICENSE).
|
||||
|
||||
## Agent yang Kompatibel
|
||||
|
||||
Skill ini bekerja dengan:
|
||||
- [Claude Code](https://claude.com/product/claude-code)
|
||||
- [AdaL](https://sylph.ai/) - Agent coding yang dapat berkembang secara mandiri ([Dokumentasi](https://docs.sylph.ai/) | [GitHub](https://github.com/SylphAI-Inc/adal-cli))
|
||||
632
README.ko.md
Normal file
632
README.ko.md
Normal file
@ -0,0 +1,632 @@
|
||||
# [UI UX Pro Max](https://uupm.cc)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.id.md">🇮🇩 Bahasa Indonesia</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.ko.md">🇰🇷 한국어</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.vi.md">🇻🇳 Tiếng Việt</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/releases"><img src="https://img.shields.io/github/v/release/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=blue" alt="GitHub 릴리스"></a>
|
||||
<img src="https://img.shields.io/badge/reasoning_rules-192-green?style=for-the-badge" alt="추론 규칙 192개">
|
||||
<img src="https://img.shields.io/badge/UI_styles-79_searchable-purple?style=for-the-badge" alt="검색 가능한 UI 스타일 79개">
|
||||
<img src="https://img.shields.io/badge/python-3.x-yellow?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.x">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/LICENSE"><img src="https://img.shields.io/github/license/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=green" alt="라이선스"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/v/ui-ux-pro-max-cli?style=flat-square&logo=npm&label=CLI" alt="npm"></a>
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/dm/ui-ux-pro-max-cli?style=flat-square&label=downloads" alt="npm 다운로드 수"></a>
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/stargazers"><img src="https://img.shields.io/github/stars/nextlevelbuilder/ui-ux-pro-max-skill?style=flat-square&logo=github" alt="GitHub 스타 수"></a>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Support%20Development-00457C?style=flat-square&logo=paypal&logoColor=white" alt="PayPal"></a>
|
||||
</p>
|
||||
|
||||
여러 플랫폼과 프레임워크에서 전문적인 UI/UX를 구축할 수 있도록 디자인 인텔리전스를 제공하는 AI 스킬입니다.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://uupm.cc">
|
||||
<img src="screenshots/website.png" alt="UI UX Pro Max" width="800">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>이 프로젝트가 유용하다면 후원을 고려해 주세요:</b><br><br>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Donate-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal 후원"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<i>다른 프로젝트</i><br>
|
||||
<a href="https://nextlevelbuilder.io">NextLevelBuilder.io</a> | <a href="https://goclaw.sh">GoClaw.sh</a> | <a href="https://claudekit.cc">ClaudeKit.cc</a> | <a href="https://tose.sh">TOSE.sh</a>
|
||||
</p>
|
||||
|
||||
## v2.0의 새로운 기능
|
||||
|
||||
### 지능형 디자인 시스템 생성
|
||||
|
||||
v2.0의 핵심 기능은 **디자인 시스템 생성기**입니다. AI 기반 추론 엔진이 프로젝트 요구 사항을 분석하고 몇 초 만에 완전한 맞춤형 디자인 시스템을 생성합니다.
|
||||
|
||||
```
|
||||
대상: Serenity Spa - 권장 디자인 시스템
|
||||
|
||||
패턴: 히어로 중심 + 사회적 증거
|
||||
전환: 신뢰 요소를 활용한 감성 중심 설계
|
||||
CTA: 첫 화면에 배치하고 후기 다음에 반복
|
||||
섹션:
|
||||
1. 히어로
|
||||
2. 서비스
|
||||
3. 고객 후기
|
||||
4. 예약
|
||||
5. 연락처
|
||||
|
||||
스타일: Soft UI Evolution
|
||||
키워드: 부드러운 그림자, 은은한 깊이, 편안함, 고급스러움, 유기적 형태
|
||||
추천 대상: 웰니스, 뷰티, 라이프스타일 브랜드, 프리미엄 서비스
|
||||
성능: cost:low | 접근성: risk:conditional; 요구 사항 확인 필요
|
||||
|
||||
색상:
|
||||
기본: #E8B4B8 (연한 분홍색)
|
||||
보조: #A8D5BA (세이지 그린)
|
||||
CTA: #D4AF37 (금색)
|
||||
배경: #FFF5F5 (따뜻한 흰색)
|
||||
텍스트: #2D3436 (차콜)
|
||||
참고: 차분한 팔레트에 금색 포인트를 더해 고급스러운 분위기 연출
|
||||
|
||||
타이포그래피: Cormorant Garamond / Montserrat
|
||||
분위기: 우아함, 차분함, 세련됨
|
||||
추천 대상: 명품 브랜드, 웰니스, 뷰티, 에디토리얼
|
||||
Google Fonts: https://fonts.google.com/share?selection.family=...
|
||||
|
||||
핵심 효과:
|
||||
부드러운 그림자 + 맥락에 맞는 전환 + 은은한 호버 상태
|
||||
|
||||
피해야 할 요소(안티패턴):
|
||||
밝은 네온 색상 + 과한 애니메이션 + 다크 모드 + AI 스타일 보라/분홍 그라디언트
|
||||
|
||||
전달 전 체크리스트:
|
||||
[ ] 이모지를 아이콘으로 사용하지 않음(SVG: Heroicons/Lucide 사용)
|
||||
[ ] 클릭 가능한 모든 요소에 cursor-pointer 적용
|
||||
[ ] 플랫폼, 컴포넌트, 사용자 환경설정에 맞는 상호작용 타이밍 적용
|
||||
[ ] 라이트 모드: 텍스트 명암비 최소 4.5:1
|
||||
[ ] 키보드 탐색 시 포커스 상태 표시
|
||||
[ ] prefers-reduced-motion 준수
|
||||
[ ] 텍스트, 칩, 배지가 잘리거나 깨지지 않고 재배치
|
||||
[ ] 반응형: 375px, 768px, 1024px, 1440px
|
||||
```
|
||||
|
||||
### 디자인 시스템 생성 방식
|
||||
|
||||
```
|
||||
1. 사용자 요청
|
||||
"내 뷰티 스파의 랜딩 페이지를 만들어 줘"
|
||||
|
||||
↓
|
||||
|
||||
2. 다중 도메인 검색(5개 병렬 검색)
|
||||
• 제품 유형 매칭(192개 분야)
|
||||
• 스타일 추천(검색 가능 79개, 활성 50개)
|
||||
• 색상 팔레트 선택(192개 팔레트)
|
||||
• 랜딩 페이지 패턴(34개 패턴)
|
||||
• 타이포그래피 조합(74개 글꼴 조합)
|
||||
|
||||
↓
|
||||
|
||||
3. 추론 엔진
|
||||
• 제품 → UI 분야 규칙 매칭
|
||||
• 스타일 우선순위 적용(BM25 순위)
|
||||
• 산업별 안티패턴 필터링
|
||||
• 의사결정 규칙 처리(JSON 조건)
|
||||
|
||||
↓
|
||||
|
||||
4. 완전한 디자인 시스템 출력
|
||||
패턴 + 스타일 + 색상 + 타이포그래피 + 효과
|
||||
+ 피해야 할 안티패턴 + 전달 전 체크리스트
|
||||
```
|
||||
|
||||
### 192개의 산업별 추론 규칙
|
||||
|
||||
추론 엔진에는 다음 분야에 특화된 규칙이 포함되어 있습니다.
|
||||
|
||||
| 분야 | 예시 |
|
||||
|------|------|
|
||||
| **기술 및 SaaS** | SaaS, 마이크로 SaaS, B2B 서비스, 개발자 도구/IDE, AI/챗봇 플랫폼, 사이버 보안 플랫폼 |
|
||||
| **금융** | 핀테크/암호화폐, 은행, 보험, 개인 재무 추적기, 청구서 및 결제 도구 |
|
||||
| **헬스케어** | 병원, 약국, 치과, 동물병원, 정신 건강, 복약 알림 |
|
||||
| **전자상거래** | 일반, 명품, 마켓플레이스(P2P), 구독 상자, 음식 배달 |
|
||||
| **서비스** | 뷰티/스파, 레스토랑, 호텔, 법률, 홈 서비스, 예약 및 일정 관리 |
|
||||
| **크리에이티브** | 포트폴리오, 에이전시, 사진, 게임, 음악 스트리밍, 사진/동영상 편집기 |
|
||||
| **라이프스타일** | 습관 추적기, 요리 및 레시피, 명상, 날씨, 일기, 기분 추적기 |
|
||||
| **신기술** | Web3/NFT, 공간 컴퓨팅, 양자 컴퓨팅, 자율 드론 함대 |
|
||||
|
||||
각 규칙에는 다음 항목이 포함됩니다.
|
||||
- **권장 패턴** - 랜딩 페이지 구조
|
||||
- **스타일 우선순위** - 가장 적합한 UI 스타일
|
||||
- **색상 분위기** - 산업에 적합한 색상 팔레트
|
||||
- **타이포그래피 분위기** - 글꼴의 개성 조합
|
||||
- **핵심 효과** - 애니메이션 및 상호작용
|
||||
- **안티패턴** - 피해야 할 요소(예: 은행 서비스의 "AI 스타일 보라색/분홍색 그라디언트")
|
||||
|
||||
## 기능
|
||||
|
||||
- **검색 가능한 UI 스타일 79개(활성 50개)** - 글래스모피즘, 클레이모피즘, 미니멀리즘, 브루탈리즘, 뉴모피즘, 벤토 그리드, 다크 모드, AI 네이티브 UI 등
|
||||
- **색상 팔레트 192개** - 192개 제품 유형과 1:1로 정렬된 산업별 팔레트
|
||||
- **글꼴 조합 74개** - Google Fonts 가져오기 코드가 포함된 엄선된 타이포그래피 조합
|
||||
- **차트 유형 25개** - 대시보드 및 분석 화면을 위한 권장 사항
|
||||
- **기술 스택 22개** - React, Next.js, Astro, Vue, Nuxt.js, Nuxt UI, Svelte, SwiftUI, React Native, Flutter, HTML+Tailwind, shadcn/ui, Jetpack Compose, Angular, Laravel, Three.js, JavaFX, WPF, WinUI 3, UWP, Avalonia, Uno Platform
|
||||
- **UX 가이드라인 119개** - 모범 사례, 안티패턴, 접근성 규칙, 유연한 텍스트 레이아웃, 간결한 레이블, 취소 가능한 상호작용
|
||||
- **추론 규칙 192개** - 산업별 디자인 시스템 생성(v2.0의 새로운 기능)
|
||||
|
||||
### 유연한 텍스트와 컴팩트 UI
|
||||
|
||||
이 가이드는 제목, 긴 토큰, 칩, 배지, 중단된 마이크로 인터랙션에서 흔히 발생하는 실제 서비스 문제를 다룹니다.
|
||||
|
||||
- 제목의 균형 잡힌 줄바꿈은 점진적 향상 기능일 뿐, 특정 단어가 마지막 줄에 남는다는 보장은 아닙니다. 너비, 글꼴, 로케일에 따른 자연스러운 줄바꿈에서도 디자인이 정상적으로 작동해야 합니다.
|
||||
- 필수 텍스트는 좁은 화면, 브라우저 확대, 텍스트 크기 조정, 사용자 간격 설정에서도 잘리지 않고 재배치되어야 합니다. 긴 URL과 식별자는 안전하게 줄바꿈할 수 있어야 합니다.
|
||||
- 칩과 태그 모음은 줄바꿈되거나 조작 가능한 `+n` 펼치기 기능을 사용해야 합니다. 간결한 레이블은 가능한 한 온전하게 유지하고, 불가피하게 잘라야 한다면 키보드·포인터·터치 사용자가 전체 값을 확인할 수 있는 접근 가능한 경로를 제공해야 합니다.
|
||||
- 배지의 의미를 색상에만 의존해서는 안 됩니다. 상호작용 가능한 칩에는 네이티브 시맨틱, 명확한 포커스, 프로그래밍 방식의 상태가 필요하며 실시간 개수에는 의미 있는 맥락이 필요합니다.
|
||||
- 빠른 상호작용은 애니메이션을 취소할 수 있지만, 최종 시맨틱 상태·포커스·콘텐츠는 정확해야 합니다. 타이밍은 플랫폼과 컴포넌트에 맞게 선택하고 모션 감소 환경설정을 존중해야 합니다.
|
||||
|
||||
### 스타일 분류 체계
|
||||
|
||||
카탈로그에는 안정적인 ID와 별칭으로 뒷받침되는 **검색 가능한 스타일 79개**가 포함됩니다.
|
||||
|
||||
| 상태 | 개수 | 검색 동작 |
|
||||
|------|-----:|-----------|
|
||||
| 활성 | 50 | 일반 추천에 포함되며 갤러리에 기본 표시 |
|
||||
| 보조 | 29 | 정확하거나 명시적인 변형/시스템 의도일 때 반환되며 갤러리 상태 필터에서 사용 가능 |
|
||||
| 사용 중단 | 9 | 일반 순위에서 제외되며 기존 이름은 표준 스타일 또는 랜딩 패턴으로 연결 |
|
||||
|
||||
활성 세트는 일반 시각 스타일군 43개, 모바일 전용 스타일 2개, 공식 플랫폼/디자인 시스템 3개, 플랫폼 소재 1개, 핵심 분석 스타일 1개를 포함합니다. 현재 공식 시스템에는 Fluent 2, Shopify Polaris, Adobe Spectrum이 포함됩니다. Liquid Glass는 Apple 플랫폼 소재로 한정되고, Material 3 Expressive는 모바일 Material 변형으로 유지되며, Spectrum 2는 보조 스타일입니다. 랜딩 페이지 구조는 BM25 순위에서 시각 스타일과 경쟁하지 않고 별도의 34개 패턴 랜딩 데이터셋에 포함됩니다.
|
||||
|
||||
전체 분류 체계와 출처 인식 메타데이터는 [`styles.csv`](src/ui-ux-pro-max/data/styles.csv)를 참고하세요.
|
||||
|
||||
## 💎 기본 버전과 프리미엄 버전 비교
|
||||
|
||||
많은 사용자가 오픈 소스 버전과 프리미엄 버전의 차이를 묻습니다. 워크플로에 적합한 버전을 선택할 수 있도록 자세히 비교했습니다.
|
||||
|
||||
### 🟢 기본 버전(이 저장소)
|
||||
* **완전한 오픈 소스:** 개인 개발자, 취미 개발자, 일반 프로젝트에 적합합니다.
|
||||
* **핵심 UI/UX 인텔리전스:** 검색 가능한 UI 스타일 79개(활성 50개), 제품 유형 192개, 색상 팔레트, 엄선된 글꼴 조합을 모두 사용할 수 있습니다.
|
||||
* **스마트 추천:** 내장 BM25 검색 엔진으로 정확도 높은 디자인 매칭을 제공합니다.
|
||||
* **크로스 플랫폼 지원:** 22개 주요 프레임워크(React, Vue, Tailwind, iOS, Android 등)를 위한 스택별 가이드라인을 제공합니다.
|
||||
* **디자인 시스템 생성:** CLI를 통해 맞춤형 UI 규칙, 패턴, 로직을 즉시 생성합니다.
|
||||
|
||||
### 🟡 프리미엄 버전
|
||||
* **확장된 브랜드 디자인 스킬:** UI/UX를 넘어 브랜드 아이덴티티 생성, 로고 디자인, 기업 아이덴티티 프로그램(CIP), 배너, 프레젠테이션 슬라이드, 맞춤형 아이콘 제작을 포함합니다.
|
||||
* **고급 에셋 생성:** AI 기반 이미지 생성과 긴밀하게 통합되어 플레이스홀더가 아닌 실제 시각 에셋을 만듭니다.
|
||||
* **엔터프라이즈 아키텍처:** 대규모 팀 배포를 위한 더 포괄적이고 확장 가능한 디자인 토큰 아키텍처를 제공합니다.
|
||||
* **우선 지원:** 중단 없는 전체 디자인 워크플로가 필요한 팀과 전문가에게 전용 기술 지원을 제공합니다.
|
||||
|
||||
👉 *프리미엄 등급 업그레이드에 관한 자세한 내용은 [uupm.cc](https://uupm.cc)를 참고하세요.*
|
||||
|
||||
## 설치
|
||||
|
||||
### Claude Marketplace 사용(Claude Code)
|
||||
|
||||
Claude Code에서 다음 두 명령어로 바로 설치할 수 있습니다.
|
||||
|
||||
```
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
### CLI 사용(권장)
|
||||
|
||||
```bash
|
||||
# CLI 전역 설치
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
|
||||
# 프로젝트로 이동
|
||||
cd /path/to/your/project
|
||||
|
||||
# 사용하는 AI 어시스턴트용으로 설치
|
||||
uipro init --ai claude # Claude Code
|
||||
uipro init --ai cursor # Cursor
|
||||
uipro init --ai windsurf # Windsurf
|
||||
uipro init --ai antigravity # Antigravity
|
||||
uipro init --ai copilot # GitHub Copilot
|
||||
uipro init --ai kiro # Kiro
|
||||
uipro init --ai codex # Codex CLI
|
||||
uipro init --ai qoder # Qoder
|
||||
uipro init --ai roocode # Roo Code
|
||||
uipro init --ai gemini # Gemini CLI
|
||||
uipro init --ai trae # Trae
|
||||
uipro init --ai opencode # OpenCode
|
||||
uipro init --ai continue # Continue
|
||||
uipro init --ai codebuddy # CodeBuddy
|
||||
uipro init --ai droid # Droid (Factory)
|
||||
uipro init --ai kilocode # KiloCode
|
||||
uipro init --ai warp # Warp
|
||||
uipro init --ai augment # Augment
|
||||
uipro init --ai codewhale # CodeWhale
|
||||
uipro init --ai openclaw # OpenClaw
|
||||
uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
|
||||
uipro init --ai all # All assistants
|
||||
```
|
||||
|
||||
npm 패키지명은 `ui-ux-pro-max-cli`이며, 설치되는 명령어는 `uipro`입니다. 이전 `uipro-cli` 릴리스는 오래되었으므로 현재 에셋과 함께 사용하지 마세요.
|
||||
|
||||
### 전역 설치(모든 프로젝트에서 사용)
|
||||
|
||||
```bash
|
||||
uipro init --ai claude --global # Install to ~/.claude/skills/
|
||||
uipro init --ai cursor --global # Install to ~/.cursor/skills/
|
||||
uipro init --ai universal --global # Install to ~/.agents/skills/
|
||||
```
|
||||
|
||||
### 기타 CLI 명령어
|
||||
|
||||
```bash
|
||||
uipro versions # List available versions
|
||||
uipro update # Refresh skill files from installed CLI package
|
||||
uipro update --global # Refresh global skill files from installed CLI package
|
||||
uipro init --offline # Compatibility flag; installs bundled templates
|
||||
uipro uninstall # Remove skill (auto-detect platform)
|
||||
uipro uninstall --ai claude # Remove specific platform
|
||||
uipro uninstall --global # Remove from global install
|
||||
```
|
||||
|
||||
## 사전 요구 사항
|
||||
|
||||
검색 스크립트를 실행하려면 Python 3.x가 필요합니다. 표준 라이브러리만 사용하며, 스크립트는 아무것도 설치하지 않고 네트워크 요청도 보내지 않습니다.
|
||||
|
||||
Python 설치 여부를 확인하세요.
|
||||
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
설치되어 있지 않다면 [python.org](https://www.python.org/downloads/) 또는 운영체제의 패키지 관리자(Homebrew, apt, winget)를 사용해 직접 설치하세요. 이 설치 단계는 **사람인 사용자**가 수행해야 합니다. 이 스킬을 사용하는 AI 에이전트는 사용자 컴퓨터에 소프트웨어를 직접 설치하지 않고, 사용자에게 설치를 요청하도록 지시되어 있습니다.
|
||||
|
||||
## 사용법
|
||||
|
||||
### 스킬 모드(자동 활성화)
|
||||
|
||||
**지원:** Claude Code, Cursor, Windsurf, Antigravity, Codex CLI, Continue, Gemini CLI, OpenCode, Qoder, CodeBuddy, Droid (Factory), KiloCode, Warp, Augment, CodeWhale
|
||||
|
||||
UI/UX 작업을 요청하면 스킬이 자동으로 활성화됩니다. 자연스럽게 요청하세요.
|
||||
|
||||
```
|
||||
내 SaaS 제품의 랜딩 페이지를 만들어 줘
|
||||
```
|
||||
|
||||
> **Trae**: 먼저 **SOLO** 모드로 전환하세요. UI/UX 요청 시 스킬이 활성화됩니다.
|
||||
|
||||
### 워크플로 모드(슬래시 명령어)
|
||||
|
||||
**지원:** Kiro, GitHub Copilot, Roo Code, KiloCode
|
||||
|
||||
슬래시 명령어로 스킬을 실행하세요.
|
||||
|
||||
```
|
||||
/ui-ux-pro-max 내 SaaS 제품의 랜딩 페이지를 만들어 줘
|
||||
```
|
||||
|
||||
### 프롬프트 예시
|
||||
|
||||
```
|
||||
내 SaaS 제품의 랜딩 페이지를 만들어 줘
|
||||
|
||||
헬스케어 분석 대시보드를 만들어 줘
|
||||
|
||||
다크 모드 포트폴리오 웹사이트를 디자인해 줘
|
||||
|
||||
전자상거래 모바일 앱 UI를 만들어 줘
|
||||
|
||||
어두운 테마의 핀테크 뱅킹 앱을 만들어 줘
|
||||
```
|
||||
|
||||
### 작동 방식
|
||||
|
||||
1. **요청** - UI/UX 작업을 요청합니다(구축, 디자인, 생성, 구현, 검토, 수정, 개선).
|
||||
2. **디자인 시스템 생성** - AI가 추론 엔진을 사용해 완전한 디자인 시스템을 자동으로 생성합니다.
|
||||
3. **스마트 추천** - 제품 유형과 요구 사항에 따라 가장 적합한 스타일, 색상, 타이포그래피를 찾습니다.
|
||||
4. **코드 생성** - 적절한 색상, 글꼴, 간격, 모범 사례를 적용해 UI를 구현합니다.
|
||||
5. **전달 전 검사** - 일반적인 UI/UX 안티패턴을 기준으로 검증합니다.
|
||||
|
||||
### 지원 스택
|
||||
|
||||
이 스킬은 다음 스택별 가이드라인을 제공합니다.
|
||||
|
||||
| 분야 | 스택 |
|
||||
|------|------|
|
||||
| **웹(HTML)** | HTML + Tailwind(기본값) |
|
||||
| **React 생태계** | React, Next.js, shadcn/ui |
|
||||
| **Vue 생태계** | Vue, Nuxt.js, Nuxt UI |
|
||||
| **Angular** | Angular |
|
||||
| **PHP** | Laravel(Blade, Livewire, Inertia.js) |
|
||||
| **기타 웹** | Svelte, Astro, Three.js |
|
||||
| **데스크톱** | JavaFX, WPF, WinUI 3, Avalonia, Uno Platform, UWP |
|
||||
| **iOS** | SwiftUI |
|
||||
| **Android** | Jetpack Compose |
|
||||
| **크로스 플랫폼** | React Native, Flutter |
|
||||
|
||||
프롬프트에서 원하는 스택을 언급하거나 기본값인 HTML + Tailwind를 사용하세요.
|
||||
|
||||
## 디자인 시스템 명령어(고급)
|
||||
|
||||
디자인 시스템 생성기에 직접 접근하려면 다음 명령어를 사용하세요.
|
||||
|
||||
> 참고: Continue로 설치했다면 아래 명령어의 `.claude/skills/`를 `.continue/skills/`로 바꾸세요. Droid(Factory)는 `.factory/skills/`를 사용합니다.
|
||||
|
||||
```bash
|
||||
# ASCII 출력으로 디자인 시스템 생성
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --design-system -p "Serenity Spa"
|
||||
|
||||
# Markdown 출력으로 생성
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "fintech banking" --design-system -f markdown
|
||||
|
||||
# 도메인별 검색
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "glassmorphism" --domain style
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant serif" --domain typography
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "dashboard" --domain chart
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "error summary validation" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "decorative icon aria hidden" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "icon button accessible label" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "orphan heading line balance" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "badge chip label wraps to second line" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "rapid chip animation interrupted" --domain ux
|
||||
|
||||
# 스택별 가이드라인
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "form validation" --stack react
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "responsive layout" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "chip badge overflow nowrap" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "tableview binding" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "atlantafx primer enterprise theme" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "enterprise tableview density permission" --stack javafx
|
||||
```
|
||||
|
||||
웹 스택 검색은 버전을 인식합니다. 이전 메이저 버전을 지정하지 않은 쿼리는 현재 활성 가이드라인을 반환합니다. 명시적인 레거시 용어나 이전 메이저 버전(예: `Svelte 4`, `Next.js 15`)을 지정하면 `Status`와 `Applies To`가 표시된 엄선된 레거시 행만 반환합니다. 일치하는 레거시 가이드라인이 없다면 프레임워크 세대를 섞지 않고 결과를 반환하지 않습니다.
|
||||
|
||||
### 디자인 시스템 저장(마스터 + 오버라이드 패턴)
|
||||
|
||||
세션 간 **계층적 검색**을 위해 디자인 시스템을 파일로 저장하세요.
|
||||
|
||||
```bash
|
||||
# 디자인 시스템을 생성하여 design-system/myapp/MASTER.md에 저장
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp"
|
||||
|
||||
# 페이지별 오버라이드 파일도 생성
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp" --page "dashboard"
|
||||
```
|
||||
|
||||
다음과 같은 `design-system/` 폴더 구조가 생성됩니다.
|
||||
|
||||
```
|
||||
design-system/
|
||||
└── myapp/ # 프로젝트마다 폴더 하나(-p "MyApp"의 슬러그)
|
||||
├── MASTER.md # 전역 단일 정보 출처(색상, 타이포그래피, 간격, 컴포넌트)
|
||||
└── pages/
|
||||
└── dashboard.md # 페이지별 오버라이드(마스터와 다른 내용만 기록)
|
||||
```
|
||||
|
||||
**계층적 검색 방식:**
|
||||
1. 특정 페이지(예: "결제")를 만들 때 먼저 `design-system/[project-slug]/pages/checkout.md`를 확인합니다.
|
||||
2. 페이지 파일이 있으면 해당 규칙이 마스터 파일을 **재정의**합니다.
|
||||
3. 없으면 `design-system/[project-slug]/MASTER.md`만 사용합니다.
|
||||
|
||||
**컨텍스트 인식 검색 프롬프트:**
|
||||
```
|
||||
[페이지 이름] 페이지를 만들고 있습니다. design-system/[project-slug]/MASTER.md를 읽어 주세요.
|
||||
design-system/[project-slug]/pages/[page-name].md 파일이 있는지도 확인해 주세요.
|
||||
페이지 파일이 있으면 해당 규칙을 우선 적용하세요.
|
||||
없으면 마스터 규칙만 사용하세요.
|
||||
이제 코드를 생성해 주세요...
|
||||
```
|
||||
|
||||
## 아키텍처 및 기여
|
||||
|
||||
### 사용자 안내
|
||||
|
||||
코드베이스는 **템플릿 기반 생성 시스템**을 사용하도록 재구성되었습니다. 모든 플랫폼별 파일(`.cursor/`, `.windsurf/`, `.kiro/`, `.factory/` 등)은 이제 CLI에서 동적으로 생성됩니다.
|
||||
|
||||
**항상 CLI를 사용해 설치하세요.**
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai <platform>
|
||||
```
|
||||
|
||||
이 방법을 사용하면 설치된 CLI 패키지에 포함된 최신 템플릿과 AI 어시스턴트에 맞는 올바른 파일 구조를 받을 수 있습니다. 새 릴리스가 배포되면 먼저 npm 패키지를 업데이트하세요.
|
||||
|
||||
### 기여자 안내
|
||||
|
||||
이 프로젝트에 기여하려면 다음 절차를 따르세요.
|
||||
|
||||
```bash
|
||||
# 1. 저장소 복제
|
||||
git clone https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
|
||||
cd ui-ux-pro-max-skill
|
||||
|
||||
# 2. 구조 이해
|
||||
src/ui-ux-pro-max/ # 단일 정보 출처(데이터, 스크립트, 템플릿)
|
||||
cli/ # CLI 설치 도구(템플릿에서 파일 생성)
|
||||
.claude/ # Claude Code 스킬 로컬 개발/테스트
|
||||
.factory/ # Droid(Factory) 스킬 로컬 개발/테스트
|
||||
|
||||
# 3. src/ui-ux-pro-max/에서 변경
|
||||
# - data/*.csv → 데이터베이스 파일
|
||||
# - scripts/*.py → 검색 엔진 및 디자인 시스템
|
||||
# - templates/ → 플랫폼별 템플릿
|
||||
|
||||
# 4. CLI에 동기화하고 로컬 테스트
|
||||
cd cli
|
||||
npm run sync:assets
|
||||
npm run check:assets
|
||||
npm run verify:data
|
||||
npm run typecheck
|
||||
|
||||
# 5. CLI 빌드 및 테스트
|
||||
# `npm run build`는 Bun이 있으면 사용하고, 없으면 `npm ci` 후 TypeScript 컴파일러 출력을 사용합니다.
|
||||
npm run build
|
||||
node dist/index.js init --ai claude --offline # Test in a temp folder
|
||||
|
||||
# 6. PR 생성(main에 직접 푸시하지 않음)
|
||||
git checkout -b feat/your-feature
|
||||
git commit -m "feat: description"
|
||||
git push -u origin feat/your-feature
|
||||
gh pr create
|
||||
```
|
||||
|
||||
자세한 개발 가이드라인은 [CLAUDE.md](CLAUDE.md)를 참고하세요.
|
||||
|
||||
### 카탈로그 출처 및 갱신
|
||||
|
||||
커밋된 카탈로그 요약에는 현재 **승인된 Google Fonts 1,934개**와 공식 라이선스 메타데이터가 일치하지 않아 반영되지 않은 **검토 제외 항목 8개**가 기록되어 있습니다. 아이콘 가이드는 **엄선된 105개 행**(Phosphor 웹 직접 가져오기 100개와 React Native/대체 가이드)으로 유지됩니다. 별도의 **1,512개 아이콘이 포함된 업스트림 Phosphor 매니페스트**는 검색 결과를 업스트림 패키지 전체로 채우지 않으면서 이름, 두께, React/SSR 가져오기를 검증합니다.
|
||||
|
||||
일반 개발 및 풀 리퀘스트 CI는 네트워크에 의존하지 않습니다. 스냅샷 해시와 생성 개수 검증을 포함한 전체 오프라인 게이트를 실행하려면 다음 명령어를 사용하세요.
|
||||
|
||||
```bash
|
||||
npm --prefix cli run verify:data
|
||||
# 또는 생성된 카탈로그 요약만 검사:
|
||||
npm --prefix cli run validate:catalog-summary
|
||||
```
|
||||
|
||||
갱신 정규화도 커밋된 픽스처를 사용해 완전히 오프라인으로 실행할 수 있습니다. 출력은 임시 후보 디렉터리에 저장되며 표준 데이터를 대체하지 않습니다.
|
||||
|
||||
```bash
|
||||
candidate_dir="$(mktemp -d)"
|
||||
python3 scripts/refresh-google-fonts.py \
|
||||
--api-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json \
|
||||
--metadata-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json \
|
||||
--existing-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv \
|
||||
--overrides src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json \
|
||||
--output-csv "$candidate_dir/google-fonts.csv" \
|
||||
--license-output "$candidate_dir/google-font-licenses.json" \
|
||||
--metadata-revision fixture-catalogs-v1 \
|
||||
--verified-at 2026-08-13 --expected-count 2 --approve-changes
|
||||
python3 scripts/refresh-icon-catalog.py \
|
||||
--input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json \
|
||||
--package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json \
|
||||
--react-package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json \
|
||||
--react-exports-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json \
|
||||
--curated-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv \
|
||||
--output "$candidate_dir/phosphor-icons-upstream.json" \
|
||||
--verified-at 2026-08-13 --expected-count 2
|
||||
```
|
||||
|
||||
실시간 업스트림 갱신은 의도적으로 `refresh-catalogs.yml` 워크플로에 격리되어 있으며, 매주 월요일 03:17 UTC에 실행되거나 필요할 때 수동으로 실행할 수 있습니다. `GOOGLE_FONTS_API_KEY`를 GitHub Actions 시크릿으로 설정한 다음 워크플로를 실행하고 검토용 아티팩트를 다운로드하세요.
|
||||
|
||||
```bash
|
||||
gh workflow run refresh-catalogs.yml
|
||||
run_id="$(gh run list --workflow refresh-catalogs.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
||||
gh run watch "$run_id"
|
||||
gh run download "$run_id" --name "catalog-refresh-review-$run_id"
|
||||
```
|
||||
|
||||
이 워크플로는 Google Fonts Developer API와 버전이 고정된 공식 Phosphor 패키지를 읽고, 후보 파일과 통합 diff를 아티팩트에 기록하며, 저장소 읽기 전용 권한만 가집니다. 커밋, 푸시, PR 생성, 병합은 수행하지 않습니다. 후보 파일을 `src/ui-ux-pro-max/data/`에 수동으로 반영하기 전에 변경 보고서, 제외 항목, 라이선스, 관련성 지표, 오프라인 게이트를 검토하세요.
|
||||
|
||||
|
||||
## 자동 릴리스
|
||||
|
||||
이 저장소는 Conventional Commits와 semantic-release를 사용해 GitHub 릴리스를 자동으로 생성합니다.
|
||||
|
||||
- `dev` 브랜치는 `2.6.0-beta.1`과 같은 베타 GitHub 프리릴리스를 생성합니다.
|
||||
- `main` 브랜치는 `2.6.0`과 같은 공식 안정 릴리스를 생성합니다.
|
||||
|
||||
릴리스 노트와 `CHANGELOG.md`는 Conventional Commit 메시지에서 생성됩니다. 릴리스 준비 중 `skill.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `cli/package.json`, `cli/package-lock.json`의 버전 번호가 동기화됩니다.
|
||||
|
||||
올바른 버전 증가를 위해 다음 커밋 유형을 사용하세요.
|
||||
|
||||
- `fix:` -> 패치 릴리스
|
||||
- `feat:` -> 마이너 릴리스
|
||||
- `feat!:` 또는 `BREAKING CHANGE:` -> 메이저 릴리스
|
||||
|
||||
릴리스 워크플로는 GitHub 릴리스에 기본 `GITHUB_TOKEN`을 사용하고, `ui-ux-pro-max-cli`를 npm에 배포할 때 저장소의 `NPM_TOKEN` 시크릿을 사용합니다.
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### `uipro: unknown command 'uninstall'` 또는 `unknown command 'update'`
|
||||
|
||||
설치된 `ui-ux-pro-max-cli` 버전이 오래되었습니다. 업데이트한 후 다시 시도하세요.
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli@latest
|
||||
uipro uninstall
|
||||
```
|
||||
|
||||
### `uipro uninstall` 실행 시 "No installed AI skill directories detected"가 표시되는 경우
|
||||
|
||||
명령어를 실행한 디렉터리가 스킬을 설치한 디렉터리와 다릅니다. 다음 중 하나를 수행하세요.
|
||||
|
||||
```bash
|
||||
# 방법 A — 처음 설치한 프로젝트 루트에서 실행
|
||||
cd /path/to/your/project
|
||||
uipro uninstall
|
||||
|
||||
# 방법 B — 전역 설치 제거
|
||||
uipro uninstall --global
|
||||
|
||||
# 방법 C — 수동 제거
|
||||
rm -rf .claude/skills/ui-ux-pro-max # Claude Code
|
||||
rm -rf .cursor/skills/ui-ux-pro-max # Cursor
|
||||
rm -rf .windsurf/skills/ui-ux-pro-max # Windsurf
|
||||
rm -rf .agents/skills/ui-ux-pro-max # Antigravity / Codex
|
||||
```
|
||||
|
||||
### Claude.ai의 "Upload a skill" 대화상자에서 "Zip contains too many files (maximum 200)"가 표시되는 경우
|
||||
|
||||
GitHub 저장소 전체 ZIP을 업로드하지 마세요. 이 ZIP은 소스 코드, CLI 에셋, 문서, 미리보기, 여러 번들 스킬을 포함한 개발용 체크아웃이므로 Claude의 파일 200개 업로드 제한을 초과합니다. Claude 스킬 업로드용 아티팩트가 아니며, 이 프로젝트는 현재 Claude.ai 수동 업로드용 ZIP을 별도로 배포하지 않습니다.
|
||||
|
||||
Claude Code에서는 Marketplace를 통해 설치하세요.
|
||||
|
||||
```bash
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
또는 CLI 설치 도구를 사용하세요.
|
||||
|
||||
```bash
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### Claude Marketplace 설치가 "Zip file contains a symbolic link" 오류로 실패하는 경우
|
||||
|
||||
v2.5.1 이전 버전에서 알려진 문제입니다. 저장소 내부에서 일부 설치 도구가 처리할 수 없는 심볼릭 링크를 사용했습니다. **해결 방법:** CLI 설치 도구를 사용하세요.
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai claude
|
||||
```
|
||||
|
||||
또는 이 문제가 해결된 다음 릴리스를 기다리세요.
|
||||
|
||||
### `npm install -g ui-ux-pro-max-cli` 명령이 권한 오류로 실패하는 경우
|
||||
|
||||
Node 버전 관리자를 사용하거나(권장) 전역 설치를 생략하세요.
|
||||
|
||||
```bash
|
||||
# 전역 설치 없이 npx 사용
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### 디자인 시스템 명령어 실행 시 Python을 찾을 수 없는 경우
|
||||
|
||||
검색 스크립트에는 Python 3.x가 필요합니다. [python.org](https://www.python.org/downloads/) 또는 운영체제의 패키지 관리자(Homebrew, apt, winget)를 사용해 직접 설치하세요. AI 에이전트는 대신 설치하지 않으며 사용자에게 설치를 요청하도록 지시되어 있습니다.
|
||||
|
||||
### 디자인 시스템 출력 또는 필드가 잘리는 경우
|
||||
|
||||
사람이 읽기 쉬운 출력에서는 긴 필드가 300자로 잘립니다. 잘리지 않은 전체 데이터를 받으려면 `--json`을 사용하세요.
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS" --domain style --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 스타 기록
|
||||
|
||||
[](https://star-history.dera.page/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
|
||||
## 라이선스
|
||||
|
||||
이 프로젝트는 [MIT 라이선스](LICENSE)에 따라 배포됩니다.
|
||||
|
||||
## 호환 에이전트
|
||||
|
||||
이 스킬은 다음 에이전트에서 사용할 수 있습니다.
|
||||
- [Claude Code](https://claude.com/product/claude-code)
|
||||
- [AdaL](https://sylph.ai/) - 스스로 진화하는 AI 코딩 에이전트([문서](https://docs.sylph.ai/) | [GitHub](https://github.com/SylphAI-Inc/adal-cli))
|
||||
57
README.md
57
README.md
@ -1,7 +1,10 @@
|
||||
# [UI UX Pro Max](https://uupm.cc)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.id.md">🇮🇩 Bahasa Indonesia</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.ko.md">🇰🇷 한국어</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.vi.md">🇻🇳 Tiếng Việt</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
@ -35,9 +38,38 @@ An AI skill that provides design intelligence for building professional UI/UX ac
|
||||
|
||||
<p align="center">
|
||||
<i>Other projects</i><br>
|
||||
<a href="https://nextlevelbuilder.io">NextLevelBuilder.io</a> | <a href="https://goclaw.sh">GoClaw.sh</a> | <a href="https://claudekit.cc">ClaudeKit.cc</a> | <a href="https://tose.sh">TOSE.sh</a>
|
||||
<a href="https://nextlevelbuilder.io">NextLevelBuilder.io</a> | <a href="https://goclaw.sh">GoClaw.sh</a> | <a href="https://agentkit.best">AgentKit.best</a> | <a href="https://tose.sh">TOSE.sh</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<span>Check Out Our New Skill:</span>
|
||||
<br/>
|
||||
<a href="https://github.com/viettranx/3dviz-pro-max" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.nextlevelbuilder.io/skills/3dviz/wordmark-dark.svg">
|
||||
<img src="https://cdn.nextlevelbuilder.io/skills/3dviz/wordmark.svg" alt="3Dviz Pro Max" height="56">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center"><b>Turn an idea into a 3D scene worth exploring.</b></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.nextlevelbuilder.io/skills/3dviz/harness-village.gif" width="800" alt="Harness Village: a fantasy village with camera navigation and animated creatures">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub><b>Visual inspiration, not a benchmark.</b> An author-supplied project recorded <i>before</i> this skill existed; its UI contains Vietnamese. Historical footage, not an English demo or a runtime test of the skill — see <a href="https://github.com/viettranx/3dviz-pro-max/blob/main/docs/demos/README.md">media provenance</a>.</sub>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
🤌 Website: <a href="https://3dviz.dev/" target="_blank">https://3dviz.dev/</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.0
|
||||
|
||||
### Intelligent Design System Generation
|
||||
@ -277,6 +309,7 @@ uipro versions # List available versions
|
||||
uipro update # Refresh skill files from installed CLI package
|
||||
uipro update --global # Refresh global skill files from installed CLI package
|
||||
uipro init --offline # Compatibility flag; installs bundled templates
|
||||
uipro init --dry-run # Preview install actions without writing files
|
||||
uipro uninstall # Remove skill (auto-detect platform)
|
||||
uipro uninstall --ai claude # Remove specific platform
|
||||
uipro uninstall --global # Remove from global install
|
||||
@ -403,7 +436,7 @@ results instead of mixing framework generations.
|
||||
Save your design system to files for **hierarchical retrieval across sessions**:
|
||||
|
||||
```bash
|
||||
# Generate and persist to design-system/MASTER.md
|
||||
# Generate and persist to design-system/myapp/MASTER.md
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp"
|
||||
|
||||
# Also create a page-specific override file
|
||||
@ -414,20 +447,21 @@ This creates a `design-system/` folder structure:
|
||||
|
||||
```
|
||||
design-system/
|
||||
├── MASTER.md # Global Source of Truth (colors, typography, spacing, components)
|
||||
└── pages/
|
||||
└── dashboard.md # Page-specific overrides (only deviations from Master)
|
||||
└── myapp/ # One folder per project (slug of -p "MyApp")
|
||||
├── MASTER.md # Global Source of Truth (colors, typography, spacing, components)
|
||||
└── pages/
|
||||
└── dashboard.md # Page-specific overrides (only deviations from Master)
|
||||
```
|
||||
|
||||
**How hierarchical retrieval works:**
|
||||
1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md`
|
||||
1. When building a specific page (e.g., "Checkout"), first check `design-system/[project-slug]/pages/checkout.md`
|
||||
2. If the page file exists, its rules **override** the Master file
|
||||
3. If not, use `design-system/MASTER.md` exclusively
|
||||
3. If not, use `design-system/[project-slug]/MASTER.md` exclusively
|
||||
|
||||
**Context-aware retrieval prompt:**
|
||||
```
|
||||
I am building the [Page Name] page. Please read design-system/MASTER.md.
|
||||
Also check if design-system/pages/[page-name].md exists.
|
||||
I am building the [Page Name] page. Please read design-system/[project-slug]/MASTER.md.
|
||||
Also check if design-system/[project-slug]/pages/[page-name].md exists.
|
||||
If the page file exists, prioritize its rules.
|
||||
If not, use the Master rules exclusively.
|
||||
Now, generate the code...
|
||||
@ -479,6 +513,7 @@ npm run typecheck
|
||||
# `npm run build` uses Bun when available and falls back to TypeScript compiler output after `npm ci`.
|
||||
npm run build
|
||||
node dist/index.js init --ai claude --offline # Test in a temp folder
|
||||
node dist/index.js init --ai claude --dry-run # Preview install actions (no writes)
|
||||
|
||||
# 6. Create PR (never push directly to main)
|
||||
git checkout -b feat/your-feature
|
||||
@ -652,7 +687,7 @@ python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS" --domain style --j
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
[](https://star-history.dera.page/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
645
README.vi.md
Normal file
645
README.vi.md
Normal file
@ -0,0 +1,645 @@
|
||||
# [UI UX Pro Max](https://uupm.cc)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.id.md">🇮🇩 Bahasa Indonesia</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.ko.md">🇰🇷 한국어</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.vi.md">🇻🇳 Tiếng Việt</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/releases"><img src="https://img.shields.io/github/v/release/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=blue" alt="Bản phát hành GitHub"></a>
|
||||
<img src="https://img.shields.io/badge/reasoning_rules-192-green?style=for-the-badge" alt="192 quy tắc suy luận">
|
||||
<img src="https://img.shields.io/badge/UI_styles-79_searchable-purple?style=for-the-badge" alt="79 phong cách UI có thể tìm kiếm">
|
||||
<img src="https://img.shields.io/badge/python-3.x-yellow?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.x">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/LICENSE"><img src="https://img.shields.io/github/license/nextlevelbuilder/ui-ux-pro-max-skill?style=for-the-badge&color=green" alt="Giấy phép"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/v/ui-ux-pro-max-cli?style=flat-square&logo=npm&label=CLI" alt="npm"></a>
|
||||
<a href="https://www.npmjs.com/package/ui-ux-pro-max-cli"><img src="https://img.shields.io/npm/dm/ui-ux-pro-max-cli?style=flat-square&label=downloads" alt="Lượt tải npm"></a>
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/stargazers"><img src="https://img.shields.io/github/stars/nextlevelbuilder/ui-ux-pro-max-skill?style=flat-square&logo=github" alt="Lượt sao GitHub"></a>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Support%20Development-00457C?style=flat-square&logo=paypal&logoColor=white" alt="Hỗ trợ qua PayPal"></a>
|
||||
</p>
|
||||
|
||||
Một kỹ năng AI cung cấp tri thức thiết kế để xây dựng UI/UX chuyên nghiệp trên nhiều nền tảng và framework.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://uupm.cc">
|
||||
<img src="screenshots/website.png" alt="UI UX Pro Max" width="800">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>Nếu dự án hữu ích với bạn, hãy cân nhắc ủng hộ:</b><br><br>
|
||||
<a href="https://paypal.me/uiuxpromax"><img src="https://img.shields.io/badge/PayPal-Donate-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="Ủng hộ qua PayPal"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<i>Các dự án khác</i><br>
|
||||
<a href="https://nextlevelbuilder.io">NextLevelBuilder.io</a> | <a href="https://goclaw.sh">GoClaw.sh</a> | <a href="https://claudekit.cc">ClaudeKit.cc</a> | <a href="https://tose.sh">TOSE.sh</a>
|
||||
</p>
|
||||
|
||||
## Có gì mới trong v2.0
|
||||
|
||||
### Tạo hệ thống thiết kế thông minh
|
||||
|
||||
Tính năng chủ lực của v2.0 là **Trình tạo hệ thống thiết kế** — một bộ máy suy luận được hỗ trợ bởi AI, có thể phân tích yêu cầu dự án và tạo ra một hệ thống thiết kế hoàn chỉnh, phù hợp chỉ trong vài giây.
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| MỤC TIÊU: Serenity Spa - HỆ THỐNG THIẾT KẾ ĐƯỢC ĐỀ XUẤT |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| BỐ CỤC: Tập trung vào Hero + Bằng chứng xã hội |
|
||||
| Chuyển đổi: Thúc đẩy bằng cảm xúc kết hợp các yếu tố tạo niềm tin |
|
||||
| CTA: Nằm trong màn hình đầu tiên, lặp lại sau phần đánh giá |
|
||||
| Các phần: |
|
||||
| 1. Hero |
|
||||
| 2. Dịch vụ |
|
||||
| 3. Đánh giá |
|
||||
| 4. Đặt lịch |
|
||||
| 5. Liên hệ |
|
||||
| |
|
||||
| PHONG CÁCH: Soft UI Evolution |
|
||||
| Từ khóa: Bóng đổ mềm, chiều sâu tinh tế, thư thái, cao cấp, hình dạng hữu cơ |
|
||||
| Phù hợp: Chăm sóc sức khỏe, làm đẹp, thương hiệu phong cách sống, dịch vụ cao cấp |
|
||||
| Hiệu năng: cost:low | Khả năng tiếp cận: risk:conditional; cần xác minh yêu cầu |
|
||||
| |
|
||||
| MÀU SẮC: |
|
||||
| Chính: #E8B4B8 (Hồng nhạt) |
|
||||
| Phụ: #A8D5BA (Xanh xô thơm) |
|
||||
| CTA: #D4AF37 (Vàng kim) |
|
||||
| Nền: #FFF5F5 (Trắng ấm) |
|
||||
| Văn bản: #2D3436 (Xám than) |
|
||||
| Ghi chú: Bảng màu dịu nhẹ, nhấn vàng kim để tạo cảm giác sang trọng |
|
||||
| |
|
||||
| KIỂU CHỮ: Cormorant Garamond / Montserrat |
|
||||
| Sắc thái: Thanh lịch, thư thái, tinh tế |
|
||||
| Phù hợp: Thương hiệu xa xỉ, chăm sóc sức khỏe, làm đẹp, biên tập |
|
||||
| Google Fonts: https://fonts.google.com/share?selection.family=... |
|
||||
| |
|
||||
| HIỆU ỨNG CHÍNH: |
|
||||
| Bóng đổ mềm + Chuyển tiếp phù hợp ngữ cảnh + Trạng thái hover nhẹ nhàng |
|
||||
| |
|
||||
| CẦN TRÁNH (Anti-pattern): |
|
||||
| Màu neon chói + Hoạt ảnh gắt + Chế độ tối + Gradient tím/hồng kiểu AI |
|
||||
| |
|
||||
| DANH SÁCH KIỂM TRA TRƯỚC KHI BÀN GIAO: |
|
||||
| [ ] Không dùng emoji làm biểu tượng (dùng SVG: Heroicons/Lucide) |
|
||||
| [ ] cursor-pointer cho mọi phần tử có thể nhấp |
|
||||
| [ ] Thời lượng tương tác phù hợp nền tảng, thành phần và tùy chọn người dùng |
|
||||
| [ ] Chế độ sáng: độ tương phản văn bản tối thiểu 4.5:1 |
|
||||
| [ ] Trạng thái focus hiển thị rõ khi điều hướng bằng bàn phím |
|
||||
| [ ] Tôn trọng prefers-reduced-motion |
|
||||
| [ ] Văn bản, chip và badge tự dàn lại, không bị cắt hay vỡ nhãn |
|
||||
| [ ] Responsive: 375px, 768px, 1024px, 1440px |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Cách hệ thống thiết kế được tạo ra
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. YÊU CẦU CỦA NGƯỜI DÙNG │
|
||||
│ "Xây dựng landing page cho spa làm đẹp của tôi" │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. TÌM KIẾM ĐA MIỀN (5 lượt tìm kiếm song song) │
|
||||
│ • Đối sánh loại sản phẩm (192 danh mục) │
|
||||
│ • Đề xuất phong cách (79 có thể tìm kiếm; 50 đang hoạt động)│
|
||||
│ • Chọn bảng màu (192 bảng màu) │
|
||||
│ • Mẫu landing page (34 mẫu) │
|
||||
│ • Kết hợp kiểu chữ (74 cặp phông chữ) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. BỘ MÁY SUY LUẬN │
|
||||
│ • Đối sánh sản phẩm → quy tắc danh mục UI │
|
||||
│ • Áp dụng mức ưu tiên phong cách (xếp hạng BM25) │
|
||||
│ • Lọc anti-pattern theo ngành │
|
||||
│ • Xử lý quy tắc quyết định (điều kiện JSON) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. ĐẦU RA HỆ THỐNG THIẾT KẾ HOÀN CHỈNH │
|
||||
│ Bố cục + Phong cách + Màu sắc + Kiểu chữ + Hiệu ứng │
|
||||
│ + Anti-pattern cần tránh + Danh sách kiểm tra trước bàn giao│
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 192 quy tắc suy luận dành riêng cho từng ngành
|
||||
|
||||
Bộ máy suy luận có các quy tắc chuyên biệt cho:
|
||||
|
||||
| Danh mục | Ví dụ |
|
||||
|----------|-------|
|
||||
| **Công nghệ & SaaS** | SaaS, Micro SaaS, dịch vụ B2B, công cụ lập trình/IDE, nền tảng AI/chatbot, nền tảng an ninh mạng |
|
||||
| **Tài chính** | Fintech/crypto, ngân hàng, bảo hiểm, theo dõi tài chính cá nhân, công cụ hóa đơn & thanh toán |
|
||||
| **Chăm sóc sức khỏe** | Phòng khám, nhà thuốc, nha khoa, thú y, sức khỏe tinh thần, nhắc uống thuốc |
|
||||
| **Thương mại điện tử** | Tổng hợp, xa xỉ, chợ P2P, hộp đăng ký định kỳ, giao đồ ăn |
|
||||
| **Dịch vụ** | Làm đẹp/spa, nhà hàng, khách sạn, pháp lý, dịch vụ gia đình, đặt lịch & cuộc hẹn |
|
||||
| **Sáng tạo** | Portfolio, agency, nhiếp ảnh, trò chơi, phát nhạc trực tuyến, trình chỉnh sửa ảnh/video |
|
||||
| **Phong cách sống** | Theo dõi thói quen, công thức & nấu ăn, thiền, thời tiết, nhật ký, theo dõi tâm trạng |
|
||||
| **Công nghệ mới nổi** | Web3/NFT, điện toán không gian, điện toán lượng tử, đội máy bay không người lái tự hành |
|
||||
|
||||
Mỗi quy tắc bao gồm:
|
||||
|
||||
- **Bố cục đề xuất** — Cấu trúc landing page
|
||||
- **Ưu tiên phong cách** — Các phong cách UI phù hợp nhất
|
||||
- **Sắc thái màu sắc** — Bảng màu phù hợp với ngành
|
||||
- **Sắc thái kiểu chữ** — Cá tính phông chữ phù hợp
|
||||
- **Hiệu ứng chính** — Hoạt ảnh và tương tác
|
||||
- **Anti-pattern** — Những điều KHÔNG nên làm (ví dụ: "gradient tím/hồng kiểu AI" cho ứng dụng ngân hàng)
|
||||
|
||||
## Tính năng
|
||||
|
||||
- **79 phong cách UI có thể tìm kiếm (50 đang hoạt động)** — Glassmorphism, Claymorphism, Minimalism, Brutalism, Neumorphism, Bento Grid, Dark Mode, AI-Native UI và nhiều hơn nữa
|
||||
- **192 bảng màu** — Bảng màu theo ngành, tương ứng 1:1 với 192 loại sản phẩm
|
||||
- **74 cặp phông chữ** — Các tổ hợp kiểu chữ được tuyển chọn, kèm câu lệnh import Google Fonts
|
||||
- **25 loại biểu đồ** — Đề xuất cho dashboard và phân tích dữ liệu
|
||||
- **22 tech stack** — React, Next.js, Astro, Vue, Nuxt.js, Nuxt UI, Svelte, SwiftUI, React Native, Flutter, HTML+Tailwind, shadcn/ui, Jetpack Compose, Angular, Laravel, Three.js, JavaFX, WPF, WinUI 3, UWP, Avalonia, Uno Platform
|
||||
- **119 hướng dẫn UX** — Thực hành tốt, anti-pattern, quy tắc khả năng tiếp cận, bố cục văn bản bền vững, nhãn gọn và tương tác có thể hủy
|
||||
- **192 quy tắc suy luận** — Tạo hệ thống thiết kế dành riêng cho từng ngành (MỚI trong v2.0)
|
||||
|
||||
### Văn bản bền vững và UI nhỏ gọn
|
||||
|
||||
Hướng dẫn hiện bao quát các lỗi thường gặp trong môi trường production liên quan đến tiêu đề, chuỗi dài, chip, badge và các vi tương tác bị gián đoạn:
|
||||
|
||||
- Ngắt dòng tiêu đề cân đối là một cải tiến tăng dần, không phải bảo đảm rằng một từ cụ thể sẽ luôn nằm ở dòng cuối. Thiết kế vẫn phải hoạt động tốt với cách ngắt dòng tự nhiên trên nhiều độ rộng, phông chữ và ngôn ngữ.
|
||||
- Văn bản thiết yếu phải tự dàn lại mà không bị cắt ở màn hình hẹp, khi phóng to trình duyệt, tăng cỡ chữ hoặc áp dụng thiết lập giãn cách của người dùng. URL và mã định danh dài có thể xuống dòng an toàn.
|
||||
- Nhóm chip và tag nên tự xuống dòng hoặc dùng cơ chế mở rộng `+n` có thể thao tác. Nhãn ngắn nên được giữ nguyên vẹn khi có thể; nếu buộc phải cắt ngắn, cần có cách truy cập giá trị đầy đủ cho người dùng bàn phím, con trỏ và cảm ứng.
|
||||
- Ý nghĩa của badge không được chỉ dựa vào màu sắc. Chip tương tác cần ngữ nghĩa gốc phù hợp, trạng thái focus rõ ràng và trạng thái có thể xác định bằng chương trình; số đếm trực tiếp cần ngữ cảnh có ý nghĩa.
|
||||
- Tương tác nhanh có thể hủy hoạt ảnh, nhưng trạng thái ngữ nghĩa cuối cùng, focus và nội dung vẫn phải chính xác. Thời lượng cần phù hợp với nền tảng và thành phần, đồng thời tôn trọng tùy chọn giảm chuyển động.
|
||||
|
||||
### Phân loại phong cách
|
||||
|
||||
Danh mục chứa **79 phong cách có thể tìm kiếm**, được hỗ trợ bởi ID và bí danh ổn định:
|
||||
|
||||
| Trạng thái | Số lượng | Hành vi tìm kiếm |
|
||||
|------------|---------:|------------------|
|
||||
| Đang hoạt động | 50 | Có trong các đề xuất thông thường và được hiển thị mặc định trong thư viện |
|
||||
| Bổ sung | 29 | Được trả về khi người dùng yêu cầu chính xác hoặc nêu rõ biến thể/hệ thống; có thể xem bằng bộ lọc trạng thái của thư viện |
|
||||
| Không còn dùng | 9 | Bị loại khỏi xếp hạng thông thường; tên cũ chuyển hướng đến phong cách chuẩn hoặc mẫu landing page tương ứng |
|
||||
|
||||
Nhóm đang hoạt động bao gồm 43 họ phong cách trực quan phổ biến, 2 phong cách riêng cho thiết bị di động, 3 nền tảng/hệ thống thiết kế chính thức, 1 vật liệu nền tảng và 1 phong cách phân tích cốt lõi. Các hệ thống chính thức hiện tại gồm Fluent 2, Shopify Polaris và Adobe Spectrum; Liquid Glass được giới hạn trong phạm vi vật liệu nền tảng Apple, Material 3 Expressive vẫn là một biến thể Material dành cho thiết bị di động, còn Spectrum 2 thuộc nhóm bổ sung. Cấu trúc landing page nằm trong tập dữ liệu riêng gồm 34 mẫu thay vì cạnh tranh với phong cách trực quan trong xếp hạng BM25.
|
||||
|
||||
Xem [`styles.csv`](src/ui-ux-pro-max/data/styles.csv) để biết toàn bộ hệ thống phân loại và siêu dữ liệu có thông tin nguồn gốc.
|
||||
|
||||
## 💎 So sánh phiên bản Cơ bản và Cao cấp
|
||||
|
||||
Nhiều người dùng hỏi về sự khác biệt giữa phiên bản mã nguồn mở và phiên bản cao cấp. Bảng phân tích sau sẽ giúp bạn chọn phiên bản phù hợp với quy trình làm việc.
|
||||
|
||||
### 🟢 Phiên bản Cơ bản (repository này)
|
||||
|
||||
- **Hoàn toàn mã nguồn mở:** Phù hợp với lập trình viên cá nhân, người làm dự án sở thích và các dự án thông thường.
|
||||
- **Tri thức UI/UX cốt lõi:** Truy cập đầy đủ 79 phong cách UI có thể tìm kiếm (50 đang hoạt động), 192 loại sản phẩm, bảng màu và các cặp phông chữ được tuyển chọn.
|
||||
- **Đề xuất thông minh:** Bộ máy tìm kiếm BM25 tích hợp giúp đối sánh thiết kế với độ chính xác cao.
|
||||
- **Hỗ trợ đa nền tảng:** Hướng dẫn riêng theo stack, hỗ trợ 22 framework lớn (React, Vue, Tailwind, iOS, Android, v.v.).
|
||||
- **Tạo hệ thống thiết kế:** Tạo tức thì các quy tắc UI, mẫu và logic phù hợp thông qua CLI.
|
||||
|
||||
### 🟡 Phiên bản Cao cấp
|
||||
|
||||
- **Kỹ năng thiết kế thương hiệu mở rộng:** Không chỉ UI/UX mà còn hỗ trợ tạo bộ nhận diện thương hiệu, thiết kế logo, chương trình nhận diện doanh nghiệp (CIP), banner, slide thuyết trình và biểu tượng tùy chỉnh.
|
||||
- **Tạo tài nguyên nâng cao:** Tích hợp sâu với công nghệ tạo ảnh bằng AI để tạo tài nguyên trực quan thực tế, không chỉ là placeholder.
|
||||
- **Kiến trúc doanh nghiệp:** Kiến trúc design token toàn diện và có khả năng mở rộng hơn, dành cho việc triển khai trong các nhóm quy mô lớn.
|
||||
- **Hỗ trợ ưu tiên:** Hỗ trợ kỹ thuật chuyên biệt cho các nhóm và chuyên gia cần quy trình thiết kế xuyên suốt, không gián đoạn.
|
||||
|
||||
👉 *Để biết thêm thông tin về việc nâng cấp lên gói Cao cấp, hãy truy cập [uupm.cc](https://uupm.cc).*
|
||||
|
||||
## Cài đặt
|
||||
|
||||
### Sử dụng Claude Marketplace (Claude Code)
|
||||
|
||||
Cài đặt trực tiếp trong Claude Code bằng hai lệnh:
|
||||
|
||||
```
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
### Sử dụng CLI (khuyến nghị)
|
||||
|
||||
```bash
|
||||
# Cài đặt CLI trên toàn hệ thống
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
|
||||
# Đi đến dự án của bạn
|
||||
cd /path/to/your/project
|
||||
|
||||
# Cài đặt cho trợ lý AI của bạn
|
||||
uipro init --ai claude # Claude Code
|
||||
uipro init --ai cursor # Cursor
|
||||
uipro init --ai windsurf # Windsurf
|
||||
uipro init --ai antigravity # Antigravity
|
||||
uipro init --ai copilot # GitHub Copilot
|
||||
uipro init --ai kiro # Kiro
|
||||
uipro init --ai codex # Codex CLI
|
||||
uipro init --ai qoder # Qoder
|
||||
uipro init --ai roocode # Roo Code
|
||||
uipro init --ai gemini # Gemini CLI
|
||||
uipro init --ai trae # Trae
|
||||
uipro init --ai opencode # OpenCode
|
||||
uipro init --ai continue # Continue
|
||||
uipro init --ai codebuddy # CodeBuddy
|
||||
uipro init --ai droid # Droid (Factory)
|
||||
uipro init --ai kilocode # KiloCode
|
||||
uipro init --ai warp # Warp
|
||||
uipro init --ai augment # Augment
|
||||
uipro init --ai codewhale # CodeWhale
|
||||
uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
|
||||
uipro init --ai all # Tất cả trợ lý
|
||||
```
|
||||
|
||||
Gói npm là `ui-ux-pro-max-cli`; gói này vẫn cài đặt lệnh `uipro`. Các bản phát hành `uipro-cli` cũ đã lỗi thời và không nên dùng với tài nguyên hiện tại.
|
||||
|
||||
### Cài đặt toàn hệ thống (dùng cho mọi dự án)
|
||||
|
||||
```bash
|
||||
uipro init --ai claude --global # Cài vào ~/.claude/skills/
|
||||
uipro init --ai cursor --global # Cài vào ~/.cursor/skills/
|
||||
uipro init --ai universal --global # Cài vào ~/.agents/skills/
|
||||
```
|
||||
|
||||
### Các lệnh CLI khác
|
||||
|
||||
```bash
|
||||
uipro versions # Liệt kê các phiên bản có sẵn
|
||||
uipro update # Làm mới tệp kỹ năng từ gói CLI đã cài đặt
|
||||
uipro update --global # Làm mới tệp kỹ năng toàn hệ thống từ gói CLI đã cài đặt
|
||||
uipro init --offline # Cờ tương thích; cài đặt các template đi kèm
|
||||
uipro uninstall # Gỡ kỹ năng (tự phát hiện nền tảng)
|
||||
uipro uninstall --ai claude # Gỡ khỏi một nền tảng cụ thể
|
||||
uipro uninstall --global # Gỡ bản cài đặt toàn hệ thống
|
||||
```
|
||||
|
||||
## Điều kiện tiên quyết
|
||||
|
||||
Python 3.x là bắt buộc để chạy script tìm kiếm (chỉ sử dụng thư viện chuẩn — các script không cài đặt gì và không thực hiện yêu cầu mạng).
|
||||
|
||||
Kiểm tra Python đã được cài đặt hay chưa:
|
||||
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
Nếu chưa có, hãy tự cài đặt từ [python.org](https://www.python.org/downloads/) hoặc bằng trình quản lý gói của hệ điều hành (Homebrew, apt, winget). Các bước cài đặt này dành cho **bạn, người dùng trực tiếp** — các AI agent sử dụng kỹ năng này không bao giờ được tự ý cài phần mềm trên máy của bạn; chúng được hướng dẫn phải hỏi bạn trước.
|
||||
|
||||
## Cách sử dụng
|
||||
|
||||
### Chế độ Kỹ năng (tự động kích hoạt)
|
||||
|
||||
**Được hỗ trợ:** Claude Code, Cursor, Windsurf, Antigravity, Codex CLI, Continue, Gemini CLI, OpenCode, Qoder, CodeBuddy, Droid (Factory), KiloCode, Warp, Augment, CodeWhale
|
||||
|
||||
Kỹ năng tự động kích hoạt khi bạn yêu cầu công việc UI/UX. Chỉ cần trò chuyện tự nhiên:
|
||||
|
||||
```
|
||||
Xây dựng landing page cho sản phẩm SaaS của tôi
|
||||
```
|
||||
|
||||
> **Trae**: Trước tiên, hãy chuyển sang chế độ **SOLO**. Kỹ năng sẽ kích hoạt khi có yêu cầu UI/UX.
|
||||
|
||||
### Chế độ Quy trình (lệnh slash)
|
||||
|
||||
**Được hỗ trợ:** Kiro, GitHub Copilot, Roo Code, KiloCode
|
||||
|
||||
Dùng lệnh slash để gọi kỹ năng:
|
||||
|
||||
```
|
||||
/ui-ux-pro-max Xây dựng landing page cho sản phẩm SaaS của tôi
|
||||
```
|
||||
|
||||
### Prompt mẫu
|
||||
|
||||
```
|
||||
Xây dựng landing page cho sản phẩm SaaS của tôi
|
||||
|
||||
Tạo dashboard phân tích dữ liệu chăm sóc sức khỏe
|
||||
|
||||
Thiết kế website portfolio có chế độ tối
|
||||
|
||||
Tạo UI ứng dụng thương mại điện tử trên thiết bị di động
|
||||
|
||||
Xây dựng ứng dụng ngân hàng fintech với giao diện tối
|
||||
```
|
||||
|
||||
### Cách hoạt động
|
||||
|
||||
1. **Bạn đưa ra yêu cầu** — Yêu cầu bất kỳ tác vụ UI/UX nào (xây dựng, thiết kế, tạo, triển khai, đánh giá, sửa hoặc cải thiện)
|
||||
2. **Hệ thống thiết kế được tạo ra** — AI tự động tạo một hệ thống thiết kế hoàn chỉnh bằng bộ máy suy luận
|
||||
3. **Đề xuất thông minh** — Dựa trên loại sản phẩm và yêu cầu của bạn, hệ thống tìm phong cách, màu sắc và kiểu chữ phù hợp nhất
|
||||
4. **Tạo mã nguồn** — Triển khai UI với màu sắc, phông chữ, khoảng cách và các thực hành tốt phù hợp
|
||||
5. **Kiểm tra trước khi bàn giao** — Đối chiếu với những anti-pattern UI/UX thường gặp
|
||||
|
||||
### Các stack được hỗ trợ
|
||||
|
||||
Kỹ năng cung cấp hướng dẫn riêng cho từng stack:
|
||||
|
||||
| Danh mục | Stack |
|
||||
|----------|-------|
|
||||
| **Web (HTML)** | HTML + Tailwind (mặc định) |
|
||||
| **Hệ sinh thái React** | React, Next.js, shadcn/ui |
|
||||
| **Hệ sinh thái Vue** | Vue, Nuxt.js, Nuxt UI |
|
||||
| **Angular** | Angular |
|
||||
| **PHP** | Laravel (Blade, Livewire, Inertia.js) |
|
||||
| **Web khác** | Svelte, Astro, Three.js |
|
||||
| **Máy tính để bàn** | JavaFX, WPF, WinUI 3, Avalonia, Uno Platform, UWP |
|
||||
| **iOS** | SwiftUI |
|
||||
| **Android** | Jetpack Compose |
|
||||
| **Đa nền tảng** | React Native, Flutter |
|
||||
|
||||
Chỉ cần nêu stack bạn muốn trong prompt, hoặc để hệ thống dùng HTML + Tailwind theo mặc định.
|
||||
|
||||
## Lệnh tạo hệ thống thiết kế (nâng cao)
|
||||
|
||||
Để truy cập trực tiếp trình tạo hệ thống thiết kế:
|
||||
|
||||
> Lưu ý: Nếu bạn cài đặt qua Continue, hãy thay `.claude/skills/` bằng `.continue/skills/` trong các lệnh bên dưới. Với Droid (Factory), hãy dùng `.factory/skills/`.
|
||||
|
||||
```bash
|
||||
# Tạo hệ thống thiết kế với đầu ra ASCII
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --design-system -p "Serenity Spa"
|
||||
|
||||
# Tạo đầu ra Markdown
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "fintech banking" --design-system -f markdown
|
||||
|
||||
# Tìm kiếm theo miền
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "glassmorphism" --domain style
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant serif" --domain typography
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "dashboard" --domain chart
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "error summary validation" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "decorative icon aria hidden" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "icon button accessible label" --domain icons
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "orphan heading line balance" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "badge chip label wraps to second line" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "rapid chip animation interrupted" --domain ux
|
||||
|
||||
# Hướng dẫn riêng cho từng stack
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "form validation" --stack react
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "responsive layout" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "chip badge overflow nowrap" --stack html-tailwind
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "tableview binding" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "atlantafx primer enterprise theme" --stack javafx
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "enterprise tableview density permission" --stack javafx
|
||||
```
|
||||
|
||||
Tìm kiếm cho web stack có nhận biết phiên bản. Truy vấn không nêu phiên bản chính cũ sẽ trả về hướng dẫn hiện hành, đang được áp dụng. Các thuật ngữ cũ hoặc phiên bản chính cũ được nêu rõ (ví dụ `Svelte 4` hoặc `Next.js 15`) chỉ trả về những dòng hướng dẫn cũ đã được tuyển chọn, có nhãn `Status` và `Applies To`; nếu chưa có hướng dẫn cũ phù hợp, tìm kiếm sẽ không trả về kết quả thay vì trộn lẫn các thế hệ framework.
|
||||
|
||||
### Lưu hệ thống thiết kế (mô hình Master + Overrides)
|
||||
|
||||
Lưu hệ thống thiết kế vào tệp để **truy xuất phân cấp giữa các phiên làm việc**:
|
||||
|
||||
```bash
|
||||
# Tạo và lưu vào design-system/myapp/MASTER.md
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp"
|
||||
|
||||
# Đồng thời tạo tệp ghi đè riêng cho một trang
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp" --page "dashboard"
|
||||
```
|
||||
|
||||
Lệnh trên tạo cấu trúc thư mục `design-system/`:
|
||||
|
||||
```
|
||||
design-system/
|
||||
└── myapp/ # Mỗi dự án một thư mục (slug của -p "MyApp")
|
||||
├── MASTER.md # Nguồn tham chiếu chung (màu sắc, kiểu chữ, khoảng cách, thành phần)
|
||||
└── pages/
|
||||
└── dashboard.md # Ghi đè riêng cho trang (chỉ những điểm khác với Master)
|
||||
```
|
||||
|
||||
**Cách truy xuất phân cấp hoạt động:**
|
||||
|
||||
1. Khi xây dựng một trang cụ thể (ví dụ: "Checkout"), trước tiên kiểm tra `design-system/[project-slug]/pages/checkout.md`
|
||||
2. Nếu tệp của trang tồn tại, các quy tắc trong đó sẽ **ghi đè** tệp Master
|
||||
3. Nếu không, chỉ sử dụng `design-system/[project-slug]/MASTER.md`
|
||||
|
||||
**Prompt truy xuất theo ngữ cảnh:**
|
||||
|
||||
```
|
||||
Tôi đang xây dựng trang [Tên trang]. Hãy đọc design-system/[project-slug]/MASTER.md.
|
||||
Đồng thời kiểm tra xem design-system/[project-slug]/pages/[page-name].md có tồn tại hay không.
|
||||
Nếu tệp của trang tồn tại, hãy ưu tiên các quy tắc trong đó.
|
||||
Nếu không, chỉ sử dụng các quy tắc trong tệp Master.
|
||||
Bây giờ, hãy tạo mã nguồn...
|
||||
```
|
||||
|
||||
## Kiến trúc & Đóng góp
|
||||
|
||||
### Dành cho người dùng
|
||||
|
||||
Mã nguồn đã được tái cấu trúc để sử dụng **hệ thống tạo dựa trên template**. Tất cả các tệp riêng cho từng nền tảng (`.cursor/`, `.windsurf/`, `.kiro/`, `.factory/`, v.v.) hiện được CLI tạo động.
|
||||
|
||||
**Luôn cài đặt bằng CLI:**
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai <platform>
|
||||
```
|
||||
|
||||
Cách này bảo đảm bạn nhận được các template mới nhất đi kèm gói CLI đã cài và cấu trúc tệp chính xác cho trợ lý AI của mình. Khi có bản phát hành mới, hãy cập nhật gói npm trước.
|
||||
|
||||
### Dành cho người đóng góp
|
||||
|
||||
Nếu bạn muốn đóng góp cho dự án:
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
|
||||
cd ui-ux-pro-max-skill
|
||||
|
||||
# 2. Tìm hiểu cấu trúc
|
||||
src/ui-ux-pro-max/ # Nguồn chuẩn (dữ liệu, script, template)
|
||||
cli/ # Trình cài đặt CLI (tạo tệp từ template)
|
||||
.claude/ # Môi trường phát triển/kiểm thử cục bộ cho kỹ năng Claude Code
|
||||
.factory/ # Môi trường phát triển/kiểm thử cục bộ cho kỹ năng Droid (Factory)
|
||||
|
||||
# 3. Thực hiện thay đổi trong src/ui-ux-pro-max/
|
||||
# - data/*.csv → Tệp cơ sở dữ liệu
|
||||
# - scripts/*.py → Bộ máy tìm kiếm & hệ thống thiết kế
|
||||
# - templates/ → Template riêng cho từng nền tảng
|
||||
|
||||
# 4. Đồng bộ sang CLI và kiểm thử cục bộ
|
||||
cd cli
|
||||
npm run sync:assets
|
||||
npm run check:assets
|
||||
npm run verify:data
|
||||
npm run typecheck
|
||||
|
||||
# 5. Build và kiểm thử CLI
|
||||
# `npm run build` dùng Bun khi có, nếu không sẽ dùng đầu ra của trình biên dịch TypeScript sau `npm ci`.
|
||||
npm run build
|
||||
node dist/index.js init --ai claude --offline # Kiểm thử trong thư mục tạm
|
||||
|
||||
# 6. Tạo PR (không bao giờ push trực tiếp lên main)
|
||||
git checkout -b feat/your-feature
|
||||
git commit -m "feat: description"
|
||||
git push -u origin feat/your-feature
|
||||
gh pr create
|
||||
```
|
||||
|
||||
Xem [CLAUDE.md](CLAUDE.md) để biết hướng dẫn phát triển chi tiết.
|
||||
|
||||
### Nguồn gốc và cách làm mới danh mục
|
||||
|
||||
Bản tóm tắt danh mục đã commit hiện ghi nhận **1.934 Google Fonts được phê duyệt** cùng **8 mục loại trừ cần xem xét**, không được đưa vào sử dụng nếu thiếu siêu dữ liệu giấy phép chính thức phù hợp. Hướng dẫn biểu tượng vẫn gồm **105 dòng được tuyển chọn** (100 import Phosphor trực tiếp trên web cùng hướng dẫn cho React Native/phương án dự phòng); manifest Phosphor upstream riêng gồm **1.512 biểu tượng** dùng để xác thực tên, độ đậm và import React/SSR mà không làm kết quả tìm kiếm bị ngập bởi toàn bộ gói upstream.
|
||||
|
||||
Quy trình phát triển thông thường và CI cho pull request không phụ thuộc mạng. Chạy toàn bộ cổng kiểm tra offline, bao gồm hash snapshot và xác thực số lượng đã tạo, bằng:
|
||||
|
||||
```bash
|
||||
npm --prefix cli run verify:data
|
||||
# Hoặc chỉ kiểm tra bản tóm tắt danh mục đã tạo:
|
||||
npm --prefix cli run validate:catalog-summary
|
||||
```
|
||||
|
||||
Quy trình chuẩn hóa khi làm mới cũng có thể chạy hoàn toàn offline với các fixture đã commit. Kết quả được ghi vào một thư mục ứng viên tạm thời và không bao giờ thay thế dữ liệu chuẩn:
|
||||
|
||||
```bash
|
||||
candidate_dir="$(mktemp -d)"
|
||||
python3 scripts/refresh-google-fonts.py \
|
||||
--api-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json \
|
||||
--metadata-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json \
|
||||
--existing-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv \
|
||||
--overrides src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json \
|
||||
--output-csv "$candidate_dir/google-fonts.csv" \
|
||||
--license-output "$candidate_dir/google-font-licenses.json" \
|
||||
--metadata-revision fixture-catalogs-v1 \
|
||||
--verified-at 2026-08-13 --expected-count 2 --approve-changes
|
||||
python3 scripts/refresh-icon-catalog.py \
|
||||
--input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json \
|
||||
--package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json \
|
||||
--react-package-json src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json \
|
||||
--react-exports-input src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json \
|
||||
--curated-csv src/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv \
|
||||
--output "$candidate_dir/phosphor-icons-upstream.json" \
|
||||
--verified-at 2026-08-13 --expected-count 2
|
||||
```
|
||||
|
||||
Quy trình làm mới trực tiếp từ upstream được tách riêng có chủ đích trong workflow `refresh-catalogs.yml`, chạy theo lịch vào 03:17 UTC mỗi thứ Hai và cũng có thể chạy theo yêu cầu. Cấu hình `GOOGLE_FONTS_API_KEY` làm GitHub Actions secret, sau đó chạy workflow và tải artifact để xem xét:
|
||||
|
||||
```bash
|
||||
gh workflow run refresh-catalogs.yml
|
||||
run_id="$(gh run list --workflow refresh-catalogs.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
||||
gh run watch "$run_id"
|
||||
gh run download "$run_id" --name "catalog-refresh-review-$run_id"
|
||||
```
|
||||
|
||||
Workflow đọc Google Fonts Developer API và các gói Phosphor chính thức đã được cố định phiên bản, ghi các tệp ứng viên cùng unified diff vào artifact và chỉ có quyền đọc repository. Workflow không bao giờ commit, push, mở PR hoặc merge. Hãy xem xét báo cáo thay đổi, mục loại trừ, giấy phép, chỉ số liên quan và cổng kiểm tra offline trước khi tự đưa các tệp ứng viên vào `src/ui-ux-pro-max/data/`.
|
||||
|
||||
## Phát hành tự động
|
||||
|
||||
Repository này sử dụng semantic-release cùng Conventional Commits để tự động tạo bản phát hành GitHub:
|
||||
|
||||
- Nhánh `dev` tạo các bản phát hành thử nghiệm GitHub, chẳng hạn `2.6.0-beta.1`.
|
||||
- Nhánh `main` tạo các bản phát hành ổn định chính thức, chẳng hạn `2.6.0`.
|
||||
|
||||
Ghi chú phát hành và `CHANGELOG.md` được tạo từ các thông điệp Conventional Commit. Trong quá trình chuẩn bị phát hành, số phiên bản được đồng bộ giữa `skill.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `cli/package.json` và `cli/package-lock.json`.
|
||||
|
||||
Dùng các loại commit sau để tăng phiên bản chính xác:
|
||||
|
||||
- `fix:` → bản vá
|
||||
- `feat:` → phiên bản phụ
|
||||
- `feat!:` hoặc `BREAKING CHANGE:` → phiên bản chính
|
||||
|
||||
Workflow phát hành sử dụng `GITHUB_TOKEN` mặc định để tạo bản phát hành GitHub và secret `NPM_TOKEN` của repository để phát hành `ui-ux-pro-max-cli` lên npm.
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### `uipro: unknown command 'uninstall'` hoặc `unknown command 'update'`
|
||||
|
||||
Phiên bản `ui-ux-pro-max-cli` đã cài của bạn đã cũ. Hãy cập nhật rồi thử lại:
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli@latest
|
||||
uipro uninstall
|
||||
```
|
||||
|
||||
### `uipro uninstall` báo "No installed AI skill directories detected"
|
||||
|
||||
Kỹ năng được cài trong một thư mục khác với thư mục bạn đang chạy lệnh. Bạn có thể:
|
||||
|
||||
```bash
|
||||
# Phương án A — chạy từ thư mục gốc của dự án nơi bạn đã cài đặt ban đầu
|
||||
cd /path/to/your/project
|
||||
uipro uninstall
|
||||
|
||||
# Phương án B — gỡ bản cài đặt toàn hệ thống
|
||||
uipro uninstall --global
|
||||
|
||||
# Phương án C — gỡ thủ công
|
||||
rm -rf .claude/skills/ui-ux-pro-max # Claude Code
|
||||
rm -rf .cursor/skills/ui-ux-pro-max # Cursor
|
||||
rm -rf .windsurf/skills/ui-ux-pro-max # Windsurf
|
||||
rm -rf .agents/skills/ui-ux-pro-max # Antigravity / Codex
|
||||
```
|
||||
|
||||
### Hộp thoại "Upload a skill" của Claude.ai báo "Zip contains too many files (maximum 200)"
|
||||
|
||||
Không tải lên tệp ZIP chứa toàn bộ repository GitHub. Đây là bản mã nguồn dành cho phát triển, bao gồm mã nguồn, tài nguyên CLI, tài liệu, bản xem trước và nhiều kỹ năng đóng gói nên vượt quá giới hạn 200 tệp của Claude. Tệp này không phải artifact để tải thủ công kỹ năng lên Claude và hiện dự án chưa phát hành tệp ZIP riêng cho việc tải thủ công lên Claude.ai.
|
||||
|
||||
Với Claude Code, hãy cài đặt qua Marketplace:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
|
||||
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
|
||||
```
|
||||
|
||||
Hoặc dùng trình cài đặt CLI:
|
||||
|
||||
```bash
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### Cài đặt qua Claude Marketplace thất bại với lỗi "Zip file contains a symbolic link"
|
||||
|
||||
Đây là lỗi đã biết ở các phiên bản trước v2.5.1. Repository từng sử dụng symlink nội bộ mà một số công cụ cài đặt không thể xử lý. **Cách khắc phục:** dùng trình cài đặt CLI:
|
||||
|
||||
```bash
|
||||
npm install -g ui-ux-pro-max-cli
|
||||
uipro init --ai claude
|
||||
```
|
||||
|
||||
Hoặc chờ bản phát hành tiếp theo có bản sửa lỗi này.
|
||||
|
||||
### `npm install -g ui-ux-pro-max-cli` thất bại do lỗi quyền truy cập
|
||||
|
||||
Hãy dùng trình quản lý phiên bản Node (khuyến nghị), hoặc bỏ qua bước cài đặt toàn hệ thống:
|
||||
|
||||
```bash
|
||||
# Dùng npx mà không cài đặt toàn hệ thống
|
||||
npx ui-ux-pro-max-cli init --ai claude
|
||||
```
|
||||
|
||||
### Không tìm thấy Python khi chạy lệnh tạo hệ thống thiết kế
|
||||
|
||||
Các script tìm kiếm yêu cầu Python 3.x. Hãy tự cài đặt từ [python.org](https://www.python.org/downloads/) hoặc bằng trình quản lý gói của hệ điều hành (Homebrew, apt, winget). AI agent không nên tự cài đặt thay bạn — chúng được hướng dẫn phải hỏi bạn trước.
|
||||
|
||||
### Đầu ra hệ thống thiết kế bị cắt hoặc thiếu trường
|
||||
|
||||
Đầu ra dành cho người đọc sẽ cắt các trường dài ở mức 300 ký tự. Dùng `--json` để nhận dữ liệu đầy đủ, không bị cắt:
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS" --domain style --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lịch sử lượt sao
|
||||
|
||||
[](https://star-history.dera.page/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
|
||||
## Giấy phép
|
||||
|
||||
Dự án này được phát hành theo [Giấy phép MIT](LICENSE).
|
||||
|
||||
## Các agent tương thích
|
||||
|
||||
Kỹ năng này hoạt động với:
|
||||
|
||||
- [Claude Code](https://claude.com/product/claude-code)
|
||||
- [AdaL](https://sylph.ai/) — AI coding agent có khả năng tự tiến hóa ([Tài liệu](https://docs.sylph.ai/) | [GitHub](https://github.com/SylphAI-Inc/adal-cli))
|
||||
24
README.zh.md
24
README.zh.md
@ -1,7 +1,10 @@
|
||||
# [UI UX Pro Max](https://uupm.cc)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.id.md">🇮🇩 Bahasa Indonesia</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.ko.md">🇰🇷 한국어</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.vi.md">🇻🇳 Tiếng Việt</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.zh.md">🇨🇳 简体中文</a> |
|
||||
<a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/blob/main/README.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
@ -397,7 +400,7 @@ legacy 条目,并通过 `Status` 和 `Applies To` 标识。若没有对应的
|
||||
将设计系统保存到文件,实现**跨会话的层级检索**:
|
||||
|
||||
```bash
|
||||
# 生成并持久化到 design-system/MASTER.md
|
||||
# 生成并持久化到 design-system/myapp/MASTER.md
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design-system --persist -p "MyApp"
|
||||
|
||||
# 同时创建页面特定的覆盖文件
|
||||
@ -408,20 +411,21 @@ python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS dashboard" --design
|
||||
|
||||
```
|
||||
design-system/
|
||||
├── MASTER.md # 全局唯一真相源 (颜色、字体、间距、组件)
|
||||
└── pages/
|
||||
└── dashboard.md # 页面特定覆盖 (仅与主配置的偏差)
|
||||
└── myapp/ # 每个项目一个文件夹 (-p "MyApp" 的 slug)
|
||||
├── MASTER.md # 全局唯一真相源 (颜色、字体、间距、组件)
|
||||
└── pages/
|
||||
└── dashboard.md # 页面特定覆盖 (仅与主配置的偏差)
|
||||
```
|
||||
|
||||
**层级检索工作原理:**
|
||||
1. 构建特定页面 (如"结账页") 时,先检查 `design-system/pages/checkout.md`
|
||||
1. 构建特定页面 (如"结账页") 时,先检查 `design-system/[project-slug]/pages/checkout.md`
|
||||
2. 如果页面文件存在,其规则**覆盖**主配置文件
|
||||
3. 如果不存在,仅使用 `design-system/MASTER.md`
|
||||
3. 如果不存在,仅使用 `design-system/[project-slug]/MASTER.md`
|
||||
|
||||
**上下文感知检索提示词:**
|
||||
```
|
||||
我正在构建 [页面名称] 页面。请阅读 design-system/MASTER.md。
|
||||
同时检查 design-system/pages/[page-name].md 是否存在。
|
||||
我正在构建 [页面名称] 页面。请阅读 design-system/[project-slug]/MASTER.md。
|
||||
同时检查 design-system/[project-slug]/pages/[page-name].md 是否存在。
|
||||
如果页面文件存在,优先使用其规则。
|
||||
如果不存在,仅使用主配置规则。
|
||||
现在,生成代码...
|
||||
@ -641,7 +645,7 @@ python3 .claude/skills/ui-ux-pro-max/scripts/search.py "SaaS" --domain style --j
|
||||
|
||||
## Star 历史
|
||||
|
||||
[](https://star-history.com/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
[](https://star-history.dera.page/#nextlevelbuilder/ui-ux-pro-max-skill&Date)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@ -98,4 +98,4 @@ bun link
|
||||
|
||||
## License
|
||||
|
||||
CC-BY-NC-4.0
|
||||
MIT
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"verifiedAt": "2026-08-13",
|
||||
"verifiedAt": "2026-08-26",
|
||||
"counts": {
|
||||
"styles": {
|
||||
"total": 88,
|
||||
|
||||
@ -120,6 +120,12 @@ if __name__ == "__main__":
|
||||
|
||||
# Design system takes priority
|
||||
if args.design_system:
|
||||
if args.stack:
|
||||
print(
|
||||
f"note: --stack {args.stack} is ignored in --design-system mode; "
|
||||
"run a separate --stack query for stack-specific guidelines",
|
||||
file=sys.stderr,
|
||||
)
|
||||
result = generate_design_system(
|
||||
args.query,
|
||||
args.project_name,
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The catalog snapshot must not depend on the checkout's line endings.
|
||||
|
||||
Regression test for bd19ab9 (#462), where catalog-summary.json was regenerated
|
||||
on a CRLF checkout. Every recorded sha256 was the CRLF hash of the source file,
|
||||
so `verify:data` failed on every LF platform, including CI.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
DATA = REPO / "src/ui-ux-pro-max/data"
|
||||
SNAPSHOT_FILES = (
|
||||
"google-fonts.csv",
|
||||
"google-font-licenses.json",
|
||||
"icons.csv",
|
||||
"phosphor-icons-upstream.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_generator():
|
||||
path = REPO / "scripts" / "generate-catalog-summary.py"
|
||||
spec = importlib.util.spec_from_file_location("generate_catalog_summary", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class CatalogSummaryLineEndingsTest(unittest.TestCase):
|
||||
def test_digest_is_identical_for_lf_and_crlf(self):
|
||||
digest = _load_generator().digest
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lf = Path(tmp) / "lf.csv"
|
||||
crlf = Path(tmp) / "crlf.csv"
|
||||
lf.write_bytes(b"id,name\n1,alpha\n2,beta\n")
|
||||
crlf.write_bytes(b"id,name\r\n1,alpha\r\n2,beta\r\n")
|
||||
self.assertEqual(
|
||||
digest(lf), digest(crlf),
|
||||
"snapshot hashes must not change with the checkout's line endings",
|
||||
)
|
||||
|
||||
def test_committed_snapshot_matches_normalized_sources(self):
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
for name in SNAPSHOT_FILES:
|
||||
expected = hashlib.sha256(
|
||||
(DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
self.assertEqual(
|
||||
summary["snapshots"][name]["sha256"], expected,
|
||||
f"{name}: committed snapshot hash does not match the LF-normalized source",
|
||||
)
|
||||
|
||||
def test_crlf_checkout_produces_the_committed_hashes(self):
|
||||
"""Simulate a Windows checkout: the recorded hashes must still validate."""
|
||||
digest = _load_generator().digest
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name in SNAPSHOT_FILES:
|
||||
crlf_copy = Path(tmp) / name
|
||||
raw = (DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
crlf_copy.write_bytes(raw.replace(b"\n", b"\r\n"))
|
||||
self.assertEqual(
|
||||
digest(crlf_copy), summary["snapshots"][name]["sha256"],
|
||||
f"{name}: a CRLF checkout would record a different hash",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
61
cli/assets/scripts/tests/test_design_system_stack.py
Normal file
61
cli/assets/scripts/tests/test_design_system_stack.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Regression tests for the dropped --stack flag in --design-system mode (issue #484).
|
||||
|
||||
`search.py "<query>" --design-system --stack nextjs` used to exit successfully
|
||||
without any indication that the stack was never applied, so a caller following
|
||||
SKILL.md's "never assume a stack" guidance could believe stack guidance was
|
||||
part of the generated design system. The combination must stay valid, but it
|
||||
must say that --stack was ignored.
|
||||
|
||||
Stdlib-only (unittest, not pytest) to match test_core.py -- this project ships
|
||||
with zero external dependencies.
|
||||
|
||||
Run with:
|
||||
python -m unittest discover -s scripts/tests -v
|
||||
or directly:
|
||||
python scripts/tests/test_design_system_stack.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
||||
SEARCH = SCRIPTS_DIR / "search.py"
|
||||
|
||||
|
||||
class TestStackFlagWithDesignSystem(unittest.TestCase):
|
||||
def run_search(self, *args):
|
||||
# The child forces UTF-8 on its streams (search.py), so decode as UTF-8
|
||||
# regardless of the parent's locale on Windows.
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SEARCH), *map(str, args)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_design_system_with_stack_succeeds_and_says_stack_is_ignored(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("--stack", proc.stderr)
|
||||
self.assertIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_design_system_without_stack_stays_quiet(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_stack_search_alone_never_warns(self):
|
||||
proc = self.run_search("dashboard table density", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
82
cli/assets/scripts/tests/test_skill_script_paths.py
Normal file
82
cli/assets/scripts/tests/test_skill_script_paths.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""Every script invocation in the shipped skill markdown resolves from the skill directory.
|
||||
|
||||
Regression test for #474. The sub-skills ship in two copies (.claude/skills/<skill>/
|
||||
for the plugin, cli/assets/skills/<skill>/ for CLI installs) and land in layouts where
|
||||
neither the project root nor ~/.claude/skills/ is a valid anchor: the plugin cache, a
|
||||
project's .claude/skills/, ~/.claude/skills/ (--global), or a manual copy. The one anchor
|
||||
that exists in all of them is the skill's own directory, so documented commands use
|
||||
`scripts/<file>` for the skill's own scripts and `../<skill>/scripts/<file>` for a
|
||||
sibling sub-skill (the sub-skills are always installed side by side).
|
||||
|
||||
This test extracts every `python|python3|node|bash <path>` invocation from every
|
||||
markdown file under both trees and asserts that the path is skill-relative and names a
|
||||
file that ships. The core skill's `${CLAUDE_PLUGIN_ROOT}/.claude/skills/...` form is
|
||||
resolved against the repository root, which is what that variable denotes under a
|
||||
plugin install - and accepted only in that file, because the sub-skills also ship
|
||||
through the CLI, where the variable does not exist. The grep-based path contract in check-asset-sync.yml is the negative
|
||||
side (no home-, project- or variable-rooted paths anywhere, code included); this is
|
||||
the positive side (every documented invocation points at a real file).
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
SKILL_TREES = ("cli/assets/skills", ".claude/skills")
|
||||
# The only file that may use the plugin-root form: hand-authored for the plugin install
|
||||
# and not shipped by the CLI (sync-assets.mjs mirrors data/ and scripts/, never SKILL.md).
|
||||
# (Built from segments: the path contract in check-asset-sync.yml scans this file too.)
|
||||
PLUGIN_ONLY_FILE = Path(".claude") / "skills" / "ui-ux-pro-max" / "SKILL.md"
|
||||
INVOCATION = re.compile(r'(?<![\w/.-])(?:python3?|node|bash)\s+"?([^\s"`\']+\.(?:py|cjs|js|mjs|sh))')
|
||||
PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}/"
|
||||
|
||||
|
||||
def shipped_invocations():
|
||||
for tree in SKILL_TREES:
|
||||
for skill_dir in sorted((REPO / tree).iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
for md in sorted(skill_dir.rglob("*.md")):
|
||||
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
|
||||
for match in INVOCATION.finditer(line):
|
||||
yield skill_dir, md, lineno, match.group(1)
|
||||
|
||||
|
||||
def resolve(skill_dir, md, path):
|
||||
"""Return (target, None) for a skill-relative path, or (None, reason)."""
|
||||
if path.startswith(PLUGIN_ROOT):
|
||||
if md.relative_to(REPO) != PLUGIN_ONLY_FILE:
|
||||
return None, "the ${CLAUDE_PLUGIN_ROOT} form is only valid in the plugin-only core SKILL.md"
|
||||
return REPO / path[len(PLUGIN_ROOT):], None
|
||||
if path.startswith("scripts/"):
|
||||
return skill_dir / path, None
|
||||
if path.startswith("../"):
|
||||
parts = path.split("/")
|
||||
if len(parts) > 3 and parts[2] == "scripts" and (skill_dir.parent / parts[1]).is_dir():
|
||||
return skill_dir.parent / parts[1] / "/".join(parts[2:]), None
|
||||
return None, "a sibling invocation must be ../<skill>/scripts/<file> and the sibling must ship"
|
||||
return None, "not skill-relative (expected scripts/<file> or ../<skill>/scripts/<file>)"
|
||||
|
||||
|
||||
class SkillScriptPathsTest(unittest.TestCase):
|
||||
def test_every_shipped_markdown_invocation_resolves_from_the_skill_directory(self):
|
||||
problems, seen = [], 0
|
||||
for skill_dir, md, lineno, path in shipped_invocations():
|
||||
seen += 1
|
||||
target, reason = resolve(skill_dir, md, path)
|
||||
if reason is None and not target.is_file():
|
||||
reason = f"no such file: {target}"
|
||||
if reason:
|
||||
problems.append(f"{md.relative_to(REPO)}:{lineno}: {path} -- {reason}")
|
||||
# Guard against a silently broken extractor: the two trees carry well over
|
||||
# a hundred documented invocations between them.
|
||||
self.assertGreater(seen, 100, f"extractor found only {seen} invocations")
|
||||
self.assertEqual(problems, [], "\n" + "\n".join(problems))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -663,7 +663,11 @@ def _check_catalog_summary(summary, licenses, phosphor, problems):
|
||||
problems.append(f"[catalog:summary] stale count for {key}")
|
||||
snapshots = summary.get("snapshots") if isinstance(summary.get("snapshots"), dict) else {}
|
||||
for name in ("google-fonts.csv", "google-font-licenses.json", "icons.csv", "phosphor-icons-upstream.json"):
|
||||
digest = hashlib.sha256((DATA_DIR / name).read_bytes()).hexdigest()
|
||||
# Line endings are normalized so the check matches
|
||||
# generate-catalog-summary.py on CRLF checkouts too.
|
||||
digest = hashlib.sha256(
|
||||
(DATA_DIR / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
if snapshots.get(name) != {"sha256": digest}:
|
||||
problems.append(f"[catalog:summary] stale snapshot for {name}")
|
||||
policy = summary.get("promotionPolicy")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: banner-design
|
||||
description: "Design banners for social media, ads, website heroes, creative assets, and print. Multiple art direction options with AI-generated visuals. Actions: design, create, generate banner. Platforms: Facebook, Twitter/X, LinkedIn, YouTube, Instagram, Google Display, website hero, print. Styles: minimalist, gradient, bold typography, photo-based, illustrated, geometric, retro, glassmorphism, 3D, neon, duotone, editorial, collage. Uses ui-ux-pro-max, frontend-design, ai-artist, ai-multimodal skills."
|
||||
description: "Design banners for social media, ads, website heroes, creative assets, and print. Multiple art direction options with optional generated or supplied visuals. Actions: design, create, generate banner. Platforms: Facebook, Twitter/X, LinkedIn, YouTube, Instagram, Google Display, website hero, print. Styles: minimalist, gradient, bold typography, photo-based, illustrated, geometric, retro, glassmorphism, 3D, neon, duotone, editorial, collage."
|
||||
argument-hint: "[platform] [style] [dimensions]"
|
||||
license: MIT
|
||||
metadata:
|
||||
@ -10,7 +10,7 @@ metadata:
|
||||
|
||||
# Banner Design - Multi-Format Creative Banner System
|
||||
|
||||
Design banners across social, ads, web, and print formats. Generates multiple art direction options per request with AI-powered visual elements. This skill handles banner design only. Does NOT handle video editing, full website design, or print production.
|
||||
Design banners across social, ads, web, and print formats. Generate multiple art direction options with CSS-built, user-supplied, or optionally generated visual elements. This skill handles banner design only. It does not handle video editing, full website design, or print production.
|
||||
|
||||
## When to Activate
|
||||
|
||||
@ -21,9 +21,9 @@ Design banners across social, ads, web, and print formats. Generates multiple ar
|
||||
- Event/print banner design
|
||||
- Creative asset generation for campaigns
|
||||
|
||||
## Prerequisites
|
||||
## Available Resources
|
||||
|
||||
**Python:** This skill uses Python scripts. On Windows, use `python` instead of `python3` (e.g., `python scripts/search.py` instead of `python3 scripts/search.py`).
|
||||
This workflow is self-contained: it requires no sibling skills or skill-relative scripts. Use `references/banner-sizes-and-styles.md` for the bundled size, safe-zone, and art-direction guidance. Browser research, image generation, and screenshot tooling are optional capabilities; when unavailable, use supplied assets, CSS-built visuals, and the runtime's standard preview or capture workflow.
|
||||
|
||||
## Workflow
|
||||
|
||||
@ -33,95 +33,44 @@ Collect via AskUserQuestion:
|
||||
1. **Purpose** — social cover, ad banner, website hero, print, or creative asset?
|
||||
2. **Platform/size** — which platform or custom dimensions?
|
||||
3. **Content** — headline, subtext, CTA, logo placement?
|
||||
4. **Brand** — existing brand guidelines? (check `docs/brand-guidelines.md`)
|
||||
4. **Brand** — existing brand guidelines, logo files, colors, or typography?
|
||||
5. **Style preference** — any art direction? (show style options if unsure)
|
||||
6. **Quantity** — how many options to generate? (default: 3)
|
||||
|
||||
### Step 2: Research & Art Direction
|
||||
|
||||
1. Activate `ui-ux-pro-max` skill for design intelligence
|
||||
2. Use Chrome browser to research Pinterest for design references:
|
||||
```
|
||||
Navigate to pinterest.com → search "[purpose] banner design [style]"
|
||||
Screenshot 3-5 reference pins for art direction inspiration
|
||||
```
|
||||
3. Select 2-3 complementary art direction styles from references:
|
||||
`references/banner-sizes-and-styles.md`
|
||||
1. Read `references/banner-sizes-and-styles.md` for the target format, safe zone, and suitable styles.
|
||||
2. If browser research is available and permitted, collect 3–5 references for composition and art-direction inspiration. Otherwise, work from the bundled reference and any examples supplied by the user.
|
||||
3. Select 2–3 complementary art directions and state how each supports the banner's purpose.
|
||||
|
||||
### Step 3: Design & Generate Options
|
||||
|
||||
For each art direction option:
|
||||
|
||||
1. **Create HTML/CSS banner** using `frontend-design` skill
|
||||
- Use exact platform dimensions from size reference
|
||||
- Apply safe zone rules (critical content in central 70-80%)
|
||||
- Max 2 typefaces, single CTA, 4.5:1 contrast ratio
|
||||
- Inject brand context via `inject-brand-context.cjs`
|
||||
1. **Create the banner in HTML/CSS**
|
||||
- Use the exact platform dimensions from the size reference
|
||||
- Apply safe-zone rules (critical content in the central 70–80%)
|
||||
- Use at most 2 typefaces, a single CTA, and text contrast of at least 4.5:1
|
||||
- Apply the user's supplied logo, colors, typography, and imagery; do not invent brand rules
|
||||
|
||||
2. **Generate visual elements** with `ai-artist` + `ai-multimodal` skills
|
||||
2. **Choose a visual source**
|
||||
- Prefer user-supplied or appropriately licensed assets when provided
|
||||
- Use gradients, geometric forms, type, and other CSS-built visuals for a dependency-free result
|
||||
- If the runtime provides an authorized image-generation capability, it may generate a background or illustration at the target aspect ratio
|
||||
- Keep generated visual prompts free of text, letters, and words so final copy remains editable and accessible in HTML
|
||||
|
||||
**a) Search prompt inspiration** (6000+ examples in ai-artist):
|
||||
```bash
|
||||
python3 .claude/skills/ai-artist/scripts/search.py "<banner style keywords>"
|
||||
```
|
||||
|
||||
**b) Generate with Standard model** (fast, good for backgrounds/patterns):
|
||||
```bash
|
||||
.claude/skills/.venv/bin/python3 .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \
|
||||
--task generate --model gemini-2.5-flash-image \
|
||||
--prompt "<banner visual prompt>" --aspect-ratio <platform-ratio> \
|
||||
--size 2K --output assets/banners/
|
||||
```
|
||||
|
||||
**c) Generate with Pro model** (4K, complex illustrations/hero visuals):
|
||||
```bash
|
||||
.claude/skills/.venv/bin/python3 .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \
|
||||
--task generate --model gemini-3-pro-image-preview \
|
||||
--prompt "<creative banner prompt>" --aspect-ratio <platform-ratio> \
|
||||
--size 4K --output assets/banners/
|
||||
```
|
||||
|
||||
**When to use which model:**
|
||||
| Use Case | Model | Quality |
|
||||
|----------|-------|---------|
|
||||
| Backgrounds, gradients, patterns | Standard (Flash) | 2K, fast |
|
||||
| Hero illustrations, product shots | Pro | 4K, detailed |
|
||||
| Photorealistic scenes, complex art | Pro | 4K, best quality |
|
||||
| Quick iterations, A/B variants | Standard (Flash) | 2K, fast |
|
||||
|
||||
**Aspect ratios:** `1:1`, `16:9`, `9:16`, `3:4`, `4:3`, `2:3`, `3:2`
|
||||
Match to platform - e.g., Twitter header = `3:1` (use `3:2` closest), Instagram story = `9:16`
|
||||
|
||||
**Pro model prompt tips** (see `ai-artist` references/nano-banana-pro-examples.md):
|
||||
- Be descriptive: style, lighting, mood, composition, color palette
|
||||
- Include art direction: "minimalist flat design", "cyberpunk neon", "editorial photography"
|
||||
- Specify no-text: "no text, no letters, no words" (text overlaid in HTML step)
|
||||
|
||||
3. **Compose final banner** — overlay text, CTA, logo on generated visual in HTML/CSS
|
||||
3. **Compose the final banner** — overlay the headline, supporting copy, CTA, and logo in HTML/CSS, then verify hierarchy, safe zones, contrast, and crop behavior at the exact target size
|
||||
|
||||
### Step 4: Export Banners to Images
|
||||
|
||||
After designing HTML banners, export each to PNG using `chrome-devtools` skill:
|
||||
After designing the HTML banners:
|
||||
|
||||
1. **Serve HTML files** via local server (python http.server or similar)
|
||||
2. **Screenshot each banner** at exact platform dimensions:
|
||||
```bash
|
||||
# Export banner to PNG at exact dimensions
|
||||
node .claude/skills/chrome-devtools/scripts/screenshot.js \
|
||||
--url "http://localhost:8765/banner-01-minimalist.html" \
|
||||
--width 1500 --height 500 \
|
||||
--output "assets/banners/{campaign}/{variant}-{size}.png"
|
||||
```
|
||||
3. **Auto-compress** if >5MB (Sharp compression built-in):
|
||||
```bash
|
||||
# With custom max size threshold
|
||||
node .claude/skills/chrome-devtools/scripts/screenshot.js \
|
||||
--url "http://localhost:8765/banner-02-gradient.html" \
|
||||
--width 1500 --height 500 --max-size 3 \
|
||||
--output "assets/banners/{campaign}/{variant}-{size}.png"
|
||||
```
|
||||
1. Preview each banner in an available browser at the exact target viewport.
|
||||
2. Capture the banner element as PNG with the runtime's standard browser or screenshot capability. If capture is unavailable, deliver the HTML/CSS source and clearly mark PNG export as pending rather than naming an uninstalled tool.
|
||||
3. Verify the exported pixel dimensions, safe-zone crop, font loading, and image quality.
|
||||
4. If an exported file exceeds the platform limit, use an available image optimizer or reduce image quality and dimensions within the platform specification.
|
||||
|
||||
**Output path convention** (per `assets-organizing` skill):
|
||||
**Output path convention:**
|
||||
```
|
||||
assets/banners/{campaign}/
|
||||
├── minimalist-1500x500.png
|
||||
@ -139,7 +88,7 @@ assets/banners/{campaign}/
|
||||
|
||||
Present all exported images side-by-side. For each option show:
|
||||
- Art direction style name
|
||||
- Exported PNG preview (use `ai-multimodal` skill to display if needed)
|
||||
- Exported PNG preview, or an HTML/CSS preview when image capture is unavailable
|
||||
- Key design rationale
|
||||
- File path & dimensions
|
||||
|
||||
@ -185,7 +134,7 @@ Full 22 styles: `references/banner-sizes-and-styles.md`
|
||||
- **Typography**: max 2 fonts, min 16px body, ≥32px headline
|
||||
- **Text ratio**: under 20% for ads (Meta penalizes heavy text)
|
||||
- **Print**: 300 DPI, CMYK, 3-5mm bleed
|
||||
- **Brand**: always inject via `inject-brand-context.cjs`
|
||||
- **Brand**: apply only supplied, verified brand guidance and assets
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
@ -20,6 +20,10 @@ Brand identity, voice, messaging, asset management, and consistency frameworks.
|
||||
- Asset organization, naming, and approval
|
||||
- Color palette management and typography specs
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Inject brand context into prompts:**
|
||||
|
||||
@ -157,7 +157,7 @@ The `validate-asset.cjs` script can auto-check:
|
||||
- Naming convention
|
||||
- Basic metadata
|
||||
|
||||
Run: `node .claude/skills/brand/scripts/validate-asset.cjs <asset-path>`
|
||||
Run: `node scripts/validate-asset.cjs <asset-path>`
|
||||
|
||||
## Archival
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Edit `docs/brand-guidelines.md`:
|
||||
|
||||
Run the sync script:
|
||||
```bash
|
||||
node .claude/skills/brand/scripts/sync-brand-to-tokens.cjs
|
||||
node scripts/sync-brand-to-tokens.cjs
|
||||
```
|
||||
|
||||
This will:
|
||||
@ -58,7 +58,7 @@ This will:
|
||||
Confirm all files are updated:
|
||||
```bash
|
||||
# Check brand context extraction
|
||||
node .claude/skills/brand/scripts/inject-brand-context.cjs --json | head -30
|
||||
node scripts/inject-brand-context.cjs --json | head -30
|
||||
|
||||
# Check CSS variables
|
||||
grep "primary" assets/design-tokens.css | head -5
|
||||
|
||||
@ -287,11 +287,7 @@ function main() {
|
||||
"1. Run the ImageMagick command to extract colors:",
|
||||
` ${generateImageMagickCommand(resolvedPath)}`,
|
||||
"",
|
||||
"2. Or use the ai-multimodal skill:",
|
||||
` python .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \\`,
|
||||
` --files "${resolvedPath}" \\`,
|
||||
` --task analyze \\`,
|
||||
` --prompt "Extract the 10 most dominant colors as hex values"`,
|
||||
"2. Or use an image-analysis skill (e.g. ai-multimodal, if installed) to extract the 10 most dominant colors as hex values",
|
||||
"",
|
||||
"3. Then compare extracted colors against brand palette",
|
||||
],
|
||||
|
||||
@ -17,7 +17,10 @@ const { execFileSync } = require('child_process');
|
||||
const BRAND_GUIDELINES = 'docs/brand-guidelines.md';
|
||||
const DESIGN_TOKENS_JSON = 'assets/design-tokens.json';
|
||||
const DESIGN_TOKENS_CSS = 'assets/design-tokens.css';
|
||||
const GENERATE_TOKENS_SCRIPT = '.claude/skills/design-system/scripts/generate-tokens.cjs';
|
||||
// Sibling sub-skill, resolved from this file's location so it works in every
|
||||
// install context (plugin cache, project or --global CLI install), not only
|
||||
// when the process runs from a project root that contains .claude/skills/.
|
||||
const GENERATE_TOKENS_SCRIPT = path.resolve(__dirname, '..', '..', 'design-system', 'scripts', 'generate-tokens.cjs');
|
||||
|
||||
/**
|
||||
* Extract color info from brand guidelines markdown
|
||||
@ -96,15 +99,34 @@ function generateColorScale(baseHex, darkHex, lightHex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust hex color brightness
|
||||
* Adjust hex color brightness.
|
||||
*
|
||||
* Blends each channel proportionally toward white (percent > 0) or toward
|
||||
* black (percent < 0) instead of adding/subtracting a flat 255*percent to
|
||||
* every channel. The flat-shift approach clamped all three channels to 0
|
||||
* (or 255) whenever the base color's channels were already low (or high)
|
||||
* relative to the shift — e.g. darkening a dark brand color like #4A3228
|
||||
* by -0.3/-0.45/-0.6 produced #000000 for all three, collapsing shades
|
||||
* 700/800/900 into an identical, useless black.
|
||||
*/
|
||||
function adjustBrightness(hex, percent) {
|
||||
if (typeof hex !== 'string') return '#000000';
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent)));
|
||||
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent)));
|
||||
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + Math.round(255 * percent)));
|
||||
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
const r = (num >> 16) & 0xFF;
|
||||
const g = (num >> 8) & 0xFF;
|
||||
const b = num & 0xFF;
|
||||
|
||||
const adjustChannel = (channel) => {
|
||||
const adjusted = percent >= 0
|
||||
? channel + (255 - channel) * percent
|
||||
: channel * (1 + percent);
|
||||
return Math.min(255, Math.max(0, Math.round(adjusted)));
|
||||
};
|
||||
|
||||
const newR = adjustChannel(r);
|
||||
const newG = adjustChannel(g);
|
||||
const newB = adjustChannel(b);
|
||||
return `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,7 +251,7 @@ function main() {
|
||||
console.log(`✅ Updated: ${DESIGN_TOKENS_JSON}`);
|
||||
|
||||
// Regenerate CSS
|
||||
const generateScript = path.resolve(process.cwd(), GENERATE_TOKENS_SCRIPT);
|
||||
const generateScript = GENERATE_TOKENS_SCRIPT;
|
||||
if (fs.existsSync(generateScript)) {
|
||||
try {
|
||||
execFileSync('node', [generateScript, '--config', DESIGN_TOKENS_JSON, '-o', DESIGN_TOKENS_CSS], {
|
||||
@ -240,6 +262,8 @@ function main() {
|
||||
} catch (e) {
|
||||
console.error('⚠️ Failed to regenerate CSS:', e.message);
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ design-system sub-skill not found at ${generateScript}; ${DESIGN_TOKENS_CSS} not regenerated`);
|
||||
}
|
||||
|
||||
console.log('\n✨ Brand sync complete!');
|
||||
|
||||
@ -24,22 +24,33 @@ TOKENS_STARTER = (
|
||||
)
|
||||
|
||||
|
||||
def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
def _run(tmp_path: Path) -> subprocess.CompletedProcess:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node not available")
|
||||
return subprocess.run(
|
||||
[node, str(SCRIPT)],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# sync-brand-to-tokens.cjs prints emoji. Without an explicit encoding,
|
||||
# `text=True` decodes the pipe with the locale codec, and several of
|
||||
# those emoji have UTF-8 bytes that cp1252 has no character for
|
||||
# (0x8F in the warning, 0x9D in the error, 0x8F in the dry-run notice).
|
||||
# Decoding then raises inside subprocess's reader thread, the stream
|
||||
# comes back as None, and assertions against it fail with a TypeError
|
||||
# that hides the real result.
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "assets").mkdir()
|
||||
shutil.copy(BRAND_STARTER, tmp_path / "docs" / "brand-guidelines.md")
|
||||
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
||||
|
||||
result = subprocess.run(
|
||||
[node, str(SCRIPT)],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
result = _run(tmp_path)
|
||||
|
||||
# Must not crash (the bug raised an unhandled TypeError).
|
||||
assert "TypeError" not in result.stderr, result.stderr
|
||||
@ -50,3 +61,62 @@ def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
assert primitive["primary"]["500"]["$value"] == "#2563EB"
|
||||
assert primitive["secondary"]["500"]["$value"] == "#8B5CF6"
|
||||
assert primitive["accent"]["500"]["$value"] == "#10B981"
|
||||
|
||||
# #474: the sibling design-system script is resolved from this skill's own
|
||||
# location, so the CSS regeneration must run even though tmp_path has no
|
||||
# .claude/skills/ tree. Before the fix it was resolved from the working
|
||||
# directory and silently skipped in every layout but a project install.
|
||||
assert "Regenerated" in result.stdout, result.stdout
|
||||
css = tmp_path / "assets" / "design-tokens.css"
|
||||
assert css.exists() and css.stat().st_size > 0
|
||||
|
||||
|
||||
def test_dark_base_color_does_not_collapse_shades_to_black(tmp_path):
|
||||
"""adjustBrightness() used to add/subtract a flat 255*percent per channel.
|
||||
|
||||
For a dark base color (channels already close to 0), darkening by
|
||||
-0.3/-0.45/-0.6 clamped every channel to 0, so shades 700, 800, and 900
|
||||
all came back as the identical, useless #000000 instead of a graded dark
|
||||
scale. This runs the sync against a dark, coffee-roastery-style brand
|
||||
color and asserts the three shades stay distinct and non-black.
|
||||
"""
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "assets").mkdir()
|
||||
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
||||
(tmp_path / "docs" / "brand-guidelines.md").write_text(
|
||||
"## Quick Reference\n\n"
|
||||
"| Element | Value |\n"
|
||||
"|---------|-------|\n"
|
||||
"| Primary Color | #4A3228 |\n"
|
||||
"| Secondary Color | #C08A3E |\n"
|
||||
"| Accent Color | #6B8F71 |\n"
|
||||
)
|
||||
|
||||
result = _run(tmp_path)
|
||||
assert result.returncode == 0, result.stderr + result.stdout
|
||||
|
||||
tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text())
|
||||
primary = tokens["primitive"]["color"]["primary"]
|
||||
dark_shades = [primary[shade]["$value"] for shade in ("700", "800", "900")]
|
||||
|
||||
assert len(set(dark_shades)) == 3, (
|
||||
f"expected three distinct dark shades, got {dark_shades}"
|
||||
)
|
||||
assert "#000000" not in dark_shades, dark_shades
|
||||
|
||||
|
||||
def test_reports_missing_guidelines_without_breaking_the_harness(tmp_path):
|
||||
"""The missing-guidelines path is the one that breaks a locale-decoded pipe.
|
||||
|
||||
It is also the default state of any project that has not run the brand skill
|
||||
yet, so it is the path a contributor hits first. The script prints its error
|
||||
with a leading emoji whose UTF-8 encoding contains 0x9D; cp1252 has no
|
||||
character there, so on Windows this test fails with
|
||||
``TypeError: argument of type 'NoneType' is not a container`` unless the
|
||||
subprocess pipe is pinned to UTF-8.
|
||||
"""
|
||||
result = _run(tmp_path)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert result.stderr is not None
|
||||
assert "Brand guidelines not found" in result.stderr
|
||||
|
||||
@ -48,6 +48,10 @@ Component (component-specific)
|
||||
--button-bg: var(--color-primary);
|
||||
```
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Generate tokens:**
|
||||
|
||||
@ -15,12 +15,16 @@ const path = require('path');
|
||||
|
||||
// Find project root (look for assets/design-tokens.css)
|
||||
function findProjectRoot(startDir) {
|
||||
// Walk up until dirname stops changing: on Windows the root is 'C:\', so a
|
||||
// `dir !== '/'` guard never terminates.
|
||||
let dir = startDir;
|
||||
while (dir !== '/') {
|
||||
for (;;) {
|
||||
if (fs.existsSync(path.join(dir, 'assets', 'design-tokens.css'))) {
|
||||
return dir;
|
||||
}
|
||||
dir = path.dirname(dir);
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -9,10 +9,32 @@ import json
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Project root relative to this script
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
||||
# The skill can be installed outside the project it operates on (user-level
|
||||
# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from
|
||||
# this file's location. Resolve it from the working directory instead -- the same
|
||||
# convention generate-tokens.cjs and validate-tokens.cjs already use via
|
||||
# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly.
|
||||
def _find_project_root():
|
||||
override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT')
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
start = Path.cwd().resolve()
|
||||
markers = (
|
||||
Path('assets') / 'design-tokens.json',
|
||||
Path('assets') / 'design-tokens.css',
|
||||
Path('package.json'),
|
||||
Path('.git'),
|
||||
)
|
||||
for candidate in (start, *start.parents):
|
||||
if any((candidate / marker).exists() for marker in markers):
|
||||
return candidate
|
||||
return start
|
||||
|
||||
|
||||
PROJECT_ROOT = _find_project_root()
|
||||
TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv'
|
||||
|
||||
|
||||
@ -15,11 +15,43 @@ Usage:
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
# Project root relative to this script
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
||||
# The skill can be installed outside the project it operates on (user-level
|
||||
# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from
|
||||
# this file's location. Resolve it from the working directory instead -- the same
|
||||
# convention generate-tokens.cjs and validate-tokens.cjs already use via
|
||||
# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly.
|
||||
def _find_project_root():
|
||||
override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT')
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
start = Path.cwd().resolve()
|
||||
markers = (
|
||||
Path('assets') / 'design-tokens.json',
|
||||
Path('assets') / 'design-tokens.css',
|
||||
Path('package.json'),
|
||||
Path('.git'),
|
||||
)
|
||||
for candidate in (start, *start.parents):
|
||||
if any((candidate / marker).exists() for marker in markers):
|
||||
return candidate
|
||||
return start
|
||||
|
||||
|
||||
PROJECT_ROOT = _find_project_root()
|
||||
|
||||
# Force UTF-8 on stdout/stderr: this script prints emoji, which raises
|
||||
# UnicodeEncodeError on a Windows console (cp1252). Same guard as
|
||||
# src/ui-ux-pro-max/scripts/search.py.
|
||||
import io
|
||||
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css'
|
||||
|
||||
|
||||
@ -13,6 +13,16 @@ from slide_search_core import (
|
||||
get_color_for_emotion, get_background_config
|
||||
)
|
||||
|
||||
# Force UTF-8 on stdout/stderr: this script prints emoji, which raises
|
||||
# UnicodeEncodeError on a Windows console (cp1252). Same guard as
|
||||
# src/ui-ux-pro-max/scripts/search.py.
|
||||
import io
|
||||
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
|
||||
|
||||
def format_result(result, domain):
|
||||
"""Format a single search result for display"""
|
||||
|
||||
@ -20,11 +20,15 @@ def _run(tmp_path: Path, css: str) -> subprocess.CompletedProcess:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node not available")
|
||||
(tmp_path / "sample.css").write_text(css)
|
||||
(tmp_path / "sample.css").write_text(css, encoding="utf-8")
|
||||
return subprocess.run(
|
||||
[node, str(SCRIPT), "--dir", str(tmp_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# validate-tokens.cjs prints emoji; without an explicit encoding Python
|
||||
# decodes the pipe with the locale codec (cp1252 on Windows), which
|
||||
# raises in the reader thread and leaves result.stdout set to None.
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: design
|
||||
description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads."
|
||||
description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads."
|
||||
argument-hint: "[design-type] [context]"
|
||||
license: MIT
|
||||
metadata:
|
||||
@ -27,9 +27,9 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot
|
||||
|
||||
| Task | Sub-skill | Details |
|
||||
|------|-----------|---------|
|
||||
| Brand identity, voice, assets | `brand` | External skill |
|
||||
| Tokens, specs, CSS vars | `design-system` | External skill |
|
||||
| shadcn/ui, Tailwind, code | `ui-styling` | External skill |
|
||||
| Brand identity, voice, assets | `brand` | Bundled sibling skill |
|
||||
| Tokens, specs, CSS vars | `design-system` | Bundled sibling skill |
|
||||
| shadcn/ui, Tailwind, code | `ui-styling` | Bundled sibling skill |
|
||||
| Logo creation, AI generation | Logo (built-in) | `references/logo-design.md` |
|
||||
| CIP mockups, deliverables | CIP (built-in) | `references/cip-design.md` |
|
||||
| Presentations, pitch decks | Slides (built-in) | `references/slides.md` |
|
||||
@ -37,22 +37,27 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot
|
||||
| Social media images/photos | Social Photos (built-in) | `references/social-photos-design.md` |
|
||||
| SVG icons, icon sets | Icon (built-in) | `references/icon-design.md` |
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Logo Design (Built-in)
|
||||
|
||||
55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana models.
|
||||
55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana, Atlas
|
||||
Cloud, and MuAPI image generation.
|
||||
|
||||
### Logo: Generate Design Brief
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
python3 scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
```
|
||||
|
||||
### Logo: Search Styles/Colors/Industries
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech professional" --domain color
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --domain industry
|
||||
python3 scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 scripts/logo/search.py "tech professional" --domain color
|
||||
python3 scripts/logo/search.py "healthcare medical" --domain industry
|
||||
```
|
||||
|
||||
### Logo: Generate with AI
|
||||
@ -60,13 +65,16 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do
|
||||
**ALWAYS** generate output logo images with white background.
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
```
|
||||
|
||||
**IMPORTANT:** When scripts fail, try to fix them directly.
|
||||
|
||||
After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`. If yes, invoke `/ui-ux-pro-max` for gallery.
|
||||
After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`. If yes, use the bundled `ui-ux-pro-max` skill for the gallery.
|
||||
|
||||
## CIP Design (Built-in)
|
||||
|
||||
@ -75,32 +83,32 @@ After generation, **ALWAYS** ask user about HTML preview via `AskUserQuestion`.
|
||||
### CIP: Generate Brief
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
python3 scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
```
|
||||
|
||||
### CIP: Search Domains
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "office reception" --domain mockup
|
||||
python3 scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 scripts/cip/search.py "office reception" --domain mockup
|
||||
```
|
||||
|
||||
### CIP: Generate Mockups
|
||||
|
||||
```bash
|
||||
# With logo (RECOMMENDED)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
|
||||
# Full CIP set
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
|
||||
# Pro model (4K text)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
|
||||
# Without logo
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
python3 scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
```
|
||||
|
||||
Models: `flash` (default, `gemini-2.5-flash-image`), `pro` (`gemini-3-pro-image-preview`)
|
||||
@ -108,7 +116,7 @@ Models: `flash` (default, `gemini-2.5-flash-image`), `pro` (`gemini-3-pro-image-
|
||||
### CIP: Render HTML Presentation
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
```
|
||||
|
||||
**Tip:** If no logo exists, use Logo Design section above first.
|
||||
@ -131,16 +139,16 @@ Load `references/slides-create.md` for the creation workflow.
|
||||
|
||||
## Banner Design (Built-in)
|
||||
|
||||
22 art direction styles across social, ads, web, print. Uses `frontend-design`, `ai-artist`, `ai-multimodal`, `chrome-devtools` skills.
|
||||
22 art direction styles across social, ads, web, print. This workflow needs nothing outside the bundle: `references/banner-sizes-and-styles.md` and the bundled `ui-ux-pro-max` skill for style and palette guidance. Browser research, image generation, and screenshot capture are optional runtime capabilities; when unavailable, use supplied assets, CSS-built visuals, and the runtime's standard preview or capture workflow.
|
||||
|
||||
Load `references/banner-sizes-and-styles.md` for complete sizes and styles reference.
|
||||
|
||||
### Banner: Workflow
|
||||
|
||||
1. **Gather requirements** via `AskUserQuestion` — purpose, platform, content, brand, style, quantity
|
||||
2. **Research** — Activate `ui-ux-pro-max`, browse Pinterest for references
|
||||
3. **Design** — Create HTML/CSS banner with `frontend-design`, generate visuals with `ai-artist`/`ai-multimodal`
|
||||
4. **Export** — Screenshot to PNG at exact dimensions via `chrome-devtools`
|
||||
2. **Research** — Read `references/banner-sizes-and-styles.md` and use the bundled `ui-ux-pro-max` skill for style and palette guidance; if browser research is available and permitted, collect 3–5 references
|
||||
3. **Design** — Create the HTML/CSS banner at exact platform dimensions; use supplied assets or CSS-built visuals, or an authorized image-generation capability if the runtime provides one
|
||||
4. **Export** — Capture PNG at exact dimensions with the runtime's browser or screenshot capability; if unavailable, deliver the HTML/CSS source and mark PNG export as pending
|
||||
5. **Present** — Show all options side-by-side, iterate on feedback
|
||||
|
||||
### Banner: Quick Size Reference
|
||||
@ -183,21 +191,21 @@ Load `references/banner-sizes-and-styles.md` for complete sizes and styles refer
|
||||
### Icon: Generate Single Icon
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
python3 scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
```
|
||||
|
||||
### Icon: Generate Batch Variations
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
```
|
||||
|
||||
### Icon: Multi-size Export
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
```
|
||||
|
||||
### Icon: Top Styles
|
||||
@ -216,20 +224,20 @@ python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile"
|
||||
|
||||
## Social Photos (Built-in)
|
||||
|
||||
Multi-platform social image design: HTML/CSS → screenshot export. Uses `ui-ux-pro-max`, `brand`, `design-system`, `chrome-devtools` skills.
|
||||
Multi-platform social image design: HTML/CSS → screenshot export. Uses the bundled `ui-ux-pro-max`, `brand`, and `design-system` skills; screenshot export runs through Chrome headless, Playwright, or Puppeteer (see the reference).
|
||||
|
||||
Load `references/social-photos-design.md` for sizes, templates, best practices.
|
||||
|
||||
### Social Photos: Workflow
|
||||
|
||||
1. **Orchestrate** — `project-management` skill for TODO tasks; parallel subagents for independent work
|
||||
1. **Orchestrate** — Track the steps below with the runtime's native task list; parallel subagents for independent work
|
||||
2. **Analyze** — Parse prompt: subject, platforms, style, brand context, content elements
|
||||
3. **Ideate** — 3-5 concepts, present via `AskUserQuestion`
|
||||
4. **Design** — `/ckm:brand` → `/ckm:design-system` → randomly invoke `/ck:ui-ux-pro-max` OR `/ck:frontend-design`; HTML per idea × size
|
||||
5. **Export** — `chrome-devtools` or Playwright screenshot at exact px (2x deviceScaleFactor)
|
||||
6. **Verify** — Use Chrome MCP or `chrome-devtools` skill to visually inspect exported designs; fix layout/styling issues and re-export
|
||||
4. **Design** — bundled `brand` → `design-system` → `ui-ux-pro-max` skills; HTML per idea × size
|
||||
5. **Export** — Chrome headless, Playwright, or Puppeteer screenshot at exact px (2x device scale factor where the tool supports it; see the reference)
|
||||
6. **Verify** — Open the exported PNGs in an available browser or image viewer and inspect them; fix layout/styling issues and re-export
|
||||
7. **Report** — Summary to `plans/reports/` with design decisions
|
||||
8. **Organize** — Invoke `assets-organizing` skill to sort output files and reports
|
||||
8. **Organize** — Sort output files and reports into the project's asset directories
|
||||
|
||||
### Social Photos: Key Sizes
|
||||
|
||||
@ -303,11 +311,23 @@ python3 --version || python --version
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-key" # https://aistudio.google.com/apikey
|
||||
pip install google-genai pillow
|
||||
|
||||
# Optional MuAPI provider (no extra Python package required)
|
||||
export MUAPI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
MuAPI uses the asynchronous model endpoint and prediction result API. See the
|
||||
[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication
|
||||
and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana)
|
||||
or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro)
|
||||
for the current model-specific schemas. The logo generator supports both documented
|
||||
model slugs and sends their shared required `prompt` plus optional `aspect_ratio`
|
||||
fields; the Pro model also accepts an optional `resolution` field that this focused
|
||||
logo workflow leaves at the provider default.
|
||||
|
||||
> **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`).
|
||||
|
||||
## Integration
|
||||
|
||||
**External sub-skills:** brand, design-system, ui-styling
|
||||
**Related Skills:** frontend-design, ui-ux-pro-max, ai-multimodal, chrome-devtools
|
||||
**Bundled sub-skills:** brand, design-system, ui-styling
|
||||
**Related Skills:** ui-ux-pro-max
|
||||
|
||||
@ -16,49 +16,49 @@ Corporate Identity Program design with 50+ deliverables, 20 styles, 20 industrie
|
||||
### CIP Brief (Start Here)
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
python3 scripts/cip/search.py "tech startup" --cip-brief -b "BrandName"
|
||||
```
|
||||
|
||||
### Search Domains
|
||||
|
||||
```bash
|
||||
# Deliverables
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
python3 scripts/cip/search.py "business card letterhead" --domain deliverable
|
||||
|
||||
# Design styles
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
python3 scripts/cip/search.py "luxury premium elegant" --domain style
|
||||
|
||||
# Industry guidelines
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
python3 scripts/cip/search.py "hospitality hotel" --domain industry
|
||||
|
||||
# Mockup contexts
|
||||
python3 ~/.claude/skills/design/scripts/cip/search.py "office reception" --domain mockup
|
||||
python3 scripts/cip/search.py "office reception" --domain mockup
|
||||
```
|
||||
|
||||
### Generate Mockups
|
||||
|
||||
```bash
|
||||
# With logo (RECOMMENDED - uses image editing)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" --industry "consulting"
|
||||
|
||||
# Full CIP set with logo
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
|
||||
# Pro model for 4K text rendering
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
python3 scripts/cip/generate.py --brand "TopGroup" --logo logo.png --deliverable "business card" --model pro
|
||||
|
||||
# Custom deliverables with aspect ratio
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "GreenLeaf" --logo logo.png --industry "organic food" --deliverables "letterhead,packaging,vehicle" --ratio 16:9
|
||||
python3 scripts/cip/generate.py --brand "GreenLeaf" --logo logo.png --industry "organic food" --deliverables "letterhead,packaging,vehicle" --ratio 16:9
|
||||
|
||||
# Without logo (AI generates interpretation)
|
||||
python3 ~/.claude/skills/design/scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
python3 scripts/cip/generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
```
|
||||
|
||||
### Render HTML Presentation
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 ~/.claude/skills/design/scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip --output presentation.html
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images /path/to/cip-output
|
||||
python3 scripts/cip/render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip --output presentation.html
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
@ -164,14 +164,14 @@ Application Code
|
||||
|
||||
**Brand:**
|
||||
```bash
|
||||
node .claude/skills/brand/scripts/inject-brand-context.cjs
|
||||
node .claude/skills/brand/scripts/validate-asset.cjs <path>
|
||||
node ../brand/scripts/inject-brand-context.cjs
|
||||
node ../brand/scripts/validate-asset.cjs <path>
|
||||
```
|
||||
|
||||
**Tokens:**
|
||||
```bash
|
||||
node .claude/skills/design-system/scripts/generate-tokens.cjs -c tokens.json
|
||||
node .claude/skills/design-system/scripts/validate-tokens.cjs -d src/
|
||||
node ../design-system/scripts/generate-tokens.cjs -c tokens.json
|
||||
node ../design-system/scripts/validate-tokens.cjs -d src/
|
||||
```
|
||||
|
||||
**Components:**
|
||||
|
||||
@ -13,29 +13,29 @@ AI-powered SVG icon generation using Gemini 3.1 Pro Preview. 15 styles, 12 categ
|
||||
### Generate Single Icon
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
python3 scripts/icon/generate.py --prompt "settings gear" --style outlined
|
||||
python3 scripts/icon/generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python3 scripts/icon/generate.py --name "dashboard" --category navigation --style duotone
|
||||
```
|
||||
|
||||
### Generate Batch Variations
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "notification bell" --batch 6 --style outlined --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "notification bell" --batch 6 --style outlined --output-dir ./icons
|
||||
```
|
||||
|
||||
### Generate Multiple Sizes
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
python3 scripts/icon/generate.py --prompt "user profile" --sizes "16,24,32,48" --output-dir ./icons
|
||||
```
|
||||
|
||||
### List Styles/Categories
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --list-styles
|
||||
python3 ~/.claude/skills/design/scripts/icon/generate.py --list-categories
|
||||
python3 scripts/icon/generate.py --list-styles
|
||||
python3 scripts/icon/generate.py --list-categories
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# Logo Design Reference
|
||||
|
||||
AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Uses Gemini Nano Banana models.
|
||||
AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud and MuAPI are also available as explicit opt-in providers.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs |
|
||||
| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana |
|
||||
| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana, Atlas Cloud, or MuAPI |
|
||||
| `scripts/logo/core.py` | BM25 search engine for logo data |
|
||||
|
||||
## Commands
|
||||
@ -15,20 +15,20 @@ AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. U
|
||||
### Design Brief (Start Here)
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
python3 scripts/logo/search.py "tech startup modern" --design-brief -p "BrandName"
|
||||
```
|
||||
|
||||
### Search Domains
|
||||
|
||||
```bash
|
||||
# Styles
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "minimalist clean" --domain style
|
||||
python3 scripts/logo/search.py "minimalist clean" --domain style
|
||||
|
||||
# Color palettes
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "tech professional" --domain color
|
||||
python3 scripts/logo/search.py "tech professional" --domain color
|
||||
|
||||
# Industry guidelines
|
||||
python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --domain industry
|
||||
python3 scripts/logo/search.py "healthcare medical" --domain industry
|
||||
```
|
||||
|
||||
### Generate Logo
|
||||
@ -36,11 +36,14 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do
|
||||
**ALWAYS** use white background for output logos.
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech
|
||||
python3 scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi
|
||||
python3 scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
```
|
||||
|
||||
Options: `--style`, `--industry`, `--prompt`
|
||||
Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model`, `--muapi-model`
|
||||
|
||||
## Available Styles
|
||||
|
||||
@ -76,7 +79,7 @@ Options: `--style`, `--industry`, `--prompt`
|
||||
1. Generate design brief → `scripts/logo/search.py --design-brief`
|
||||
2. Generate logo variations → `scripts/logo/generate.py --brand --style --industry`
|
||||
3. Ask user about HTML preview → `AskUserQuestion` tool
|
||||
4. If yes, invoke `/ui-ux-pro-max` for HTML gallery
|
||||
4. If yes, use the bundled `ui-ux-pro-max` skill for the HTML gallery
|
||||
|
||||
## Detailed References
|
||||
|
||||
@ -89,4 +92,19 @@ Options: `--style`, `--industry`, `--prompt`
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-key"
|
||||
pip install google-genai
|
||||
|
||||
# Optional Atlas Cloud provider (no extra Python package required)
|
||||
export ATLASCLOUD_API_KEY="your-key"
|
||||
|
||||
# Optional MuAPI provider (no extra Python package required)
|
||||
export MUAPI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
MuAPI uses the asynchronous model endpoint and prediction result API. See the
|
||||
[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication
|
||||
and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana)
|
||||
or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro)
|
||||
for the current model-specific schemas. The logo generator supports both documented
|
||||
model slugs and sends their shared required `prompt` plus optional `aspect_ratio`
|
||||
fields; the Pro model also accepts an optional `resolution` field that this focused
|
||||
logo workflow leaves at the provider default.
|
||||
|
||||
@ -66,10 +66,10 @@
|
||||
|
||||
```bash
|
||||
# Find formula for slide type
|
||||
python .claude/skills/design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
python ../design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
|
||||
# Get emotion-appropriate formula
|
||||
python .claude/skills/design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
python ../design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@ -113,10 +113,10 @@
|
||||
|
||||
```bash
|
||||
# Find layout for specific use
|
||||
python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
python ../design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
|
||||
# Contextual recommendation
|
||||
python .claude/skills/design-system/scripts/search-slides.py "traction slide" \
|
||||
python ../design-system/scripts/search-slides.py "traction slide" \
|
||||
--context --position 4 --total 10
|
||||
```
|
||||
|
||||
|
||||
@ -76,10 +76,10 @@ Pattern breaks at 1/3 and 2/3 positions create engagement peaks.
|
||||
|
||||
```bash
|
||||
# Find strategy by goal
|
||||
python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
python ../design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
|
||||
# Get emotion arc
|
||||
python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
python ../design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
```
|
||||
|
||||
## Matching Strategy to Context
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Social Photos Design Guide
|
||||
|
||||
Design social media images via HTML/CSS rendering + screenshot export. Orchestrates `ui-ux-pro-max`, `brand`, `design-system`, and `chrome-devtools` skills.
|
||||
Design social media images via HTML/CSS rendering + screenshot export. Orchestrates the bundled `ui-ux-pro-max`, `brand`, and `design-system` skills; screenshot export runs through Chrome headless, Playwright, or Puppeteer.
|
||||
|
||||
## Platform Sizes
|
||||
|
||||
@ -22,9 +22,9 @@ Design social media images via HTML/CSS rendering + screenshot export. Orchestra
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Activate Project Management
|
||||
### Step 1: Plan the Work
|
||||
|
||||
Invoke `project-management` skill to create persistent TODO tasks via Claude's native task orchestration. Break down into:
|
||||
Create TODO tasks with the runtime's native task list. Break down into:
|
||||
- Requirement analysis task
|
||||
- Idea generation task(s)
|
||||
- HTML design task(s) — can parallelize per size/variant
|
||||
@ -55,11 +55,11 @@ Present ideas to user via `AskUserQuestion` for approval before designing.
|
||||
|
||||
### Step 4: Design HTML Files
|
||||
|
||||
Activate these skills in sequence:
|
||||
Use these bundled skills in sequence:
|
||||
|
||||
1. **`/ckm:brand`** — Extract brand colors, fonts, voice from user's project
|
||||
2. **`/ckm:design-system`** — Get design tokens (spacing, typography scale, color palette)
|
||||
3. **Randomly invoke ONE of:** `/ck:ui-ux-pro-max` OR `/ck:frontend-design` — for layout, hierarchy, visual balance. Pick one at random each run for design variety.
|
||||
1. **`brand`** — Extract brand colors, fonts, voice from user's project
|
||||
2. **`design-system`** — Get design tokens (spacing, typography scale, color palette)
|
||||
3. **`ui-ux-pro-max`** — Layout, hierarchy, visual balance; search a different style, palette, or font pairing per concept for design variety.
|
||||
|
||||
For each approved idea + each target size, create an HTML file:
|
||||
|
||||
@ -119,7 +119,7 @@ output/social-photos/
|
||||
|
||||
### Step 5: Screenshot Export
|
||||
|
||||
Use Chrome headless, `chrome-devtools` skill, or Playwright/Puppeteer to capture exact-size screenshots.
|
||||
Use Chrome headless, Playwright, or Puppeteer to capture exact-size screenshots.
|
||||
|
||||
**IMPORTANT:** Always add a delay (3-5s) after page load for fonts/images to fully render before capture.
|
||||
|
||||
@ -145,9 +145,9 @@ Key flags:
|
||||
- `--hide-scrollbars` — prevents scrollbar artifacts in screenshots
|
||||
- `--window-size=WxH` — sets exact pixel dimensions
|
||||
|
||||
#### Option B: chrome-devtools skill
|
||||
#### Option B: Browser automation provided by the runtime
|
||||
|
||||
Invoke `/chrome-devtools` with instructions to:
|
||||
If the runtime offers a browser-automation or screenshot capability (for example a browser MCP server), use it to:
|
||||
1. Open each HTML file in browser
|
||||
2. Set viewport to exact target dimensions
|
||||
3. Wait 3-5s for fonts/images to fully load
|
||||
@ -210,7 +210,7 @@ async function captureScreenshots(htmlFiles) {
|
||||
|
||||
### Step 6: Verify & Fix Designs
|
||||
|
||||
Use Chrome MCP or `chrome-devtools` skill to visually inspect each exported PNG:
|
||||
Open each exported PNG in an available browser or image viewer and inspect it:
|
||||
|
||||
1. Open exported screenshots and check for layout/styling issues
|
||||
2. Verify: fonts rendered correctly, colors match brand, text readable at thumbnail size
|
||||
@ -227,7 +227,7 @@ Use Chrome MCP or `chrome-devtools` skill to visually inspect each exported PNG:
|
||||
|
||||
### Step 7: Generate Summary Report
|
||||
|
||||
Save report to `plans/reports/` with naming pattern from session hooks.
|
||||
Save the report as `plans/reports/{YYMMDD}-social-photos-{topic}.md`.
|
||||
|
||||
Report structure:
|
||||
|
||||
@ -269,9 +269,9 @@ Report structure:
|
||||
|
||||
### Step 8: Organize Output
|
||||
|
||||
Invoke `assets-organizing` skill to organize all output files and reports:
|
||||
Organize all output files and reports:
|
||||
- Move/copy exported PNGs to proper asset directories
|
||||
- Ensure reports are in `plans/reports/` with correct naming
|
||||
- Ensure reports are in `plans/reports/` under the name from Step 7
|
||||
- Clean up intermediate HTML files if requested
|
||||
- Tag outputs with metadata (platform, size, concept name)
|
||||
|
||||
@ -326,4 +326,4 @@ This sub-skill handles social media image design only. Does NOT handle:
|
||||
- Animation/motion graphics
|
||||
- Print production files (CMYK, bleed)
|
||||
- Direct social media posting/scheduling
|
||||
- AI image generation (use `ai-artist` skill for that)
|
||||
- AI image generation (supply images, or generate them with a separate authorized capability)
|
||||
|
||||
@ -427,7 +427,10 @@ Image Editing Mode:
|
||||
action = check_logo_required(args.brand, skip_prompt=args.no_logo_prompt)
|
||||
if action == 'generate':
|
||||
print("\n💡 To generate a logo, use the logo-design skill:")
|
||||
print(f" python ~/.claude/skills/design/scripts/logo/generate.py --brand \"{args.brand}\" --industry \"{args.industry}\"")
|
||||
# Resolved from this file so the hint is correct from any cwd and in
|
||||
# every install layout (plugin cache, project or --global install).
|
||||
logo_script = Path(__file__).resolve().parents[1] / "logo" / "generate.py"
|
||||
print(f" python \"{logo_script}\" --brand \"{args.brand}\" --industry \"{args.industry}\"")
|
||||
print("\n Then re-run this command with --logo <generated_logo.png>")
|
||||
sys.exit(0)
|
||||
elif action == 'exit':
|
||||
|
||||
@ -1,29 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Logo Generation Script using Gemini Nano Banana API
|
||||
Uses Gemini 2.5 Flash Image and Gemini 3 Pro Image Preview models
|
||||
"""Logo generation with Gemini, Atlas Cloud, or MuAPI.
|
||||
|
||||
Gemini remains the default provider. Atlas Cloud is opt-in with
|
||||
``--provider atlas`` and uses its asynchronous image generation API. MuAPI is
|
||||
opt-in with ``--provider muapi`` and uses its asynchronous image generation API
|
||||
with the selected model's prompt/aspect-ratio contract.
|
||||
|
||||
Models:
|
||||
- Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency
|
||||
- Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning
|
||||
- MuAPI Nano Banana (--provider muapi): nano-banana - hosted asynchronous image generation
|
||||
|
||||
Usage:
|
||||
python generate.py --prompt "tech startup logo minimalist blue"
|
||||
python generate.py --prompt "coffee shop vintage badge" --style vintage --output logo.png
|
||||
python generate.py --brand "TechFlow" --industry tech --style minimalist
|
||||
python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model
|
||||
python generate.py --brand "TechFlow" --provider atlas
|
||||
python generate.py --brand "TechFlow" --provider muapi
|
||||
python generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro
|
||||
|
||||
Batch mode (generates multiple variants):
|
||||
python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
||||
|
||||
|
||||
# Load environment variables
|
||||
def load_env():
|
||||
@ -31,7 +42,7 @@ def load_env():
|
||||
env_paths = [
|
||||
Path(__file__).parent.parent.parent / ".env",
|
||||
Path.home() / ".claude" / "skills" / ".env",
|
||||
Path.home() / ".claude" / ".env"
|
||||
Path.home() / ".claude" / ".env",
|
||||
]
|
||||
|
||||
for env_path in env_paths:
|
||||
@ -39,29 +50,36 @@ def load_env():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value.strip('"\'')
|
||||
os.environ[key] = value.strip("\"'")
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed.")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
ATLASCLOUD_API_KEY = os.environ.get("ATLASCLOUD_API_KEY")
|
||||
MUAPI_API_KEY = os.environ.get("MUAPI_API_KEY")
|
||||
|
||||
# Gemini "Nano Banana" model configurations for image generation
|
||||
GEMINI_FLASH = "gemini-2.5-flash-image" # Nano Banana: fast, high-volume, low-latency
|
||||
GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quality, advanced reasoning
|
||||
|
||||
# Atlas Cloud model validated against the live model catalog and schema.
|
||||
ATLAS_MODEL = "google/nano-banana-2-lite/text-to-image"
|
||||
ATLAS_API_BASE = "https://api.atlascloud.ai/api/v1"
|
||||
MUAPI_MODEL = "nano-banana"
|
||||
MUAPI_MODELS = ("nano-banana", "nano-banana-pro")
|
||||
MUAPI_API_BASE = "https://api.muapi.ai/api/v1"
|
||||
HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (logo generation)"
|
||||
ATLAS_POLL_INTERVAL = 2
|
||||
ATLAS_MAX_POLLS = 90
|
||||
MUAPI_POLL_INTERVAL = 2
|
||||
MUAPI_MAX_POLLS = 90
|
||||
|
||||
# Supported aspect ratios
|
||||
ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
||||
DEFAULT_ASPECT_RATIO = "1:1" # Square is ideal for logos
|
||||
@ -99,7 +117,7 @@ STYLE_MODIFIERS = {
|
||||
"mascot": "mascot, character, friendly face, personified, memorable figure",
|
||||
"gradient": "gradient, color transition, vibrant, modern digital feel, smooth color flow",
|
||||
"lineart": "line art, single stroke, continuous line, elegant simplicity, wire-frame style",
|
||||
"negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion"
|
||||
"negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion",
|
||||
}
|
||||
|
||||
INDUSTRY_PROMPTS = {
|
||||
@ -112,7 +130,7 @@ INDUSTRY_PROMPTS = {
|
||||
"eco": "eco-friendly, sustainable, natural, green, leaf or earth elements",
|
||||
"education": "education, knowledge, growth, learning, book or cap symbol",
|
||||
"real-estate": "real estate, property, home, roof or building silhouette",
|
||||
"creative": "creative agency, artistic, unique, expressive, colorful"
|
||||
"creative": "creative agency, artistic, unique, expressive, colorful",
|
||||
}
|
||||
|
||||
|
||||
@ -133,101 +151,425 @@ def enhance_prompt(base_prompt, style=None, industry=None, brand_name=None):
|
||||
return LOGO_PROMPT_TEMPLATE.format(prompt=combined)
|
||||
|
||||
|
||||
def generate_logo(prompt, style=None, industry=None, brand_name=None,
|
||||
output_path=None, use_pro=False, aspect_ratio=None):
|
||||
"""Generate a logo using Gemini models with image generation
|
||||
class _SafeRedirectHandler(HTTPRedirectHandler):
|
||||
"""Reject redirects to non-public or non-HTTPS destinations."""
|
||||
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
_validate_public_https_url(newurl)
|
||||
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
||||
|
||||
|
||||
def _validate_public_https_url(url):
|
||||
parsed = urlparse(url)
|
||||
if (
|
||||
parsed.scheme != "https"
|
||||
or not parsed.hostname
|
||||
or parsed.username
|
||||
or parsed.password
|
||||
):
|
||||
raise ValueError("Provider returned an invalid media URL")
|
||||
|
||||
hostname = parsed.hostname.lower().rstrip(".")
|
||||
if hostname == "localhost" or hostname.endswith(
|
||||
(".localhost", ".local", ".internal")
|
||||
):
|
||||
raise ValueError("Provider media URL used a local hostname")
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
return
|
||||
else:
|
||||
if not ip.is_global:
|
||||
raise ValueError("Provider media URL used a non-public address")
|
||||
|
||||
|
||||
def _json_request(
|
||||
url, api_key, method="GET", payload=None, api_key_header="Authorization"
|
||||
):
|
||||
if api_key_header == "Authorization":
|
||||
auth_value = f"Bearer {api_key}"
|
||||
elif api_key_header == "x-api-key":
|
||||
auth_value = api_key
|
||||
else:
|
||||
raise ValueError("Unsupported API key header")
|
||||
|
||||
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
||||
request = Request(
|
||||
url,
|
||||
data=body,
|
||||
method=method,
|
||||
headers={
|
||||
api_key_header: auth_value,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": HTTP_USER_AGENT,
|
||||
**({"Content-Type": "application/json"} if body is not None else {}),
|
||||
},
|
||||
)
|
||||
try:
|
||||
with build_opener(_SafeRedirectHandler()).open(request, timeout=60) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(
|
||||
f"Provider request failed ({exc.code}): {detail[:300]}"
|
||||
) from exc
|
||||
except (URLError, TimeoutError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError(f"Provider request failed: {exc}") from exc
|
||||
|
||||
|
||||
def _atlas_prediction_data(response):
|
||||
if not isinstance(response, dict):
|
||||
raise TypeError("Atlas Cloud returned an invalid response")
|
||||
if response.get("code") not in (None, 0, 200):
|
||||
raise RuntimeError(response.get("message") or "Atlas Cloud request failed")
|
||||
data = response.get("data")
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("Atlas Cloud response did not include prediction data")
|
||||
return data
|
||||
|
||||
|
||||
def _download_atlas_image(url, output_path):
|
||||
_download_image(url, output_path, "image provider")
|
||||
|
||||
|
||||
def _download_image(url, output_path, provider_name):
|
||||
_validate_public_https_url(url)
|
||||
request = Request(
|
||||
url,
|
||||
headers={"Accept": "image/*", "User-Agent": HTTP_USER_AGENT},
|
||||
)
|
||||
try:
|
||||
with build_opener(_SafeRedirectHandler()).open(
|
||||
request, timeout=120
|
||||
) as response:
|
||||
content_type = response.headers.get_content_type()
|
||||
if not content_type.startswith("image/"):
|
||||
raise RuntimeError(
|
||||
f"{provider_name} output is not an image ({content_type})"
|
||||
)
|
||||
image_data = response.read()
|
||||
except (HTTPError, URLError, TimeoutError) as exc:
|
||||
raise RuntimeError(f"Unable to download {provider_name} image: {exc}") from exc
|
||||
|
||||
if not image_data:
|
||||
raise RuntimeError(f"{provider_name} returned an empty image")
|
||||
with open(output_path, "wb") as output_file:
|
||||
output_file.write(image_data)
|
||||
|
||||
|
||||
def _generate_with_atlas(prompt, output_path, aspect_ratio, api_key, model):
|
||||
if not api_key:
|
||||
raise RuntimeError("ATLASCLOUD_API_KEY not set")
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
}
|
||||
response = _json_request(
|
||||
f"{ATLAS_API_BASE}/model/generateImage",
|
||||
api_key,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
)
|
||||
data = _atlas_prediction_data(response)
|
||||
prediction_id = data.get("id")
|
||||
if not prediction_id:
|
||||
raise RuntimeError("Atlas Cloud did not return a prediction ID")
|
||||
|
||||
for poll_number in range(ATLAS_MAX_POLLS + 1):
|
||||
status = str(data.get("status", "")).lower()
|
||||
if status == "completed":
|
||||
outputs = data.get("outputs")
|
||||
if (
|
||||
not isinstance(outputs, list)
|
||||
or not outputs
|
||||
or not isinstance(outputs[0], str)
|
||||
):
|
||||
raise RuntimeError("Atlas Cloud completed without an image URL")
|
||||
_download_atlas_image(outputs[0], output_path)
|
||||
return
|
||||
if status in {"failed", "timeout", "canceled", "cancelled"}:
|
||||
raise RuntimeError(data.get("error") or f"Atlas Cloud prediction {status}")
|
||||
if poll_number == ATLAS_MAX_POLLS:
|
||||
break
|
||||
time.sleep(ATLAS_POLL_INTERVAL)
|
||||
data = _atlas_prediction_data(
|
||||
_json_request(
|
||||
f"{ATLAS_API_BASE}/model/prediction/{prediction_id}",
|
||||
api_key,
|
||||
)
|
||||
)
|
||||
|
||||
raise RuntimeError("Atlas Cloud prediction timed out while polling")
|
||||
|
||||
|
||||
def _muapi_response_objects(response):
|
||||
"""Return the response and common MuAPI envelopes without guessing fields."""
|
||||
if not isinstance(response, dict):
|
||||
raise TypeError("MuAPI returned an invalid response")
|
||||
|
||||
objects = [response]
|
||||
for key in ("data", "output", "result"):
|
||||
value = response.get(key)
|
||||
if isinstance(value, dict) and value not in objects:
|
||||
objects.append(value)
|
||||
return objects
|
||||
|
||||
|
||||
def _muapi_response_value(response, keys):
|
||||
for item in _muapi_response_objects(response):
|
||||
for key in keys:
|
||||
value = item.get(key)
|
||||
if value not in (None, ""):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _muapi_error(response):
|
||||
value = _muapi_response_value(response, ("error", "message", "detail"))
|
||||
if isinstance(value, str):
|
||||
return value[:300]
|
||||
return "MuAPI request failed"
|
||||
|
||||
|
||||
def _muapi_result_url(response):
|
||||
"""Return the documented result URL from the creation response."""
|
||||
for item in _muapi_response_objects(response):
|
||||
urls = item.get("urls")
|
||||
if not isinstance(urls, dict) or "get" not in urls:
|
||||
continue
|
||||
|
||||
result_url = urls.get("get")
|
||||
if not isinstance(result_url, str) or not result_url:
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
)
|
||||
try:
|
||||
_validate_public_https_url(result_url)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
) from exc
|
||||
return result_url
|
||||
|
||||
raise RuntimeError(
|
||||
"MuAPI creation response did not include a valid HTTPS result URL"
|
||||
)
|
||||
|
||||
|
||||
def _muapi_output_url(response):
|
||||
for item in _muapi_response_objects(response):
|
||||
outputs = item.get("outputs")
|
||||
if isinstance(outputs, list):
|
||||
for output in outputs:
|
||||
if isinstance(output, str) and output.startswith("https://"):
|
||||
return output
|
||||
if isinstance(output, dict):
|
||||
for key in ("url", "image_url"):
|
||||
value = output.get(key)
|
||||
if isinstance(value, str) and value.startswith("https://"):
|
||||
return value
|
||||
raise RuntimeError("MuAPI completed without an HTTPS image URL")
|
||||
|
||||
|
||||
def _download_muapi_image(url, output_path):
|
||||
_download_image(url, output_path, "MuAPI")
|
||||
|
||||
|
||||
def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model):
|
||||
if not api_key:
|
||||
raise RuntimeError("MUAPI_API_KEY not set")
|
||||
if model not in MUAPI_MODELS:
|
||||
raise RuntimeError(
|
||||
f"Unsupported MuAPI logo model: {model}. "
|
||||
f"Choose one of: {', '.join(MUAPI_MODELS)}"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
}
|
||||
response = _json_request(
|
||||
f"{MUAPI_API_BASE}/{model}",
|
||||
api_key,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
request_id = _muapi_response_value(response, ("request_id", "id"))
|
||||
if not isinstance(request_id, str) or not request_id:
|
||||
raise RuntimeError("MuAPI did not return a request ID")
|
||||
result_url = _muapi_result_url(response)
|
||||
|
||||
data = response
|
||||
for poll_number in range(MUAPI_MAX_POLLS + 1):
|
||||
status = _muapi_response_value(data, ("status",))
|
||||
normalized_status = str(status or "").lower()
|
||||
if normalized_status in {"completed", "succeeded", "success"}:
|
||||
_download_muapi_image(_muapi_output_url(data), output_path)
|
||||
return
|
||||
if normalized_status in {
|
||||
"failed",
|
||||
"error",
|
||||
"timeout",
|
||||
"canceled",
|
||||
"cancelled",
|
||||
}:
|
||||
raise RuntimeError(f"MuAPI generation {normalized_status}: {_muapi_error(data)}")
|
||||
if poll_number == MUAPI_MAX_POLLS:
|
||||
break
|
||||
|
||||
time.sleep(MUAPI_POLL_INTERVAL)
|
||||
data = _json_request(
|
||||
result_url,
|
||||
api_key,
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
|
||||
raise RuntimeError("MuAPI prediction timed out while polling")
|
||||
|
||||
|
||||
def _generate_with_gemini(prompt, output_path, aspect_ratio, use_pro):
|
||||
if not GEMINI_API_KEY:
|
||||
raise RuntimeError("GEMINI_API_KEY not set")
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"google-genai package not installed; run: pip install google-genai"
|
||||
) from exc
|
||||
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
model = GEMINI_PRO if use_pro else GEMINI_FLASH
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
image_config=types.ImageConfig(aspect_ratio=aspect_ratio),
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HARASSMENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
for part in response.candidates[0].content.parts:
|
||||
if (
|
||||
hasattr(part, "inline_data")
|
||||
and part.inline_data
|
||||
and part.inline_data.mime_type.startswith("image/")
|
||||
):
|
||||
with open(output_path, "wb") as output_file:
|
||||
output_file.write(part.inline_data.data)
|
||||
return
|
||||
raise RuntimeError("Gemini did not return an image")
|
||||
|
||||
|
||||
def generate_logo(
|
||||
prompt,
|
||||
style=None,
|
||||
industry=None,
|
||||
brand_name=None,
|
||||
output_path=None,
|
||||
use_pro=False,
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate a logo using Gemini, Atlas Cloud, or MuAPI image generation.
|
||||
|
||||
Args:
|
||||
aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4"
|
||||
Default is "1:1" (square) for logos.
|
||||
"""
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
print("Error: GEMINI_API_KEY not set")
|
||||
print("Set it with: export GEMINI_API_KEY='your-key'")
|
||||
return None
|
||||
|
||||
# Initialize client
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
|
||||
# Enhance the prompt
|
||||
full_prompt = enhance_prompt(prompt, style, industry, brand_name)
|
||||
|
||||
# Select model
|
||||
model = GEMINI_PRO if use_pro else GEMINI_FLASH
|
||||
model_label = "Nano Banana Pro (gemini-3-pro-image-preview)" if use_pro else "Nano Banana (gemini-2.5-flash-image)"
|
||||
|
||||
# Set aspect ratio (default to 1:1 for logos)
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # noqa: DTZ005
|
||||
brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo"
|
||||
output_path = f"{brand_slug}_{timestamp}.png"
|
||||
|
||||
if provider == "atlas":
|
||||
model_label = f"Atlas Cloud ({atlas_model})"
|
||||
elif provider == "muapi":
|
||||
model_label = f"MuAPI ({muapi_model})"
|
||||
else:
|
||||
model_label = (
|
||||
"Nano Banana Pro (gemini-3-pro-image-preview)"
|
||||
if use_pro
|
||||
else "Nano Banana (gemini-2.5-flash-image)"
|
||||
)
|
||||
|
||||
print(f"Generating logo with {model_label}...")
|
||||
print(f"Aspect ratio: {ratio}")
|
||||
print(f"Prompt: {full_prompt[:150]}...")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Generate image using Gemini with image generation capability
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
image_config=types.ImageConfig(
|
||||
aspect_ratio=ratio
|
||||
),
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HARASSMENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
]
|
||||
if provider == "atlas":
|
||||
_generate_with_atlas(
|
||||
full_prompt,
|
||||
output_path,
|
||||
ratio,
|
||||
ATLASCLOUD_API_KEY,
|
||||
atlas_model,
|
||||
)
|
||||
)
|
||||
|
||||
# Extract image from response
|
||||
image_data = None
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'inline_data') and part.inline_data:
|
||||
if part.inline_data.mime_type.startswith('image/'):
|
||||
image_data = part.inline_data.data
|
||||
break
|
||||
|
||||
if not image_data:
|
||||
print("No image generated. The model may not have produced an image.")
|
||||
print("Try a different prompt or check if the model supports image generation.")
|
||||
return None
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo"
|
||||
output_path = f"{brand_slug}_{timestamp}.png"
|
||||
|
||||
# Save image
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
elif provider == "muapi":
|
||||
_generate_with_muapi(
|
||||
full_prompt,
|
||||
output_path,
|
||||
ratio,
|
||||
MUAPI_API_KEY,
|
||||
muapi_model,
|
||||
)
|
||||
else:
|
||||
_generate_with_gemini(full_prompt, output_path, ratio, use_pro)
|
||||
|
||||
print(f"Logo saved to: {output_path}")
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating logo: {e}")
|
||||
except Exception as exc: # noqa: BLE001 - provider SDK errors are not standardized
|
||||
print(f"Error generating logo: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_context=None, aspect_ratio=None):
|
||||
def generate_batch(
|
||||
prompt,
|
||||
brand_name,
|
||||
count,
|
||||
output_dir,
|
||||
use_pro=False,
|
||||
brand_context=None,
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate multiple logo variants with different styles"""
|
||||
|
||||
# Select appropriate styles for batch generation
|
||||
@ -247,16 +589,22 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
results = []
|
||||
model_label = "Pro" if use_pro else "Flash"
|
||||
model_label = (
|
||||
f"Atlas Cloud ({atlas_model})"
|
||||
if provider == "atlas"
|
||||
else f"MuAPI ({muapi_model})"
|
||||
if provider == "muapi"
|
||||
else f"Nano Banana {'Pro' if use_pro else 'Flash'}"
|
||||
)
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" BATCH LOGO GENERATION: {brand_name}")
|
||||
print(f" Model: Nano Banana {model_label}")
|
||||
print(f" Model: {model_label}")
|
||||
print(f" Aspect Ratio: {ratio}")
|
||||
print(f" Variants: {count}")
|
||||
print(f" Output: {output_dir}")
|
||||
print(f"{'='*60}\n")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
for i in range(min(count, len(batch_styles))):
|
||||
style_key, style_desc = batch_styles[i]
|
||||
@ -267,10 +615,10 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
enhanced_prompt = f"{brand_context}, {enhanced_prompt}"
|
||||
|
||||
# Generate filename
|
||||
filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i+1:02d}.png"
|
||||
filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i + 1:02d}.png"
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
|
||||
print(f"[{i+1}/{count}] Generating {style_key} variant...")
|
||||
print(f"[{i + 1}/{count}] Generating {style_key} variant...")
|
||||
|
||||
result = generate_logo(
|
||||
prompt=enhanced_prompt,
|
||||
@ -279,7 +627,10 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
brand_name=brand_name,
|
||||
output_path=output_path,
|
||||
use_pro=use_pro,
|
||||
aspect_ratio=aspect_ratio
|
||||
aspect_ratio=aspect_ratio,
|
||||
provider=provider,
|
||||
atlas_model=atlas_model,
|
||||
muapi_model=muapi_model,
|
||||
)
|
||||
|
||||
if result:
|
||||
@ -292,31 +643,79 @@ def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_c
|
||||
if i < count - 1:
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" BATCH COMPLETE: {len(results)}/{count} logos generated")
|
||||
print(f"{'='*60}\n")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate logos using Gemini Nano Banana models")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate logos using Gemini, Atlas Cloud, or MuAPI"
|
||||
)
|
||||
parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt")
|
||||
parser.add_argument("--brand", "-b", type=str, help="Brand name")
|
||||
parser.add_argument("--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style")
|
||||
parser.add_argument("--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type")
|
||||
parser.add_argument(
|
||||
"--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type"
|
||||
)
|
||||
parser.add_argument("--output", "-o", type=str, help="Output file path")
|
||||
parser.add_argument("--output-dir", type=str, help="Output directory for batch generation")
|
||||
parser.add_argument("--batch", type=int, help="Number of logo variants to generate (batch mode)")
|
||||
parser.add_argument("--brand-context", type=str, help="Additional brand context for prompts")
|
||||
parser.add_argument("--pro", action="store_true", help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality")
|
||||
parser.add_argument("--aspect-ratio", "-r", choices=ASPECT_RATIOS, default=DEFAULT_ASPECT_RATIO,
|
||||
help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)")
|
||||
parser.add_argument("--list-styles", action="store_true", help="List available styles")
|
||||
parser.add_argument("--list-industries", action="store_true", help="List available industries")
|
||||
parser.add_argument(
|
||||
"--output-dir", type=str, help="Output directory for batch generation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch", type=int, help="Number of logo variants to generate (batch mode)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brand-context", type=str, help="Additional brand context for prompts"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pro",
|
||||
action="store_true",
|
||||
help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provider",
|
||||
choices=["gemini", "atlas", "muapi"],
|
||||
default="gemini",
|
||||
help="Image provider (default: gemini)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--atlas-model",
|
||||
default=ATLAS_MODEL,
|
||||
help=f"Atlas Cloud image model (default: {ATLAS_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--muapi-model",
|
||||
choices=MUAPI_MODELS,
|
||||
default=MUAPI_MODEL,
|
||||
help=f"MuAPI image model (default: {MUAPI_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--aspect-ratio",
|
||||
"-r",
|
||||
choices=ASPECT_RATIOS,
|
||||
default=DEFAULT_ASPECT_RATIO,
|
||||
help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-styles", action="store_true", help="List available styles"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-industries", action="store_true", help="List available industries"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.provider != "gemini" and args.pro:
|
||||
parser.error(
|
||||
"--pro is only available with --provider gemini; "
|
||||
"use --muapi-model nano-banana-pro for MuAPI"
|
||||
)
|
||||
|
||||
if args.list_styles:
|
||||
print("Available styles:")
|
||||
for style, desc in STYLE_MODIFIERS.items():
|
||||
@ -336,7 +735,9 @@ def main():
|
||||
|
||||
# Batch mode
|
||||
if args.batch:
|
||||
output_dir = args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos"
|
||||
output_dir = (
|
||||
args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos"
|
||||
)
|
||||
generate_batch(
|
||||
prompt=prompt,
|
||||
brand_name=args.brand or "Logo",
|
||||
@ -344,7 +745,10 @@ def main():
|
||||
output_dir=output_dir,
|
||||
use_pro=args.pro,
|
||||
brand_context=args.brand_context,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
else:
|
||||
generate_logo(
|
||||
@ -354,7 +758,10 @@ def main():
|
||||
brand_name=args.brand,
|
||||
output_path=args.output,
|
||||
use_pro=args.pro,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
|
||||
|
||||
|
||||
288
cli/assets/skills/design/scripts/logo/tests/test_generate.py
Normal file
288
cli/assets/skills/design/scripts/logo/tests/test_generate.py
Normal file
@ -0,0 +1,288 @@
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import call, patch
|
||||
|
||||
MODULE_PATH = Path(__file__).parents[1] / "generate.py"
|
||||
SPEC = importlib.util.spec_from_file_location("logo_generate", MODULE_PATH)
|
||||
logo_generate = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(logo_generate)
|
||||
|
||||
|
||||
class AtlasGenerationTests(unittest.TestCase):
|
||||
@patch.object(logo_generate, "_download_atlas_image")
|
||||
@patch.object(logo_generate.time, "sleep")
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_atlas_submits_once_and_polls_until_completed(
|
||||
self, json_request, sleep, download
|
||||
):
|
||||
json_request.side_effect = [
|
||||
{"code": 200, "data": {"id": "pred-123", "status": "created"}},
|
||||
{"code": 200, "data": {"id": "pred-123", "status": "processing"}},
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"id": "pred-123",
|
||||
"status": "completed",
|
||||
"outputs": ["https://media.example.com/logo.png"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", "atlas-key", "atlas/model"
|
||||
)
|
||||
|
||||
self.assertEqual(json_request.call_count, 3)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[0],
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/generateImage",
|
||||
"atlas-key",
|
||||
method="POST",
|
||||
payload={
|
||||
"model": "atlas/model",
|
||||
"prompt": "logo prompt",
|
||||
"aspect_ratio": "1:1",
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[1:],
|
||||
[
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/prediction/pred-123",
|
||||
"atlas-key",
|
||||
),
|
||||
call(
|
||||
f"{logo_generate.ATLAS_API_BASE}/model/prediction/pred-123",
|
||||
"atlas-key",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(sleep.call_count, 2)
|
||||
download.assert_called_once_with(
|
||||
"https://media.example.com/logo.png", "logo.png"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_atlas_does_not_retry_generation_post(self, json_request):
|
||||
json_request.side_effect = RuntimeError("network error")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "network error"):
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", "atlas-key", "atlas/model"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
@patch.object(logo_generate, "_validate_public_https_url")
|
||||
@patch.object(logo_generate, "build_opener")
|
||||
def test_media_download_never_forwards_api_key(self, build_opener, validate):
|
||||
class Headers:
|
||||
@staticmethod
|
||||
def get_content_type():
|
||||
return "image/png"
|
||||
|
||||
class Response:
|
||||
headers = Headers()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
return b"png-bytes"
|
||||
|
||||
build_opener.return_value.open.return_value = Response()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir) / "logo.png"
|
||||
logo_generate._download_atlas_image(
|
||||
"https://media.example.com/logo.png", output
|
||||
)
|
||||
self.assertEqual(output.read_bytes(), b"png-bytes")
|
||||
|
||||
request = build_opener.return_value.open.call_args.args[0]
|
||||
headers = {key.lower(): value for key, value in request.header_items()}
|
||||
self.assertNotIn("authorization", headers)
|
||||
self.assertEqual(headers["accept"], "image/*")
|
||||
self.assertEqual(headers["user-agent"], logo_generate.HTTP_USER_AGENT)
|
||||
validate.assert_called_once_with("https://media.example.com/logo.png")
|
||||
|
||||
def test_atlas_requires_api_key(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "ATLASCLOUD_API_KEY not set"):
|
||||
logo_generate._generate_with_atlas(
|
||||
"logo prompt", "logo.png", "1:1", None, "atlas/model"
|
||||
)
|
||||
|
||||
def test_media_url_rejects_private_addresses(self):
|
||||
with self.assertRaisesRegex(ValueError, "non-public address"):
|
||||
logo_generate._validate_public_https_url("https://127.0.0.1/logo.png")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "local hostname"):
|
||||
logo_generate._validate_public_https_url("https://assets.local/logo.png")
|
||||
|
||||
|
||||
class MuapiGenerationTests(unittest.TestCase):
|
||||
@patch.object(logo_generate, "_download_muapi_image")
|
||||
@patch.object(logo_generate.time, "sleep")
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_submits_once_and_polls_until_completed(
|
||||
self, json_request, sleep, download
|
||||
):
|
||||
json_request.side_effect = [
|
||||
{
|
||||
"id": "req-123",
|
||||
"status": "created",
|
||||
"output": {
|
||||
"urls": {
|
||||
"get": "https://api.muapi.ai/api/v1/results/req-123"
|
||||
}
|
||||
},
|
||||
},
|
||||
{"id": "req-123", "status": "processing"},
|
||||
{
|
||||
"id": "req-123",
|
||||
"status": "completed",
|
||||
"output": {"outputs": ["https://media.example.com/logo.png"]},
|
||||
},
|
||||
]
|
||||
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
self.assertEqual(json_request.call_count, 3)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[0],
|
||||
call(
|
||||
f"{logo_generate.MUAPI_API_BASE}/nano-banana",
|
||||
"muapi-key",
|
||||
method="POST",
|
||||
payload={"prompt": "logo prompt", "aspect_ratio": "1:1"},
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
json_request.call_args_list[1:],
|
||||
[
|
||||
call(
|
||||
"https://api.muapi.ai/api/v1/results/req-123",
|
||||
"muapi-key",
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
call(
|
||||
"https://api.muapi.ai/api/v1/results/req-123",
|
||||
"muapi-key",
|
||||
api_key_header="x-api-key",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(sleep.call_count, 2)
|
||||
download.assert_called_once_with(
|
||||
"https://media.example.com/logo.png", "logo.png"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_does_not_retry_generation_post(self, json_request):
|
||||
json_request.side_effect = RuntimeError("network error")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "network error"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
def test_muapi_requires_key_and_known_model(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "MUAPI_API_KEY not set"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", None, "nano-banana"
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "Unsupported MuAPI logo model"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "unknown-model"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "build_opener")
|
||||
def test_muapi_uses_x_api_key_header(self, build_opener):
|
||||
class Response:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
return b"{}"
|
||||
|
||||
build_opener.return_value.open.return_value = Response()
|
||||
|
||||
logo_generate._json_request(
|
||||
"https://api.muapi.ai/api/v1/nano-banana",
|
||||
"muapi-key",
|
||||
method="POST",
|
||||
payload={"prompt": "logo"},
|
||||
api_key_header="x-api-key",
|
||||
)
|
||||
|
||||
request = build_opener.return_value.open.call_args.args[0]
|
||||
headers = {key.lower(): value for key, value in request.header_items()}
|
||||
self.assertEqual(headers["x-api-key"], "muapi-key")
|
||||
self.assertNotIn("authorization", headers)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_reports_failed_prediction(self, json_request):
|
||||
json_request.side_effect = [
|
||||
{
|
||||
"request_id": "req-123",
|
||||
"output": {
|
||||
"urls": {
|
||||
"get": "https://api.muapi.ai/api/v1/results/req-123"
|
||||
}
|
||||
},
|
||||
},
|
||||
{"status": "failed", "error": "invalid prompt"},
|
||||
]
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "invalid prompt"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_requires_creation_result_url(self, json_request):
|
||||
json_request.return_value = {"request_id": "req-123", "status": "created"}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
@patch.object(logo_generate, "_json_request")
|
||||
def test_muapi_rejects_invalid_creation_result_url(self, json_request):
|
||||
json_request.return_value = {
|
||||
"request_id": "req-123",
|
||||
"status": "created",
|
||||
"output": {"urls": {"get": "http://api.muapi.ai/results/req-123"}},
|
||||
}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"):
|
||||
logo_generate._generate_with_muapi(
|
||||
"logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana"
|
||||
)
|
||||
|
||||
json_request.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -24,6 +24,10 @@ Strategic HTML presentation design with data visualization.
|
||||
|------------|-------------|-----------|
|
||||
| `create` | Create strategic presentation slides | `references/create.md` |
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## References (Knowledge Base)
|
||||
|
||||
| Topic | File |
|
||||
|
||||
@ -66,10 +66,10 @@
|
||||
|
||||
```bash
|
||||
# Find formula for slide type
|
||||
python .claude/skills/design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
python ../design-system/scripts/search-slides.py "problem agitation" -d copy
|
||||
|
||||
# Get emotion-appropriate formula
|
||||
python .claude/skills/design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
python ../design-system/scripts/search-slides.py "urgency cta" -d copy
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@ -113,10 +113,10 @@
|
||||
|
||||
```bash
|
||||
# Find layout for specific use
|
||||
python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
python ../design-system/scripts/search-slides.py "metrics dashboard" -d layout
|
||||
|
||||
# Contextual recommendation
|
||||
python .claude/skills/design-system/scripts/search-slides.py "traction slide" \
|
||||
python ../design-system/scripts/search-slides.py "traction slide" \
|
||||
--context --position 4 --total 10
|
||||
```
|
||||
|
||||
|
||||
@ -76,10 +76,10 @@ Pattern breaks at 1/3 and 2/3 positions create engagement peaks.
|
||||
|
||||
```bash
|
||||
# Find strategy by goal
|
||||
python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
python ../design-system/scripts/search-slides.py "investor pitch" -d strategy
|
||||
|
||||
# Get emotion arc
|
||||
python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
python ../design-system/scripts/search-slides.py "series a funding" -d strategy --json
|
||||
```
|
||||
|
||||
## Matching Strategy to Context
|
||||
|
||||
@ -53,6 +53,10 @@ Use when:
|
||||
- Minimal text, maximum visual impact
|
||||
- Systematic patterns and refined aesthetics
|
||||
|
||||
## Script Paths
|
||||
|
||||
Script paths in this skill and its `references/` are relative to the directory that contains this SKILL.md, not to the project: `scripts/<file>` is this skill's own `scripts/` folder, and `../<skill>/scripts/<file>` is a sibling sub-skill installed alongside it. Build the full path from that directory (Claude Code reports it as the skill's base directory when the skill loads) and keep the working directory at the project root — the scripts read and write project files such as `docs/brand-guidelines.md`, `assets/design-tokens.json` or `src/` relative to it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Component + Styling Setup
|
||||
|
||||
@ -15,14 +15,14 @@
|
||||
"dev": "bun run src/index.ts",
|
||||
"sync:assets": "node scripts/sync-assets.mjs",
|
||||
"check:assets": "node scripts/sync-assets.mjs --check",
|
||||
"validate:csv": "cd .. && python3 scripts/validate-csv.py",
|
||||
"validate:semantic": "cd .. && python3 src/ui-ux-pro-max/scripts/validate_data.py",
|
||||
"validate:agent-guide": "cd .. && python3 scripts/validate-agent-guide.py",
|
||||
"validate:catalog-summary": "cd .. && python3 scripts/generate-catalog-summary.py --check",
|
||||
"test:python": "cd .. && python3 -m unittest discover -s src/ui-ux-pro-max/scripts/tests -p 'test_*.py'",
|
||||
"evaluate:relevance": "cd .. && python3 scripts/evaluate-relevance.py",
|
||||
"evaluate:relevance:calibration": "cd .. && python3 scripts/evaluate-relevance.py --split calibration",
|
||||
"evaluate:relevance:held-out": "cd .. && python3 scripts/evaluate-relevance.py --split held_out",
|
||||
"validate:csv": "cd .. && node cli/scripts/run-python.mjs scripts/validate-csv.py",
|
||||
"validate:semantic": "cd .. && node cli/scripts/run-python.mjs src/ui-ux-pro-max/scripts/validate_data.py",
|
||||
"validate:agent-guide": "cd .. && node cli/scripts/run-python.mjs scripts/validate-agent-guide.py",
|
||||
"validate:catalog-summary": "cd .. && node cli/scripts/run-python.mjs scripts/generate-catalog-summary.py --check",
|
||||
"test:python": "cd .. && node cli/scripts/run-python.mjs -m unittest discover -s src/ui-ux-pro-max/scripts/tests -p 'test_*.py'",
|
||||
"evaluate:relevance": "cd .. && node cli/scripts/run-python.mjs scripts/evaluate-relevance.py",
|
||||
"evaluate:relevance:calibration": "cd .. && node cli/scripts/run-python.mjs scripts/evaluate-relevance.py --split calibration",
|
||||
"evaluate:relevance:held-out": "cd .. && node cli/scripts/run-python.mjs scripts/evaluate-relevance.py --split held_out",
|
||||
"smoke:domains": "cd .. && bash scripts/smoke-domains.sh",
|
||||
"smoke:stacks": "cd .. && bash scripts/smoke-stacks.sh",
|
||||
"verify:data": "npm run validate:csv && npm run validate:semantic && npm run validate:agent-guide && npm run validate:catalog-summary && npm run test:python && npm run evaluate:relevance && npm run smoke:domains && npm run smoke:stacks && npm run check:assets",
|
||||
|
||||
18
cli/scripts/run-python.mjs
Normal file
18
cli/scripts/run-python.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
|
||||
const cmd = platform() === 'win32' ? 'python' : 'python3';
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const result = spawnSync(cmd, args, {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Failed to start ${cmd}: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(result.status ?? 0);
|
||||
@ -7,7 +7,12 @@ import prompts from 'prompts';
|
||||
import type { AIType } from '../types/index.js';
|
||||
import { AI_TYPES } from '../types/index.js';
|
||||
import { copyFolders, installFromZip, createTempDir, cleanup } from '../utils/extract.js';
|
||||
import { generatePlatformFiles, generateAllPlatformFiles } from '../utils/template.js';
|
||||
import {
|
||||
generatePlatformFiles,
|
||||
generateAllPlatformFiles,
|
||||
planPlatformInstallActions,
|
||||
planAllPlatformInstallActions,
|
||||
} from '../utils/template.js';
|
||||
import { detectAIType, getAITypeDescription } from '../utils/detect.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import {
|
||||
@ -34,6 +39,7 @@ interface InitOptions {
|
||||
offline?: boolean;
|
||||
legacy?: boolean; // Use old ZIP-based install
|
||||
global?: boolean; // Install to home directory (global mode)
|
||||
dryRun?: boolean; // Preview install actions without writing files
|
||||
token?: string; // GitHub PAT for higher API rate limits
|
||||
}
|
||||
|
||||
@ -160,6 +166,29 @@ export async function initCommand(options: InitOptions): Promise<void> {
|
||||
const modeLabel = isGlobal ? ' (global)' : '';
|
||||
logger.info(`Installing for: ${chalk.cyan(getAITypeDescription(aiType))}${modeLabel}`);
|
||||
|
||||
// Dry run: print what the install would do, write nothing, exit 0
|
||||
if (options.dryRun) {
|
||||
const cwd = process.cwd();
|
||||
console.log();
|
||||
logger.info('Planned install actions (nothing will be written):');
|
||||
|
||||
if (aiType === 'all') {
|
||||
const planned = await planAllPlatformInstallActions(cwd, isGlobal, options.force);
|
||||
planned.forEach((actions, type) => {
|
||||
console.log();
|
||||
console.log(chalk.bold(getAITypeDescription(type as AIType)));
|
||||
actions.forEach(action => console.log(` ${chalk.cyan('·')} ${action}`));
|
||||
});
|
||||
} else {
|
||||
const actions = await planPlatformInstallActions(cwd, aiType, isGlobal, options.force);
|
||||
actions.forEach(action => console.log(` ${chalk.cyan('·')} ${action}`));
|
||||
}
|
||||
|
||||
console.log();
|
||||
logger.success('Dry run complete — no files were written.');
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora('Installing files...').start();
|
||||
const cwd = process.cwd();
|
||||
let copiedFolders: string[] = [];
|
||||
|
||||
@ -30,6 +30,7 @@ program
|
||||
.option('-o, --offline', 'Compatibility flag; template installs use bundled assets')
|
||||
.option('-g, --global', 'Install globally to home directory (~/) instead of current project')
|
||||
.option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits')
|
||||
.option('--dry-run', 'Preview install actions without writing files')
|
||||
.action(async (options) => {
|
||||
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
||||
console.error(`Invalid AI type: ${options.ai}`);
|
||||
@ -41,6 +42,7 @@ program
|
||||
force: options.force,
|
||||
offline: options.offline,
|
||||
global: options.global,
|
||||
dryRun: options.dryRun,
|
||||
token: options.token,
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ interface DetectionResult {
|
||||
export function detectAIType(cwd: string = process.cwd()): DetectionResult {
|
||||
const detected: ConcreteAIType[] = [];
|
||||
|
||||
if (existsSync(join(cwd, '.claude'))) {
|
||||
if (existsSync(join(cwd, '.claude')) || existsSync(join(cwd, '.claude-plugin'))) {
|
||||
detected.push('claude');
|
||||
}
|
||||
if (existsSync(join(cwd, '.cursor'))) {
|
||||
|
||||
@ -232,6 +232,52 @@ async function copySubSkills(skillsParentDir: string, force: boolean): Promise<v
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every path an install touches for one platform. Shared by the real
|
||||
* install (generatePlatformFiles) and the read-only preview
|
||||
* (planPlatformInstallActions) so the two can never disagree.
|
||||
*/
|
||||
function resolveInstallPaths(
|
||||
config: PlatformConfig,
|
||||
targetDir: string,
|
||||
isGlobal: boolean
|
||||
): {
|
||||
skillDir: string;
|
||||
skillFilePath: string;
|
||||
dataDir: string;
|
||||
skillsParentDir: string;
|
||||
} {
|
||||
// For global install, target the user's home directory
|
||||
const effectiveDir = isGlobal ? homedir() : targetDir;
|
||||
|
||||
// Determine full skill directory path
|
||||
const skillDir = join(
|
||||
effectiveDir,
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.skillPath
|
||||
);
|
||||
const skillFilePath = join(skillDir, config.folderStructure.filename);
|
||||
|
||||
// Copy data and scripts into the data directory (may differ from skill file location)
|
||||
const dataDir = config.folderStructure.dataPath
|
||||
? join(effectiveDir, config.folderStructure.root, config.folderStructure.dataPath)
|
||||
: skillDir;
|
||||
|
||||
// The skills parent is the orchestrator's parent dir (skills/ for most
|
||||
// platforms, prompts/ for copilot, steering/ for kiro) — derived, not
|
||||
// hardcoded. For platforms with a separate dataPath (copilot), the
|
||||
// orchestrator's data dir is the anchor.
|
||||
const skillsParentDir = join(
|
||||
effectiveDir,
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.dataPath
|
||||
? dirname(config.folderStructure.dataPath)
|
||||
: dirname(config.folderStructure.skillPath)
|
||||
);
|
||||
|
||||
return { skillDir, skillFilePath, dataDir, skillsParentDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate platform files for a specific AI type
|
||||
* All platforms use self-contained installation with data and scripts
|
||||
@ -245,15 +291,10 @@ export async function generatePlatformFiles(
|
||||
): Promise<string[]> {
|
||||
const config = await loadPlatformConfig(aiType);
|
||||
const createdFolders: string[] = [];
|
||||
|
||||
// For global install, target the user's home directory
|
||||
const effectiveDir = isGlobal ? homedir() : targetDir;
|
||||
|
||||
// Determine full skill directory path
|
||||
const skillDir = join(
|
||||
effectiveDir,
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.skillPath
|
||||
const { skillDir, skillFilePath, dataDir, skillsParentDir } = resolveInstallPaths(
|
||||
config,
|
||||
targetDir,
|
||||
isGlobal
|
||||
);
|
||||
|
||||
// Create directory structure
|
||||
@ -261,7 +302,6 @@ export async function generatePlatformFiles(
|
||||
|
||||
// Render and write skill file (pass isGlobal to adjust paths)
|
||||
const skillContent = await renderSkillFile(config, isGlobal);
|
||||
const skillFilePath = join(skillDir, config.folderStructure.filename);
|
||||
|
||||
const fileAlreadyExists = await exists(skillFilePath);
|
||||
if (fileAlreadyExists && !force) {
|
||||
@ -272,30 +312,80 @@ export async function generatePlatformFiles(
|
||||
await writeFile(skillFilePath, skillContent, 'utf-8');
|
||||
createdFolders.push(config.folderStructure.root);
|
||||
|
||||
// Copy data and scripts into the data directory (may differ from skill file location)
|
||||
const dataDir = config.folderStructure.dataPath
|
||||
? join(effectiveDir, config.folderStructure.root, config.folderStructure.dataPath)
|
||||
: skillDir;
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
await copyDataAndScripts(dataDir);
|
||||
|
||||
// Install the sibling sub-skills (banner-design, brand, design, ...) next to
|
||||
// the orchestrator so all 7 skills are delivered. The skills parent is the
|
||||
// orchestrator's parent dir (skills/ for most platforms, prompts/ for
|
||||
// copilot, steering/ for kiro) — derived, not hardcoded. For platforms with
|
||||
// a separate dataPath (copilot), the orchestrator's data dir is the anchor.
|
||||
const skillsParentDir = join(
|
||||
effectiveDir,
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.dataPath
|
||||
? dirname(config.folderStructure.dataPath)
|
||||
: dirname(config.folderStructure.skillPath)
|
||||
);
|
||||
// the orchestrator so all 7 skills are delivered.
|
||||
await copySubSkills(skillsParentDir, force);
|
||||
|
||||
return createdFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the actions generatePlatformFiles would take for one AI type,
|
||||
* without writing anything. Used by `uipro init --dry-run`.
|
||||
*/
|
||||
export async function planPlatformInstallActions(
|
||||
targetDir: string,
|
||||
aiType: string,
|
||||
isGlobal = false,
|
||||
force = false
|
||||
): Promise<string[]> {
|
||||
const config = await loadPlatformConfig(aiType);
|
||||
const { skillFilePath, dataDir, skillsParentDir } = resolveInstallPaths(config, targetDir, isGlobal);
|
||||
|
||||
const actions: string[] = [];
|
||||
const skillFileExists = await exists(skillFilePath);
|
||||
actions.push(
|
||||
skillFileExists && !force
|
||||
? `Would skip (exists, use --force): ${skillFilePath}`
|
||||
: `Would write: ${skillFilePath}`
|
||||
);
|
||||
actions.push(`Would copy data + scripts: ${dataDir}`);
|
||||
|
||||
const subSkills = await listBundledSubSkills();
|
||||
if (subSkills.length > 0) {
|
||||
actions.push(
|
||||
`Would copy ${subSkills.length} sub-skills (${subSkills.join(', ')}): ${skillsParentDir}`
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the actions generateAllPlatformFiles would take, grouped per unique
|
||||
* platform layout, without writing anything. Used by `uipro init --dry-run`.
|
||||
*/
|
||||
export async function planAllPlatformInstallActions(
|
||||
targetDir: string,
|
||||
isGlobal = false,
|
||||
force = false
|
||||
): Promise<Map<string, string[]>> {
|
||||
const planned = new Map<string, string[]>();
|
||||
const generatedSkillFiles = new Set<string>();
|
||||
|
||||
for (const aiType of Object.keys(AI_TO_PLATFORM)) {
|
||||
try {
|
||||
const config = await loadPlatformConfig(aiType);
|
||||
const skillFile = join(
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.skillPath,
|
||||
config.folderStructure.filename
|
||||
);
|
||||
if (generatedSkillFiles.has(skillFile)) continue;
|
||||
generatedSkillFiles.add(skillFile);
|
||||
|
||||
planned.set(aiType, await planPlatformInstallActions(targetDir, aiType, isGlobal, force));
|
||||
} catch {
|
||||
// Skip if config doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
return planned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files for all AI types
|
||||
*/
|
||||
|
||||
66
cli/tests/e2e/banner-design-path-contract.spec.ts
Normal file
66
cli/tests/e2e/banner-design-path-contract.spec.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { generatePlatformFiles } from '../../src/utils/template.js';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||
const skillFiles = [
|
||||
'.claude/skills/banner-design/SKILL.md',
|
||||
'cli/assets/skills/banner-design/SKILL.md',
|
||||
];
|
||||
const unavailableDependencies = [
|
||||
'frontend-design',
|
||||
'ai-artist',
|
||||
'ai-multimodal',
|
||||
'chrome-devtools',
|
||||
'assets-organizing',
|
||||
'docs/brand-guidelines.md',
|
||||
'scripts/search.py',
|
||||
'inject-brand-context.cjs',
|
||||
'gemini_batch_process.py',
|
||||
'screenshot.js',
|
||||
'nano-banana-pro-examples.md',
|
||||
];
|
||||
|
||||
function extractLocalReferences(content: string): string[] {
|
||||
return [...content.matchAll(/`((?:references|scripts)\/[\w./-]+)`/g)].map(match => match[1]);
|
||||
}
|
||||
|
||||
async function expectSelfContained(skillFile: string): Promise<void> {
|
||||
const content = await readFile(skillFile, 'utf8');
|
||||
|
||||
for (const dependency of unavailableDependencies) {
|
||||
expect(content, dependency).not.toContain(dependency);
|
||||
}
|
||||
|
||||
const references = extractLocalReferences(content);
|
||||
expect(references).toContain('references/banner-sizes-and-styles.md');
|
||||
for (const reference of references) {
|
||||
await access(join(dirname(skillFile), reference));
|
||||
}
|
||||
}
|
||||
|
||||
for (const relativeSkillFile of skillFiles) {
|
||||
test(`${relativeSkillFile} is self-contained`, async () => {
|
||||
await expectSelfContained(join(repoRoot, relativeSkillFile));
|
||||
});
|
||||
}
|
||||
|
||||
test('Claude CLI installation preserves the banner path contract', async () => {
|
||||
const targetDir = await mkdtemp(join(tmpdir(), 'uipro-banner-'));
|
||||
try {
|
||||
await generatePlatformFiles(targetDir, 'claude');
|
||||
await expectSelfContained(join(targetDir, '.claude/skills/banner-design/SKILL.md'));
|
||||
} finally {
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('the bundled banner skill matches the plugin source', async () => {
|
||||
const [source, bundled] = await Promise.all(
|
||||
skillFiles.map(skillFile => readFile(join(repoRoot, skillFile), 'utf8')),
|
||||
);
|
||||
expect(bundled).toBe(source);
|
||||
});
|
||||
40
cli/tests/e2e/dry-run.spec.ts
Normal file
40
cli/tests/e2e/dry-run.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { mkdtemp, readdir } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
planAllPlatformInstallActions,
|
||||
planPlatformInstallActions,
|
||||
} from '../../src/utils/template.js';
|
||||
|
||||
test('dry-run plan lists the install actions for one platform', async () => {
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'uipro-dry-run-'));
|
||||
|
||||
const actions = await planPlatformInstallActions(scratch, 'claude');
|
||||
|
||||
const joined = actions.join('\n');
|
||||
expect(joined).toContain(
|
||||
join(scratch, '.claude', 'skills', 'ui-ux-pro-max', 'SKILL.md')
|
||||
);
|
||||
expect(joined).toContain('Would copy data + scripts:');
|
||||
expect(joined).toContain('Would copy 6 sub-skills (');
|
||||
});
|
||||
|
||||
test('dry-run plan writes nothing to the target directory', async () => {
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'uipro-dry-run-write-'));
|
||||
const before = await readdir(scratch);
|
||||
|
||||
await planPlatformInstallActions(scratch, 'claude');
|
||||
await planAllPlatformInstallActions(scratch);
|
||||
|
||||
expect(await readdir(scratch)).toEqual(before);
|
||||
});
|
||||
|
||||
test('dry-run plan for all platforms covers every unique layout', async () => {
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'uipro-dry-run-all-'));
|
||||
|
||||
const planned = await planAllPlatformInstallActions(scratch);
|
||||
|
||||
expect(planned.size).toBeGreaterThan(1);
|
||||
expect(planned.get('claude')!.length).toBeGreaterThan(0);
|
||||
});
|
||||
@ -6,6 +6,7 @@ const cases = [
|
||||
['codex', '.agents/skills/ui-ux-pro-max/scripts/search.py'],
|
||||
['copilot', '.github/prompts/ui-ux-pro-max/scripts/search.py'],
|
||||
['kiro', '.kiro/steering/ui-ux-pro-max/scripts/search.py'],
|
||||
['droid', '.factory/skills/ui-ux-pro-max/scripts/search.py'],
|
||||
] as const;
|
||||
const SEARCH_COMMAND_COUNT = 17;
|
||||
|
||||
|
||||
@ -20,7 +20,9 @@ def rows(name):
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
# Normalize line endings so the snapshot does not depend on whether the
|
||||
# working tree was checked out with LF or CRLF.
|
||||
return hashlib.sha256(path.read_bytes().replace(b"\r\n", b"\n")).hexdigest()
|
||||
|
||||
|
||||
def load_json(name):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ui-ux-pro-max",
|
||||
"displayName": "UI/UX Pro Max",
|
||||
"description": "AI-powered design intelligence with 84 UI styles, 192 color palettes, 74 font pairings, 98 UX guidelines, and 25 chart types across 22 tech stacks.",
|
||||
"description": "AI-powered design intelligence with 79 UI styles, 192 color palettes, 74 font pairings, 119 UX guidelines, and 25 chart types across 22 tech stacks.",
|
||||
"version": "2.13.0",
|
||||
"author": "NextLevelBuilder",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"verifiedAt": "2026-08-13",
|
||||
"verifiedAt": "2026-08-26",
|
||||
"counts": {
|
||||
"styles": {
|
||||
"total": 88,
|
||||
|
||||
@ -120,6 +120,12 @@ if __name__ == "__main__":
|
||||
|
||||
# Design system takes priority
|
||||
if args.design_system:
|
||||
if args.stack:
|
||||
print(
|
||||
f"note: --stack {args.stack} is ignored in --design-system mode; "
|
||||
"run a separate --stack query for stack-specific guidelines",
|
||||
file=sys.stderr,
|
||||
)
|
||||
result = generate_design_system(
|
||||
args.query,
|
||||
args.project_name,
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The catalog snapshot must not depend on the checkout's line endings.
|
||||
|
||||
Regression test for bd19ab9 (#462), where catalog-summary.json was regenerated
|
||||
on a CRLF checkout. Every recorded sha256 was the CRLF hash of the source file,
|
||||
so `verify:data` failed on every LF platform, including CI.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
DATA = REPO / "src/ui-ux-pro-max/data"
|
||||
SNAPSHOT_FILES = (
|
||||
"google-fonts.csv",
|
||||
"google-font-licenses.json",
|
||||
"icons.csv",
|
||||
"phosphor-icons-upstream.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_generator():
|
||||
path = REPO / "scripts" / "generate-catalog-summary.py"
|
||||
spec = importlib.util.spec_from_file_location("generate_catalog_summary", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class CatalogSummaryLineEndingsTest(unittest.TestCase):
|
||||
def test_digest_is_identical_for_lf_and_crlf(self):
|
||||
digest = _load_generator().digest
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lf = Path(tmp) / "lf.csv"
|
||||
crlf = Path(tmp) / "crlf.csv"
|
||||
lf.write_bytes(b"id,name\n1,alpha\n2,beta\n")
|
||||
crlf.write_bytes(b"id,name\r\n1,alpha\r\n2,beta\r\n")
|
||||
self.assertEqual(
|
||||
digest(lf), digest(crlf),
|
||||
"snapshot hashes must not change with the checkout's line endings",
|
||||
)
|
||||
|
||||
def test_committed_snapshot_matches_normalized_sources(self):
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
for name in SNAPSHOT_FILES:
|
||||
expected = hashlib.sha256(
|
||||
(DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
self.assertEqual(
|
||||
summary["snapshots"][name]["sha256"], expected,
|
||||
f"{name}: committed snapshot hash does not match the LF-normalized source",
|
||||
)
|
||||
|
||||
def test_crlf_checkout_produces_the_committed_hashes(self):
|
||||
"""Simulate a Windows checkout: the recorded hashes must still validate."""
|
||||
digest = _load_generator().digest
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name in SNAPSHOT_FILES:
|
||||
crlf_copy = Path(tmp) / name
|
||||
raw = (DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
crlf_copy.write_bytes(raw.replace(b"\n", b"\r\n"))
|
||||
self.assertEqual(
|
||||
digest(crlf_copy), summary["snapshots"][name]["sha256"],
|
||||
f"{name}: a CRLF checkout would record a different hash",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
61
src/ui-ux-pro-max/scripts/tests/test_design_system_stack.py
Normal file
61
src/ui-ux-pro-max/scripts/tests/test_design_system_stack.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Regression tests for the dropped --stack flag in --design-system mode (issue #484).
|
||||
|
||||
`search.py "<query>" --design-system --stack nextjs` used to exit successfully
|
||||
without any indication that the stack was never applied, so a caller following
|
||||
SKILL.md's "never assume a stack" guidance could believe stack guidance was
|
||||
part of the generated design system. The combination must stay valid, but it
|
||||
must say that --stack was ignored.
|
||||
|
||||
Stdlib-only (unittest, not pytest) to match test_core.py -- this project ships
|
||||
with zero external dependencies.
|
||||
|
||||
Run with:
|
||||
python -m unittest discover -s scripts/tests -v
|
||||
or directly:
|
||||
python scripts/tests/test_design_system_stack.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
||||
SEARCH = SCRIPTS_DIR / "search.py"
|
||||
|
||||
|
||||
class TestStackFlagWithDesignSystem(unittest.TestCase):
|
||||
def run_search(self, *args):
|
||||
# The child forces UTF-8 on its streams (search.py), so decode as UTF-8
|
||||
# regardless of the parent's locale on Windows.
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SEARCH), *map(str, args)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_design_system_with_stack_succeeds_and_says_stack_is_ignored(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("--stack", proc.stderr)
|
||||
self.assertIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_design_system_without_stack_stays_quiet(self):
|
||||
proc = self.run_search("platform engineer dashboard", "--design-system")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
def test_stack_search_alone_never_warns(self):
|
||||
proc = self.run_search("dashboard table density", "--stack", "nextjs")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotIn("ignored", proc.stderr.lower())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
82
src/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py
Normal file
82
src/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""Every script invocation in the shipped skill markdown resolves from the skill directory.
|
||||
|
||||
Regression test for #474. The sub-skills ship in two copies (.claude/skills/<skill>/
|
||||
for the plugin, cli/assets/skills/<skill>/ for CLI installs) and land in layouts where
|
||||
neither the project root nor ~/.claude/skills/ is a valid anchor: the plugin cache, a
|
||||
project's .claude/skills/, ~/.claude/skills/ (--global), or a manual copy. The one anchor
|
||||
that exists in all of them is the skill's own directory, so documented commands use
|
||||
`scripts/<file>` for the skill's own scripts and `../<skill>/scripts/<file>` for a
|
||||
sibling sub-skill (the sub-skills are always installed side by side).
|
||||
|
||||
This test extracts every `python|python3|node|bash <path>` invocation from every
|
||||
markdown file under both trees and asserts that the path is skill-relative and names a
|
||||
file that ships. The core skill's `${CLAUDE_PLUGIN_ROOT}/.claude/skills/...` form is
|
||||
resolved against the repository root, which is what that variable denotes under a
|
||||
plugin install - and accepted only in that file, because the sub-skills also ship
|
||||
through the CLI, where the variable does not exist. The grep-based path contract in check-asset-sync.yml is the negative
|
||||
side (no home-, project- or variable-rooted paths anywhere, code included); this is
|
||||
the positive side (every documented invocation points at a real file).
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
SKILL_TREES = ("cli/assets/skills", ".claude/skills")
|
||||
# The only file that may use the plugin-root form: hand-authored for the plugin install
|
||||
# and not shipped by the CLI (sync-assets.mjs mirrors data/ and scripts/, never SKILL.md).
|
||||
# (Built from segments: the path contract in check-asset-sync.yml scans this file too.)
|
||||
PLUGIN_ONLY_FILE = Path(".claude") / "skills" / "ui-ux-pro-max" / "SKILL.md"
|
||||
INVOCATION = re.compile(r'(?<![\w/.-])(?:python3?|node|bash)\s+"?([^\s"`\']+\.(?:py|cjs|js|mjs|sh))')
|
||||
PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}/"
|
||||
|
||||
|
||||
def shipped_invocations():
|
||||
for tree in SKILL_TREES:
|
||||
for skill_dir in sorted((REPO / tree).iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
for md in sorted(skill_dir.rglob("*.md")):
|
||||
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
|
||||
for match in INVOCATION.finditer(line):
|
||||
yield skill_dir, md, lineno, match.group(1)
|
||||
|
||||
|
||||
def resolve(skill_dir, md, path):
|
||||
"""Return (target, None) for a skill-relative path, or (None, reason)."""
|
||||
if path.startswith(PLUGIN_ROOT):
|
||||
if md.relative_to(REPO) != PLUGIN_ONLY_FILE:
|
||||
return None, "the ${CLAUDE_PLUGIN_ROOT} form is only valid in the plugin-only core SKILL.md"
|
||||
return REPO / path[len(PLUGIN_ROOT):], None
|
||||
if path.startswith("scripts/"):
|
||||
return skill_dir / path, None
|
||||
if path.startswith("../"):
|
||||
parts = path.split("/")
|
||||
if len(parts) > 3 and parts[2] == "scripts" and (skill_dir.parent / parts[1]).is_dir():
|
||||
return skill_dir.parent / parts[1] / "/".join(parts[2:]), None
|
||||
return None, "a sibling invocation must be ../<skill>/scripts/<file> and the sibling must ship"
|
||||
return None, "not skill-relative (expected scripts/<file> or ../<skill>/scripts/<file>)"
|
||||
|
||||
|
||||
class SkillScriptPathsTest(unittest.TestCase):
|
||||
def test_every_shipped_markdown_invocation_resolves_from_the_skill_directory(self):
|
||||
problems, seen = [], 0
|
||||
for skill_dir, md, lineno, path in shipped_invocations():
|
||||
seen += 1
|
||||
target, reason = resolve(skill_dir, md, path)
|
||||
if reason is None and not target.is_file():
|
||||
reason = f"no such file: {target}"
|
||||
if reason:
|
||||
problems.append(f"{md.relative_to(REPO)}:{lineno}: {path} -- {reason}")
|
||||
# Guard against a silently broken extractor: the two trees carry well over
|
||||
# a hundred documented invocations between them.
|
||||
self.assertGreater(seen, 100, f"extractor found only {seen} invocations")
|
||||
self.assertEqual(problems, [], "\n" + "\n".join(problems))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -663,7 +663,11 @@ def _check_catalog_summary(summary, licenses, phosphor, problems):
|
||||
problems.append(f"[catalog:summary] stale count for {key}")
|
||||
snapshots = summary.get("snapshots") if isinstance(summary.get("snapshots"), dict) else {}
|
||||
for name in ("google-fonts.csv", "google-font-licenses.json", "icons.csv", "phosphor-icons-upstream.json"):
|
||||
digest = hashlib.sha256((DATA_DIR / name).read_bytes()).hexdigest()
|
||||
# Line endings are normalized so the check matches
|
||||
# generate-catalog-summary.py on CRLF checkouts too.
|
||||
digest = hashlib.sha256(
|
||||
(DATA_DIR / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
if snapshots.get(name) != {"sha256": digest}:
|
||||
problems.append(f"[catalog:summary] stale snapshot for {name}")
|
||||
policy = summary.get("promotionPolicy")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user