aboutsummaryrefslogtreecommitdiffstats
path: root/.agents/skills/ui-ux-pro-max/scripts/search.py
blob: f8403159532d38ae176c4f54b5b8e19e7bacb799 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import csv
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class DesignRule:
    slug: str
    keywords: tuple[str, ...]
    style: str
    palette: str
    typography: str
    effects: str
    landing_pattern: str
    anti_patterns: str
    notes: str


DOMAIN_LIBRARY = {
    "style": [
        {"name": "minimal-modern", "tags": {"saas", "productivity", "software"}, "summary": "Clean grids, restrained color, strong spacing rhythm."},
        {"name": "trust-dark", "tags": {"fintech", "security", "analytics"}, "summary": "Dark surfaces with high-contrast metrics and controlled glow."},
        {"name": "elegant-soft", "tags": {"beauty", "wellness", "premium"}, "summary": "Soft neutrals, editorial typography, gentle depth."},
        {"name": "editorial-bold", "tags": {"portfolio", "studio", "creator"}, "summary": "Typography-led layouts with fewer but stronger components."},
    ],
    "color": [
        {"name": "slate-indigo", "tags": {"saas", "b2b", "dashboard"}, "summary": "Reliable product palette with strong focus color."},
        {"name": "navy-cyan", "tags": {"fintech", "security", "crypto"}, "summary": "Trust-heavy dark palette with crisp accent highlights."},
        {"name": "teal-blue", "tags": {"healthcare", "medical", "education"}, "summary": "Calm, legible palette for guidance-heavy flows."},
        {"name": "rose-stone", "tags": {"beauty", "wellness", "service"}, "summary": "Soft premium palette without losing contrast."},
    ],
    "typography": [
        {"name": "Inter + Space Grotesk", "tags": {"saas", "dashboard", "tech"}, "summary": "Functional UI body with sharper display headlines."},
        {"name": "Inter + IBM Plex Sans", "tags": {"fintech", "data", "ops"}, "summary": "Trustworthy and technical without feeling cold."},
        {"name": "DM Sans + Cormorant Garamond", "tags": {"beauty", "luxury", "editorial"}, "summary": "Soft body copy plus premium display contrast."},
        {"name": "Inter + Nunito", "tags": {"education", "learning", "friendly"}, "summary": "Warm, approachable pair for guided workflows."},
    ],
    "landing": [
        {"name": "hero + product proof", "tags": {"saas", "b2b", "product"}, "summary": "Lead with problem, product UI, and trust proof."},
        {"name": "hero + trust signals + metrics", "tags": {"fintech", "security"}, "summary": "Move quickly from promise to credibility to evidence."},
        {"name": "hero + treatments + testimonials", "tags": {"beauty", "service"}, "summary": "Show the offering, then emotion, then social proof."},
        {"name": "summary row + filters + charts", "tags": {"dashboard", "analytics", "ops"}, "summary": "Start with key numbers, then enable fast drill-down."},
    ],
    "chart": [
        {"name": "line chart", "tags": {"trend", "time-series", "monitoring"}, "summary": "Use for change over time and directional narratives."},
        {"name": "bar chart", "tags": {"comparison", "ranking", "category"}, "summary": "Use for category comparison with clear labels."},
        {"name": "funnel chart", "tags": {"conversion", "checkout", "pipeline"}, "summary": "Use only when the user cares about progressive drop-off."},
        {"name": "table + sparkline", "tags": {"dashboard", "ops", "dense"}, "summary": "Best when users must compare many rows quickly."},
    ],
    "ux": [
        {"name": "contrast-first", "tags": {"accessibility", "contrast", "readability"}, "summary": "Keep body text at 4.5:1 or better and check muted states."},
        {"name": "touch targets", "tags": {"mobile", "touch", "interaction"}, "summary": "Keep tap targets at 44x44px or larger."},
        {"name": "motion restraint", "tags": {"animation", "performance", "a11y"}, "summary": "Prefer opacity/transform and honor reduced motion."},
        {"name": "feedback locality", "tags": {"forms", "validation", "errors"}, "summary": "Place errors next to the field or action that caused them."},
    ],
    "product": [
        {"name": "B2B SaaS", "tags": {"saas", "b2b", "workflow"}, "summary": "Prioritize clarity, trust, and information hierarchy."},
        {"name": "consumer marketplace", "tags": {"ecommerce", "retail", "marketplace"}, "summary": "Prioritize CTA hierarchy and decision speed."},
        {"name": "editorial portfolio", "tags": {"portfolio", "agency", "studio"}, "summary": "Let type and spacing carry most of the brand signal."},
    ],
}

