ui-ux-pro-max-cli 2.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/assets/data/_sync_all.py +414 -0
- package/assets/data/app-interface.csv +31 -0
- package/assets/data/charts.csv +26 -0
- package/assets/data/colors.csv +162 -0
- package/assets/data/design.csv +1776 -0
- package/assets/data/draft.csv +1779 -0
- package/assets/data/google-fonts.csv +1924 -0
- package/assets/data/icons.csv +106 -0
- package/assets/data/landing.csv +35 -0
- package/assets/data/products.csv +162 -0
- package/assets/data/react-performance.csv +45 -0
- package/assets/data/stacks/angular.csv +51 -0
- package/assets/data/stacks/astro.csv +54 -0
- package/assets/data/stacks/flutter.csv +53 -0
- package/assets/data/stacks/html-tailwind.csv +56 -0
- package/assets/data/stacks/javafx.csv +76 -0
- package/assets/data/stacks/jetpack-compose.csv +53 -0
- package/assets/data/stacks/laravel.csv +51 -0
- package/assets/data/stacks/nextjs.csv +53 -0
- package/assets/data/stacks/nuxt-ui.csv +71 -0
- package/assets/data/stacks/nuxtjs.csv +59 -0
- package/assets/data/stacks/react-native.csv +52 -0
- package/assets/data/stacks/react.csv +54 -0
- package/assets/data/stacks/shadcn.csv +61 -0
- package/assets/data/stacks/svelte.csv +54 -0
- package/assets/data/stacks/swiftui.csv +51 -0
- package/assets/data/stacks/threejs.csv +54 -0
- package/assets/data/stacks/vue.csv +50 -0
- package/assets/data/styles.csv +85 -0
- package/assets/data/typography.csv +74 -0
- package/assets/data/ui-reasoning.csv +162 -0
- package/assets/data/ux-guidelines.csv +100 -0
- package/assets/scripts/core.py +263 -0
- package/assets/scripts/design_system.py +1157 -0
- package/assets/scripts/search.py +114 -0
- package/assets/skills/banner-design/SKILL.md +196 -0
- package/assets/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
- package/assets/skills/brand/SKILL.md +97 -0
- package/assets/skills/brand/references/approval-checklist.md +169 -0
- package/assets/skills/brand/references/asset-organization.md +157 -0
- package/assets/skills/brand/references/brand-guideline-template.md +140 -0
- package/assets/skills/brand/references/color-palette-management.md +186 -0
- package/assets/skills/brand/references/consistency-checklist.md +94 -0
- package/assets/skills/brand/references/logo-usage-rules.md +185 -0
- package/assets/skills/brand/references/messaging-framework.md +85 -0
- package/assets/skills/brand/references/typography-specifications.md +214 -0
- package/assets/skills/brand/references/update.md +118 -0
- package/assets/skills/brand/references/visual-identity.md +96 -0
- package/assets/skills/brand/references/voice-framework.md +88 -0
- package/assets/skills/brand/scripts/extract-colors.cjs +341 -0
- package/assets/skills/brand/scripts/inject-brand-context.cjs +349 -0
- package/assets/skills/brand/scripts/sync-brand-to-tokens.cjs +266 -0
- package/assets/skills/brand/scripts/validate-asset.cjs +387 -0
- package/assets/skills/brand/templates/brand-guidelines-starter.md +275 -0
- package/assets/skills/design/SKILL.md +313 -0
- package/assets/skills/design/data/cip/deliverables.csv +51 -0
- package/assets/skills/design/data/cip/industries.csv +21 -0
- package/assets/skills/design/data/cip/mockup-contexts.csv +21 -0
- package/assets/skills/design/data/cip/styles.csv +21 -0
- package/assets/skills/design/data/icon/styles.csv +16 -0
- package/assets/skills/design/data/logo/colors.csv +56 -0
- package/assets/skills/design/data/logo/industries.csv +56 -0
- package/assets/skills/design/data/logo/styles.csv +56 -0
- package/assets/skills/design/references/banner-sizes-and-styles.md +118 -0
- package/assets/skills/design/references/cip-deliverable-guide.md +95 -0
- package/assets/skills/design/references/cip-design.md +121 -0
- package/assets/skills/design/references/cip-prompt-engineering.md +84 -0
- package/assets/skills/design/references/cip-style-guide.md +68 -0
- package/assets/skills/design/references/design-routing.md +207 -0
- package/assets/skills/design/references/icon-design.md +122 -0
- package/assets/skills/design/references/logo-color-psychology.md +101 -0
- package/assets/skills/design/references/logo-design.md +92 -0
- package/assets/skills/design/references/logo-prompt-engineering.md +158 -0
- package/assets/skills/design/references/logo-style-guide.md +109 -0
- package/assets/skills/design/references/slides-copywriting-formulas.md +84 -0
- package/assets/skills/design/references/slides-create.md +4 -0
- package/assets/skills/design/references/slides-html-template.md +295 -0
- package/assets/skills/design/references/slides-layout-patterns.md +137 -0
- package/assets/skills/design/references/slides-strategies.md +94 -0
- package/assets/skills/design/references/slides.md +42 -0
- package/assets/skills/design/references/social-photos-design.md +329 -0
- package/assets/skills/design/scripts/cip/core.py +215 -0
- package/assets/skills/design/scripts/cip/generate.py +484 -0
- package/assets/skills/design/scripts/cip/render-html.py +424 -0
- package/assets/skills/design/scripts/cip/search.py +127 -0
- package/assets/skills/design/scripts/icon/generate.py +487 -0
- package/assets/skills/design/scripts/logo/core.py +175 -0
- package/assets/skills/design/scripts/logo/generate.py +362 -0
- package/assets/skills/design/scripts/logo/search.py +114 -0
- package/assets/skills/design-system/SKILL.md +244 -0
- package/assets/skills/design-system/data/slide-backgrounds.csv +11 -0
- package/assets/skills/design-system/data/slide-charts.csv +26 -0
- package/assets/skills/design-system/data/slide-color-logic.csv +14 -0
- package/assets/skills/design-system/data/slide-copy.csv +26 -0
- package/assets/skills/design-system/data/slide-layout-logic.csv +16 -0
- package/assets/skills/design-system/data/slide-layouts.csv +26 -0
- package/assets/skills/design-system/data/slide-strategies.csv +16 -0
- package/assets/skills/design-system/data/slide-typography.csv +15 -0
- package/assets/skills/design-system/references/component-specs.md +236 -0
- package/assets/skills/design-system/references/component-tokens.md +214 -0
- package/assets/skills/design-system/references/primitive-tokens.md +203 -0
- package/assets/skills/design-system/references/semantic-tokens.md +215 -0
- package/assets/skills/design-system/references/states-and-variants.md +241 -0
- package/assets/skills/design-system/references/tailwind-integration.md +251 -0
- package/assets/skills/design-system/references/token-architecture.md +224 -0
- package/assets/skills/design-system/scripts/embed-tokens.cjs +99 -0
- package/assets/skills/design-system/scripts/fetch-background.py +317 -0
- package/assets/skills/design-system/scripts/generate-slide.py +770 -0
- package/assets/skills/design-system/scripts/generate-tokens.cjs +205 -0
- package/assets/skills/design-system/scripts/html-token-validator.py +327 -0
- package/assets/skills/design-system/scripts/search-slides.py +218 -0
- package/assets/skills/design-system/scripts/slide-token-validator.py +35 -0
- package/assets/skills/design-system/scripts/slide_search_core.py +453 -0
- package/assets/skills/design-system/scripts/validate-tokens.cjs +251 -0
- package/assets/skills/design-system/templates/design-tokens-starter.json +143 -0
- package/assets/skills/slides/SKILL.md +40 -0
- package/assets/skills/slides/references/copywriting-formulas.md +84 -0
- package/assets/skills/slides/references/create.md +4 -0
- package/assets/skills/slides/references/html-template.md +295 -0
- package/assets/skills/slides/references/layout-patterns.md +137 -0
- package/assets/skills/slides/references/slide-strategies.md +94 -0
- package/assets/skills/ui-styling/LICENSE.txt +202 -0
- package/assets/skills/ui-styling/SKILL.md +324 -0
- package/assets/skills/ui-styling/references/canvas-design-system.md +320 -0
- package/assets/skills/ui-styling/references/shadcn-accessibility.md +471 -0
- package/assets/skills/ui-styling/references/shadcn-components.md +424 -0
- package/assets/skills/ui-styling/references/shadcn-theming.md +373 -0
- package/assets/skills/ui-styling/references/tailwind-customization.md +483 -0
- package/assets/skills/ui-styling/references/tailwind-responsive.md +382 -0
- package/assets/skills/ui-styling/references/tailwind-utilities.md +455 -0
- package/assets/skills/ui-styling/scripts/requirements.txt +17 -0
- package/assets/skills/ui-styling/scripts/shadcn_add.py +308 -0
- package/assets/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
- package/assets/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
- package/assets/skills/ui-styling/scripts/tests/requirements.txt +3 -0
- package/assets/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
- package/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
- package/assets/templates/base/quick-reference.md +297 -0
- package/assets/templates/base/skill-content.md +368 -0
- package/assets/templates/platforms/agent.json +21 -0
- package/assets/templates/platforms/augment.json +18 -0
- package/assets/templates/platforms/claude.json +21 -0
- package/assets/templates/platforms/codebuddy.json +21 -0
- package/assets/templates/platforms/codex.json +21 -0
- package/assets/templates/platforms/continue.json +21 -0
- package/assets/templates/platforms/copilot.json +21 -0
- package/assets/templates/platforms/cursor.json +21 -0
- package/assets/templates/platforms/droid.json +21 -0
- package/assets/templates/platforms/gemini.json +21 -0
- package/assets/templates/platforms/kilocode.json +21 -0
- package/assets/templates/platforms/kiro.json +21 -0
- package/assets/templates/platforms/opencode.json +21 -0
- package/assets/templates/platforms/qoder.json +21 -0
- package/assets/templates/platforms/roocode.json +21 -0
- package/assets/templates/platforms/trae.json +21 -0
- package/assets/templates/platforms/warp.json +18 -0
- package/assets/templates/platforms/windsurf.json +21 -0
- package/dist/index.js +10630 -0
- package/package.json +51 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Slide Search Core - BM25 search engine for slide design databases
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from math import log
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
|
|
13
|
+
# ============ CONFIGURATION ============
|
|
14
|
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
|
15
|
+
MAX_RESULTS = 3
|
|
16
|
+
|
|
17
|
+
CSV_CONFIG = {
|
|
18
|
+
"strategy": {
|
|
19
|
+
"file": "slide-strategies.csv",
|
|
20
|
+
"search_cols": ["strategy_name", "keywords", "goal", "audience", "narrative_arc"],
|
|
21
|
+
"output_cols": ["strategy_name", "keywords", "slide_count", "structure", "goal", "audience", "tone", "narrative_arc", "sources"]
|
|
22
|
+
},
|
|
23
|
+
"layout": {
|
|
24
|
+
"file": "slide-layouts.csv",
|
|
25
|
+
"search_cols": ["layout_name", "keywords", "use_case", "recommended_for"],
|
|
26
|
+
"output_cols": ["layout_name", "keywords", "use_case", "content_zones", "visual_weight", "cta_placement", "recommended_for", "avoid_for", "css_structure"]
|
|
27
|
+
},
|
|
28
|
+
"copy": {
|
|
29
|
+
"file": "slide-copy.csv",
|
|
30
|
+
"search_cols": ["formula_name", "keywords", "use_case", "emotion_trigger", "slide_type"],
|
|
31
|
+
"output_cols": ["formula_name", "keywords", "components", "use_case", "example_template", "emotion_trigger", "slide_type", "source"]
|
|
32
|
+
},
|
|
33
|
+
"chart": {
|
|
34
|
+
"file": "slide-charts.csv",
|
|
35
|
+
"search_cols": ["chart_type", "keywords", "best_for", "when_to_use", "slide_context"],
|
|
36
|
+
"output_cols": ["chart_type", "keywords", "best_for", "data_type", "when_to_use", "when_to_avoid", "max_categories", "slide_context", "css_implementation", "accessibility_notes"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
AVAILABLE_DOMAINS = list(CSV_CONFIG.keys())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============ BM25 IMPLEMENTATION ============
|
|
44
|
+
class BM25:
|
|
45
|
+
"""BM25 ranking algorithm for text search"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, k1=1.5, b=0.75):
|
|
48
|
+
self.k1 = k1
|
|
49
|
+
self.b = b
|
|
50
|
+
self.corpus = []
|
|
51
|
+
self.doc_lengths = []
|
|
52
|
+
self.avgdl = 0
|
|
53
|
+
self.idf = {}
|
|
54
|
+
self.doc_freqs = defaultdict(int)
|
|
55
|
+
self.N = 0
|
|
56
|
+
|
|
57
|
+
def tokenize(self, text):
|
|
58
|
+
"""Lowercase, split, remove punctuation, filter short words"""
|
|
59
|
+
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
|
60
|
+
return [w for w in text.split() if len(w) > 2]
|
|
61
|
+
|
|
62
|
+
def fit(self, documents):
|
|
63
|
+
"""Build BM25 index from documents"""
|
|
64
|
+
self.corpus = [self.tokenize(doc) for doc in documents]
|
|
65
|
+
self.N = len(self.corpus)
|
|
66
|
+
if self.N == 0:
|
|
67
|
+
return
|
|
68
|
+
self.doc_lengths = [len(doc) for doc in self.corpus]
|
|
69
|
+
self.avgdl = sum(self.doc_lengths) / self.N
|
|
70
|
+
|
|
71
|
+
for doc in self.corpus:
|
|
72
|
+
seen = set()
|
|
73
|
+
for word in doc:
|
|
74
|
+
if word not in seen:
|
|
75
|
+
self.doc_freqs[word] += 1
|
|
76
|
+
seen.add(word)
|
|
77
|
+
|
|
78
|
+
for word, freq in self.doc_freqs.items():
|
|
79
|
+
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
|
80
|
+
|
|
81
|
+
def score(self, query):
|
|
82
|
+
"""Score all documents against query"""
|
|
83
|
+
query_tokens = self.tokenize(query)
|
|
84
|
+
scores = []
|
|
85
|
+
|
|
86
|
+
for idx, doc in enumerate(self.corpus):
|
|
87
|
+
score = 0
|
|
88
|
+
doc_len = self.doc_lengths[idx]
|
|
89
|
+
term_freqs = defaultdict(int)
|
|
90
|
+
for word in doc:
|
|
91
|
+
term_freqs[word] += 1
|
|
92
|
+
|
|
93
|
+
for token in query_tokens:
|
|
94
|
+
if token in self.idf:
|
|
95
|
+
tf = term_freqs[token]
|
|
96
|
+
idf = self.idf[token]
|
|
97
|
+
numerator = tf * (self.k1 + 1)
|
|
98
|
+
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
|
99
|
+
score += idf * numerator / denominator
|
|
100
|
+
|
|
101
|
+
scores.append((idx, score))
|
|
102
|
+
|
|
103
|
+
return sorted(scores, key=lambda x: x[1], reverse=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ============ SEARCH FUNCTIONS ============
|
|
107
|
+
def _load_csv(filepath):
|
|
108
|
+
"""Load CSV and return list of dicts"""
|
|
109
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
110
|
+
return list(csv.DictReader(f))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
|
114
|
+
"""Core search function using BM25"""
|
|
115
|
+
if not filepath.exists():
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
data = _load_csv(filepath)
|
|
119
|
+
|
|
120
|
+
# Build documents from search columns
|
|
121
|
+
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
|
122
|
+
|
|
123
|
+
# BM25 search
|
|
124
|
+
bm25 = BM25()
|
|
125
|
+
bm25.fit(documents)
|
|
126
|
+
ranked = bm25.score(query)
|
|
127
|
+
|
|
128
|
+
# Get top results with score > 0
|
|
129
|
+
results = []
|
|
130
|
+
for idx, score in ranked[:max_results]:
|
|
131
|
+
if score > 0:
|
|
132
|
+
row = data[idx]
|
|
133
|
+
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
|
134
|
+
|
|
135
|
+
return results
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def detect_domain(query):
|
|
139
|
+
"""Auto-detect the most relevant domain from query"""
|
|
140
|
+
query_lower = query.lower()
|
|
141
|
+
|
|
142
|
+
domain_keywords = {
|
|
143
|
+
"strategy": ["pitch", "deck", "investor", "yc", "seed", "series", "demo", "sales", "webinar",
|
|
144
|
+
"conference", "board", "qbr", "all-hands", "duarte", "kawasaki", "structure"],
|
|
145
|
+
"layout": ["slide", "layout", "grid", "column", "title", "hero", "section", "cta",
|
|
146
|
+
"screenshot", "quote", "timeline", "comparison", "pricing", "team"],
|
|
147
|
+
"copy": ["headline", "copy", "formula", "aida", "pas", "hook", "cta", "benefit",
|
|
148
|
+
"objection", "proof", "testimonial", "urgency", "scarcity"],
|
|
149
|
+
"chart": ["chart", "graph", "bar", "line", "pie", "funnel", "metrics", "data",
|
|
150
|
+
"visualization", "kpi", "trend", "comparison", "heatmap", "gauge"]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
|
154
|
+
best = max(scores, key=scores.get)
|
|
155
|
+
return best if scores[best] > 0 else "strategy"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def search(query, domain=None, max_results=MAX_RESULTS):
|
|
159
|
+
"""Main search function with auto-domain detection"""
|
|
160
|
+
if domain is None:
|
|
161
|
+
domain = detect_domain(query)
|
|
162
|
+
|
|
163
|
+
config = CSV_CONFIG.get(domain, CSV_CONFIG["strategy"])
|
|
164
|
+
filepath = DATA_DIR / config["file"]
|
|
165
|
+
|
|
166
|
+
if not filepath.exists():
|
|
167
|
+
return {"error": f"File not found: {filepath}", "domain": domain}
|
|
168
|
+
|
|
169
|
+
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"domain": domain,
|
|
173
|
+
"query": query,
|
|
174
|
+
"file": config["file"],
|
|
175
|
+
"count": len(results),
|
|
176
|
+
"results": results
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def search_all(query, max_results=2):
|
|
181
|
+
"""Search across all domains for comprehensive results"""
|
|
182
|
+
all_results = {}
|
|
183
|
+
|
|
184
|
+
for domain in AVAILABLE_DOMAINS:
|
|
185
|
+
result = search(query, domain, max_results)
|
|
186
|
+
if result.get("count", 0) > 0:
|
|
187
|
+
all_results[domain] = result
|
|
188
|
+
|
|
189
|
+
return all_results
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ============ CONTEXTUAL SEARCH (Premium Slide System) ============
|
|
193
|
+
|
|
194
|
+
# New CSV configurations for decision system
|
|
195
|
+
DECISION_CSV_CONFIG = {
|
|
196
|
+
"layout-logic": {
|
|
197
|
+
"file": "slide-layout-logic.csv",
|
|
198
|
+
"key_col": "goal"
|
|
199
|
+
},
|
|
200
|
+
"typography": {
|
|
201
|
+
"file": "slide-typography.csv",
|
|
202
|
+
"key_col": "content_type"
|
|
203
|
+
},
|
|
204
|
+
"color-logic": {
|
|
205
|
+
"file": "slide-color-logic.csv",
|
|
206
|
+
"key_col": "emotion"
|
|
207
|
+
},
|
|
208
|
+
"backgrounds": {
|
|
209
|
+
"file": "slide-backgrounds.csv",
|
|
210
|
+
"key_col": "slide_type"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _load_decision_csv(csv_type):
|
|
216
|
+
"""Load a decision CSV and return as dict keyed by primary column."""
|
|
217
|
+
config = DECISION_CSV_CONFIG.get(csv_type)
|
|
218
|
+
if not config:
|
|
219
|
+
return {}
|
|
220
|
+
|
|
221
|
+
filepath = DATA_DIR / config["file"]
|
|
222
|
+
if not filepath.exists():
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
data = _load_csv(filepath)
|
|
226
|
+
return {row[config["key_col"]]: row for row in data if config["key_col"] in row}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_layout_for_goal(goal, previous_emotion=None):
|
|
230
|
+
"""
|
|
231
|
+
Get layout recommendation based on slide goal.
|
|
232
|
+
Uses slide-layout-logic.csv for decision.
|
|
233
|
+
"""
|
|
234
|
+
layouts = _load_decision_csv("layout-logic")
|
|
235
|
+
row = layouts.get(goal, layouts.get("features", {}))
|
|
236
|
+
|
|
237
|
+
result = dict(row) if row else {}
|
|
238
|
+
|
|
239
|
+
# Apply pattern-breaking logic
|
|
240
|
+
if result.get("break_pattern") == "true" and previous_emotion:
|
|
241
|
+
result["_pattern_break"] = True
|
|
242
|
+
result["_contrast_with"] = previous_emotion
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_typography_for_slide(slide_type, has_metrics=False, has_quote=False):
|
|
248
|
+
"""
|
|
249
|
+
Get typography recommendation based on slide content.
|
|
250
|
+
Uses slide-typography.csv for decision.
|
|
251
|
+
"""
|
|
252
|
+
typography = _load_decision_csv("typography")
|
|
253
|
+
|
|
254
|
+
if has_metrics:
|
|
255
|
+
return typography.get("metric-callout", {})
|
|
256
|
+
if has_quote:
|
|
257
|
+
return typography.get("quote-block", {})
|
|
258
|
+
|
|
259
|
+
# Map slide types to typography
|
|
260
|
+
type_map = {
|
|
261
|
+
"hero": "hero-statement",
|
|
262
|
+
"hook": "hero-statement",
|
|
263
|
+
"title": "title-only",
|
|
264
|
+
"problem": "subtitle-heavy",
|
|
265
|
+
"agitation": "metric-callout",
|
|
266
|
+
"solution": "subtitle-heavy",
|
|
267
|
+
"features": "feature-grid",
|
|
268
|
+
"proof": "metric-callout",
|
|
269
|
+
"traction": "data-insight",
|
|
270
|
+
"social": "quote-block",
|
|
271
|
+
"testimonial": "testimonial",
|
|
272
|
+
"pricing": "pricing",
|
|
273
|
+
"team": "team",
|
|
274
|
+
"cta": "cta-action",
|
|
275
|
+
"comparison": "comparison",
|
|
276
|
+
"timeline": "timeline",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
content_type = type_map.get(slide_type, "feature-grid")
|
|
280
|
+
return typography.get(content_type, {})
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_color_for_emotion(emotion):
|
|
284
|
+
"""
|
|
285
|
+
Get color treatment based on emotional beat.
|
|
286
|
+
Uses slide-color-logic.csv for decision.
|
|
287
|
+
"""
|
|
288
|
+
colors = _load_decision_csv("color-logic")
|
|
289
|
+
return colors.get(emotion, colors.get("clarity", {}))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_background_config(slide_type):
|
|
293
|
+
"""
|
|
294
|
+
Get background image configuration.
|
|
295
|
+
Uses slide-backgrounds.csv for decision.
|
|
296
|
+
"""
|
|
297
|
+
backgrounds = _load_decision_csv("backgrounds")
|
|
298
|
+
return backgrounds.get(slide_type, {})
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def should_use_full_bleed(slide_index, total_slides, emotion):
|
|
302
|
+
"""
|
|
303
|
+
Determine if slide should use full-bleed background.
|
|
304
|
+
Premium decks use 2-3 full-bleed slides strategically.
|
|
305
|
+
|
|
306
|
+
Rules:
|
|
307
|
+
1. Never consecutive full-bleed
|
|
308
|
+
2. One in first third, one in middle, one at end
|
|
309
|
+
3. Reserved for high-emotion beats (hope, urgency, fear)
|
|
310
|
+
"""
|
|
311
|
+
high_emotion_beats = ["hope", "urgency", "fear", "curiosity"]
|
|
312
|
+
|
|
313
|
+
if emotion not in high_emotion_beats:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
if total_slides < 3:
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
third = total_slides // 3
|
|
320
|
+
strategic_positions = [1, third, third * 2, total_slides - 1]
|
|
321
|
+
|
|
322
|
+
return slide_index in strategic_positions
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def calculate_pattern_break(slide_index, total_slides, previous_emotion=None):
|
|
326
|
+
"""
|
|
327
|
+
Determine if this slide should break the visual pattern.
|
|
328
|
+
Used for emotional contrast (Duarte Sparkline technique).
|
|
329
|
+
"""
|
|
330
|
+
# Pattern breaks at strategic positions
|
|
331
|
+
if total_slides < 5:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
# Break at 1/3 and 2/3 points
|
|
335
|
+
third = total_slides // 3
|
|
336
|
+
if slide_index in [third, third * 2]:
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
# Break when switching between frustration and hope
|
|
340
|
+
contrasting_emotions = {
|
|
341
|
+
"frustration": ["hope", "relief"],
|
|
342
|
+
"hope": ["frustration", "fear"],
|
|
343
|
+
"fear": ["hope", "relief"],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if previous_emotion in contrasting_emotions:
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def search_with_context(query, slide_position=1, total_slides=9, previous_emotion=None):
|
|
353
|
+
"""
|
|
354
|
+
Enhanced search that considers deck context.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
query: Search query
|
|
358
|
+
slide_position: Current slide index (1-based)
|
|
359
|
+
total_slides: Total slides in deck
|
|
360
|
+
previous_emotion: Emotion of previous slide (for contrast)
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Search results enriched with contextual recommendations
|
|
364
|
+
"""
|
|
365
|
+
# Get base results from existing BM25 search
|
|
366
|
+
base_results = search_all(query, max_results=2)
|
|
367
|
+
|
|
368
|
+
# Detect likely slide goal from query
|
|
369
|
+
goal = detect_domain(query.lower())
|
|
370
|
+
if "problem" in query.lower():
|
|
371
|
+
goal = "problem"
|
|
372
|
+
elif "solution" in query.lower():
|
|
373
|
+
goal = "solution"
|
|
374
|
+
elif "cta" in query.lower() or "call to action" in query.lower():
|
|
375
|
+
goal = "cta"
|
|
376
|
+
elif "hook" in query.lower() or "title" in query.lower():
|
|
377
|
+
goal = "hook"
|
|
378
|
+
elif "traction" in query.lower() or "metric" in query.lower():
|
|
379
|
+
goal = "traction"
|
|
380
|
+
|
|
381
|
+
# Enrich with contextual recommendations
|
|
382
|
+
context = {
|
|
383
|
+
"slide_position": slide_position,
|
|
384
|
+
"total_slides": total_slides,
|
|
385
|
+
"previous_emotion": previous_emotion,
|
|
386
|
+
"inferred_goal": goal,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Get layout recommendation
|
|
390
|
+
layout = get_layout_for_goal(goal, previous_emotion)
|
|
391
|
+
if layout:
|
|
392
|
+
context["recommended_layout"] = layout.get("layout_pattern")
|
|
393
|
+
context["layout_direction"] = layout.get("direction")
|
|
394
|
+
context["visual_weight"] = layout.get("visual_weight")
|
|
395
|
+
context["use_background_image"] = layout.get("use_bg_image") == "true"
|
|
396
|
+
|
|
397
|
+
# Get typography recommendation
|
|
398
|
+
typography = get_typography_for_slide(goal)
|
|
399
|
+
if typography:
|
|
400
|
+
context["typography"] = {
|
|
401
|
+
"primary_size": typography.get("primary_size"),
|
|
402
|
+
"secondary_size": typography.get("secondary_size"),
|
|
403
|
+
"weight_contrast": typography.get("weight_contrast"),
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Get color treatment
|
|
407
|
+
emotion = layout.get("emotion", "clarity") if layout else "clarity"
|
|
408
|
+
color = get_color_for_emotion(emotion)
|
|
409
|
+
if color:
|
|
410
|
+
context["color_treatment"] = {
|
|
411
|
+
"background": color.get("background"),
|
|
412
|
+
"text_color": color.get("text_color"),
|
|
413
|
+
"accent_usage": color.get("accent_usage"),
|
|
414
|
+
"card_style": color.get("card_style"),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Calculate pattern breaking
|
|
418
|
+
context["should_break_pattern"] = calculate_pattern_break(
|
|
419
|
+
slide_position, total_slides, previous_emotion
|
|
420
|
+
)
|
|
421
|
+
context["should_use_full_bleed"] = should_use_full_bleed(
|
|
422
|
+
slide_position, total_slides, emotion
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Get background config if needed
|
|
426
|
+
if context.get("use_background_image"):
|
|
427
|
+
bg_config = get_background_config(goal)
|
|
428
|
+
if bg_config:
|
|
429
|
+
context["background"] = {
|
|
430
|
+
"image_category": bg_config.get("image_category"),
|
|
431
|
+
"overlay_style": bg_config.get("overlay_style"),
|
|
432
|
+
"search_keywords": bg_config.get("search_keywords"),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Suggested animation classes
|
|
436
|
+
animation_map = {
|
|
437
|
+
"hook": "animate-fade-up",
|
|
438
|
+
"problem": "animate-fade-up",
|
|
439
|
+
"agitation": "animate-count animate-stagger",
|
|
440
|
+
"solution": "animate-scale",
|
|
441
|
+
"features": "animate-stagger",
|
|
442
|
+
"traction": "animate-chart animate-count",
|
|
443
|
+
"proof": "animate-stagger-scale",
|
|
444
|
+
"social": "animate-fade-up",
|
|
445
|
+
"cta": "animate-pulse",
|
|
446
|
+
}
|
|
447
|
+
context["animation_class"] = animation_map.get(goal, "animate-fade-up")
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
"query": query,
|
|
451
|
+
"context": context,
|
|
452
|
+
"base_results": base_results,
|
|
453
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate token usage in codebase
|
|
4
|
+
* Finds hardcoded values that should use design tokens
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node validate-tokens.cjs --dir src/
|
|
8
|
+
* node validate-tokens.cjs --dir src/ --fix
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse command line arguments
|
|
16
|
+
*/
|
|
17
|
+
function parseArgs() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const options = {
|
|
20
|
+
dir: null,
|
|
21
|
+
fix: false,
|
|
22
|
+
ignore: ['node_modules', '.git', 'dist', 'build', '.next']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === '--dir' || args[i] === '-d') {
|
|
27
|
+
options.dir = args[++i];
|
|
28
|
+
} else if (args[i] === '--fix') {
|
|
29
|
+
options.fix = true;
|
|
30
|
+
} else if (args[i] === '--ignore' || args[i] === '-i') {
|
|
31
|
+
options.ignore.push(args[++i]);
|
|
32
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
33
|
+
console.log(`
|
|
34
|
+
Usage: node validate-tokens.cjs [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
-d, --dir <path> Directory to scan (required)
|
|
38
|
+
--fix Show suggested fixes (no auto-fix)
|
|
39
|
+
-i, --ignore <dir> Additional directories to ignore
|
|
40
|
+
-h, --help Show this help
|
|
41
|
+
|
|
42
|
+
Checks for:
|
|
43
|
+
- Hardcoded hex colors (#RGB, #RRGGBB)
|
|
44
|
+
- Hardcoded pixel values (except 0, 1px)
|
|
45
|
+
- Hardcoded rem values in CSS
|
|
46
|
+
`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Patterns to detect hardcoded values
|
|
56
|
+
*/
|
|
57
|
+
const patterns = {
|
|
58
|
+
hexColor: {
|
|
59
|
+
regex: /#([0-9A-Fa-f]{3}){1,2}\b/g,
|
|
60
|
+
message: 'Hardcoded hex color',
|
|
61
|
+
suggestion: 'Use var(--color-*) token'
|
|
62
|
+
},
|
|
63
|
+
rgbColor: {
|
|
64
|
+
regex: /rgb\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)/gi,
|
|
65
|
+
message: 'Hardcoded RGB color',
|
|
66
|
+
suggestion: 'Use var(--color-*) token'
|
|
67
|
+
},
|
|
68
|
+
pixelValue: {
|
|
69
|
+
regex: /:\s*(\d{2,})px/g, // 2+ digit px values
|
|
70
|
+
message: 'Hardcoded pixel value',
|
|
71
|
+
suggestion: 'Use var(--space-*) or var(--radius-*) token'
|
|
72
|
+
},
|
|
73
|
+
remValue: {
|
|
74
|
+
regex: /:\s*\d+\.?\d*rem(?![^{]*\$value)/g, // rem not in token definition
|
|
75
|
+
message: 'Hardcoded rem value',
|
|
76
|
+
suggestion: 'Use var(--space-*) or var(--font-size-*) token'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* File extensions to scan
|
|
82
|
+
*/
|
|
83
|
+
const extensions = ['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.vue', '.svelte'];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Files/patterns to skip
|
|
87
|
+
*/
|
|
88
|
+
const skipPatterns = [
|
|
89
|
+
/\.min\.(css|js)$/,
|
|
90
|
+
/tailwind\.config/,
|
|
91
|
+
/globals\.css/, // Token definitions
|
|
92
|
+
/tokens\.(css|json)/
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get all files recursively
|
|
97
|
+
*/
|
|
98
|
+
function getFiles(dir, ignore, files = []) {
|
|
99
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (!ignore.includes(entry.name)) {
|
|
106
|
+
getFiles(fullPath, ignore, files);
|
|
107
|
+
}
|
|
108
|
+
} else if (entry.isFile()) {
|
|
109
|
+
const ext = path.extname(entry.name);
|
|
110
|
+
if (extensions.includes(ext)) {
|
|
111
|
+
files.push(fullPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return files;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if file should be skipped
|
|
121
|
+
*/
|
|
122
|
+
function shouldSkip(filePath) {
|
|
123
|
+
return skipPatterns.some(pattern => pattern.test(filePath));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Scan file for violations
|
|
128
|
+
*/
|
|
129
|
+
function scanFile(filePath) {
|
|
130
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
const violations = [];
|
|
133
|
+
|
|
134
|
+
lines.forEach((line, index) => {
|
|
135
|
+
// Skip comments
|
|
136
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('/*')) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Skip lines that already use CSS variables
|
|
141
|
+
if (line.includes('var(--')) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [name, pattern] of Object.entries(patterns)) {
|
|
146
|
+
const matches = line.match(pattern.regex);
|
|
147
|
+
if (matches) {
|
|
148
|
+
matches.forEach(match => {
|
|
149
|
+
// Skip common exceptions
|
|
150
|
+
if (name === 'hexColor' && ['#000', '#fff', '#FFF', '#000000', '#FFFFFF'].includes(match.toUpperCase())) {
|
|
151
|
+
return; // Skip black/white, often intentional
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
violations.push({
|
|
155
|
+
file: filePath,
|
|
156
|
+
line: index + 1,
|
|
157
|
+
column: line.indexOf(match) + 1,
|
|
158
|
+
value: match,
|
|
159
|
+
type: name,
|
|
160
|
+
message: pattern.message,
|
|
161
|
+
suggestion: pattern.suggestion,
|
|
162
|
+
context: line.trim().substring(0, 80)
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return violations;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format violation report
|
|
174
|
+
*/
|
|
175
|
+
function formatReport(violations) {
|
|
176
|
+
if (violations.length === 0) {
|
|
177
|
+
return 'ā
No token violations found';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let report = `ā ļø Found ${violations.length} potential token violations:\n\n`;
|
|
181
|
+
|
|
182
|
+
// Group by file
|
|
183
|
+
const byFile = {};
|
|
184
|
+
violations.forEach(v => {
|
|
185
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
186
|
+
byFile[v.file].push(v);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
for (const [file, fileViolations] of Object.entries(byFile)) {
|
|
190
|
+
report += `š ${file}\n`;
|
|
191
|
+
fileViolations.forEach(v => {
|
|
192
|
+
report += ` Line ${v.line}: ${v.message}\n`;
|
|
193
|
+
report += ` Found: ${v.value}\n`;
|
|
194
|
+
report += ` Suggestion: ${v.suggestion}\n`;
|
|
195
|
+
report += ` Context: ${v.context}\n\n`;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Summary
|
|
200
|
+
const byType = {};
|
|
201
|
+
violations.forEach(v => {
|
|
202
|
+
byType[v.type] = (byType[v.type] || 0) + 1;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
report += `\nš Summary:\n`;
|
|
206
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
207
|
+
report += ` ${patterns[type].message}: ${count}\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return report;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Main
|
|
215
|
+
*/
|
|
216
|
+
function main() {
|
|
217
|
+
const options = parseArgs();
|
|
218
|
+
|
|
219
|
+
if (!options.dir) {
|
|
220
|
+
console.error('Error: --dir is required');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const dirPath = path.resolve(process.cwd(), options.dir);
|
|
225
|
+
|
|
226
|
+
if (!fs.existsSync(dirPath)) {
|
|
227
|
+
console.error(`Error: Directory not found: ${dirPath}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`Scanning ${dirPath} for token violations...\n`);
|
|
232
|
+
|
|
233
|
+
const files = getFiles(dirPath, options.ignore);
|
|
234
|
+
const allViolations = [];
|
|
235
|
+
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
if (shouldSkip(file)) continue;
|
|
238
|
+
|
|
239
|
+
const violations = scanFile(file);
|
|
240
|
+
allViolations.push(...violations);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(formatReport(allViolations));
|
|
244
|
+
|
|
245
|
+
// Exit with error code if violations found
|
|
246
|
+
if (allViolations.length > 0) {
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main();
|