📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"verifiedAt": "2026-08-13",
|
||||
"verifiedAt": "2026-08-26",
|
||||
"counts": {
|
||||
"styles": {
|
||||
"total": 88,
|
||||
|
||||
@@ -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,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
|
||||
@@ -229,7 +232,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 +243,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!');
|
||||
|
||||
@@ -62,6 +62,14 @@ def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
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_reports_missing_guidelines_without_breaking_the_harness(tmp_path):
|
||||
"""The missing-guidelines path is the one that breaks a locale-decoded pipe.
|
||||
|
||||
@@ -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 or Atlas Cloud 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:
|
||||
@@ -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,9 +65,11 @@ 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 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
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.
|
||||
@@ -76,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`)
|
||||
@@ -109,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.
|
||||
@@ -184,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
|
||||
@@ -304,8 +311,20 @@ 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
|
||||
|
||||
@@ -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. Gemini Nano Banana is the default provider; Atlas Cloud is also available as an explicit opt-in.
|
||||
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 or Atlas Cloud |
|
||||
| `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. G
|
||||
### 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,12 +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 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas
|
||||
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`, `--provider`, `--atlas-model`
|
||||
Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model`, `--muapi-model`
|
||||
|
||||
## Available Styles
|
||||
|
||||
@@ -93,4 +95,16 @@ 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
|
||||
|
||||
@@ -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,12 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Logo generation with Gemini or Atlas Cloud.
|
||||
"""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.
|
||||
``--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"
|
||||
@@ -14,6 +17,8 @@ Usage:
|
||||
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
|
||||
@@ -57,6 +62,7 @@ load_env()
|
||||
# ============ 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
|
||||
@@ -65,9 +71,14 @@ GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quali
|
||||
# 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"
|
||||
HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (Atlas Cloud logo provider)"
|
||||
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"]
|
||||
@@ -156,13 +167,13 @@ def _validate_public_https_url(url):
|
||||
or parsed.username
|
||||
or parsed.password
|
||||
):
|
||||
raise ValueError("Atlas Cloud returned an invalid media URL")
|
||||
raise ValueError("Provider returned an invalid media URL")
|
||||
|
||||
hostname = parsed.hostname.lower().rstrip(".")
|
||||
if hostname == "localhost" or hostname.endswith(
|
||||
(".localhost", ".local", ".internal")
|
||||
):
|
||||
raise ValueError("Atlas Cloud media URL used a local hostname")
|
||||
raise ValueError("Provider media URL used a local hostname")
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
@@ -170,17 +181,26 @@ def _validate_public_https_url(url):
|
||||
return
|
||||
else:
|
||||
if not ip.is_global:
|
||||
raise ValueError("Atlas Cloud media URL used a non-public address")
|
||||
raise ValueError("Provider media URL used a non-public address")
|
||||
|
||||
|
||||
def _json_request(url, api_key, method="GET", payload=None):
|
||||
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={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
api_key_header: auth_value,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": HTTP_USER_AGENT,
|
||||
**({"Content-Type": "application/json"} if body is not None else {}),
|
||||
@@ -192,10 +212,10 @@ def _json_request(url, api_key, method="GET", payload=None):
|
||||
except HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(
|
||||
f"Atlas Cloud request failed ({exc.code}): {detail[:300]}"
|
||||
f"Provider request failed ({exc.code}): {detail[:300]}"
|
||||
) from exc
|
||||
except (URLError, TimeoutError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError(f"Atlas Cloud request failed: {exc}") from exc
|
||||
raise RuntimeError(f"Provider request failed: {exc}") from exc
|
||||
|
||||
|
||||
def _atlas_prediction_data(response):
|
||||
@@ -210,6 +230,10 @@ def _atlas_prediction_data(response):
|
||||
|
||||
|
||||
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,
|
||||
@@ -222,14 +246,14 @@ def _download_atlas_image(url, output_path):
|
||||
content_type = response.headers.get_content_type()
|
||||
if not content_type.startswith("image/"):
|
||||
raise RuntimeError(
|
||||
f"Atlas Cloud output is not an image ({content_type})"
|
||||
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 Atlas Cloud image: {exc}") from exc
|
||||
raise RuntimeError(f"Unable to download {provider_name} image: {exc}") from exc
|
||||
|
||||
if not image_data:
|
||||
raise RuntimeError("Atlas Cloud returned an empty image")
|
||||
raise RuntimeError(f"{provider_name} returned an empty image")
|
||||
with open(output_path, "wb") as output_file:
|
||||
output_file.write(image_data)
|
||||
|
||||
@@ -281,6 +305,132 @@ def _generate_with_atlas(prompt, output_path, aspect_ratio, api_key, model):
|
||||
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")
|
||||
@@ -344,8 +494,9 @@ def generate_logo(
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate a logo using Gemini or Atlas Cloud image generation.
|
||||
"""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"
|
||||
@@ -365,6 +516,8 @@ def generate_logo(
|
||||
|
||||
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)"
|
||||
@@ -386,6 +539,14 @@ def generate_logo(
|
||||
ATLASCLOUD_API_KEY,
|
||||
atlas_model,
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -407,6 +568,7 @@ def generate_batch(
|
||||
aspect_ratio=None,
|
||||
provider="gemini",
|
||||
atlas_model=ATLAS_MODEL,
|
||||
muapi_model=MUAPI_MODEL,
|
||||
):
|
||||
"""Generate multiple logo variants with different styles"""
|
||||
|
||||
@@ -430,6 +592,8 @@ def generate_batch(
|
||||
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
|
||||
@@ -466,6 +630,7 @@ def generate_batch(
|
||||
aspect_ratio=aspect_ratio,
|
||||
provider=provider,
|
||||
atlas_model=atlas_model,
|
||||
muapi_model=muapi_model,
|
||||
)
|
||||
|
||||
if result:
|
||||
@@ -487,7 +652,7 @@ def generate_batch(
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate logos using Gemini or Atlas Cloud"
|
||||
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")
|
||||
@@ -514,7 +679,7 @@ def main():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provider",
|
||||
choices=["gemini", "atlas"],
|
||||
choices=["gemini", "atlas", "muapi"],
|
||||
default="gemini",
|
||||
help="Image provider (default: gemini)",
|
||||
)
|
||||
@@ -523,6 +688,12 @@ def main():
|
||||
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",
|
||||
@@ -539,8 +710,11 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.provider == "atlas" and args.pro:
|
||||
parser.error("--pro is only available with --provider gemini")
|
||||
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:")
|
||||
@@ -574,6 +748,7 @@ def main():
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
else:
|
||||
generate_logo(
|
||||
@@ -586,6 +761,7 @@ def main():
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
provider=args.provider,
|
||||
atlas_model=args.atlas_model,
|
||||
muapi_model=args.muapi_model,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -128,5 +128,161 @@ class AtlasGenerationTests(unittest.TestCase):
|
||||
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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user