STACK_LIBRARY = {
    "html-tailwind": [
        "Use spacing and typography scales consistently; avoid arbitrary values unless they encode a real token.",
        "Prefer semantic HTML first, then layer Tailwind utilities for layout and state.",
        "Use visible focus rings, not shadow-only focus states.",
    ],
    "react": [
        "Separate presentational components from stateful orchestration when the screen gets dense.",
        "Memoize only after identifying re-render pressure; do not cargo-cult memo everywhere.",
        "Keep loading, error, and empty states explicit in component contracts.",
    ],
    "nextjs": [
        "Use server components for data-heavy static surfaces and client components only for interaction islands.",
        "Optimize images and fonts early; layout shift hurts perceived polish.",
        "Keep route segments and layout nesting readable; complexity leaks into UX fast.",
    ],
    "vue": [
        "Keep composables focused and avoid mixing data fetching, presentation logic, and event plumbing in one file.",
        "Use Pinia only when state truly spans multiple views.",
        "Prefer explicit prop contracts over implicit slot conventions for complex widgets.",
    ],
    "svelte": [
        "Use stores sparingly; local state is usually enough for page-scoped interaction.",
        "Keep transitions subtle and intentional.",
        "Use semantic markup because Svelte speed does not compensate for weak accessibility.",
    ],
    "swiftui": [
        "Prefer a small number of reusable view modifiers instead of repeated inline styling.",
        "Use state hoisting for reusable components.",
        "Make navigation state obvious; hidden transitions confuse users quickly.",
    ],
    "react-native": [
        "Design for touch targets and text scaling first.",
        "Avoid over-nesting views; spacing bugs compound quickly on mobile.",
        "Budget for offline and loading states in every primary screen.",
    ],
    "flutter": [
        "Use a small, explicit theme surface; avoid per-widget color drift.",
        "Keep widget trees readable by extracting repeated sections early.",
        "Use platform-appropriate motion and haptics sparingly.",
    ],
    "shadcn": [
        "Treat shadcn/ui as a starting system, not the finished product identity.",
        "Normalize spacing and type across imported primitives before scaling the page.",
        "Audit keyboard and dialog behavior after customization.",
    ],
    "jetpack-compose": [
        "Keep state hoisting explicit and avoid business logic inside composables.",
        "Use Material theming consistently before introducing custom tokens.",
        "Measure recomposition hotspots before optimizing prematurely.",
    ],
}


def load_rules(csv_path: Path) -> list[DesignRule]:
    rows: list[DesignRule] = []
    with csv_path.open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        for row in reader:
            rows.append(
                DesignRule(
                    slug=row["slug"],
                    keywords=tuple(token.strip() for token in row["keywords"].split(",") if token.strip()),
                    style=row["style"],
                    palette=row["palette"],
                    typography=row["typography"],
                    effects=row["effects"],
                    landing_pattern=row["landing_pattern"],
                    anti_patterns=row["anti_patterns"],
                    notes=row["notes"],
                )
            )
    return rows


def score_rule(query_tokens: set[str], rule: DesignRule) -> int:
    overlap = len(query_tokens & set(rule.keywords))
    return overlap * 10 + (1 if rule.slug in query_tokens else 0)


def pick_rule(query: str, rules: list[DesignRule]) -> DesignRule:
    query_tokens = set(token.lower() for token in query.replace("-", " ").split())
    return max(rules, key=lambda rule: (score_rule(query_tokens, rule), -len(rule.keywords)))


