mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-09-25 13:36:15 +00:00
* fix(design-system): resolve project root from cwd, not __file__ fetch-background.py and html-token-validator.py derived PROJECT_ROOT with five .parent hops, which only reaches the project root when the skill is vendored at <project>/.claude/skills/design-system/scripts/. Installed at user level (~/.claude/skills/) or as a plugin, PROJECT_ROOT pointed outside the project, so both scripts silently ran against no tokens at all. Resolve from the working directory instead, matching generate-tokens.cjs and validate-tokens.cjs which already use process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it when the project root cannot be inferred. slide_search_core.py is left alone: it resolves skill-relative data, which is the correct use of __file__. Refs #459 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(design-system): stop findProjectRoot from hanging on Windows embed-tokens.cjs walked up the tree with `while (dir !== '/')`. On Windows the filesystem root is 'C:\', so that condition is never true, and path.dirname('C:\') returns 'C:\' unchanged -- the loop spins forever at 100% CPU instead of erroring out, whenever assets/design-tokens.css is not found above the cwd. Stop when dirname stops changing, which terminates on every platform. Refs #459 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(design-system): force UTF-8 stdout so emoji output works on cp1252 consoles search-slides.py --context and html-token-validator.py print emoji. On a Windows console the default encoding is cp1252, so the first emoji raises UnicodeEncodeError and the command dies with a traceback instead of output -- this takes out --context, the entry point of the contextual slide system. Reuse the guard already shipped in src/ui-ux-pro-max/scripts/search.py. Refs #459 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(design-system): decode subprocess output as UTF-8 in validate-tokens tests test_validate_tokens.py drives validate-tokens.cjs through subprocess.run with text=True but no explicit encoding, so Python decodes the pipe with the locale codec. On Windows (cp1252) the validator's emoji output raises UnicodeDecodeError inside the reader thread, result.stdout comes back as None, and the assertion fails with a confusing `TypeError: argument of type 'NoneType' is not a container` -- this suite cannot pass on Windows at all today. Pin the pipe and the fixture write to UTF-8. The validator itself was never at fault: run by hand it flags the hardcoded hex correctly. Note: brand/scripts/tests/test_sync_brand_to_tokens.py uses the same text=True-without-encoding pattern and is one emoji away from failing the same way. Left alone to keep this PR scoped to design-system. Refs #459 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
229 lines
9.4 KiB
Python
229 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Slide Search CLI - Search slide design databases for strategies, layouts, copy, and charts
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import argparse
|
|
from slide_search_core import (
|
|
search, search_all, AVAILABLE_DOMAINS,
|
|
search_with_context, get_layout_for_goal, get_typography_for_slide,
|
|
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"""
|
|
output = []
|
|
|
|
if domain == "strategy":
|
|
output.append(f"**{result.get('strategy_name', 'N/A')}**")
|
|
output.append(f" Slides: {result.get('slide_count', 'N/A')}")
|
|
output.append(f" Structure: {result.get('structure', 'N/A')}")
|
|
output.append(f" Goal: {result.get('goal', 'N/A')}")
|
|
output.append(f" Audience: {result.get('audience', 'N/A')}")
|
|
output.append(f" Tone: {result.get('tone', 'N/A')}")
|
|
output.append(f" Arc: {result.get('narrative_arc', 'N/A')}")
|
|
output.append(f" Source: {result.get('sources', 'N/A')}")
|
|
|
|
elif domain == "layout":
|
|
output.append(f"**{result.get('layout_name', 'N/A')}**")
|
|
output.append(f" Use case: {result.get('use_case', 'N/A')}")
|
|
output.append(f" Zones: {result.get('content_zones', 'N/A')}")
|
|
output.append(f" Visual weight: {result.get('visual_weight', 'N/A')}")
|
|
output.append(f" CTA: {result.get('cta_placement', 'N/A')}")
|
|
output.append(f" Recommended: {result.get('recommended_for', 'N/A')}")
|
|
output.append(f" Avoid: {result.get('avoid_for', 'N/A')}")
|
|
output.append(f" CSS: {result.get('css_structure', 'N/A')}")
|
|
|
|
elif domain == "copy":
|
|
output.append(f"**{result.get('formula_name', 'N/A')}**")
|
|
output.append(f" Components: {result.get('components', 'N/A')}")
|
|
output.append(f" Use case: {result.get('use_case', 'N/A')}")
|
|
output.append(f" Template: {result.get('example_template', 'N/A')}")
|
|
output.append(f" Emotion: {result.get('emotion_trigger', 'N/A')}")
|
|
output.append(f" Slide type: {result.get('slide_type', 'N/A')}")
|
|
output.append(f" Source: {result.get('source', 'N/A')}")
|
|
|
|
elif domain == "chart":
|
|
output.append(f"**{result.get('chart_type', 'N/A')}**")
|
|
output.append(f" Best for: {result.get('best_for', 'N/A')}")
|
|
output.append(f" Data type: {result.get('data_type', 'N/A')}")
|
|
output.append(f" When to use: {result.get('when_to_use', 'N/A')}")
|
|
output.append(f" When to avoid: {result.get('when_to_avoid', 'N/A')}")
|
|
output.append(f" Max categories: {result.get('max_categories', 'N/A')}")
|
|
output.append(f" Slide context: {result.get('slide_context', 'N/A')}")
|
|
output.append(f" CSS: {result.get('css_implementation', 'N/A')}")
|
|
output.append(f" Accessibility: {result.get('accessibility_notes', 'N/A')}")
|
|
|
|
return "\n".join(output)
|
|
|
|
|
|
def format_context(context):
|
|
"""Format contextual recommendations for display."""
|
|
output = []
|
|
output.append(f"\n=== CONTEXTUAL RECOMMENDATIONS ===")
|
|
output.append(f"Inferred Goal: {context.get('inferred_goal', 'N/A')}")
|
|
output.append(f"Position: Slide {context.get('slide_position')} of {context.get('total_slides')}")
|
|
|
|
if context.get('recommended_layout'):
|
|
output.append(f"\n📐 Layout: {context['recommended_layout']}")
|
|
output.append(f" Direction: {context.get('layout_direction', 'N/A')}")
|
|
output.append(f" Visual Weight: {context.get('visual_weight', 'N/A')}")
|
|
|
|
if context.get('typography'):
|
|
typo = context['typography']
|
|
output.append(f"\n📝 Typography:")
|
|
output.append(f" Primary: {typo.get('primary_size', 'N/A')}")
|
|
output.append(f" Secondary: {typo.get('secondary_size', 'N/A')}")
|
|
output.append(f" Contrast: {typo.get('weight_contrast', 'N/A')}")
|
|
|
|
if context.get('color_treatment'):
|
|
color = context['color_treatment']
|
|
output.append(f"\n🎨 Color Treatment:")
|
|
output.append(f" Background: {color.get('background', 'N/A')}")
|
|
output.append(f" Text: {color.get('text_color', 'N/A')}")
|
|
output.append(f" Accent: {color.get('accent_usage', 'N/A')}")
|
|
|
|
if context.get('should_break_pattern'):
|
|
output.append(f"\n⚡ Pattern Break: YES (use contrasting layout)")
|
|
|
|
if context.get('should_use_full_bleed'):
|
|
output.append(f"\n🖼️ Full Bleed: Recommended for emotional impact")
|
|
|
|
if context.get('use_background_image') and context.get('background'):
|
|
bg = context['background']
|
|
output.append(f"\n📸 Background Image:")
|
|
output.append(f" Category: {bg.get('image_category', 'N/A')}")
|
|
output.append(f" Overlay: {bg.get('overlay_style', 'N/A')}")
|
|
output.append(f" Keywords: {bg.get('search_keywords', 'N/A')}")
|
|
|
|
output.append(f"\n✨ Animation: {context.get('animation_class', 'animate-fade-up')}")
|
|
|
|
return "\n".join(output)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Search slide design databases",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
search-slides.py "investor pitch" # Auto-detect domain (strategy)
|
|
search-slides.py "funnel conversion" -d chart
|
|
search-slides.py "headline hook" -d copy
|
|
search-slides.py "two column" -d layout
|
|
search-slides.py "startup funding" --all # Search all domains
|
|
search-slides.py "metrics dashboard" --json # JSON output
|
|
|
|
Contextual Search (Premium System):
|
|
search-slides.py "problem slide" --context --position 2 --total 9
|
|
search-slides.py "cta" --context --position 9 --total 9 --prev-emotion frustration
|
|
"""
|
|
)
|
|
|
|
parser.add_argument("query", help="Search query")
|
|
parser.add_argument("-d", "--domain", choices=AVAILABLE_DOMAINS,
|
|
help="Specific domain to search (auto-detected if not specified)")
|
|
parser.add_argument("-n", "--max-results", type=int, default=3,
|
|
help="Maximum results to return (default: 3)")
|
|
parser.add_argument("--all", action="store_true",
|
|
help="Search across all domains")
|
|
parser.add_argument("--json", action="store_true",
|
|
help="Output as JSON")
|
|
|
|
# Contextual search options
|
|
parser.add_argument("--context", action="store_true",
|
|
help="Use contextual search with layout/typography/color recommendations")
|
|
parser.add_argument("--position", type=int, default=1,
|
|
help="Slide position in deck (1-based, default: 1)")
|
|
parser.add_argument("--total", type=int, default=9,
|
|
help="Total slides in deck (default: 9)")
|
|
parser.add_argument("--prev-emotion", type=str, default=None,
|
|
help="Previous slide's emotion for contrast calculation")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Contextual search mode
|
|
if args.context:
|
|
result = search_with_context(
|
|
args.query,
|
|
slide_position=args.position,
|
|
total_slides=args.total,
|
|
previous_emotion=args.prev_emotion
|
|
)
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print(format_context(result['context']))
|
|
|
|
# Also show base search results
|
|
if result.get('base_results'):
|
|
print("\n\n=== RELATED SEARCH RESULTS ===")
|
|
for domain, data in result['base_results'].items():
|
|
print(f"\n--- {domain.upper()} ---")
|
|
for item in data['results']:
|
|
print(format_result(item, domain))
|
|
print()
|
|
return
|
|
|
|
if args.all:
|
|
results = search_all(args.query, args.max_results)
|
|
|
|
if args.json:
|
|
print(json.dumps(results, indent=2))
|
|
else:
|
|
if not results:
|
|
print(f"No results found for: {args.query}")
|
|
return
|
|
|
|
for domain, data in results.items():
|
|
print(f"\n=== {domain.upper()} ===")
|
|
print(f"File: {data['file']}")
|
|
print(f"Results: {data['count']}")
|
|
print()
|
|
for result in data['results']:
|
|
print(format_result(result, domain))
|
|
print()
|
|
else:
|
|
result = search(args.query, args.domain, args.max_results)
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
if result.get("error"):
|
|
print(f"Error: {result['error']}")
|
|
return
|
|
|
|
print(f"Domain: {result['domain']}")
|
|
print(f"Query: {result['query']}")
|
|
print(f"File: {result['file']}")
|
|
print(f"Results: {result['count']}")
|
|
print()
|
|
|
|
if result['count'] == 0:
|
|
print("No matching results found.")
|
|
return
|
|
|
|
for i, item in enumerate(result['results'], 1):
|
|
print(f"--- Result {i} ---")
|
|
print(format_result(item, result['domain']))
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|