def score_entry(query_tokens: set[str], entry: dict[str, object]) -> tuple[int, str]:
    tags = set(entry.get("tags", set()))
    score = len(query_tokens & tags)
    return score, str(entry["name"])


def top_domain_entries(domain: str, query: str, limit: int) -> list[dict[str, object]]:
    query_tokens = set(token.lower() for token in query.replace("-", " ").split())
    entries = DOMAIN_LIBRARY[domain]
    ranked = sorted(entries, key=lambda entry: score_entry(query_tokens, entry), reverse=True)
    return ranked[:limit]


def render_design_system(rule: DesignRule, query: str, project: str | None, markdown: bool) -> str:
    title = project or query.strip() or rule.slug
    lines = [
        f"# Design system: {title}" if markdown else f"Design system: {title}",
        "",
        f"- Query: {query}",
        f"- Matched profile: {rule.slug}",
        f"- Style direction: {rule.style}",
        f"- Palette: {rule.palette}",
        f"- Typography: {rule.typography}",
        f"- Effects: {rule.effects}",
        f"- Landing/layout bias: {rule.landing_pattern}",
        f"- Avoid: {rule.anti_patterns}",
        f"- Notes: {rule.notes}",
    ]
    return "\n".join(lines)


def render_domain(domain: str, query: str, limit: int, markdown: bool) -> str:
    entries = top_domain_entries(domain, query, limit)
    header = f"# Domain lookup: {domain}" if markdown else f"Domain lookup: {domain}"
    lines = [header, "", f"Query: {query}", ""]
    for entry in entries:
        lines.append(f"- {entry['name']}: {entry['summary']}")
    return "\n".join(lines)


def render_stack(stack: str, markdown: bool) -> str:
    header = f"# Stack guidance: {stack}" if markdown else f"Stack guidance: {stack}"
    lines = [header, ""]
    for item in STACK_LIBRARY[stack]:
        lines.append(f"- {item}")
    return "\n".join(lines)


def persist_design_system(text: str, page: str | None) -> list[Path]:
    base = Path.cwd() / "design-system"
    base.mkdir(exist_ok=True)
    pages = base / "pages"
    pages.mkdir(exist_ok=True)
    created: list[Path] = []
    master = base / "MASTER.md"
    master.write_text(text + "\n", encoding="utf-8")
    created.append(master)
    if page:
        page_path = pages / f"{page}.md"
        page_path.write_text(
            f"# Page override: {page}\n\n- Base authority: ../MASTER.md\n- Record only page-specific deviations here.\n",
            encoding="utf-8",
        )
        created.append(page_path)
    return created


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Search the bundled UI/UX guidance library.")
    parser.add_argument("query", nargs="?", default="")
    parser.add_argument("--design-system", action="store_true")
    parser.add_argument("--domain", choices=sorted(DOMAIN_LIBRARY))
    parser.add_argument("--stack", choices=sorted(STACK_LIBRARY))
    parser.add_argument("--persist", action="store_true")
    parser.add_argument("-p", "--project")
    parser.add_argument("--page")
    parser.add_argument("-n", type=int, default=5)
    parser.add_argument("-f", "--format", choices=("ascii", "markdown"), default="ascii")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    markdown = args.format == "markdown"
    script_dir = Path(__file__).resolve().parent
    rules = load_rules(script_dir.parent / "data" / "ui-reasoning.csv")

    if args.design_system:
        rule = pick_rule(args.query, rules)
        output = render_design_system(rule, args.query, args.project, markdown)
        print(output)
        if args.persist:
            created = persist_design_system(output, args.page)
            print("\nPersisted:")
            for path in created:
                print(f"- {path}")
        return 0

    if args.domain:
        print(render_domain(args.domain, args.query, args.n, markdown))
        return 0

    if args.stack:
        print(render_stack(args.stack, markdown))
        return 0

    print("No mode selected. Use --design-system, --domain, or --stack.")
    return 1


if __name__ == "__main__":
    raise SystemExit(main())