mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-04-25 11:18:17 +00:00
feat(skills): add design-system skill
This commit is contained in:
parent
ace775926f
commit
4f78df83ef
244
.claude/skills/design-system/SKILL.md
Normal file
244
.claude/skills/design-system/SKILL.md
Normal file
@ -0,0 +1,244 @@
|
||||
---
|
||||
name: ckm:design-system
|
||||
description: Token architecture, component specifications, and slide generation. Three-layer tokens (primitive→semantic→component), CSS variables, spacing/typography scales, component specs, strategic slide creation. Use for design tokens, systematic design, brand-compliant presentations.
|
||||
argument-hint: "[component or token]"
|
||||
license: MIT
|
||||
metadata:
|
||||
author: claudekit
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Design System
|
||||
|
||||
Token architecture, component specifications, systematic design, slide generation.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Design token creation
|
||||
- Component state definitions
|
||||
- CSS variable systems
|
||||
- Spacing/typography scales
|
||||
- Design-to-code handoff
|
||||
- Tailwind theme configuration
|
||||
- **Slide/presentation generation**
|
||||
|
||||
## Token Architecture
|
||||
|
||||
Load: `references/token-architecture.md`
|
||||
|
||||
### Three-Layer Structure
|
||||
|
||||
```
|
||||
Primitive (raw values)
|
||||
↓
|
||||
Semantic (purpose aliases)
|
||||
↓
|
||||
Component (component-specific)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```css
|
||||
/* Primitive */
|
||||
--color-blue-600: #2563EB;
|
||||
|
||||
/* Semantic */
|
||||
--color-primary: var(--color-blue-600);
|
||||
|
||||
/* Component */
|
||||
--button-bg: var(--color-primary);
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Generate tokens:**
|
||||
```bash
|
||||
node scripts/generate-tokens.cjs --config tokens.json -o tokens.css
|
||||
```
|
||||
|
||||
**Validate usage:**
|
||||
```bash
|
||||
node scripts/validate-tokens.cjs --dir src/
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Token Architecture | `references/token-architecture.md` |
|
||||
| Primitive Tokens | `references/primitive-tokens.md` |
|
||||
| Semantic Tokens | `references/semantic-tokens.md` |
|
||||
| Component Tokens | `references/component-tokens.md` |
|
||||
| Component Specs | `references/component-specs.md` |
|
||||
| States & Variants | `references/states-and-variants.md` |
|
||||
| Tailwind Integration | `references/tailwind-integration.md` |
|
||||
|
||||
## Component Spec Pattern
|
||||
|
||||
| Property | Default | Hover | Active | Disabled |
|
||||
|----------|---------|-------|--------|----------|
|
||||
| Background | primary | primary-dark | primary-darker | muted |
|
||||
| Text | white | white | white | muted-fg |
|
||||
| Border | none | none | none | muted-border |
|
||||
| Shadow | sm | md | none | none |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `generate-tokens.cjs` | Generate CSS from JSON token config |
|
||||
| `validate-tokens.cjs` | Check for hardcoded values in code |
|
||||
| `search-slides.py` | BM25 search + contextual recommendations |
|
||||
| `slide-token-validator.py` | Validate slide HTML for token compliance |
|
||||
| `fetch-background.py` | Fetch images from Pexels/Unsplash |
|
||||
|
||||
## Templates
|
||||
|
||||
| Template | Purpose |
|
||||
|----------|---------|
|
||||
| `design-tokens-starter.json` | Starter JSON with three-layer structure |
|
||||
|
||||
## Integration
|
||||
|
||||
**With brand:** Extract primitives from brand colors/typography
|
||||
**With ui-styling:** Component tokens → Tailwind config
|
||||
|
||||
**Skill Dependencies:** brand, ui-styling
|
||||
**Primary Agents:** ui-ux-designer, frontend-developer
|
||||
|
||||
## Slide System
|
||||
|
||||
Brand-compliant presentations using design tokens + Chart.js + contextual decision system.
|
||||
|
||||
### Source of Truth
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/brand-guidelines.md` | Brand identity, voice, colors |
|
||||
| `assets/design-tokens.json` | Token definitions (primitive→semantic→component) |
|
||||
| `assets/design-tokens.css` | CSS variables (import in slides) |
|
||||
| `assets/css/slide-animations.css` | CSS animation library |
|
||||
|
||||
### Slide Search (BM25)
|
||||
|
||||
```bash
|
||||
# Basic search (auto-detect domain)
|
||||
python scripts/search-slides.py "investor pitch"
|
||||
|
||||
# Domain-specific search
|
||||
python scripts/search-slides.py "problem agitation" -d copy
|
||||
python scripts/search-slides.py "revenue growth" -d chart
|
||||
|
||||
# Contextual search (Premium System)
|
||||
python scripts/search-slides.py "problem slide" --context --position 2 --total 9
|
||||
python scripts/search-slides.py "cta" --context --position 9 --prev-emotion frustration
|
||||
```
|
||||
|
||||
### Decision System CSVs
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `data/slide-strategies.csv` | 15 deck structures + emotion arcs + sparkline beats |
|
||||
| `data/slide-layouts.csv` | 25 layouts + component variants + animations |
|
||||
| `data/slide-layout-logic.csv` | Goal → Layout + break_pattern flag |
|
||||
| `data/slide-typography.csv` | Content type → Typography scale |
|
||||
| `data/slide-color-logic.csv` | Emotion → Color treatment |
|
||||
| `data/slide-backgrounds.csv` | Slide type → Image category (Pexels/Unsplash) |
|
||||
| `data/slide-copy.csv` | 25 copywriting formulas (PAS, AIDA, FAB) |
|
||||
| `data/slide-charts.csv` | 25 chart types with Chart.js config |
|
||||
|
||||
### Contextual Decision Flow
|
||||
|
||||
```
|
||||
1. Parse goal/context
|
||||
↓
|
||||
2. Search slide-strategies.csv → Get strategy + emotion beats
|
||||
↓
|
||||
3. For each slide:
|
||||
a. Query slide-layout-logic.csv → layout + break_pattern
|
||||
b. Query slide-typography.csv → type scale
|
||||
c. Query slide-color-logic.csv → color treatment
|
||||
d. Query slide-backgrounds.csv → image if needed
|
||||
e. Apply animation class from slide-animations.css
|
||||
↓
|
||||
4. Generate HTML with design tokens
|
||||
↓
|
||||
5. Validate with slide-token-validator.py
|
||||
```
|
||||
|
||||
### Pattern Breaking (Duarte Sparkline)
|
||||
|
||||
Premium decks alternate between emotions for engagement:
|
||||
```
|
||||
"What Is" (frustration) ↔ "What Could Be" (hope)
|
||||
```
|
||||
|
||||
System calculates pattern breaks at 1/3 and 2/3 positions.
|
||||
|
||||
### Slide Requirements
|
||||
|
||||
**ALL slides MUST:**
|
||||
1. Import `assets/design-tokens.css` - single source of truth
|
||||
2. Use CSS variables: `var(--color-primary)`, `var(--slide-bg)`, etc.
|
||||
3. Use Chart.js for charts (NOT CSS-only bars)
|
||||
4. Include navigation (keyboard arrows, click, progress bar)
|
||||
5. Center align content
|
||||
6. Focus on persuasion/conversion
|
||||
|
||||
### Chart.js Integration
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<canvas id="revenueChart"></canvas>
|
||||
<script>
|
||||
new Chart(document.getElementById('revenueChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
data: [5, 12, 28, 45],
|
||||
borderColor: '#FF6B6B', // Use brand coral
|
||||
backgroundColor: 'rgba(255, 107, 107, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Token Compliance
|
||||
|
||||
```css
|
||||
/* CORRECT - uses token */
|
||||
background: var(--slide-bg);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--typography-font-heading);
|
||||
|
||||
/* WRONG - hardcoded */
|
||||
background: #0D0D0D;
|
||||
color: #FF6B6B;
|
||||
font-family: 'Space Grotesk';
|
||||
```
|
||||
|
||||
### Reference Implementation
|
||||
|
||||
Working example with all features:
|
||||
```
|
||||
assets/designs/slides/claudekit-pitch-251223.html
|
||||
```
|
||||
|
||||
### Command
|
||||
|
||||
```bash
|
||||
/slides:create "10-slide investor pitch for ClaudeKit Marketing"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Never use raw hex in components - always reference tokens
|
||||
2. Semantic layer enables theme switching (light/dark)
|
||||
3. Component tokens enable per-component customization
|
||||
4. Use HSL format for opacity control
|
||||
5. Document every token's purpose
|
||||
6. **Slides must import design-tokens.css and use var() exclusively**
|
||||
11
.claude/skills/design-system/data/slide-backgrounds.csv
Normal file
11
.claude/skills/design-system/data/slide-backgrounds.csv
Normal file
@ -0,0 +1,11 @@
|
||||
slide_type,image_category,overlay_style,text_placement,image_sources,search_keywords
|
||||
hero,abstract-tech,gradient-dark,center,pexels:unsplash,technology abstract gradient dark
|
||||
vision,future-workspace,gradient-brand,left,pexels:unsplash,futuristic office modern workspace
|
||||
team,professional-people,gradient-dark,bottom,pexels:unsplash,business team professional diverse
|
||||
testimonial,office-environment,blur-dark,center,pexels:unsplash,modern office workspace bright
|
||||
cta,celebration-success,gradient-brand,center,pexels:unsplash,success celebration achievement
|
||||
problem,frustration-pain,desaturate-dark,center,pexels:unsplash,stress frustration problem dark
|
||||
solution,breakthrough-moment,gradient-accent,right,pexels:unsplash,breakthrough success innovation light
|
||||
hook,attention-grabbing,gradient-dark,center,pexels:unsplash,dramatic abstract attention bold
|
||||
social,community-connection,blur-dark,center,pexels:unsplash,community collaboration connection
|
||||
demo,product-showcase,gradient-dark,left,pexels:unsplash,technology product showcase clean
|
||||
|
26
.claude/skills/design-system/data/slide-charts.csv
Normal file
26
.claude/skills/design-system/data/slide-charts.csv
Normal file
@ -0,0 +1,26 @@
|
||||
id,chart_type,keywords,best_for,data_type,when_to_use,when_to_avoid,max_categories,slide_context,css_implementation,accessibility_notes,sources
|
||||
1,Bar Chart Vertical,"bar, vertical, comparison, categories, ranking",Comparing values across categories,Categorical discrete,"Comparing 3-12 categories, showing ranking, highlighting differences",Continuous time data trends,12,Traction metrics feature comparison,"Chart.js or CSS flexbox with height percentage bars",Always label axes include values,Atlassian Data Charts
|
||||
2,Bar Chart Horizontal,"horizontal bar, ranking, long labels, categories",Categories with long names ranking,Categorical discrete,"Long category names, 5+ items, reading left-to-right natural",Few categories time series,15,Team performance competitor analysis,"CSS flexbox with width percentage bars",Natural reading direction for labels,Datylon Blog
|
||||
3,Line Chart,"line, trend, time series, growth, change over time",Showing trends over continuous time,Time series continuous,"Time-based data, showing growth trajectory, 10+ data points",Categorical comparisons,50+ points,Revenue growth MRR user growth,"Chart.js line or SVG path element",Include data point markers for screen readers,Tableau Best Practices
|
||||
4,Area Chart,"area, cumulative, volume, trend, filled line",Showing volume or magnitude over time,Time series cumulative,"Emphasizing magnitude, showing cumulative totals, comparing totals",Precise value comparison,3-4 series,Revenue breakdown market share over time,"Chart.js area or SVG path with fill",Use patterns not just colors for series,Data Visualization Guide
|
||||
5,Pie Chart,"pie, composition, percentage, parts, whole",Showing parts of a whole,Proportional percentage,"2-5 slices, showing simple composition, adds to 100%",More than 6 categories precise comparison,6 max,Market share budget allocation simple splits,"CSS conic-gradient or Chart.js pie",Never use 3D always include percentages,FusionCharts Blog
|
||||
6,Donut Chart,"donut, composition, percentage, center metric",Parts of whole with center highlight,Proportional percentage,"Like pie but need center space for key metric, 2-5 segments",Many categories,6 max,Composition with key stat in center,"CSS conic-gradient with inner circle",Same as pie include legend,Modern alternative to pie
|
||||
7,Stacked Bar,"stacked, composition, comparison, breakdown",Comparing composition across categories,Categorical + proportional,"Showing composition AND comparison, segment contribution",Too many segments precise values,5 segments,Revenue by segment across quarters,"Chart.js stacked bar or CSS nested divs",Order segments consistently use legend,Atlassian Data Charts
|
||||
8,Grouped Bar,"grouped, clustered, side by side, multi-series",Comparing multiple metrics per category,Multi-series categorical,"Direct comparison of 2-3 metrics per category",Too many groups (>4) or categories (>8),4 groups,Feature comparison pricing tiers,"Chart.js grouped bar CSS grid bars",Color code consistently across groups,Data Visualization Best Practices
|
||||
9,100% Stacked Bar,"100%, proportion, normalized, percentage",Comparing proportions across categories,Proportional comparative,"Comparing percentage breakdown across categories, not absolute values",Showing absolute values,5 segments,Market share comparison percentage breakdown,"CSS flexbox 100% width segments",Clearly indicate percentage scale,Proportional analysis
|
||||
10,Funnel Chart,"funnel, conversion, stages, drop-off, pipeline",Showing conversion or drop-off through stages,Sequential stage-based,"Sales funnel, conversion rates, sequential process with decreasing values",Non-sequential data equal stages,6-8 stages,User conversion sales pipeline,"CSS trapezoid shapes or SVG",Label each stage with count and percentage,Marketing/Sales standard
|
||||
11,Gauge Chart,"gauge, progress, goal, target, kpi",Showing progress toward a goal,Single metric vs target,"Single KPI progress, goal completion, health scores",Multiple metrics,1 metric,Goal progress health score,"CSS conic-gradient or arc SVG",Include numeric value not just visual,Dashboard widgets
|
||||
12,Sparkline,"sparkline, mini, inline, trend, compact",Showing trend in minimal space,Time series inline,"Inline metrics, table cells, compact trend indication",Detailed analysis,N/A,Metric cards with trend indicator,SVG path or canvas inline,Supplement with text for accessibility,Edward Tufte
|
||||
13,Scatter Plot,"scatter, correlation, relationship, distribution",Showing relationship between two variables,Bivariate continuous,"Correlation analysis, pattern detection, outlier identification",Categorical data simple comparisons,100+ points,Correlation analysis segmentation,Canvas or SVG circles positioned,Include trend line if meaningful,Statistical visualization
|
||||
14,Bubble Chart,"bubble, three variables, scatter, size",Showing three variables simultaneously,Trivariate continuous,"Three-variable comparison, population/size matters",Simple comparisons,30-50 bubbles,Market analysis with size dimension,"SVG circles with varying radius",Legend for size scale essential,Data Visualization Guide
|
||||
15,Heatmap,"heatmap, matrix, intensity, correlation, grid",Showing intensity across two dimensions,Matrix intensity,"Large data matrices, time-day patterns, correlation matrices",Few data points,Unlimited grid,Usage patterns correlation matrices,CSS grid with background-color intensity,Use colorblind-safe gradients,Datylon Blog
|
||||
16,Waterfall Chart,"waterfall, bridge, contribution, breakdown",Showing how values add to a total,Cumulative contribution,"Financial analysis, showing positive/negative contributions",Non-additive data,10-15 items,Revenue bridge profit breakdown,"CSS positioned bars with connectors",Clear positive/negative color coding,Financial reporting standard
|
||||
17,Treemap,"treemap, hierarchy, nested, proportion",Showing hierarchical proportional data,Hierarchical proportional,"Nested categories, space-efficient proportions, 2 levels max",Simple comparisons few items,50+ items,Budget breakdown category analysis,"CSS grid with calculated areas",Include text labels on larger segments,Ben Shneiderman
|
||||
18,Radar Chart,"radar, spider, multi-metric, profile",Comparing multiple metrics for single item,Multi-metric profile,"Comparing 5-8 metrics for one or two items, skill profiles",More than 3 items to compare,8 axes max,Feature profile skill assessment,SVG polygon on axes,Ensure scale is clear and consistent,Profile comparison
|
||||
19,Bullet Chart,"bullet, target, actual, performance",Showing actual vs target with ranges,KPI with target,"Progress against target with qualitative ranges",Simple goal tracking,1-3 per slide,KPI performance with targets,"CSS layered bars with markers",Clearly label target and actual,Stephen Few
|
||||
20,Timeline,"timeline, chronology, history, milestones",Showing events over time,Event-based temporal,"History roadmap milestones, showing progression",Quantitative comparison,10-15 events,Company history product roadmap,"CSS flexbox with positioned markers",Ensure logical reading order,Chronological visualization
|
||||
21,Sankey Diagram,"sankey, flow, distribution, connections",Showing flow or distribution between nodes,Flow distribution,"Showing how values flow from source to destination",Simple distributions,15-20 nodes,User flow budget flow,D3.js or dedicated library,Alternative text description essential,Complex flow visualization
|
||||
22,KPI Card,"kpi, metric, number, stat, scorecard",Highlighting single important metric,Single metric,"Dashboard hero metrics, emphasizing one key number",Showing trends or comparisons,1 number,Main KPI highlight,"Large font-size centered number",Include trend context if relevant,Dashboard design
|
||||
23,Progress Bar,"progress, completion, percentage, bar",Showing completion percentage,Single percentage,"Simple progress indication, goal completion",Multiple goals comparison,1 per context,Project completion goal progress,"CSS width with percentage gradient",Include numeric percentage,UI/UX standard
|
||||
24,Comparison Table,"table, comparison, matrix, features",Detailed feature or value comparison,Multi-attribute categorical,"Detailed comparison, many attributes, exact values matter",Visual impact storytelling,10-15 rows,Feature matrix pricing comparison,"HTML table with CSS styling",Proper table headers and scope,Information design
|
||||
25,Icon Array,"icon array, pictogram, proportion, visual",Showing proportions with visual metaphor,Proportional visual,"Making statistics tangible (e.g. 1 in 10 people), visual impact",Precise values large numbers,100 icons,Statistics visualization impact slides,"CSS grid or flexbox with icons",Describe proportion in text,ISOTYPE Otto Neurath
|
||||
|
14
.claude/skills/design-system/data/slide-color-logic.csv
Normal file
14
.claude/skills/design-system/data/slide-color-logic.csv
Normal file
@ -0,0 +1,14 @@
|
||||
emotion,background,text_color,accent_usage,use_full_bleed,gradient,card_style
|
||||
frustration,dark-surface,foreground,minimal,false,none,subtle-border
|
||||
hope,accent-bleed,dark,none,true,none,none
|
||||
fear,dark-background,primary,stats-only,false,none,glow-primary
|
||||
relief,surface,foreground,icons,false,none,accent-bar
|
||||
trust,surface-elevated,foreground,metrics,false,none,subtle-border
|
||||
urgency,gradient,white,cta-button,true,primary,none
|
||||
curiosity,dark-glow,gradient-text,badge,false,glow,glow-secondary
|
||||
confidence,surface,foreground,chart-accent,false,none,none
|
||||
warmth,dark-surface,foreground,avatar-ring,false,none,none
|
||||
evaluation,surface-elevated,foreground,highlight,false,none,comparison
|
||||
narrative,dark-background,foreground-secondary,timeline-dots,false,none,none
|
||||
clarity,surface,foreground,icons,false,none,feature-card
|
||||
interest,dark-glow,foreground,demo-highlight,false,glow,none
|
||||
|
26
.claude/skills/design-system/data/slide-copy.csv
Normal file
26
.claude/skills/design-system/data/slide-copy.csv
Normal file
@ -0,0 +1,26 @@
|
||||
id,formula_name,keywords,components,use_case,example_template,emotion_trigger,slide_type,source
|
||||
1,AIDA,"aida, attention, interest, desire, action",Attention→Interest→Desire→Action,Lead-gen CTAs general persuasion,"{Attention hook} → {Interesting detail} → {Desirable outcome} → {Action step}",Curiosity→Engagement→Want→Urgency,CTA slides,Classic copywriting 1898
|
||||
2,PAS,"pas, problem, agitation, solution, dan kennedy",Problem→Agitate→Solution,Sales pages problem slides most reliable,"You're struggling with {problem}. It's costing you {agitation}. {Solution} fixes this.",Frustration→Fear→Relief,Problem slides,Dan Kennedy
|
||||
3,4Ps,"4ps, promise, picture, proof, push, ray edwards",Promise→Picture→Proof→Push,Home pages lead-gen,"{Promise benefit} → {Picture future state} → {Proof it works} → {Push to act}",Hope→Vision→Trust→Action,Solution slides,Ray Edwards
|
||||
4,Before-After-Bridge,"bab, before, after, bridge, transformation",Before→After→Bridge,Transformation case studies,"Before: {old state}. After: {new state}. Bridge: {how to get there}",Pain→Pleasure→Path,Before/after slides,Copywriting classic
|
||||
5,QUEST,"quest, qualify, understand, educate, stimulate, transition",Qualify→Understand→Educate→Stimulate→Transition,Matching solution to prospect,"{Qualify audience} → {Show understanding} → {Educate on solution} → {Stimulate desire} → {Transition to CTA}",Recognition→Empathy→Learning→Excitement,Educational slides,Michel Fortin
|
||||
6,Star-Story-Solution,"star, story, solution, narrative",Star→Story→Solution,Personality brands info products,"{Introduce character} → {Tell their struggle} → {Reveal their solution}",Connection→Empathy→Hope,Case study slides,CopyHackers
|
||||
7,Feature-Advantage-Benefit,"fab, feature, advantage, benefit",Feature→Advantage→Benefit,Feature explanations product slides,"{Feature name}: {What it does} → {Why that matters} → {How it helps you}",Curiosity→Understanding→Desire,Feature slides,Sales training classic
|
||||
8,What If,"what if, imagination, possibility, hook",What if + Possibility,Opening hooks vision slides,"What if you could {desirable outcome} without {common obstacle}?",Wonder→Possibility,Title problem slides,Headline formula
|
||||
9,How To,"how to, tutorial, guide, instruction",How to + Specific outcome,Educational actionable content,"How to {achieve specific result} in {timeframe or steps}",Curiosity→Empowerment,Educational slides,Headline formula
|
||||
10,Number List,"number, list, reasons, ways, tips",Number + Topic + Promise,Scannable benefit lists,"{Number} {Ways/Reasons/Tips} to {achieve outcome}",Curiosity→Completeness,Feature summary slides,Content marketing
|
||||
11,Question Hook,"question, hook, curiosity, engagement",Question that implies answer,Opening engagement slides,"{Question that reader answers yes to}? Here's how.",Recognition→Curiosity,Opening slides,Rhetorical technique
|
||||
12,Proof Stack,"proof, evidence, credibility, stats",Stat→Source→Implication,Building credibility trust,"{Impressive stat} (Source: {credible source}). This means {implication for audience}.",Trust→Validation,Traction proof slides,Social proof theory
|
||||
13,Future Pacing,"future, vision, imagine, picture this",Imagine + Future state,Vision and aspiration slides,"Imagine: {desirable future scenario}. That's what {solution} delivers.",Aspiration→Desire,Solution CTA slides,NLP technique
|
||||
14,Social Proof,"social proof, testimonial, customers, trust",Who + Result + Quote,Credibility through others,"{Customer name} increased {metric} by {amount}. '{Quote about experience}'",Trust→FOMO,Testimonial slides,Robert Cialdini
|
||||
15,Scarcity Urgency,"scarcity, urgency, limited, deadline, fomo",Limited + Deadline + Consequence,Driving action urgency,"Only {quantity} available. Offer ends {date}. {Consequence of missing out}.",Fear of loss→Action,CTA closing slides,Cialdini influence
|
||||
16,Cost of Inaction,"cost, inaction, consequence, loss",Current cost + Future cost + Comparison,Motivating change,"Every {timeframe} without {solution} costs you {quantified loss}. That's {larger number} per year.",Loss aversion→Urgency,Problem agitation slides,Loss aversion psychology
|
||||
17,Simple Benefit,"benefit, value, outcome, result",You get + Specific benefit,Clear value communication,"{Solution}: You get {specific tangible benefit}.",Clarity→Desire,Any slide,Direct response
|
||||
18,Objection Preempt,"objection, concern, but, however, faq",Objection + Response + Proof,"Handling concerns proactively","You might think {objection}. Actually, {counter with proof}.",Doubt→Resolution,FAQ objection slides,Sales training
|
||||
19,Comparison Frame,"comparison, versus, than, better, alternative",Us vs Them + Specific difference,Competitive positioning,"{Competitor approach}: {limitation}. {Our approach}: {advantage}.",Evaluation→Preference,Comparison slides,Positioning strategy
|
||||
20,Pain-Claim-Gain,"pcg, pain, claim, gain",Pain point→Bold claim→Specific gain,Concise value proposition,"{Pain point}? {Bold claim about solution}. Result: {specific gain}.",Frustration→Hope→Excitement,Problem/solution slides,Copywriting framework
|
||||
21,One Thing,"one thing, single, focus, key",The one thing + Why it matters,Focus and clarity,"The #1 thing {audience} needs to {outcome} is {one thing}.",Focus→Clarity,Key message slides,Gary Keller concept
|
||||
22,Riddle Open,"riddle, mystery, puzzle, question",Mystery + Reveal + Implication,Engagement through curiosity,"{Intriguing mystery or paradox}. The answer: {reveal}. For you: {implication}.",Mystery→Insight,Opening slides,Storytelling technique
|
||||
23,Hero Journey,"hero, journey, transformation, story",Ordinary→Call→Challenge→Triumph,Narrative structure,"{Character in ordinary world} → {Discovers challenge} → {Overcomes with solution} → {Achieves transformation}",Identification→Tension→Triumph,Full deck structure,Joseph Campbell
|
||||
24,Value Stack,"value, stack, bundle, worth",Component + Value → Total value,Justifying price/investment,"{Item 1} (Worth ${X}) + {Item 2} (Worth ${Y}) + ... = Total value ${Z}. Your investment: ${actual price}.",Value perception,Pricing offer slides,Info product marketing
|
||||
25,Power Statement,"power, statement, bold, declaration",Bold declaration + Supporting fact,Authority and confidence,"{Bold declaration}. {Supporting evidence or fact}.",Confidence→Trust,Key message slides,Thought leadership
|
||||
|
16
.claude/skills/design-system/data/slide-layout-logic.csv
Normal file
16
.claude/skills/design-system/data/slide-layout-logic.csv
Normal file
@ -0,0 +1,16 @@
|
||||
goal,emotion,layout_pattern,direction,visual_weight,break_pattern,use_bg_image
|
||||
hook,curiosity,split-hero,visual-right,70-visual,false,true
|
||||
problem,frustration,card-grid,centered,balanced,false,false
|
||||
agitation,fear,full-bleed-stat,centered,100-text,true,false
|
||||
solution,relief,split-feature,visual-left,50-50,false,true
|
||||
proof,trust,metric-grid,centered,numbers-dominant,false,false
|
||||
social,connection,quote-hero,centered,80-text,true,true
|
||||
comparison,evaluation,split-compare,side-by-side,balanced,false,false
|
||||
traction,confidence,chart-insight,chart-left,60-chart,false,false
|
||||
cta,urgency,gradient-cta,centered,100-text,true,true
|
||||
team,warmth,team-grid,centered,balanced,false,true
|
||||
pricing,evaluation,pricing-cards,centered,balanced,false,false
|
||||
demo,interest,split-demo,visual-left,60-visual,false,false
|
||||
vision,hope,full-bleed-hero,centered,100-visual,true,true
|
||||
timeline,narrative,timeline-flow,horizontal,balanced,false,false
|
||||
features,clarity,feature-grid,centered,balanced,false,false
|
||||
|
26
.claude/skills/design-system/data/slide-layouts.csv
Normal file
26
.claude/skills/design-system/data/slide-layouts.csv
Normal file
@ -0,0 +1,26 @@
|
||||
id,layout_name,keywords,use_case,content_zones,visual_weight,cta_placement,recommended_for,avoid_for,css_structure,card_variant,metric_style,quote_style,grid_columns,visual_treatment,animation_class
|
||||
1,Title Slide,"title, cover, opening, intro, hero",Opening slide first impression,"Center: Logo + Title + Tagline, Bottom: Date/Presenter",Visual-heavy minimal text,None or subtle,All presentations,Never skip,"display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center",none,none,none,1,gradient-glow,animate-fade-up
|
||||
2,Problem Statement,"problem, pain, challenge, issue",Establish the problem being solved,"Left: Problem headline, Right: Pain point bullets or icon grid",50/50 text visual balance,None,Pitch decks sales,Internal updates,"display:grid; grid-template-columns:1fr 1fr; gap:48px; align-items:center",icon-left,none,none,2,subtle-border,animate-stagger
|
||||
3,Solution Overview,"solution, answer, approach, how",Introduce your solution,"Top: Solution headline, Center: Solution visual/diagram, Bottom: 3 key points",Visual-dominant,Subtle learn more,After problem slide,Without context,"display:flex; flex-direction:column; gap:32px",accent-bar,none,none,3,icon-top,animate-scale
|
||||
4,Feature Grid,"features, grid, cards, capabilities, 3-column",Showcase multiple features,"Top: Section title, Grid: 3-6 feature cards with icon+title+description",Balanced grid,Bottom CTA optional,Product demos SaaS,Storytelling slides,"display:grid; grid-template-columns:repeat(3,1fr); gap:24px",accent-bar,none,none,3,icon-top,animate-stagger
|
||||
5,Metrics Dashboard,"metrics, kpis, numbers, stats, data",Display key performance data,"Top: Context headline, Center: 3-4 large metric cards, Bottom: Trend context",Numbers-dominant,None,Traction slides QBRs,Early-stage no data,"display:grid; grid-template-columns:repeat(4,1fr); gap:16px",metric-card,gradient-number,none,4,none,animate-stagger-scale
|
||||
6,Comparison Table,"comparison, vs, versus, table, matrix",Compare options or competitors,"Top: Comparison title, Center: Feature comparison table, Bottom: Conclusion",Table-heavy,Highlight winner row,Competitive analysis,Storytelling,"display:flex; flex-direction:column; table width:100%",comparison,none,none,2,highlight-winner,animate-fade-up
|
||||
7,Timeline Flow,"timeline, roadmap, journey, steps, process",Show progression over time,"Top: Timeline title, Center: Horizontal timeline with milestones, Bottom: Current status",Visual timeline,End milestone CTA,Roadmaps history,Dense data,"display:flex; flex-direction:column; timeline:flex with arrows",none,none,none,1,timeline-dots,animate-stagger
|
||||
8,Team Grid,"team, people, founders, leadership",Introduce team members,"Top: Team title, Grid: Photo + Name + Title + Brief bio cards",Photo-heavy,None or careers link,Investor decks about,Technical content,"display:grid; grid-template-columns:repeat(4,1fr); gap:24px",avatar-card,none,none,4,avatar-ring,animate-stagger
|
||||
9,Quote Testimonial,"quote, testimonial, social proof, customer",Feature customer endorsement,"Center: Large quote text, Bottom: Photo + Name + Title + Company logo",Quote-dominant minimal UI,None,Sales case studies,Without real quotes,"display:flex; flex-direction:column; justify-content:center; font-size:large; font-style:italic",none,none,large-italic,1,author-avatar,animate-fade-up
|
||||
10,Two Column Split,"split, two-column, side-by-side, comparison",Present two related concepts,"Left column: Content A, Right column: Content B",50/50 balanced,Either column bottom,Comparisons before/after,Single concept,display:grid; grid-template-columns:1fr 1fr; gap:48px,none,none,none,2,offset-image,animate-fade-up
|
||||
11,Big Number Hero,"big number, stat, impact, headline metric",Emphasize one powerful metric,"Center: Massive number, Below: Context label and trend",Number-dominant,None,Impact slides traction,Multiple metrics,"display:flex; flex-direction:column; justify-content:center; align-items:center; font-size:120px",none,oversized,none,1,centered,animate-count
|
||||
12,Product Screenshot,"screenshot, product, demo, ui, interface",Show product in action,"Top: Feature headline, Center: Product screenshot with annotations, Bottom: Key callouts",Screenshot-dominant,Try it CTA,Product demos,Abstract concepts,"display:flex; flex-direction:column; img max-height:60vh",none,none,none,1,screenshot-shadow,animate-scale
|
||||
13,Pricing Cards,"pricing, plans, tiers, packages",Present pricing options,"Top: Pricing headline, Center: 2-4 pricing cards side by side, Bottom: FAQ or guarantee",Cards balanced,Each card has CTA,Sales pricing pages,Free products,"display:grid; grid-template-columns:repeat(3,1fr); gap:24px; .popular:scale(1.05)",pricing-card,none,none,3,popular-highlight,animate-stagger
|
||||
14,CTA Closing,"cta, closing, call to action, next steps, final",Drive action end presentation,"Center: Bold headline + Value reminder, Center: Primary CTA button, Below: Secondary option",CTA-dominant,Primary center,All presentations,Middle slides,"display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center",none,none,none,1,gradient-bg,animate-pulse
|
||||
15,Agenda Overview,"agenda, outline, contents, structure",Preview presentation structure,"Top: Agenda title, Center: Numbered list or visual timeline of sections",Text-light scannable,None,Long presentations,Short 3-5 slides,"display:flex; flex-direction:column; ol list-style-type:decimal",none,none,none,1,numbered-list,animate-stagger
|
||||
16,Before After,"before, after, transformation, results, comparison",Show transformation impact,"Left: Before state (muted), Right: After state (vibrant), Center: Arrow or transition",50/50 high contrast,After column CTA,Case studies results,No transformation data,"display:grid; grid-template-columns:1fr 1fr; .before:opacity(0.7)",comparison,none,none,2,contrast-pair,animate-scale
|
||||
17,Icon Grid Stats,"icons, stats, grid, key points, summary",Summarize key points visually,"Grid: 4-6 icon + stat + label combinations",Icons-dominant,None,Summary slides,Detailed explanations,"display:grid; grid-template-columns:repeat(3,1fr); gap:32px; text-align:center",icon-stat,sparkline,none,3,icon-top,animate-stagger
|
||||
18,Full Bleed Image,"image, photo, visual, background, hero",Create visual impact,"Full background image, Overlay: Text with contrast, Corner: Logo",Image-dominant,Overlay CTA optional,Emotional moments,Data-heavy,background-size:cover; color:white; text-shadow for contrast,none,none,none,1,bg-overlay,animate-ken-burns
|
||||
19,Video Embed,"video, demo, embed, multimedia",Show video content,"Top: Context headline, Center: Video player (16:9), Bottom: Key points if needed",Video-dominant,After video CTA,Demos testimonials,Reading-focused,"aspect-ratio:16/9; video controls",none,none,none,1,video-frame,animate-scale
|
||||
20,Funnel Diagram,"funnel, conversion, stages, pipeline",Show conversion or process flow,"Top: Funnel title, Center: Funnel visualization with stage labels and metrics",Diagram-dominant,None,Sales marketing funnels,Non-sequential data,SVG or CSS trapezoid shapes,none,funnel-numbers,none,1,funnel-gradient,animate-chart
|
||||
21,Quote Plus Stats,"quote, stats, hybrid, testimonial, metrics",Combine social proof with data,"Left: Customer quote with photo, Right: 3 supporting metrics",Balanced quote/data,None,Sales enablement,Without both elements,"display:grid; grid-template-columns:1.5fr 1fr; gap:48px",metric-card,gradient-number,side-quote,2,author-avatar,animate-stagger
|
||||
22,Section Divider,"section, divider, break, transition",Transition between sections,"Center: Section number + Section title, Minimal design",Typography-only,None,Long presentations,Every slide,"display:flex; justify-content:center; align-items:center; font-size:48px",none,none,none,1,section-number,animate-fade-up
|
||||
23,Logo Grid,"logos, clients, partners, trust, social proof",Display client or partner logos,"Top: Trust headline, Grid: 8-16 logos evenly spaced",Logos-only,None,Credibility slides,Few logos <6,"display:grid; grid-template-columns:repeat(4,1fr); gap:32px; filter:grayscale(1)",none,none,none,4,logo-grayscale,animate-stagger
|
||||
24,Chart Focus,"chart, graph, data, visualization, analytics",Present data visualization,"Top: Chart title and context, Center: Single large chart, Bottom: Key insight",Chart-dominant,None,Data-driven slides,Poor data quality,"chart max-height:65vh; annotation for key point",none,sparkline,none,1,chart-left,animate-chart
|
||||
25,Q&A Slide,"qa, questions, discussion, interactive",Invite audience questions,"Center: Q&A or Questions? text, Below: Contact info or submission method",Minimal text,None,End of presentations,Skip if no time,"display:flex; justify-content:center; align-items:center; font-size:64px",none,none,none,1,centered,animate-fade-up
|
||||
|
16
.claude/skills/design-system/data/slide-strategies.csv
Normal file
16
.claude/skills/design-system/data/slide-strategies.csv
Normal file
@ -0,0 +1,16 @@
|
||||
id,strategy_name,keywords,slide_count,structure,goal,audience,tone,narrative_arc,key_metrics,sources,emotion_arc,sparkline_beats
|
||||
1,YC Seed Deck,"yc, seed, startup, investor, funding, vc, venture","10-12","1.Title 2.Problem 3.Solution 4.Traction 5.Market 6.Product 7.Business Model 8.Team 9.Financials 10.Ask",Raise seed funding from VCs,Seed investors hunting asymmetric upside,Clear concise focused narrative,Problem→Solution→Evidence→Ask,MRR ARR growth rate user count,Y Combinator Library,"curiosity→frustration→hope→confidence→trust→urgency","hook|what-is|what-could-be|proof|proof|what-could-be|proof|trust|what-could-be|action"
|
||||
2,Guy Kawasaki 10/20/30,"kawasaki, pitch, investor, 10 slides, venture","10","1.Title 2.Problem/Opportunity 3.Value Proposition 4.Underlying Magic 5.Business Model 6.Go-to-Market 7.Competition 8.Team 9.Projections 10.Status/Timeline/Ask",Pitch to investors in 20 min,VCs angel investors,Confident not arrogant,Hook→Magic→Proof→Ask,5yr projections milestones,Guy Kawasaki Blog,"curiosity→frustration→hope→confidence→trust→urgency","hook|what-is|what-could-be|what-could-be|proof|proof|evaluation|trust|proof|action"
|
||||
3,Series A Deck,"series a, growth, scale, investor, traction","12-15","1.Title 2.Mission 3.Problem 4.Solution 5.Traction/Metrics 6.Product Demo 7.Market Size 8.Business Model 9.Competition 10.Team 11.Go-to-Market 12.Financials 13.Use of Funds 14.Ask",Raise Series A funding,Growth-stage VCs,Data-driven confident,Traction→Scale→Vision,Revenue growth LTV CAC cohorts,YC Library,"curiosity→hope→frustration→relief→confidence→trust→urgency","hook|what-could-be|what-is|what-could-be|proof|proof|proof|proof|evaluation|trust|proof|proof|what-could-be|action"
|
||||
4,Product Demo,"demo, product, walkthrough, features, saas","5-8","1.Hook/Problem 2.Solution Overview 3.Live Demo/Screenshots 4.Key Features 5.Benefits 6.Pricing 7.CTA",Demonstrate product value,Prospects users,Enthusiastic helpful,Problem→See it work→Value,Conversion engagement time-saved,Product-led growth best practices,"curiosity→frustration→hope→confidence→urgency","hook|what-is|what-could-be|what-could-be|what-could-be|evaluation|action"
|
||||
5,Sales Pitch,"sales, pitch, prospect, close, deal","7-10","1.Personalized Hook 2.Their Problem 3.Cost of Inaction 4.Your Solution 5.Proof/Case Studies 6.Differentiators 7.Pricing/ROI 8.Objection Handling 9.CTA 10.Next Steps",Close deal win customer,Qualified prospects,Consultative trustworthy,Pain→Agitate→Solve→Prove,ROI case study metrics,Sandler Sales Training,"connection→frustration→fear→hope→trust→confidence→urgency","hook|what-is|what-is|what-could-be|proof|what-could-be|evaluation|trust|action|action"
|
||||
6,Nancy Duarte Sparkline,"duarte, sparkline, story, transformation, resonate","Varies","Alternate: What Is→What Could Be→What Is→What Could Be→New Bliss",Transform audience perspective,Any audience needing persuasion,Inspiring visionary,Tension→Release→Tension→Release→Resolution,Audience transformation,Nancy Duarte Resonate,"frustration→hope→frustration→hope→relief","what-is|what-could-be|what-is|what-could-be|new-bliss"
|
||||
7,Problem-Solution-Benefit,"psb, simple, clear, benefit, value","3-5","1.Problem Statement 2.Solution Introduction 3.Key Benefits 4.Proof 5.CTA",Quick persuasion simple message,Time-pressed audience,Direct clear,Problem→Solution→Outcome,Core value metrics,Marketing fundamentals,"frustration→hope→confidence→urgency","what-is|what-could-be|what-could-be|proof|action"
|
||||
8,Quarterly Business Review,"qbr, business review, internal, stakeholder","10-15","1.Executive Summary 2.Goals vs Results 3.Key Metrics 4.Wins 5.Challenges 6.Learnings 7.Customer Insights 8.Competitive Update 9.Next Quarter Goals 10.Resource Needs",Update stakeholders on progress,Internal leadership,Professional factual,Review→Analyze→Plan,KPIs OKRs progress %,Internal communications,"clarity→trust→confidence→evaluation→hope","summary|proof|proof|celebration|what-is|insight|trust|evaluation|what-could-be|action"
|
||||
9,Team All-Hands,"all-hands, company, internal, culture, update","8-12","1.Opening/Energy 2.Company Wins 3.Metrics Dashboard 4.Team Spotlights 5.Product Updates 6.Customer Stories 7.Challenges/Learnings 8.Roadmap Preview 9.Q&A 10.Closing Motivation",Align and motivate team,All employees,Transparent inspiring,Celebrate→Update→Align→Energize,Company-wide KPIs,Internal communications,"warmth→confidence→trust→connection→hope→urgency","hook|celebration|proof|connection|what-could-be|trust|what-is|what-could-be|interaction|action"
|
||||
10,Conference Talk,"conference, talk, keynote, public speaking, thought leadership","15-25","1.Hook/Story 2.Credibility 3.Big Idea 4.Point 1 + Evidence 5.Point 2 + Evidence 6.Point 3 + Evidence 7.Synthesis 8.Call to Action 9.Q&A Prep",Establish thought leadership,Conference attendees,Expert engaging,Story→Teach→Inspire,Audience engagement social shares,TED Talk guidelines,"curiosity→trust→hope→confidence→confidence→confidence→clarity→urgency","hook|trust|what-could-be|proof|proof|proof|synthesis|action|interaction"
|
||||
11,Workshop Training,"workshop, training, education, how-to, tutorial","20-40","1.Welcome/Objectives 2.Agenda 3.Concept 1 4.Exercise 1 5.Concept 2 6.Exercise 2 7.Concept 3 8.Exercise 3 9.Synthesis 10.Resources 11.Q&A",Teach practical skills,Learners trainees,Patient instructive,Learn→Practice→Apply→Reflect,Skill acquisition completion,Adult learning principles,"warmth→clarity→confidence→confidence→confidence→confidence→clarity→hope","welcome|structure|teaching|practice|teaching|practice|teaching|practice|synthesis|resources|interaction"
|
||||
12,Case Study Presentation,"case study, success story, customer, results","8-12","1.Customer Introduction 2.Their Challenge 3.Why They Chose Us 4.Implementation 5.Solution Details 6.Results/Metrics 7.Customer Quote 8.Lessons Learned 9.Applicability 10.CTA",Prove value through example,Prospects similar to case,Authentic factual,Challenge→Journey→Transformation,Before/after metrics ROI,Marketing case study best practices,"connection→frustration→trust→hope→confidence→celebration→trust→clarity→urgency","connection|what-is|trust|what-could-be|what-could-be|proof|trust|insight|what-could-be|action"
|
||||
13,Competitive Analysis,"competitive, analysis, comparison, market","6-10","1.Market Landscape 2.Competitor Overview 3.Feature Comparison Matrix 4.Pricing Comparison 5.Strengths/Weaknesses 6.Our Differentiation 7.Market Positioning 8.Strategic Recommendations",Inform strategic decisions,Internal leadership,Analytical objective,Landscape→Analysis→Strategy,Market share feature gaps,Competitive intelligence,"clarity→evaluation→evaluation→evaluation→clarity→hope→confidence→urgency","overview|evaluation|comparison|comparison|analysis|what-could-be|proof|action"
|
||||
14,Board Meeting Deck,"board, governance, investor update, quarterly","15-20","1.Agenda 2.Executive Summary 3.Financial Overview 4.Key Metrics 5.Product Update 6.Sales/Marketing 7.Operations 8.Team/Hiring 9.Risks/Challenges 10.Strategic Initiatives 11.Upcoming Milestones 12.Ask/Discussion",Update board on company status,Board members,Professional detailed,Report→Analyze→Discuss→Decide,All key business metrics,Board governance best practices,"clarity→confidence→trust→trust→confidence→confidence→trust→connection→evaluation→hope→confidence→urgency","structure|summary|proof|proof|proof|proof|proof|trust|what-is|what-could-be|proof|action"
|
||||
15,Webinar Presentation,"webinar, online, education, lead gen","20-30","1.Welcome/Housekeeping 2.Presenter Intro 3.Agenda 4.Hook/Problem 5.Teaching Content 6.Case Study 7.Product Introduction 8.Demo 9.Offer/CTA 10.Q&A 11.Resources",Generate leads educate prospects,Webinar registrants,Educational helpful,Teach→Demonstrate→Offer,Registrations attendance conversion,Webinar marketing best practices,"warmth→trust→clarity→curiosity→confidence→trust→hope→confidence→urgency→connection→clarity","welcome|trust|structure|hook|teaching|trust|what-could-be|proof|action|interaction|resources"
|
||||
|
15
.claude/skills/design-system/data/slide-typography.csv
Normal file
15
.claude/skills/design-system/data/slide-typography.csv
Normal file
@ -0,0 +1,15 @@
|
||||
content_type,primary_size,secondary_size,accent_size,weight_contrast,letter_spacing,line_height
|
||||
hero-statement,120px,32px,14px,700-400,tight,1.0
|
||||
metric-callout,96px,18px,12px,700-500,normal,1.1
|
||||
feature-grid,28px,16px,12px,600-400,normal,1.4
|
||||
quote-block,36px,18px,14px,400-italic,loose,1.5
|
||||
data-insight,48px,20px,14px,700-400,normal,1.2
|
||||
cta-action,64px,24px,16px,700-500,tight,1.1
|
||||
title-only,80px,24px,14px,700-400,tight,1.0
|
||||
subtitle-heavy,56px,28px,16px,600-400,normal,1.2
|
||||
body-focus,24px,18px,14px,500-400,normal,1.6
|
||||
comparison,32px,16px,12px,600-400,normal,1.3
|
||||
timeline,28px,16px,12px,500-400,normal,1.4
|
||||
pricing,48px,20px,14px,700-500,normal,1.2
|
||||
team,24px,16px,14px,600-400,normal,1.4
|
||||
testimonial,32px,20px,14px,400-italic,loose,1.5
|
||||
|
236
.claude/skills/design-system/references/component-specs.md
Normal file
236
.claude/skills/design-system/references/component-specs.md
Normal file
@ -0,0 +1,236 @@
|
||||
# Component Specifications
|
||||
|
||||
Detailed specs for core components with states and variants.
|
||||
|
||||
## Button
|
||||
|
||||
### Variants
|
||||
|
||||
| Variant | Background | Text | Border | Use Case |
|
||||
|---------|------------|------|--------|----------|
|
||||
| default | primary | white | none | Primary actions |
|
||||
| secondary | gray-100 | gray-900 | none | Secondary actions |
|
||||
| outline | transparent | foreground | border | Tertiary actions |
|
||||
| ghost | transparent | foreground | none | Subtle actions |
|
||||
| link | transparent | primary | none | Navigation |
|
||||
| destructive | red-600 | white | none | Dangerous actions |
|
||||
|
||||
### Sizes
|
||||
|
||||
| Size | Height | Padding X | Padding Y | Font Size | Icon Size |
|
||||
|------|--------|-----------|-----------|-----------|-----------|
|
||||
| sm | 32px | 12px | 6px | 14px | 16px |
|
||||
| default | 40px | 16px | 8px | 14px | 18px |
|
||||
| lg | 48px | 24px | 12px | 16px | 20px |
|
||||
| icon | 40px | 0 | 0 | - | 18px |
|
||||
|
||||
### States
|
||||
|
||||
| State | Background | Text | Opacity | Cursor |
|
||||
|-------|------------|------|---------|--------|
|
||||
| default | token | token | 1 | pointer |
|
||||
| hover | darker | token | 1 | pointer |
|
||||
| active | darkest | token | 1 | pointer |
|
||||
| focus | token | token | 1 | pointer |
|
||||
| disabled | muted | muted-fg | 0.5 | not-allowed |
|
||||
| loading | token | token | 0.7 | wait |
|
||||
|
||||
### Anatomy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [icon] Label Text [icon] │
|
||||
└─────────────────────────────────────┘
|
||||
↑ ↑
|
||||
leading icon trailing icon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
### Variants
|
||||
|
||||
| Variant | Description |
|
||||
|---------|-------------|
|
||||
| default | Standard text input |
|
||||
| textarea | Multi-line text |
|
||||
| select | Dropdown selection |
|
||||
| checkbox | Boolean toggle |
|
||||
| radio | Single selection |
|
||||
| switch | Toggle switch |
|
||||
|
||||
### Sizes
|
||||
|
||||
| Size | Height | Padding | Font Size |
|
||||
|------|--------|---------|-----------|
|
||||
| sm | 32px | 8px 12px | 14px |
|
||||
| default | 40px | 8px 12px | 14px |
|
||||
| lg | 48px | 12px 16px | 16px |
|
||||
|
||||
### States
|
||||
|
||||
| State | Border | Background | Ring |
|
||||
|-------|--------|------------|------|
|
||||
| default | gray-300 | white | none |
|
||||
| hover | gray-400 | white | none |
|
||||
| focus | primary | white | primary/20% |
|
||||
| error | red-500 | white | red/20% |
|
||||
| disabled | gray-200 | gray-100 | none |
|
||||
|
||||
### Anatomy
|
||||
|
||||
```
|
||||
Label (optional)
|
||||
┌─────────────────────────────────────┐
|
||||
│ [icon] Placeholder/Value [action] │
|
||||
└─────────────────────────────────────┘
|
||||
Helper text or error message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Card
|
||||
|
||||
### Variants
|
||||
|
||||
| Variant | Shadow | Border | Use Case |
|
||||
|---------|--------|--------|----------|
|
||||
| default | sm | 1px | Standard card |
|
||||
| elevated | lg | none | Prominent content |
|
||||
| outline | none | 1px | Subtle container |
|
||||
| interactive | sm→md | 1px | Clickable card |
|
||||
|
||||
### Anatomy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Card Header │
|
||||
│ Title │
|
||||
│ Description │
|
||||
├─────────────────────────────────────┤
|
||||
│ Card Content │
|
||||
│ Main content area │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ Card Footer │
|
||||
│ Actions │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
| Area | Padding |
|
||||
|------|---------|
|
||||
| header | 24px 24px 0 |
|
||||
| content | 24px |
|
||||
| footer | 0 24px 24px |
|
||||
| gap | 16px |
|
||||
|
||||
---
|
||||
|
||||
## Badge
|
||||
|
||||
### Variants
|
||||
|
||||
| Variant | Background | Text |
|
||||
|---------|------------|------|
|
||||
| default | primary | white |
|
||||
| secondary | gray-100 | gray-900 |
|
||||
| outline | transparent | foreground |
|
||||
| destructive | red-600 | white |
|
||||
| success | green-600 | white |
|
||||
| warning | yellow-500 | gray-900 |
|
||||
|
||||
### Sizes
|
||||
|
||||
| Size | Padding | Font Size | Height |
|
||||
|------|---------|-----------|--------|
|
||||
| sm | 4px 8px | 11px | 20px |
|
||||
| default | 4px 10px | 12px | 24px |
|
||||
| lg | 6px 12px | 14px | 28px |
|
||||
|
||||
---
|
||||
|
||||
## Alert
|
||||
|
||||
### Variants
|
||||
|
||||
| Variant | Icon | Background | Border |
|
||||
|---------|------|------------|--------|
|
||||
| default | info | gray-50 | gray-200 |
|
||||
| destructive | alert | red-50 | red-200 |
|
||||
| success | check | green-50 | green-200 |
|
||||
| warning | warning | yellow-50 | yellow-200 |
|
||||
|
||||
### Anatomy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [icon] Title [×]│
|
||||
│ Description text │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dialog
|
||||
|
||||
### Sizes
|
||||
|
||||
| Size | Max Width | Use Case |
|
||||
|------|-----------|----------|
|
||||
| sm | 384px | Simple confirmations |
|
||||
| default | 512px | Standard dialogs |
|
||||
| lg | 640px | Complex forms |
|
||||
| xl | 768px | Data-heavy dialogs |
|
||||
| full | 100% - 32px | Full-screen on mobile |
|
||||
|
||||
### Anatomy
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────┐
|
||||
│ Dialog Header [×]│
|
||||
│ Title │
|
||||
│ Description │
|
||||
├───────────────────────────────────────┤
|
||||
│ Dialog Content │
|
||||
│ Scrollable if needed │
|
||||
│ │
|
||||
├───────────────────────────────────────┤
|
||||
│ Dialog Footer │
|
||||
│ [Cancel] [Confirm]│
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Table
|
||||
|
||||
### Row States
|
||||
|
||||
| State | Background | Use Case |
|
||||
|-------|------------|----------|
|
||||
| default | white | Normal row |
|
||||
| hover | gray-50 | Mouse over |
|
||||
| selected | primary/10% | Selected row |
|
||||
| striped | gray-50/white | Alternating |
|
||||
|
||||
### Cell Alignment
|
||||
|
||||
| Content Type | Alignment |
|
||||
|--------------|-----------|
|
||||
| Text | Left |
|
||||
| Numbers | Right |
|
||||
| Status/Badge | Center |
|
||||
| Actions | Right |
|
||||
|
||||
### Spacing
|
||||
|
||||
| Element | Value |
|
||||
|---------|-------|
|
||||
| cell padding | 12px 16px |
|
||||
| header padding | 12px 16px |
|
||||
| row height (compact) | 40px |
|
||||
| row height (default) | 48px |
|
||||
| row height (comfortable) | 56px |
|
||||
214
.claude/skills/design-system/references/component-tokens.md
Normal file
214
.claude/skills/design-system/references/component-tokens.md
Normal file
@ -0,0 +1,214 @@
|
||||
# Component Tokens
|
||||
|
||||
Component-specific tokens referencing semantic layer.
|
||||
|
||||
## Button Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Default (Primary) */
|
||||
--button-bg: var(--color-primary);
|
||||
--button-fg: var(--color-primary-foreground);
|
||||
--button-hover-bg: var(--color-primary-hover);
|
||||
--button-active-bg: var(--color-primary-active);
|
||||
|
||||
/* Secondary */
|
||||
--button-secondary-bg: var(--color-secondary);
|
||||
--button-secondary-fg: var(--color-secondary-foreground);
|
||||
--button-secondary-hover-bg: var(--color-secondary-hover);
|
||||
|
||||
/* Outline */
|
||||
--button-outline-border: var(--color-border);
|
||||
--button-outline-fg: var(--color-foreground);
|
||||
--button-outline-hover-bg: var(--color-accent);
|
||||
|
||||
/* Ghost */
|
||||
--button-ghost-fg: var(--color-foreground);
|
||||
--button-ghost-hover-bg: var(--color-accent);
|
||||
|
||||
/* Destructive */
|
||||
--button-destructive-bg: var(--color-destructive);
|
||||
--button-destructive-fg: var(--color-destructive-foreground);
|
||||
--button-destructive-hover-bg: var(--color-destructive-hover);
|
||||
|
||||
/* Sizing */
|
||||
--button-padding-x: var(--space-4);
|
||||
--button-padding-y: var(--space-2);
|
||||
--button-padding-x-sm: var(--space-3);
|
||||
--button-padding-y-sm: var(--space-1-5);
|
||||
--button-padding-x-lg: var(--space-6);
|
||||
--button-padding-y-lg: var(--space-3);
|
||||
|
||||
/* Shape */
|
||||
--button-radius: var(--radius-md);
|
||||
--button-font-size: var(--font-size-sm);
|
||||
--button-font-weight: var(--font-weight-medium);
|
||||
}
|
||||
```
|
||||
|
||||
## Input Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Background & Border */
|
||||
--input-bg: var(--color-background);
|
||||
--input-border: var(--color-input);
|
||||
--input-fg: var(--color-foreground);
|
||||
|
||||
/* Placeholder */
|
||||
--input-placeholder: var(--color-muted-foreground);
|
||||
|
||||
/* Focus */
|
||||
--input-focus-border: var(--color-ring);
|
||||
--input-focus-ring: var(--color-ring);
|
||||
|
||||
/* Error */
|
||||
--input-error-border: var(--color-error);
|
||||
--input-error-fg: var(--color-error);
|
||||
|
||||
/* Disabled */
|
||||
--input-disabled-bg: var(--color-muted);
|
||||
--input-disabled-fg: var(--color-muted-foreground);
|
||||
|
||||
/* Sizing */
|
||||
--input-padding-x: var(--space-3);
|
||||
--input-padding-y: var(--space-2);
|
||||
--input-radius: var(--radius-md);
|
||||
--input-font-size: var(--font-size-sm);
|
||||
}
|
||||
```
|
||||
|
||||
## Card Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Background & Border */
|
||||
--card-bg: var(--color-card);
|
||||
--card-fg: var(--color-card-foreground);
|
||||
--card-border: var(--color-border);
|
||||
|
||||
/* Shadow */
|
||||
--card-shadow: var(--shadow-default);
|
||||
--card-shadow-hover: var(--shadow-md);
|
||||
|
||||
/* Spacing */
|
||||
--card-padding: var(--space-6);
|
||||
--card-padding-sm: var(--space-4);
|
||||
--card-gap: var(--space-4);
|
||||
|
||||
/* Shape */
|
||||
--card-radius: var(--radius-lg);
|
||||
}
|
||||
```
|
||||
|
||||
## Badge Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Default */
|
||||
--badge-bg: var(--color-primary);
|
||||
--badge-fg: var(--color-primary-foreground);
|
||||
|
||||
/* Secondary */
|
||||
--badge-secondary-bg: var(--color-secondary);
|
||||
--badge-secondary-fg: var(--color-secondary-foreground);
|
||||
|
||||
/* Outline */
|
||||
--badge-outline-border: var(--color-border);
|
||||
--badge-outline-fg: var(--color-foreground);
|
||||
|
||||
/* Destructive */
|
||||
--badge-destructive-bg: var(--color-destructive);
|
||||
--badge-destructive-fg: var(--color-destructive-foreground);
|
||||
|
||||
/* Sizing */
|
||||
--badge-padding-x: var(--space-2-5);
|
||||
--badge-padding-y: var(--space-0-5);
|
||||
--badge-radius: var(--radius-full);
|
||||
--badge-font-size: var(--font-size-xs);
|
||||
}
|
||||
```
|
||||
|
||||
## Alert Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Default */
|
||||
--alert-bg: var(--color-background);
|
||||
--alert-fg: var(--color-foreground);
|
||||
--alert-border: var(--color-border);
|
||||
|
||||
/* Destructive */
|
||||
--alert-destructive-bg: var(--color-destructive);
|
||||
--alert-destructive-fg: var(--color-destructive-foreground);
|
||||
|
||||
/* Spacing */
|
||||
--alert-padding: var(--space-4);
|
||||
--alert-radius: var(--radius-lg);
|
||||
}
|
||||
```
|
||||
|
||||
## Dialog/Modal Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Overlay */
|
||||
--dialog-overlay-bg: rgb(0 0 0 / 0.5);
|
||||
|
||||
/* Content */
|
||||
--dialog-bg: var(--color-background);
|
||||
--dialog-fg: var(--color-foreground);
|
||||
--dialog-border: var(--color-border);
|
||||
--dialog-shadow: var(--shadow-lg);
|
||||
|
||||
/* Spacing */
|
||||
--dialog-padding: var(--space-6);
|
||||
--dialog-radius: var(--radius-lg);
|
||||
--dialog-max-width: 32rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Table Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Header */
|
||||
--table-header-bg: var(--color-muted);
|
||||
--table-header-fg: var(--color-muted-foreground);
|
||||
|
||||
/* Body */
|
||||
--table-row-bg: var(--color-background);
|
||||
--table-row-hover-bg: var(--color-muted);
|
||||
--table-row-fg: var(--color-foreground);
|
||||
|
||||
/* Border */
|
||||
--table-border: var(--color-border);
|
||||
|
||||
/* Spacing */
|
||||
--table-cell-padding-x: var(--space-4);
|
||||
--table-cell-padding-y: var(--space-3);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```css
|
||||
.button {
|
||||
background: var(--button-bg);
|
||||
color: var(--button-fg);
|
||||
padding: var(--button-padding-y) var(--button-padding-x);
|
||||
border-radius: var(--button-radius);
|
||||
font-size: var(--button-font-size);
|
||||
font-weight: var(--button-font-weight);
|
||||
transition: background var(--duration-fast);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: var(--button-secondary-bg);
|
||||
color: var(--button-secondary-fg);
|
||||
}
|
||||
```
|
||||
203
.claude/skills/design-system/references/primitive-tokens.md
Normal file
203
.claude/skills/design-system/references/primitive-tokens.md
Normal file
@ -0,0 +1,203 @@
|
||||
# Primitive Tokens
|
||||
|
||||
Raw design values - foundation of the design system.
|
||||
|
||||
## Color Scales
|
||||
|
||||
### Gray Scale
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-gray-50: #F9FAFB;
|
||||
--color-gray-100: #F3F4F6;
|
||||
--color-gray-200: #E5E7EB;
|
||||
--color-gray-300: #D1D5DB;
|
||||
--color-gray-400: #9CA3AF;
|
||||
--color-gray-500: #6B7280;
|
||||
--color-gray-600: #4B5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1F2937;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #030712;
|
||||
}
|
||||
```
|
||||
|
||||
### Primary Colors (Blue)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-blue-50: #EFF6FF;
|
||||
--color-blue-100: #DBEAFE;
|
||||
--color-blue-200: #BFDBFE;
|
||||
--color-blue-300: #93C5FD;
|
||||
--color-blue-400: #60A5FA;
|
||||
--color-blue-500: #3B82F6;
|
||||
--color-blue-600: #2563EB;
|
||||
--color-blue-700: #1D4ED8;
|
||||
--color-blue-800: #1E40AF;
|
||||
--color-blue-900: #1E3A8A;
|
||||
}
|
||||
```
|
||||
|
||||
### Status Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Success - Green */
|
||||
--color-green-500: #22C55E;
|
||||
--color-green-600: #16A34A;
|
||||
|
||||
/* Warning - Yellow */
|
||||
--color-yellow-500: #EAB308;
|
||||
--color-yellow-600: #CA8A04;
|
||||
|
||||
/* Error - Red */
|
||||
--color-red-500: #EF4444;
|
||||
--color-red-600: #DC2626;
|
||||
|
||||
/* Info - Blue */
|
||||
--color-info: var(--color-blue-500);
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
4px base unit system.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--space-0: 0;
|
||||
--space-px: 1px;
|
||||
--space-0-5: 0.125rem; /* 2px */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-1-5: 0.375rem; /* 6px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-2-5: 0.625rem; /* 10px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-3-5: 0.875rem; /* 14px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-7: 1.75rem; /* 28px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-9: 2.25rem; /* 36px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-14: 3.5rem; /* 56px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
}
|
||||
```
|
||||
|
||||
## Typography Scale
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Font Sizes */
|
||||
--font-size-xs: 0.75rem; /* 12px */
|
||||
--font-size-sm: 0.875rem; /* 14px */
|
||||
--font-size-base: 1rem; /* 16px */
|
||||
--font-size-lg: 1.125rem; /* 18px */
|
||||
--font-size-xl: 1.25rem; /* 20px */
|
||||
--font-size-2xl: 1.5rem; /* 24px */
|
||||
--font-size-3xl: 1.875rem; /* 30px */
|
||||
--font-size-4xl: 2.25rem; /* 36px */
|
||||
--font-size-5xl: 3rem; /* 48px */
|
||||
|
||||
/* Line Heights */
|
||||
--leading-none: 1;
|
||||
--leading-tight: 1.25;
|
||||
--leading-snug: 1.375;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 2;
|
||||
|
||||
/* Font Weights */
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* Letter Spacing */
|
||||
--tracking-tighter: -0.05em;
|
||||
--tracking-tight: -0.025em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.025em;
|
||||
--tracking-wider: 0.05em;
|
||||
}
|
||||
```
|
||||
|
||||
## Border Radius
|
||||
|
||||
```css
|
||||
:root {
|
||||
--radius-none: 0;
|
||||
--radius-sm: 0.125rem; /* 2px */
|
||||
--radius-default: 0.25rem; /* 4px */
|
||||
--radius-md: 0.375rem; /* 6px */
|
||||
--radius-lg: 0.5rem; /* 8px */
|
||||
--radius-xl: 0.75rem; /* 12px */
|
||||
--radius-2xl: 1rem; /* 16px */
|
||||
--radius-3xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
```
|
||||
|
||||
## Shadows
|
||||
|
||||
```css
|
||||
:root {
|
||||
--shadow-none: none;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-default: 0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1),
|
||||
0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
```
|
||||
|
||||
## Motion / Duration
|
||||
|
||||
```css
|
||||
:root {
|
||||
--duration-75: 75ms;
|
||||
--duration-100: 100ms;
|
||||
--duration-150: 150ms;
|
||||
--duration-200: 200ms;
|
||||
--duration-300: 300ms;
|
||||
--duration-500: 500ms;
|
||||
--duration-700: 700ms;
|
||||
--duration-1000: 1000ms;
|
||||
|
||||
/* Semantic durations */
|
||||
--duration-fast: var(--duration-150);
|
||||
--duration-normal: var(--duration-200);
|
||||
--duration-slow: var(--duration-300);
|
||||
}
|
||||
```
|
||||
|
||||
## Z-Index Scale
|
||||
|
||||
```css
|
||||
:root {
|
||||
--z-auto: auto;
|
||||
--z-0: 0;
|
||||
--z-10: 10;
|
||||
--z-20: 20;
|
||||
--z-30: 30;
|
||||
--z-40: 40;
|
||||
--z-50: 50;
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1100;
|
||||
--z-modal: 1200;
|
||||
--z-popover: 1300;
|
||||
--z-tooltip: 1400;
|
||||
}
|
||||
```
|
||||
215
.claude/skills/design-system/references/semantic-tokens.md
Normal file
215
.claude/skills/design-system/references/semantic-tokens.md
Normal file
@ -0,0 +1,215 @@
|
||||
# Semantic Tokens
|
||||
|
||||
Purpose-based aliases referencing primitive tokens.
|
||||
|
||||
## Color Semantics
|
||||
|
||||
### Background & Foreground
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Page background */
|
||||
--color-background: var(--color-gray-50);
|
||||
--color-foreground: var(--color-gray-900);
|
||||
|
||||
/* Card/surface background */
|
||||
--color-card: white;
|
||||
--color-card-foreground: var(--color-gray-900);
|
||||
|
||||
/* Popover/dropdown */
|
||||
--color-popover: white;
|
||||
--color-popover-foreground: var(--color-gray-900);
|
||||
}
|
||||
```
|
||||
|
||||
### Primary
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: var(--color-blue-600);
|
||||
--color-primary-hover: var(--color-blue-700);
|
||||
--color-primary-active: var(--color-blue-800);
|
||||
--color-primary-foreground: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Secondary
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-secondary: var(--color-gray-100);
|
||||
--color-secondary-hover: var(--color-gray-200);
|
||||
--color-secondary-foreground: var(--color-gray-900);
|
||||
}
|
||||
```
|
||||
|
||||
### Muted
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-muted: var(--color-gray-100);
|
||||
--color-muted-foreground: var(--color-gray-500);
|
||||
}
|
||||
```
|
||||
|
||||
### Accent
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: var(--color-gray-100);
|
||||
--color-accent-foreground: var(--color-gray-900);
|
||||
}
|
||||
```
|
||||
|
||||
### Destructive
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-destructive: var(--color-red-600);
|
||||
--color-destructive-hover: var(--color-red-700);
|
||||
--color-destructive-foreground: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Status Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-success: var(--color-green-600);
|
||||
--color-success-foreground: white;
|
||||
|
||||
--color-warning: var(--color-yellow-500);
|
||||
--color-warning-foreground: var(--color-gray-900);
|
||||
|
||||
--color-error: var(--color-red-600);
|
||||
--color-error-foreground: white;
|
||||
|
||||
--color-info: var(--color-blue-500);
|
||||
--color-info-foreground: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Border & Ring
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-border: var(--color-gray-200);
|
||||
--color-input: var(--color-gray-200);
|
||||
--color-ring: var(--color-blue-500);
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing Semantics
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Component internal spacing */
|
||||
--spacing-component-xs: var(--space-1);
|
||||
--spacing-component-sm: var(--space-2);
|
||||
--spacing-component: var(--space-3);
|
||||
--spacing-component-lg: var(--space-4);
|
||||
|
||||
/* Section spacing */
|
||||
--spacing-section-sm: var(--space-8);
|
||||
--spacing-section: var(--space-12);
|
||||
--spacing-section-lg: var(--space-16);
|
||||
|
||||
/* Page margins */
|
||||
--spacing-page-x: var(--space-4);
|
||||
--spacing-page-y: var(--space-6);
|
||||
}
|
||||
```
|
||||
|
||||
## Typography Semantics
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Headings */
|
||||
--font-heading: var(--font-size-2xl);
|
||||
--font-heading-lg: var(--font-size-3xl);
|
||||
--font-heading-xl: var(--font-size-4xl);
|
||||
|
||||
/* Body */
|
||||
--font-body: var(--font-size-base);
|
||||
--font-body-sm: var(--font-size-sm);
|
||||
--font-body-lg: var(--font-size-lg);
|
||||
|
||||
/* Labels & Captions */
|
||||
--font-label: var(--font-size-sm);
|
||||
--font-caption: var(--font-size-xs);
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive States
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Focus ring */
|
||||
--ring-width: 2px;
|
||||
--ring-offset: 2px;
|
||||
--ring-color: var(--color-ring);
|
||||
|
||||
/* Opacity for disabled */
|
||||
--opacity-disabled: 0.5;
|
||||
|
||||
/* Transitions */
|
||||
--transition-colors: color, background-color, border-color;
|
||||
--transition-transform: transform;
|
||||
--transition-all: all;
|
||||
}
|
||||
```
|
||||
|
||||
## Dark Mode Overrides
|
||||
|
||||
```css
|
||||
.dark {
|
||||
--color-background: var(--color-gray-950);
|
||||
--color-foreground: var(--color-gray-50);
|
||||
|
||||
--color-card: var(--color-gray-900);
|
||||
--color-card-foreground: var(--color-gray-50);
|
||||
|
||||
--color-popover: var(--color-gray-900);
|
||||
--color-popover-foreground: var(--color-gray-50);
|
||||
|
||||
--color-muted: var(--color-gray-800);
|
||||
--color-muted-foreground: var(--color-gray-400);
|
||||
|
||||
--color-secondary: var(--color-gray-800);
|
||||
--color-secondary-foreground: var(--color-gray-50);
|
||||
|
||||
--color-accent: var(--color-gray-800);
|
||||
--color-accent-foreground: var(--color-gray-50);
|
||||
|
||||
--color-border: var(--color-gray-800);
|
||||
--color-input: var(--color-gray-800);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Applying Semantic Tokens
|
||||
|
||||
```css
|
||||
/* Good - uses semantic tokens */
|
||||
.card {
|
||||
background: var(--color-card);
|
||||
color: var(--color-card-foreground);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Bad - uses primitive tokens directly */
|
||||
.card {
|
||||
background: var(--color-gray-50);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Switching
|
||||
|
||||
Semantic tokens enable instant theme switching:
|
||||
|
||||
```js
|
||||
// Toggle dark mode
|
||||
document.documentElement.classList.toggle('dark');
|
||||
```
|
||||
241
.claude/skills/design-system/references/states-and-variants.md
Normal file
241
.claude/skills/design-system/references/states-and-variants.md
Normal file
@ -0,0 +1,241 @@
|
||||
# States and Variants
|
||||
|
||||
Component state definitions and variant patterns.
|
||||
|
||||
## Interactive States
|
||||
|
||||
### State Definitions
|
||||
|
||||
| State | Trigger | Visual Change |
|
||||
|-------|---------|---------------|
|
||||
| default | None | Base appearance |
|
||||
| hover | Mouse over | Slight color shift |
|
||||
| focus | Tab/click | Focus ring |
|
||||
| active | Mouse down | Darkest color |
|
||||
| disabled | disabled attr | Reduced opacity |
|
||||
| loading | Async action | Spinner + opacity |
|
||||
|
||||
### State Priority
|
||||
|
||||
When multiple states apply, priority (highest to lowest):
|
||||
|
||||
1. disabled
|
||||
2. loading
|
||||
3. active
|
||||
4. focus
|
||||
5. hover
|
||||
6. default
|
||||
|
||||
### State Transitions
|
||||
|
||||
```css
|
||||
/* Standard transition for interactive elements */
|
||||
.interactive {
|
||||
transition-property: color, background-color, border-color, box-shadow;
|
||||
transition-duration: var(--duration-fast);
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
| Transition | Duration | Easing |
|
||||
|------------|----------|--------|
|
||||
| Color changes | 150ms | ease-in-out |
|
||||
| Background | 150ms | ease-in-out |
|
||||
| Transform | 200ms | ease-out |
|
||||
| Opacity | 150ms | ease |
|
||||
| Shadow | 200ms | ease-out |
|
||||
|
||||
## Focus States
|
||||
|
||||
### Focus Ring Spec
|
||||
|
||||
```css
|
||||
/* Standard focus ring */
|
||||
.focusable:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 var(--ring-offset) var(--color-background),
|
||||
0 0 0 calc(var(--ring-offset) + var(--ring-width)) var(--ring-color);
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Ring width | 2px |
|
||||
| Ring offset | 2px |
|
||||
| Ring color | primary (blue-500) |
|
||||
| Offset color | background |
|
||||
|
||||
### Focus Within
|
||||
|
||||
```css
|
||||
/* Container focus when child is focused */
|
||||
.container:focus-within {
|
||||
border-color: var(--color-ring);
|
||||
}
|
||||
```
|
||||
|
||||
## Disabled States
|
||||
|
||||
### Visual Treatment
|
||||
|
||||
```css
|
||||
.disabled {
|
||||
opacity: var(--opacity-disabled); /* 0.5 */
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Disabled Value |
|
||||
|----------|----------------|
|
||||
| Opacity | 50% |
|
||||
| Pointer events | none |
|
||||
| Cursor | not-allowed |
|
||||
| Background | muted |
|
||||
| Color | muted-foreground |
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Use `aria-disabled="true"` for semantic disabled
|
||||
- Use `disabled` attribute for form elements
|
||||
- Maintain sufficient contrast (3:1 minimum)
|
||||
|
||||
## Loading States
|
||||
|
||||
### Spinner Placement
|
||||
|
||||
| Component | Spinner Position |
|
||||
|-----------|------------------|
|
||||
| Button | Replace icon or center |
|
||||
| Input | Trailing position |
|
||||
| Card | Center overlay |
|
||||
| Page | Center of viewport |
|
||||
|
||||
### Loading Treatment
|
||||
|
||||
```css
|
||||
.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
/* spinner styles */
|
||||
}
|
||||
|
||||
.loading > * {
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
## Error States
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
```css
|
||||
.error {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.error:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--color-background),
|
||||
0 0 0 4px var(--color-error);
|
||||
}
|
||||
```
|
||||
|
||||
| Element | Error Treatment |
|
||||
|---------|-----------------|
|
||||
| Input border | red-500 |
|
||||
| Input focus ring | red/20% |
|
||||
| Helper text | red-600 |
|
||||
| Icon | red-500 |
|
||||
|
||||
### Error Messages
|
||||
|
||||
- Position below input
|
||||
- Use error color
|
||||
- Include icon for accessibility
|
||||
- Clear on valid input
|
||||
|
||||
## Variant Patterns
|
||||
|
||||
### Color Variants
|
||||
|
||||
```css
|
||||
/* Pattern for color variants */
|
||||
.component {
|
||||
--component-bg: var(--color-primary);
|
||||
--component-fg: var(--color-primary-foreground);
|
||||
background: var(--component-bg);
|
||||
color: var(--component-fg);
|
||||
}
|
||||
|
||||
.component.secondary {
|
||||
--component-bg: var(--color-secondary);
|
||||
--component-fg: var(--color-secondary-foreground);
|
||||
}
|
||||
|
||||
.component.destructive {
|
||||
--component-bg: var(--color-destructive);
|
||||
--component-fg: var(--color-destructive-foreground);
|
||||
}
|
||||
```
|
||||
|
||||
### Size Variants
|
||||
|
||||
```css
|
||||
/* Pattern for size variants */
|
||||
.component {
|
||||
--component-height: 40px;
|
||||
--component-padding: var(--space-4);
|
||||
--component-font: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.component.sm {
|
||||
--component-height: 32px;
|
||||
--component-padding: var(--space-3);
|
||||
--component-font: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.component.lg {
|
||||
--component-height: 48px;
|
||||
--component-padding: var(--space-6);
|
||||
--component-font: var(--font-size-base);
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Requirements
|
||||
|
||||
### Color Contrast
|
||||
|
||||
| Element | Minimum Ratio |
|
||||
|---------|---------------|
|
||||
| Normal text | 4.5:1 |
|
||||
| Large text (18px+) | 3:1 |
|
||||
| UI components | 3:1 |
|
||||
| Focus indicator | 3:1 |
|
||||
|
||||
### State Indicators
|
||||
|
||||
- Never rely on color alone
|
||||
- Use icons, text, or patterns
|
||||
- Ensure focus is visible
|
||||
- Provide loading announcements
|
||||
|
||||
### ARIA States
|
||||
|
||||
```html
|
||||
<!-- Disabled -->
|
||||
<button disabled aria-disabled="true">Submit</button>
|
||||
|
||||
<!-- Loading -->
|
||||
<button aria-busy="true" aria-describedby="loading-text">
|
||||
<span id="loading-text" class="sr-only">Loading...</span>
|
||||
</button>
|
||||
|
||||
<!-- Error -->
|
||||
<input aria-invalid="true" aria-describedby="error-msg">
|
||||
<span id="error-msg" role="alert">Error message</span>
|
||||
```
|
||||
251
.claude/skills/design-system/references/tailwind-integration.md
Normal file
251
.claude/skills/design-system/references/tailwind-integration.md
Normal file
@ -0,0 +1,251 @@
|
||||
# Tailwind Integration
|
||||
|
||||
Map design system tokens to Tailwind CSS configuration.
|
||||
|
||||
## CSS Variables Setup
|
||||
|
||||
### Base Layer
|
||||
|
||||
```css
|
||||
/* globals.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Primitives */
|
||||
--color-blue-600: 37 99 235; /* HSL: 217 91% 60% */
|
||||
|
||||
/* Semantic */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222 47% 11%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 220 14% 96%;
|
||||
--secondary-foreground: 222 47% 11%;
|
||||
--muted: 220 14% 96%;
|
||||
--muted-foreground: 220 9% 46%;
|
||||
--accent: 220 14% 96%;
|
||||
--accent-foreground: 222 47% 11%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 217 91% 60%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222 47% 4%;
|
||||
--foreground: 210 40% 98%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 217 33% 17%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217 33% 17%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
--accent: 217 33% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62% 30%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 217 33% 17%;
|
||||
--input: 217 33% 17%;
|
||||
--ring: 217 91% 60%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tailwind Config
|
||||
|
||||
### tailwind.config.ts
|
||||
|
||||
```typescript
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
## HSL Format Benefits
|
||||
|
||||
Using HSL without function allows opacity modifiers:
|
||||
|
||||
```tsx
|
||||
// With HSL format (space-separated)
|
||||
<div className="bg-primary/50"> // 50% opacity
|
||||
<div className="text-primary/80"> // 80% opacity
|
||||
|
||||
// CSS output
|
||||
background-color: hsl(217 91% 60% / 0.5);
|
||||
```
|
||||
|
||||
## Component Classes
|
||||
|
||||
### Button Example
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center
|
||||
rounded-md font-medium
|
||||
transition-colors
|
||||
focus-visible:outline-none focus-visible:ring-2
|
||||
focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:pointer-events-none disabled:opacity-50;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
@apply bg-primary text-primary-foreground
|
||||
hover:bg-primary/90;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary text-secondary-foreground
|
||||
hover:bg-secondary/80;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply border border-input bg-background
|
||||
hover:bg-accent hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply hover:bg-accent hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
@apply bg-destructive text-destructive-foreground
|
||||
hover:bg-destructive/90;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.btn-sm { @apply h-8 px-3 text-xs; }
|
||||
.btn-md { @apply h-10 px-4 text-sm; }
|
||||
.btn-lg { @apply h-12 px-6 text-base; }
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing Integration
|
||||
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
theme: {
|
||||
extend: {
|
||||
spacing: {
|
||||
// Map to CSS variables if needed
|
||||
'section': 'var(--spacing-section)',
|
||||
'component': 'var(--spacing-component)',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Animation Tokens
|
||||
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
theme: {
|
||||
extend: {
|
||||
transitionDuration: {
|
||||
fast: '150ms',
|
||||
normal: '200ms',
|
||||
slow: '300ms',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dark Mode Toggle
|
||||
|
||||
```typescript
|
||||
// Toggle dark mode
|
||||
function toggleDarkMode() {
|
||||
document.documentElement.classList.toggle('dark')
|
||||
}
|
||||
|
||||
// System preference
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
```
|
||||
|
||||
## shadcn/ui Alignment
|
||||
|
||||
This configuration aligns with shadcn/ui conventions:
|
||||
|
||||
- Same CSS variable naming
|
||||
- Same HSL format
|
||||
- Same color scale structure
|
||||
- Compatible with `npx shadcn@latest add` commands
|
||||
|
||||
### Using with shadcn/ui
|
||||
|
||||
```bash
|
||||
# Initialize (uses same token structure)
|
||||
npx shadcn@latest init
|
||||
|
||||
# Add components (styled with these tokens)
|
||||
npx shadcn@latest add button card input
|
||||
```
|
||||
|
||||
Components will automatically use your design system tokens.
|
||||
224
.claude/skills/design-system/references/token-architecture.md
Normal file
224
.claude/skills/design-system/references/token-architecture.md
Normal file
@ -0,0 +1,224 @@
|
||||
# Token Architecture
|
||||
|
||||
Three-layer token system for scalable, themeable design systems.
|
||||
|
||||
## Layer Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Component Tokens │ Per-component overrides
|
||||
│ --button-bg, --card-padding │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Semantic Tokens │ Purpose-based aliases
|
||||
│ --color-primary, --spacing-section │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Primitive Tokens │ Raw design values
|
||||
│ --color-blue-600, --space-4 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Why Three Layers?
|
||||
|
||||
| Layer | Purpose | When to Change |
|
||||
|-------|---------|----------------|
|
||||
| Primitive | Base values (colors, sizes) | Rarely - foundational |
|
||||
| Semantic | Meaning assignment | Theme switching |
|
||||
| Component | Component customization | Per-component needs |
|
||||
|
||||
## Layer 1: Primitive Tokens
|
||||
|
||||
Raw design values without semantic meaning.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-gray-50: #F9FAFB;
|
||||
--color-gray-900: #111827;
|
||||
--color-blue-500: #3B82F6;
|
||||
--color-blue-600: #2563EB;
|
||||
|
||||
/* Spacing (4px base) */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
|
||||
/* Typography */
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-default: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
|
||||
--shadow-default: 0 1px 3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
## Layer 2: Semantic Tokens
|
||||
|
||||
Purpose-based aliases that reference primitives.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Background */
|
||||
--color-background: var(--color-gray-50);
|
||||
--color-foreground: var(--color-gray-900);
|
||||
|
||||
/* Primary */
|
||||
--color-primary: var(--color-blue-600);
|
||||
--color-primary-hover: var(--color-blue-700);
|
||||
|
||||
/* Secondary */
|
||||
--color-secondary: var(--color-gray-100);
|
||||
--color-secondary-foreground: var(--color-gray-900);
|
||||
|
||||
/* Muted */
|
||||
--color-muted: var(--color-gray-100);
|
||||
--color-muted-foreground: var(--color-gray-500);
|
||||
|
||||
/* Destructive */
|
||||
--color-destructive: var(--color-red-600);
|
||||
--color-destructive-foreground: white;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-component: var(--space-4);
|
||||
--spacing-section: var(--space-6);
|
||||
}
|
||||
```
|
||||
|
||||
## Layer 3: Component Tokens
|
||||
|
||||
Component-specific tokens referencing semantic layer.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Button */
|
||||
--button-bg: var(--color-primary);
|
||||
--button-fg: white;
|
||||
--button-hover-bg: var(--color-primary-hover);
|
||||
--button-padding-x: var(--space-4);
|
||||
--button-padding-y: var(--space-2);
|
||||
--button-radius: var(--radius-default);
|
||||
|
||||
/* Input */
|
||||
--input-bg: var(--color-background);
|
||||
--input-border: var(--color-gray-300);
|
||||
--input-focus-ring: var(--color-primary);
|
||||
--input-padding: var(--space-2) var(--space-3);
|
||||
|
||||
/* Card */
|
||||
--card-bg: var(--color-background);
|
||||
--card-border: var(--color-gray-200);
|
||||
--card-padding: var(--space-4);
|
||||
--card-radius: var(--radius-lg);
|
||||
--card-shadow: var(--shadow-default);
|
||||
}
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Override semantic tokens for dark theme:
|
||||
|
||||
```css
|
||||
.dark {
|
||||
--color-background: var(--color-gray-900);
|
||||
--color-foreground: var(--color-gray-50);
|
||||
--color-muted: var(--color-gray-800);
|
||||
--color-muted-foreground: var(--color-gray-400);
|
||||
--color-secondary: var(--color-gray-800);
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```
|
||||
--{category}-{item}-{variant}-{state}
|
||||
|
||||
Examples:
|
||||
--color-primary # category-item
|
||||
--color-primary-hover # category-item-state
|
||||
--button-bg-hover # component-property-state
|
||||
--space-section-sm # category-semantic-variant
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
| Category | Examples |
|
||||
|----------|----------|
|
||||
| color | primary, secondary, muted, destructive |
|
||||
| space | 1, 2, 4, 8, section, component |
|
||||
| font-size | xs, sm, base, lg, xl |
|
||||
| radius | sm, default, lg, full |
|
||||
| shadow | sm, default, lg |
|
||||
| duration | fast, normal, slow |
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
tokens/
|
||||
├── primitives.css # Raw values
|
||||
├── semantic.css # Purpose aliases
|
||||
├── components.css # Component tokens
|
||||
└── index.css # Imports all
|
||||
```
|
||||
|
||||
Or single file with layer comments:
|
||||
|
||||
```css
|
||||
/* === PRIMITIVES === */
|
||||
:root { ... }
|
||||
|
||||
/* === SEMANTIC === */
|
||||
:root { ... }
|
||||
|
||||
/* === COMPONENTS === */
|
||||
:root { ... }
|
||||
|
||||
/* === DARK MODE === */
|
||||
.dark { ... }
|
||||
```
|
||||
|
||||
## Migration from Flat Tokens
|
||||
|
||||
Before (flat):
|
||||
```css
|
||||
--button-primary-bg: #2563EB;
|
||||
--button-secondary-bg: #F3F4F6;
|
||||
```
|
||||
|
||||
After (three-layer):
|
||||
```css
|
||||
/* Primitive */
|
||||
--color-blue-600: #2563EB;
|
||||
--color-gray-100: #F3F4F6;
|
||||
|
||||
/* Semantic */
|
||||
--color-primary: var(--color-blue-600);
|
||||
--color-secondary: var(--color-gray-100);
|
||||
|
||||
/* Component */
|
||||
--button-bg: var(--color-primary);
|
||||
--button-secondary-bg: var(--color-secondary);
|
||||
```
|
||||
|
||||
## W3C DTCG Alignment
|
||||
|
||||
Token JSON format (W3C Design Tokens Community Group):
|
||||
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"blue": {
|
||||
"600": {
|
||||
"$value": "#2563EB",
|
||||
"$type": "color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
99
.claude/skills/design-system/scripts/embed-tokens.cjs
Normal file
99
.claude/skills/design-system/scripts/embed-tokens.cjs
Normal file
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* embed-tokens.cjs
|
||||
* Reads design-tokens.css and outputs embeddable inline CSS.
|
||||
* Use when generating standalone HTML files (infographics, slides, etc.)
|
||||
*
|
||||
* Usage:
|
||||
* node embed-tokens.cjs # Output full CSS
|
||||
* node embed-tokens.cjs --minimal # Output only commonly used tokens
|
||||
* node embed-tokens.cjs --style # Wrap in <style> tags
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Find project root (look for assets/design-tokens.css)
|
||||
function findProjectRoot(startDir) {
|
||||
let dir = startDir;
|
||||
while (dir !== '/') {
|
||||
if (fs.existsSync(path.join(dir, 'assets', 'design-tokens.css'))) {
|
||||
return dir;
|
||||
}
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectRoot = findProjectRoot(process.cwd());
|
||||
if (!projectRoot) {
|
||||
console.error('Error: Could not find assets/design-tokens.css');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tokensPath = path.join(projectRoot, 'assets', 'design-tokens.css');
|
||||
|
||||
// Minimal tokens commonly used in infographics/slides
|
||||
const MINIMAL_TOKENS = [
|
||||
'--primitive-spacing-',
|
||||
'--primitive-fontSize-',
|
||||
'--primitive-fontWeight-',
|
||||
'--primitive-lineHeight-',
|
||||
'--primitive-radius-',
|
||||
'--primitive-shadow-glow-',
|
||||
'--primitive-gradient-',
|
||||
'--primitive-duration-',
|
||||
'--color-primary',
|
||||
'--color-secondary',
|
||||
'--color-accent',
|
||||
'--color-background',
|
||||
'--color-surface',
|
||||
'--color-foreground',
|
||||
'--color-border',
|
||||
'--typography-font-',
|
||||
'--card-',
|
||||
];
|
||||
|
||||
function extractTokens(css, minimal = false) {
|
||||
// Extract :root block
|
||||
const rootMatch = css.match(/:root\s*\{([^}]+)\}/g);
|
||||
if (!rootMatch) return '';
|
||||
|
||||
let allVars = [];
|
||||
for (const block of rootMatch) {
|
||||
const vars = block.match(/--[\w-]+:\s*[^;]+;/g) || [];
|
||||
allVars = allVars.concat(vars);
|
||||
}
|
||||
|
||||
if (minimal) {
|
||||
allVars = allVars.filter(v =>
|
||||
MINIMAL_TOKENS.some(token => v.includes(token))
|
||||
);
|
||||
}
|
||||
|
||||
// Dedupe
|
||||
allVars = [...new Set(allVars)];
|
||||
|
||||
return `:root {\n ${allVars.join('\n ')}\n}`;
|
||||
}
|
||||
|
||||
// Parse args
|
||||
const args = process.argv.slice(2);
|
||||
const minimal = args.includes('--minimal');
|
||||
const wrapStyle = args.includes('--style');
|
||||
|
||||
try {
|
||||
const css = fs.readFileSync(tokensPath, 'utf-8');
|
||||
let output = extractTokens(css, minimal);
|
||||
|
||||
if (wrapStyle) {
|
||||
output = `<style>\n/* Design Tokens (embedded for standalone HTML) */\n${output}\n</style>`;
|
||||
} else {
|
||||
output = `/* Design Tokens (embedded for standalone HTML) */\n${output}`;
|
||||
}
|
||||
|
||||
console.log(output);
|
||||
} catch (err) {
|
||||
console.error(`Error reading tokens: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
317
.claude/skills/design-system/scripts/fetch-background.py
Normal file
317
.claude/skills/design-system/scripts/fetch-background.py
Normal file
@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Background Image Fetcher
|
||||
Fetches real images from Pexels for slide backgrounds.
|
||||
Uses web scraping (no API key required) or WebFetch tool integration.
|
||||
"""
|
||||
|
||||
import json
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Project root relative to this script
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
||||
TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv'
|
||||
|
||||
|
||||
def resolve_token_reference(ref: str, tokens: dict) -> str:
|
||||
"""Resolve token reference like {primitive.color.ocean-blue.500} to hex value."""
|
||||
if not ref or not ref.startswith('{') or not ref.endswith('}'):
|
||||
return ref # Already a value, not a reference
|
||||
|
||||
# Parse reference: {primitive.color.ocean-blue.500}
|
||||
path = ref[1:-1].split('.') # ['primitive', 'color', 'ocean-blue', '500']
|
||||
current = tokens
|
||||
for key in path:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(key)
|
||||
else:
|
||||
return None # Invalid path
|
||||
# Return $value if it's a token object
|
||||
if isinstance(current, dict) and '$value' in current:
|
||||
return current['$value']
|
||||
return current
|
||||
|
||||
|
||||
def load_brand_colors():
|
||||
"""Load colors from assets/design-tokens.json for overlay gradients.
|
||||
|
||||
Resolves semantic token references to actual hex values.
|
||||
"""
|
||||
try:
|
||||
with open(TOKENS_PATH) as f:
|
||||
tokens = json.load(f)
|
||||
|
||||
colors = tokens.get('primitive', {}).get('color', {})
|
||||
semantic = tokens.get('semantic', {}).get('color', {})
|
||||
|
||||
# Try semantic tokens first (preferred) - resolve references
|
||||
if semantic:
|
||||
primary_ref = semantic.get('primary', {}).get('$value')
|
||||
secondary_ref = semantic.get('secondary', {}).get('$value')
|
||||
accent_ref = semantic.get('accent', {}).get('$value')
|
||||
background_ref = semantic.get('background', {}).get('$value')
|
||||
|
||||
primary = resolve_token_reference(primary_ref, tokens)
|
||||
secondary = resolve_token_reference(secondary_ref, tokens)
|
||||
accent = resolve_token_reference(accent_ref, tokens)
|
||||
background = resolve_token_reference(background_ref, tokens)
|
||||
|
||||
if primary and secondary:
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondary': secondary,
|
||||
'accent': accent or primary,
|
||||
'background': background or '#0D0D0D',
|
||||
}
|
||||
|
||||
# Fallback: find first color palette with 500 value (primary)
|
||||
primary_keys = ['ocean-blue', 'coral', 'blue', 'primary']
|
||||
secondary_keys = ['golden-amber', 'purple', 'amber', 'secondary']
|
||||
accent_keys = ['emerald', 'mint', 'green', 'accent']
|
||||
|
||||
primary_color = None
|
||||
secondary_color = None
|
||||
accent_color = None
|
||||
|
||||
for key in primary_keys:
|
||||
if key in colors and isinstance(colors[key], dict):
|
||||
primary_color = colors[key].get('500', {}).get('$value')
|
||||
if primary_color:
|
||||
break
|
||||
|
||||
for key in secondary_keys:
|
||||
if key in colors and isinstance(colors[key], dict):
|
||||
secondary_color = colors[key].get('500', {}).get('$value')
|
||||
if secondary_color:
|
||||
break
|
||||
|
||||
for key in accent_keys:
|
||||
if key in colors and isinstance(colors[key], dict):
|
||||
accent_color = colors[key].get('500', {}).get('$value')
|
||||
if accent_color:
|
||||
break
|
||||
|
||||
background = colors.get('dark', {}).get('800', {}).get('$value', '#0D0D0D')
|
||||
|
||||
return {
|
||||
'primary': primary_color or '#3B82F6',
|
||||
'secondary': secondary_color or '#F59E0B',
|
||||
'accent': accent_color or '#10B981',
|
||||
'background': background,
|
||||
}
|
||||
except (FileNotFoundError, KeyError, TypeError):
|
||||
# Fallback defaults
|
||||
return {
|
||||
'primary': '#3B82F6',
|
||||
'secondary': '#F59E0B',
|
||||
'accent': '#10B981',
|
||||
'background': '#0D0D0D',
|
||||
}
|
||||
|
||||
|
||||
def load_backgrounds_config():
|
||||
"""Load background configuration from CSV."""
|
||||
config = {}
|
||||
try:
|
||||
with open(BACKGROUNDS_CSV, newline='') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
config[row['slide_type']] = row
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: {BACKGROUNDS_CSV} not found")
|
||||
return config
|
||||
|
||||
|
||||
def get_overlay_css(style: str, brand_colors: dict) -> str:
|
||||
"""Generate overlay CSS using brand colors from design-tokens.json."""
|
||||
overlays = {
|
||||
'gradient-dark': f"linear-gradient(135deg, {brand_colors['background']}E6, {brand_colors['background']}B3)",
|
||||
'gradient-brand': f"linear-gradient(135deg, {brand_colors['primary']}CC, {brand_colors['secondary']}99)",
|
||||
'gradient-accent': f"linear-gradient(135deg, {brand_colors['accent']}99, transparent)",
|
||||
'blur-dark': f"rgba(13,13,13,0.8)",
|
||||
'desaturate-dark': f"rgba(13,13,13,0.7)",
|
||||
}
|
||||
return overlays.get(style, overlays['gradient-dark'])
|
||||
|
||||
|
||||
# Curated high-quality images from Pexels (free to use, pre-selected for brand aesthetic)
|
||||
CURATED_IMAGES = {
|
||||
'hero': [
|
||||
'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/2582937/pexels-photo-2582937.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/1089438/pexels-photo-1089438.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'vision': [
|
||||
'https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3182812/pexels-photo-3182812.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'team': [
|
||||
'https://images.pexels.com/photos/3184418/pexels-photo-3184418.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3184338/pexels-photo-3184338.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3182773/pexels-photo-3182773.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'testimonial': [
|
||||
'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/1181622/pexels-photo-1181622.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'cta': [
|
||||
'https://images.pexels.com/photos/3184339/pexels-photo-3184339.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3184298/pexels-photo-3184298.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'problem': [
|
||||
'https://images.pexels.com/photos/3760529/pexels-photo-3760529.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/897817/pexels-photo-897817.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'solution': [
|
||||
'https://images.pexels.com/photos/3184292/pexels-photo-3184292.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3184644/pexels-photo-3184644.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'hook': [
|
||||
'https://images.pexels.com/photos/2582937/pexels-photo-2582937.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/1089438/pexels-photo-1089438.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'social': [
|
||||
'https://images.pexels.com/photos/3184360/pexels-photo-3184360.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
'demo': [
|
||||
'https://images.pexels.com/photos/1181675/pexels-photo-1181675.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
'https://images.pexels.com/photos/3861958/pexels-photo-3861958.jpeg?auto=compress&cs=tinysrgb&w=1920',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_curated_images(slide_type: str) -> list:
|
||||
"""Get curated images for slide type."""
|
||||
return CURATED_IMAGES.get(slide_type, CURATED_IMAGES.get('hero', []))
|
||||
|
||||
|
||||
def get_pexels_search_url(keywords: str) -> str:
|
||||
"""Generate Pexels search URL for manual lookup."""
|
||||
import urllib.parse
|
||||
return f"https://www.pexels.com/search/{urllib.parse.quote(keywords)}/"
|
||||
|
||||
|
||||
def get_background_image(slide_type: str) -> dict:
|
||||
"""
|
||||
Get curated image matching slide type and brand aesthetic.
|
||||
Uses pre-selected Pexels images (no API/scraping needed).
|
||||
"""
|
||||
brand_colors = load_brand_colors()
|
||||
config = load_backgrounds_config()
|
||||
|
||||
slide_config = config.get(slide_type)
|
||||
overlay_style = 'gradient-dark'
|
||||
keywords = slide_type
|
||||
|
||||
if slide_config:
|
||||
keywords = slide_config.get('search_keywords', slide_config.get('image_category', slide_type))
|
||||
overlay_style = slide_config.get('overlay_style', 'gradient-dark')
|
||||
|
||||
# Get curated images
|
||||
urls = get_curated_images(slide_type)
|
||||
if urls:
|
||||
return {
|
||||
'url': urls[0],
|
||||
'all_urls': urls,
|
||||
'overlay': get_overlay_css(overlay_style, brand_colors),
|
||||
'attribution': 'Photo from Pexels (free to use)',
|
||||
'source': 'pexels-curated',
|
||||
'search_url': get_pexels_search_url(keywords),
|
||||
}
|
||||
|
||||
# Fallback: provide search URL for manual selection
|
||||
return {
|
||||
'url': None,
|
||||
'overlay': get_overlay_css(overlay_style, brand_colors),
|
||||
'keywords': keywords,
|
||||
'search_url': get_pexels_search_url(keywords),
|
||||
'available_types': list(CURATED_IMAGES.keys()),
|
||||
}
|
||||
|
||||
|
||||
def generate_css_for_background(result: dict, slide_class: str = '.slide-with-bg') -> str:
|
||||
"""Generate CSS for a background slide."""
|
||||
if not result.get('url'):
|
||||
search_url = result.get('search_url', '')
|
||||
return f"""/* No image scraped. Search manually: {search_url} */
|
||||
/* Overlay ready: {result.get('overlay', 'gradient-dark')} */
|
||||
"""
|
||||
|
||||
return f"""{slide_class} {{
|
||||
background-image: url('{result['url']}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}}
|
||||
|
||||
{slide_class}::before {{
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: {result['overlay']};
|
||||
}}
|
||||
|
||||
{slide_class} .content {{
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}}
|
||||
|
||||
/* {result.get('attribution', 'Pexels')} - {result.get('search_url', '')} */
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Get background images for slides')
|
||||
parser.add_argument('slide_type', nargs='?', help='Slide type (hero, vision, team, etc.)')
|
||||
parser.add_argument('--list', action='store_true', help='List available slide types')
|
||||
parser.add_argument('--css', action='store_true', help='Output CSS for the background')
|
||||
parser.add_argument('--json', action='store_true', help='Output JSON')
|
||||
parser.add_argument('--colors', action='store_true', help='Show brand colors')
|
||||
parser.add_argument('--all', action='store_true', help='Show all curated URLs')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.colors:
|
||||
colors = load_brand_colors()
|
||||
print("\nBrand Colors (from design-tokens.json):")
|
||||
for name, value in colors.items():
|
||||
print(f" {name}: {value}")
|
||||
return
|
||||
|
||||
if args.list:
|
||||
print("\nAvailable slide types (curated images):")
|
||||
for slide_type, urls in CURATED_IMAGES.items():
|
||||
print(f" {slide_type}: {len(urls)} images")
|
||||
return
|
||||
|
||||
if not args.slide_type:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
result = get_background_image(args.slide_type)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
elif args.css:
|
||||
print(generate_css_for_background(result))
|
||||
elif args.all:
|
||||
print(f"\nAll images for '{args.slide_type}':")
|
||||
for i, url in enumerate(result.get('all_urls', []), 1):
|
||||
print(f" {i}. {url}")
|
||||
else:
|
||||
print(f"\nImage URL: {result['url']}")
|
||||
print(f"Alternatives: {len(result.get('all_urls', []))} available (use --all)")
|
||||
print(f"Overlay: {result['overlay']}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
753
.claude/skills/design-system/scripts/generate-slide.py
Normal file
753
.claude/skills/design-system/scripts/generate-slide.py
Normal file
@ -0,0 +1,753 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Slide Generator - Generates HTML slides using design tokens
|
||||
ALL styles MUST use CSS variables from design-tokens.css
|
||||
NO hardcoded colors, fonts, or spacing allowed
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
DATA_DIR = SCRIPT_DIR.parent / "data"
|
||||
TOKENS_CSS = Path(__file__).resolve().parents[4] / "assets" / "design-tokens.css"
|
||||
TOKENS_JSON = Path(__file__).resolve().parents[4] / "assets" / "design-tokens.json"
|
||||
OUTPUT_DIR = Path(__file__).resolve().parents[4] / "assets" / "designs" / "slides"
|
||||
|
||||
# ============ BRAND-COMPLIANT SLIDE TEMPLATE ============
|
||||
# ALL values reference CSS variables from design-tokens.css
|
||||
|
||||
SLIDE_TEMPLATE = '''<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- Brand Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Design Tokens - SINGLE SOURCE OF TRUTH -->
|
||||
<link rel="stylesheet" href="{tokens_css_path}">
|
||||
|
||||
<style>
|
||||
/* ============================================
|
||||
STRICT TOKEN USAGE - NO HARDCODED VALUES
|
||||
All styles MUST use var(--token-name)
|
||||
============================================ */
|
||||
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
html, body {{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: var(--typography-font-body);
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
line-height: var(--primitive-lineHeight-relaxed);
|
||||
}}
|
||||
|
||||
/* Slide Container - 16:9 aspect ratio */
|
||||
.slide-deck {{
|
||||
width: 100%;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
|
||||
.slide {{
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: var(--slide-padding);
|
||||
background: var(--slide-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.slide + .slide {{
|
||||
margin-top: var(--primitive-spacing-8);
|
||||
}}
|
||||
|
||||
/* Background Variants */
|
||||
.slide--surface {{
|
||||
background: var(--slide-bg-surface);
|
||||
}}
|
||||
|
||||
.slide--gradient {{
|
||||
background: var(--slide-bg-gradient);
|
||||
}}
|
||||
|
||||
.slide--glow::before {{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 150%;
|
||||
height: 150%;
|
||||
background: var(--primitive-gradient-glow);
|
||||
pointer-events: none;
|
||||
}}
|
||||
|
||||
/* Typography - MUST use token fonts and sizes */
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
font-family: var(--typography-font-heading);
|
||||
font-weight: var(--primitive-fontWeight-bold);
|
||||
line-height: var(--primitive-lineHeight-tight);
|
||||
}}
|
||||
|
||||
.slide-title {{
|
||||
font-size: var(--slide-title-size);
|
||||
background: var(--primitive-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}}
|
||||
|
||||
.slide-heading {{
|
||||
font-size: var(--slide-heading-size);
|
||||
color: var(--color-foreground);
|
||||
}}
|
||||
|
||||
.slide-subheading {{
|
||||
font-size: var(--primitive-fontSize-3xl);
|
||||
color: var(--color-foreground-secondary);
|
||||
font-weight: var(--primitive-fontWeight-medium);
|
||||
}}
|
||||
|
||||
.slide-body {{
|
||||
font-size: var(--slide-body-size);
|
||||
color: var(--color-foreground-secondary);
|
||||
max-width: 80ch;
|
||||
}}
|
||||
|
||||
/* Brand Colors - Primary/Secondary/Accent */
|
||||
.text-primary {{ color: var(--color-primary); }}
|
||||
.text-secondary {{ color: var(--color-secondary); }}
|
||||
.text-accent {{ color: var(--color-accent); }}
|
||||
.text-muted {{ color: var(--color-foreground-muted); }}
|
||||
|
||||
.bg-primary {{ background: var(--color-primary); }}
|
||||
.bg-secondary {{ background: var(--color-secondary); }}
|
||||
.bg-accent {{ background: var(--color-accent); }}
|
||||
.bg-surface {{ background: var(--color-surface); }}
|
||||
|
||||
/* Cards - Using component tokens */
|
||||
.card {{
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--card-padding);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: border-color var(--primitive-duration-base) var(--primitive-easing-out);
|
||||
}}
|
||||
|
||||
.card:hover {{
|
||||
border-color: var(--card-border-hover);
|
||||
}}
|
||||
|
||||
/* Buttons - Using component tokens */
|
||||
.btn {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--button-primary-padding-y) var(--button-primary-padding-x);
|
||||
border-radius: var(--button-primary-radius);
|
||||
font-size: var(--button-primary-font-size);
|
||||
font-weight: var(--button-primary-font-weight);
|
||||
font-family: var(--typography-font-body);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all var(--primitive-duration-base) var(--primitive-easing-out);
|
||||
}}
|
||||
|
||||
.btn-primary {{
|
||||
background: var(--button-primary-bg);
|
||||
color: var(--button-primary-fg);
|
||||
box-shadow: var(--button-primary-shadow);
|
||||
}}
|
||||
|
||||
.btn-primary:hover {{
|
||||
background: var(--button-primary-bg-hover);
|
||||
}}
|
||||
|
||||
.btn-secondary {{
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 2px solid var(--color-primary);
|
||||
}}
|
||||
|
||||
/* Layout Utilities */
|
||||
.flex {{ display: flex; }}
|
||||
.flex-col {{ flex-direction: column; }}
|
||||
.items-center {{ align-items: center; }}
|
||||
.justify-center {{ justify-content: center; }}
|
||||
.justify-between {{ justify-content: space-between; }}
|
||||
.gap-4 {{ gap: var(--primitive-spacing-4); }}
|
||||
.gap-6 {{ gap: var(--primitive-spacing-6); }}
|
||||
.gap-8 {{ gap: var(--primitive-spacing-8); }}
|
||||
|
||||
.grid {{ display: grid; }}
|
||||
.grid-2 {{ grid-template-columns: repeat(2, 1fr); }}
|
||||
.grid-3 {{ grid-template-columns: repeat(3, 1fr); }}
|
||||
.grid-4 {{ grid-template-columns: repeat(4, 1fr); }}
|
||||
|
||||
.text-center {{ text-align: center; }}
|
||||
.mt-auto {{ margin-top: auto; }}
|
||||
.mb-4 {{ margin-bottom: var(--primitive-spacing-4); }}
|
||||
.mb-6 {{ margin-bottom: var(--primitive-spacing-6); }}
|
||||
.mb-8 {{ margin-bottom: var(--primitive-spacing-8); }}
|
||||
|
||||
/* Metric Cards */
|
||||
.metric {{
|
||||
text-align: center;
|
||||
padding: var(--primitive-spacing-6);
|
||||
}}
|
||||
|
||||
.metric-value {{
|
||||
font-family: var(--typography-font-heading);
|
||||
font-size: var(--primitive-fontSize-6xl);
|
||||
font-weight: var(--primitive-fontWeight-bold);
|
||||
background: var(--primitive-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}}
|
||||
|
||||
.metric-label {{
|
||||
font-size: var(--primitive-fontSize-lg);
|
||||
color: var(--color-foreground-secondary);
|
||||
margin-top: var(--primitive-spacing-2);
|
||||
}}
|
||||
|
||||
/* Feature List */
|
||||
.feature-item {{
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--primitive-spacing-4);
|
||||
padding: var(--primitive-spacing-4) 0;
|
||||
}}
|
||||
|
||||
.feature-icon {{
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--primitive-radius-lg);
|
||||
background: var(--color-surface-elevated);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--primitive-fontSize-xl);
|
||||
flex-shrink: 0;
|
||||
}}
|
||||
|
||||
.feature-content h4 {{
|
||||
font-size: var(--primitive-fontSize-xl);
|
||||
color: var(--color-foreground);
|
||||
margin-bottom: var(--primitive-spacing-2);
|
||||
}}
|
||||
|
||||
.feature-content p {{
|
||||
color: var(--color-foreground-secondary);
|
||||
font-size: var(--primitive-fontSize-base);
|
||||
}}
|
||||
|
||||
/* Testimonial */
|
||||
.testimonial {{
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--primitive-radius-xl);
|
||||
padding: var(--primitive-spacing-8);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}}
|
||||
|
||||
.testimonial-quote {{
|
||||
font-size: var(--primitive-fontSize-2xl);
|
||||
color: var(--color-foreground);
|
||||
font-style: italic;
|
||||
margin-bottom: var(--primitive-spacing-6);
|
||||
}}
|
||||
|
||||
.testimonial-author {{
|
||||
font-size: var(--primitive-fontSize-lg);
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--primitive-fontWeight-semibold);
|
||||
}}
|
||||
|
||||
.testimonial-role {{
|
||||
font-size: var(--primitive-fontSize-base);
|
||||
color: var(--color-foreground-muted);
|
||||
}}
|
||||
|
||||
/* Badge/Tag */
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: var(--primitive-spacing-2) var(--primitive-spacing-4);
|
||||
background: var(--color-surface-elevated);
|
||||
border-radius: var(--primitive-radius-full);
|
||||
font-size: var(--primitive-fontSize-sm);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--primitive-fontWeight-medium);
|
||||
}}
|
||||
|
||||
/* Chart Container */
|
||||
.chart-container {{
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--primitive-radius-xl);
|
||||
padding: var(--primitive-spacing-6);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
|
||||
.chart-title {{
|
||||
font-family: var(--typography-font-heading);
|
||||
font-size: var(--primitive-fontSize-xl);
|
||||
color: var(--color-foreground);
|
||||
margin-bottom: var(--primitive-spacing-4);
|
||||
}}
|
||||
|
||||
/* CSS-only Bar Chart */
|
||||
.bar-chart {{
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--primitive-spacing-4);
|
||||
height: 200px;
|
||||
padding-top: var(--primitive-spacing-4);
|
||||
}}
|
||||
|
||||
.bar {{
|
||||
flex: 1;
|
||||
background: var(--primitive-gradient-primary);
|
||||
border-radius: var(--primitive-radius-md) var(--primitive-radius-md) 0 0;
|
||||
position: relative;
|
||||
min-width: 40px;
|
||||
}}
|
||||
|
||||
.bar-label {{
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: var(--primitive-fontSize-sm);
|
||||
color: var(--color-foreground-muted);
|
||||
white-space: nowrap;
|
||||
}}
|
||||
|
||||
.bar-value {{
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: var(--primitive-fontSize-sm);
|
||||
color: var(--color-foreground);
|
||||
font-weight: var(--primitive-fontWeight-semibold);
|
||||
}}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress {{
|
||||
height: 12px;
|
||||
background: var(--color-surface-elevated);
|
||||
border-radius: var(--primitive-radius-full);
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.progress-fill {{
|
||||
height: 100%;
|
||||
background: var(--primitive-gradient-primary);
|
||||
border-radius: var(--primitive-radius-full);
|
||||
}}
|
||||
|
||||
/* Footer */
|
||||
.slide-footer {{
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: var(--primitive-spacing-6);
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-foreground-muted);
|
||||
font-size: var(--primitive-fontSize-sm);
|
||||
}}
|
||||
|
||||
/* Glow Effects */
|
||||
.glow-coral {{
|
||||
box-shadow: var(--primitive-shadow-glow-coral);
|
||||
}}
|
||||
|
||||
.glow-purple {{
|
||||
box-shadow: var(--primitive-shadow-glow-purple);
|
||||
}}
|
||||
|
||||
.glow-mint {{
|
||||
box-shadow: var(--primitive-shadow-glow-mint);
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide-deck">
|
||||
{slides_content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# ============ SLIDE GENERATORS ============
|
||||
|
||||
def generate_title_slide(data):
|
||||
"""Title slide with gradient headline"""
|
||||
return f'''
|
||||
<section class="slide slide--glow flex flex-col items-center justify-center text-center">
|
||||
<div class="badge mb-6">{data.get('badge', 'Pitch Deck')}</div>
|
||||
<h1 class="slide-title mb-6">{data.get('title', 'Your Title Here')}</h1>
|
||||
<p class="slide-subheading mb-8">{data.get('subtitle', 'Your compelling subtitle')}</p>
|
||||
<div class="flex gap-4">
|
||||
<a href="#" class="btn btn-primary">{data.get('cta', 'Get Started')}</a>
|
||||
<a href="#" class="btn btn-secondary">{data.get('secondary_cta', 'Learn More')}</a>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('date', datetime.now().strftime('%B %Y'))}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_problem_slide(data):
|
||||
"""Problem statement slide using PAS formula"""
|
||||
return f'''
|
||||
<section class="slide slide--surface">
|
||||
<div class="badge mb-6">The Problem</div>
|
||||
<h2 class="slide-heading mb-8">{data.get('headline', 'The problem your audience faces')}</h2>
|
||||
<div class="grid grid-3 gap-8">
|
||||
<div class="card">
|
||||
<div class="text-primary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">01</div>
|
||||
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_1_title', 'Pain Point 1')}</h4>
|
||||
<p class="text-muted">{data.get('pain_1_desc', 'Description of the first pain point')}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="text-secondary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">02</div>
|
||||
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_2_title', 'Pain Point 2')}</h4>
|
||||
<p class="text-muted">{data.get('pain_2_desc', 'Description of the second pain point')}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="text-accent" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">03</div>
|
||||
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_3_title', 'Pain Point 3')}</h4>
|
||||
<p class="text-muted">{data.get('pain_3_desc', 'Description of the third pain point')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('page', '2')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_solution_slide(data):
|
||||
"""Solution slide with feature highlights"""
|
||||
return f'''
|
||||
<section class="slide">
|
||||
<div class="badge mb-6">The Solution</div>
|
||||
<h2 class="slide-heading mb-8">{data.get('headline', 'How we solve this')}</h2>
|
||||
<div class="flex gap-8" style="flex: 1;">
|
||||
<div style="flex: 1;">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">✓</div>
|
||||
<div class="feature-content">
|
||||
<h4>{data.get('feature_1_title', 'Feature 1')}</h4>
|
||||
<p>{data.get('feature_1_desc', 'Description of feature 1')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">✓</div>
|
||||
<div class="feature-content">
|
||||
<h4>{data.get('feature_2_title', 'Feature 2')}</h4>
|
||||
<p>{data.get('feature_2_desc', 'Description of feature 2')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">✓</div>
|
||||
<div class="feature-content">
|
||||
<h4>{data.get('feature_3_title', 'Feature 3')}</h4>
|
||||
<p>{data.get('feature_3_desc', 'Description of feature 3')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1;" class="card flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-accent" style="font-size: 80px; margin-bottom: var(--primitive-spacing-4);">◆</div>
|
||||
<p class="text-muted">Product screenshot or demo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('page', '3')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_metrics_slide(data):
|
||||
"""Traction/metrics slide with large numbers"""
|
||||
metrics = data.get('metrics', [
|
||||
{'value': '10K+', 'label': 'Active Users'},
|
||||
{'value': '95%', 'label': 'Retention Rate'},
|
||||
{'value': '3x', 'label': 'Revenue Growth'},
|
||||
{'value': '$2M', 'label': 'ARR'}
|
||||
])
|
||||
|
||||
metrics_html = ''.join([f'''
|
||||
<div class="card metric">
|
||||
<div class="metric-value">{m['value']}</div>
|
||||
<div class="metric-label">{m['label']}</div>
|
||||
</div>
|
||||
''' for m in metrics[:4]])
|
||||
|
||||
return f'''
|
||||
<section class="slide slide--surface slide--glow">
|
||||
<div class="badge mb-6">Traction</div>
|
||||
<h2 class="slide-heading mb-8 text-center">{data.get('headline', 'Our Growth')}</h2>
|
||||
<div class="grid grid-4 gap-6" style="flex: 1; align-items: center;">
|
||||
{metrics_html}
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('page', '4')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_chart_slide(data):
|
||||
"""Chart slide with CSS bar chart"""
|
||||
bars = data.get('bars', [
|
||||
{'label': 'Q1', 'value': 40},
|
||||
{'label': 'Q2', 'value': 60},
|
||||
{'label': 'Q3', 'value': 80},
|
||||
{'label': 'Q4', 'value': 100}
|
||||
])
|
||||
|
||||
bars_html = ''.join([f'''
|
||||
<div class="bar" style="height: {b['value']}%;">
|
||||
<span class="bar-value">{b.get('display', str(b['value']) + '%')}</span>
|
||||
<span class="bar-label">{b['label']}</span>
|
||||
</div>
|
||||
''' for b in bars])
|
||||
|
||||
return f'''
|
||||
<section class="slide">
|
||||
<div class="badge mb-6">{data.get('badge', 'Growth')}</div>
|
||||
<h2 class="slide-heading mb-8">{data.get('headline', 'Revenue Growth')}</h2>
|
||||
<div class="chart-container" style="flex: 1;">
|
||||
<div class="chart-title">{data.get('chart_title', 'Quarterly Revenue')}</div>
|
||||
<div class="bar-chart" style="flex: 1; padding-bottom: 40px;">
|
||||
{bars_html}
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('page', '5')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_testimonial_slide(data):
|
||||
"""Social proof slide"""
|
||||
return f'''
|
||||
<section class="slide slide--surface flex flex-col justify-center">
|
||||
<div class="badge mb-6">What They Say</div>
|
||||
<div class="testimonial" style="max-width: 900px;">
|
||||
<p class="testimonial-quote">"{data.get('quote', 'This product changed how we work. Incredible results.')}"</p>
|
||||
<p class="testimonial-author">{data.get('author', 'Jane Doe')}</p>
|
||||
<p class="testimonial-role">{data.get('role', 'CEO, Example Company')}</p>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
<span>{data.get('company', 'Company Name')}</span>
|
||||
<span>{data.get('page', '6')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
def generate_cta_slide(data):
|
||||
"""Closing CTA slide"""
|
||||
return f'''
|
||||
<section class="slide slide--gradient flex flex-col items-center justify-center text-center">
|
||||
<h2 class="slide-heading mb-6" style="color: var(--color-foreground);">{data.get('headline', 'Ready to get started?')}</h2>
|
||||
<p class="slide-body mb-8" style="color: rgba(255,255,255,0.8);">{data.get('subheadline', 'Join thousands of teams already using our solution.')}</p>
|
||||
<div class="flex gap-4">
|
||||
<a href="{data.get('cta_url', '#')}" class="btn" style="background: var(--color-foreground); color: var(--color-primary);">{data.get('cta', 'Start Free Trial')}</a>
|
||||
</div>
|
||||
<div class="slide-footer" style="border-color: rgba(255,255,255,0.2); color: rgba(255,255,255,0.6);">
|
||||
<span>{data.get('contact', 'contact@example.com')}</span>
|
||||
<span>{data.get('website', 'www.example.com')}</span>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
|
||||
# Slide type mapping
|
||||
SLIDE_GENERATORS = {
|
||||
'title': generate_title_slide,
|
||||
'problem': generate_problem_slide,
|
||||
'solution': generate_solution_slide,
|
||||
'metrics': generate_metrics_slide,
|
||||
'traction': generate_metrics_slide,
|
||||
'chart': generate_chart_slide,
|
||||
'testimonial': generate_testimonial_slide,
|
||||
'cta': generate_cta_slide,
|
||||
'closing': generate_cta_slide
|
||||
}
|
||||
|
||||
|
||||
def generate_deck(slides_data, title="Pitch Deck"):
|
||||
"""Generate complete deck from slide data list"""
|
||||
slides_html = ""
|
||||
for slide in slides_data:
|
||||
slide_type = slide.get('type', 'title')
|
||||
generator = SLIDE_GENERATORS.get(slide_type)
|
||||
if generator:
|
||||
slides_html += generator(slide)
|
||||
else:
|
||||
print(f"Warning: Unknown slide type '{slide_type}'")
|
||||
|
||||
# Calculate relative path to tokens CSS
|
||||
tokens_rel_path = "../../../assets/design-tokens.css"
|
||||
|
||||
return SLIDE_TEMPLATE.format(
|
||||
title=title,
|
||||
tokens_css_path=tokens_rel_path,
|
||||
slides_content=slides_html
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate brand-compliant slides")
|
||||
parser.add_argument("--json", "-j", help="JSON file with slide data")
|
||||
parser.add_argument("--output", "-o", help="Output HTML file path")
|
||||
parser.add_argument("--demo", action="store_true", help="Generate demo deck")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.demo:
|
||||
# Demo deck showcasing all slide types
|
||||
demo_slides = [
|
||||
{
|
||||
'type': 'title',
|
||||
'badge': 'Investor Deck 2024',
|
||||
'title': 'ClaudeKit Marketing',
|
||||
'subtitle': 'Your AI marketing team. Always on.',
|
||||
'cta': 'Join Waitlist',
|
||||
'secondary_cta': 'See Demo',
|
||||
'company': 'ClaudeKit',
|
||||
'date': 'December 2024'
|
||||
},
|
||||
{
|
||||
'type': 'problem',
|
||||
'headline': 'Marketing teams are drowning',
|
||||
'pain_1_title': 'Content Overload',
|
||||
'pain_1_desc': 'Need to produce 10x content with same headcount',
|
||||
'pain_2_title': 'Tool Fatigue',
|
||||
'pain_2_desc': '15+ tools that don\'t talk to each other',
|
||||
'pain_3_title': 'No Time to Think',
|
||||
'pain_3_desc': 'Strategy suffers when execution consumes all hours',
|
||||
'company': 'ClaudeKit',
|
||||
'page': '2'
|
||||
},
|
||||
{
|
||||
'type': 'solution',
|
||||
'headline': 'AI agents that actually get marketing',
|
||||
'feature_1_title': 'Content Creation',
|
||||
'feature_1_desc': 'Blog posts, social, email - all on brand, all on time',
|
||||
'feature_2_title': 'Campaign Management',
|
||||
'feature_2_desc': 'Multi-channel orchestration with one command',
|
||||
'feature_3_title': 'Analytics & Insights',
|
||||
'feature_3_desc': 'Real-time optimization without the spreadsheets',
|
||||
'company': 'ClaudeKit',
|
||||
'page': '3'
|
||||
},
|
||||
{
|
||||
'type': 'metrics',
|
||||
'headline': 'Early traction speaks volumes',
|
||||
'metrics': [
|
||||
{'value': '500+', 'label': 'Beta Users'},
|
||||
{'value': '85%', 'label': 'Weekly Active'},
|
||||
{'value': '4.9', 'label': 'NPS Score'},
|
||||
{'value': '50hrs', 'label': 'Saved/Week'}
|
||||
],
|
||||
'company': 'ClaudeKit',
|
||||
'page': '4'
|
||||
},
|
||||
{
|
||||
'type': 'chart',
|
||||
'badge': 'Revenue',
|
||||
'headline': 'Growing month over month',
|
||||
'chart_title': 'MRR Growth ($K)',
|
||||
'bars': [
|
||||
{'label': 'Sep', 'value': 20, 'display': '$5K'},
|
||||
{'label': 'Oct', 'value': 40, 'display': '$12K'},
|
||||
{'label': 'Nov', 'value': 70, 'display': '$28K'},
|
||||
{'label': 'Dec', 'value': 100, 'display': '$45K'}
|
||||
],
|
||||
'company': 'ClaudeKit',
|
||||
'page': '5'
|
||||
},
|
||||
{
|
||||
'type': 'testimonial',
|
||||
'quote': 'ClaudeKit replaced 3 tools and 2 contractors. Our content output tripled while costs dropped 60%.',
|
||||
'author': 'Sarah Chen',
|
||||
'role': 'Head of Marketing, TechStartup',
|
||||
'company': 'ClaudeKit',
|
||||
'page': '6'
|
||||
},
|
||||
{
|
||||
'type': 'cta',
|
||||
'headline': 'Ship campaigns while you sleep',
|
||||
'subheadline': 'Early access available. Limited spots.',
|
||||
'cta': 'Join the Waitlist',
|
||||
'contact': 'hello@claudekit.ai',
|
||||
'website': 'claudekit.ai'
|
||||
}
|
||||
]
|
||||
|
||||
html = generate_deck(demo_slides, "ClaudeKit Marketing - Pitch Deck")
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
output_path = OUTPUT_DIR / f"demo-pitch-{datetime.now().strftime('%y%m%d')}.html"
|
||||
output_path.write_text(html, encoding='utf-8')
|
||||
print(f"Demo deck generated: {output_path}")
|
||||
|
||||
elif args.json:
|
||||
with open(args.json, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
html = generate_deck(data.get('slides', []), data.get('title', 'Presentation'))
|
||||
|
||||
output_path = Path(args.output) if args.output else OUTPUT_DIR / f"deck-{datetime.now().strftime('%y%m%d-%H%M')}.html"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(html, encoding='utf-8')
|
||||
print(f"Deck generated: {output_path}")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
205
.claude/skills/design-system/scripts/generate-tokens.cjs
Normal file
205
.claude/skills/design-system/scripts/generate-tokens.cjs
Normal file
@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate CSS variables from design tokens JSON
|
||||
*
|
||||
* Usage:
|
||||
* node generate-tokens.cjs --config tokens.json -o tokens.css
|
||||
* node generate-tokens.cjs --config tokens.json --format tailwind
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
config: null,
|
||||
output: null,
|
||||
format: 'css' // css | tailwind
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--config' || args[i] === '-c') {
|
||||
options.config = args[++i];
|
||||
} else if (args[i] === '--output' || args[i] === '-o') {
|
||||
options.output = args[++i];
|
||||
} else if (args[i] === '--format' || args[i] === '-f') {
|
||||
options.format = args[++i];
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
Usage: node generate-tokens.cjs [options]
|
||||
|
||||
Options:
|
||||
-c, --config <file> Input JSON token file (required)
|
||||
-o, --output <file> Output file (default: stdout)
|
||||
-f, --format <type> Output format: css | tailwind (default: css)
|
||||
-h, --help Show this help
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve token references like {primitive.color.blue.600}
|
||||
*/
|
||||
function resolveReference(value, tokens) {
|
||||
if (typeof value !== 'string' || !value.startsWith('{')) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const path = value.slice(1, -1).split('.');
|
||||
let result = tokens;
|
||||
|
||||
for (const key of path) {
|
||||
result = result?.[key];
|
||||
}
|
||||
|
||||
if (result?.$value) {
|
||||
return resolveReference(result.$value, tokens);
|
||||
}
|
||||
|
||||
return result || value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert token name to CSS variable name
|
||||
*/
|
||||
function toCssVarName(path) {
|
||||
return '--' + path.join('-').replace(/\./g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten tokens into CSS variables
|
||||
*/
|
||||
function flattenTokens(obj, tokens, prefix = [], result = {}) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = [...prefix, key];
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
if (value.$value !== undefined) {
|
||||
// This is a token
|
||||
const cssVar = toCssVarName(currentPath);
|
||||
const resolvedValue = resolveReference(value.$value, tokens);
|
||||
result[cssVar] = resolvedValue;
|
||||
} else {
|
||||
// Recurse into nested object
|
||||
flattenTokens(value, tokens, currentPath, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS output
|
||||
*/
|
||||
function generateCSS(tokens) {
|
||||
const primitive = flattenTokens(tokens.primitive || {}, tokens, ['primitive']);
|
||||
const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
|
||||
const component = flattenTokens(tokens.component || {}, tokens, []);
|
||||
const darkSemantic = flattenTokens(tokens.dark?.semantic || {}, tokens, []);
|
||||
|
||||
let css = `/* Design Tokens - Auto-generated */
|
||||
/* Do not edit directly - modify tokens.json instead */
|
||||
|
||||
/* === PRIMITIVES === */
|
||||
:root {
|
||||
${Object.entries(primitive).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
|
||||
}
|
||||
|
||||
/* === SEMANTIC === */
|
||||
:root {
|
||||
${Object.entries(semantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
|
||||
}
|
||||
|
||||
/* === COMPONENTS === */
|
||||
:root {
|
||||
${Object.entries(component).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
if (Object.keys(darkSemantic).length > 0) {
|
||||
css += `
|
||||
/* === DARK MODE === */
|
||||
.dark {
|
||||
${Object.entries(darkSemantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Tailwind config output
|
||||
*/
|
||||
function generateTailwind(tokens) {
|
||||
const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
|
||||
|
||||
// Extract colors for Tailwind
|
||||
const colors = {};
|
||||
for (const [key, value] of Object.entries(semantic)) {
|
||||
if (key.includes('color')) {
|
||||
const name = key.replace('--color-', '').replace(/-/g, '.');
|
||||
colors[name] = `var(${key})`;
|
||||
}
|
||||
}
|
||||
|
||||
return `// Tailwind color config - Auto-generated
|
||||
// Add to tailwind.config.ts theme.extend.colors
|
||||
|
||||
module.exports = {
|
||||
colors: ${JSON.stringify(colors, null, 2).replace(/"/g, "'")}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const options = parseArgs();
|
||||
|
||||
if (!options.config) {
|
||||
console.error('Error: --config is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Resolve config path
|
||||
const configPath = path.resolve(process.cwd(), options.config);
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`Error: Config file not found: ${configPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and parse tokens
|
||||
const tokens = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
|
||||
// Generate output
|
||||
let output;
|
||||
if (options.format === 'tailwind') {
|
||||
output = generateTailwind(tokens);
|
||||
} else {
|
||||
output = generateCSS(tokens);
|
||||
}
|
||||
|
||||
// Write output
|
||||
if (options.output) {
|
||||
const outputPath = path.resolve(process.cwd(), options.output);
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, output);
|
||||
console.log(`Generated: ${outputPath}`);
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
327
.claude/skills/design-system/scripts/html-token-validator.py
Normal file
327
.claude/skills/design-system/scripts/html-token-validator.py
Normal file
@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML Design Token Validator
|
||||
Ensures all HTML assets (slides, infographics, etc.) use design tokens.
|
||||
Source of truth: assets/design-tokens.css
|
||||
|
||||
Usage:
|
||||
python html-token-validator.py # Validate all HTML assets
|
||||
python html-token-validator.py --type slides # Validate only slides
|
||||
python html-token-validator.py --type infographics # Validate only infographics
|
||||
python html-token-validator.py path/to/file.html # Validate specific file
|
||||
python html-token-validator.py --fix # Auto-fix issues (WIP)
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
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
|
||||
TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
|
||||
TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css'
|
||||
|
||||
# Asset directories to validate
|
||||
ASSET_DIRS = {
|
||||
'slides': PROJECT_ROOT / 'assets' / 'designs' / 'slides',
|
||||
'infographics': PROJECT_ROOT / 'assets' / 'infographics',
|
||||
}
|
||||
|
||||
# Patterns that indicate hardcoded values (should use tokens)
|
||||
FORBIDDEN_PATTERNS = [
|
||||
(r'#[0-9A-Fa-f]{3,8}\b', 'hex color'),
|
||||
(r'rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)', 'rgb color'),
|
||||
(r'rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)', 'rgba color'),
|
||||
(r'hsl\([^)]+\)', 'hsl color'),
|
||||
(r"font-family:\s*'[^v][^a][^r][^']*',", 'hardcoded font'), # Exclude var()
|
||||
(r'font-family:\s*"[^v][^a][^r][^"]*",', 'hardcoded font'),
|
||||
]
|
||||
|
||||
# Allowed rgba patterns (brand colors with transparency - CSS limitation)
|
||||
# These are derived from brand tokens but need rgba for transparency
|
||||
ALLOWED_RGBA_PATTERNS = [
|
||||
r'rgba\(\s*59\s*,\s*130\s*,\s*246', # --color-primary (#3B82F6)
|
||||
r'rgba\(\s*245\s*,\s*158\s*,\s*11', # --color-secondary (#F59E0B)
|
||||
r'rgba\(\s*16\s*,\s*185\s*,\s*129', # --color-accent (#10B981)
|
||||
r'rgba\(\s*20\s*,\s*184\s*,\s*166', # --color-accent alt (#14B8A6)
|
||||
r'rgba\(\s*0\s*,\s*0\s*,\s*0', # black transparency (common)
|
||||
r'rgba\(\s*255\s*,\s*255\s*,\s*255', # white transparency (common)
|
||||
r'rgba\(\s*15\s*,\s*23\s*,\s*42', # --color-surface (#0F172A)
|
||||
r'rgba\(\s*7\s*,\s*11\s*,\s*20', # --color-background (#070B14)
|
||||
]
|
||||
|
||||
# Allowed exceptions (external images, etc.)
|
||||
ALLOWED_EXCEPTIONS = [
|
||||
'pexels.com', 'unsplash.com', 'youtube.com', 'ytimg.com',
|
||||
'googlefonts', 'fonts.googleapis.com', 'fonts.gstatic.com',
|
||||
]
|
||||
|
||||
|
||||
class ValidationResult:
|
||||
"""Validation result for a single file."""
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self.errors: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
self.passed = True
|
||||
|
||||
def add_error(self, msg: str):
|
||||
self.errors.append(msg)
|
||||
self.passed = False
|
||||
|
||||
def add_warning(self, msg: str):
|
||||
self.warnings.append(msg)
|
||||
|
||||
|
||||
def load_css_variables() -> Dict[str, str]:
|
||||
"""Load CSS variables from design-tokens.css."""
|
||||
variables = {}
|
||||
if TOKENS_CSS_PATH.exists():
|
||||
content = TOKENS_CSS_PATH.read_text()
|
||||
# Extract --var-name: value patterns
|
||||
for match in re.finditer(r'(--[\w-]+):\s*([^;]+);', content):
|
||||
variables[match.group(1)] = match.group(2).strip()
|
||||
return variables
|
||||
|
||||
|
||||
def is_inside_block(content: str, match_pos: int, open_tag: str, close_tag: str) -> bool:
|
||||
"""Check if position is inside a specific HTML block."""
|
||||
pre = content[:match_pos]
|
||||
tag_open = pre.rfind(open_tag)
|
||||
tag_close = pre.rfind(close_tag)
|
||||
return tag_open > tag_close
|
||||
|
||||
|
||||
def is_allowed_exception(context: str) -> bool:
|
||||
"""Check if the hardcoded value is in an allowed exception context."""
|
||||
context_lower = context.lower()
|
||||
return any(exc in context_lower for exc in ALLOWED_EXCEPTIONS)
|
||||
|
||||
|
||||
def is_allowed_rgba(match_text: str) -> bool:
|
||||
"""Check if rgba pattern uses brand colors (allowed for transparency)."""
|
||||
return any(re.match(pattern, match_text) for pattern in ALLOWED_RGBA_PATTERNS)
|
||||
|
||||
|
||||
def get_context(content: str, pos: int, chars: int = 100) -> str:
|
||||
"""Get surrounding context for a match position."""
|
||||
start = max(0, pos - chars)
|
||||
end = min(len(content), pos + chars)
|
||||
return content[start:end]
|
||||
|
||||
|
||||
def validate_html(content: str, file_path: Path, verbose: bool = False) -> ValidationResult:
|
||||
"""
|
||||
Validate HTML content for design token compliance.
|
||||
|
||||
Checks:
|
||||
1. design-tokens.css import present
|
||||
2. No hardcoded colors in CSS (except in <script> for Chart.js)
|
||||
3. No hardcoded fonts
|
||||
4. Uses var(--token-name) pattern
|
||||
"""
|
||||
result = ValidationResult(file_path)
|
||||
|
||||
# 1. Check for design-tokens.css import
|
||||
if 'design-tokens.css' not in content:
|
||||
result.add_error("Missing design-tokens.css import")
|
||||
|
||||
# 2. Check for forbidden patterns in CSS
|
||||
for pattern, description in FORBIDDEN_PATTERNS:
|
||||
for match in re.finditer(pattern, content):
|
||||
match_text = match.group()
|
||||
match_pos = match.start()
|
||||
context = get_context(content, match_pos)
|
||||
|
||||
# Skip if in <script> block (Chart.js allowed)
|
||||
if is_inside_block(content, match_pos, '<script', '</script>'):
|
||||
if verbose:
|
||||
result.add_warning(f"Allowed in <script>: {match_text}")
|
||||
continue
|
||||
|
||||
# Skip if in allowed exception context (external URLs)
|
||||
if is_allowed_exception(context):
|
||||
if verbose:
|
||||
result.add_warning(f"Allowed external: {match_text}")
|
||||
continue
|
||||
|
||||
# Skip rgba using brand colors (needed for transparency effects)
|
||||
if description == 'rgba color' and is_allowed_rgba(match_text):
|
||||
if verbose:
|
||||
result.add_warning(f"Allowed brand rgba: {match_text}")
|
||||
continue
|
||||
|
||||
# Skip if part of var() reference (false positive)
|
||||
if 'var(' in context and match_text in context:
|
||||
# Check if it's a fallback value in var()
|
||||
var_pattern = rf'var\([^)]*{re.escape(match_text)}[^)]*\)'
|
||||
if re.search(var_pattern, context):
|
||||
continue
|
||||
|
||||
# Error if in <style> or inline style
|
||||
if is_inside_block(content, match_pos, '<style', '</style>'):
|
||||
result.add_error(f"Hardcoded {description} in <style>: {match_text}")
|
||||
elif 'style="' in context:
|
||||
result.add_error(f"Hardcoded {description} in inline style: {match_text}")
|
||||
|
||||
# 3. Check for required var() usage indicators
|
||||
token_patterns = [
|
||||
r'var\(--color-',
|
||||
r'var\(--primitive-',
|
||||
r'var\(--typography-',
|
||||
r'var\(--card-',
|
||||
r'var\(--button-',
|
||||
]
|
||||
token_count = sum(len(re.findall(p, content)) for p in token_patterns)
|
||||
|
||||
if token_count < 5:
|
||||
result.add_warning(f"Low token usage ({token_count} var() references). Consider using more design tokens.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_file(file_path: Path, verbose: bool = False) -> ValidationResult:
|
||||
"""Validate a single HTML file."""
|
||||
if not file_path.exists():
|
||||
result = ValidationResult(file_path)
|
||||
result.add_error("File not found")
|
||||
return result
|
||||
|
||||
content = file_path.read_text()
|
||||
return validate_html(content, file_path, verbose)
|
||||
|
||||
|
||||
def validate_directory(dir_path: Path, verbose: bool = False) -> List[ValidationResult]:
|
||||
"""Validate all HTML files in a directory."""
|
||||
results = []
|
||||
if dir_path.exists():
|
||||
for html_file in sorted(dir_path.glob('*.html')):
|
||||
results.append(validate_file(html_file, verbose))
|
||||
return results
|
||||
|
||||
|
||||
def print_result(result: ValidationResult, verbose: bool = False):
|
||||
"""Print validation result for a file."""
|
||||
status = "✓" if result.passed else "✗"
|
||||
print(f" {status} {result.file_path.name}")
|
||||
|
||||
if result.errors:
|
||||
for error in result.errors[:5]: # Limit output
|
||||
print(f" ├─ {error}")
|
||||
if len(result.errors) > 5:
|
||||
print(f" └─ ... and {len(result.errors) - 5} more errors")
|
||||
|
||||
if verbose and result.warnings:
|
||||
for warning in result.warnings[:3]:
|
||||
print(f" [warn] {warning}")
|
||||
|
||||
|
||||
def print_summary(all_results: Dict[str, List[ValidationResult]]):
|
||||
"""Print summary of all validation results."""
|
||||
total_files = 0
|
||||
total_passed = 0
|
||||
total_errors = 0
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("HTML DESIGN TOKEN VALIDATION SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
for asset_type, results in all_results.items():
|
||||
if not results:
|
||||
continue
|
||||
|
||||
passed = sum(1 for r in results if r.passed)
|
||||
failed = len(results) - passed
|
||||
errors = sum(len(r.errors) for r in results)
|
||||
|
||||
total_files += len(results)
|
||||
total_passed += passed
|
||||
total_errors += errors
|
||||
|
||||
status = "✓" if failed == 0 else "✗"
|
||||
print(f"\n{status} {asset_type.upper()}: {passed}/{len(results)} passed")
|
||||
|
||||
for result in results:
|
||||
if not result.passed:
|
||||
print_result(result)
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
if total_errors == 0:
|
||||
print(f"✓ ALL PASSED: {total_passed}/{total_files} files valid")
|
||||
else:
|
||||
print(f"✗ FAILED: {total_files - total_passed}/{total_files} files have issues ({total_errors} total errors)")
|
||||
print("-" * 60)
|
||||
|
||||
return total_errors == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate HTML assets for design token compliance',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s # Validate all HTML assets
|
||||
%(prog)s --type slides # Validate only slides
|
||||
%(prog)s --type infographics # Validate only infographics
|
||||
%(prog)s path/to/file.html # Validate specific file
|
||||
%(prog)s --colors # Show brand colors from tokens
|
||||
"""
|
||||
)
|
||||
parser.add_argument('files', nargs='*', help='Specific HTML files to validate')
|
||||
parser.add_argument('-t', '--type', choices=['slides', 'infographics', 'all'],
|
||||
default='all', help='Asset type to validate')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Show warnings')
|
||||
parser.add_argument('--colors', action='store_true', help='Print CSS variables from tokens')
|
||||
parser.add_argument('--fix', action='store_true', help='Auto-fix issues (experimental)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Show colors mode
|
||||
if args.colors:
|
||||
variables = load_css_variables()
|
||||
print("\nDesign Tokens (from design-tokens.css):")
|
||||
print("-" * 40)
|
||||
for name, value in sorted(variables.items())[:30]:
|
||||
print(f" {name}: {value}")
|
||||
if len(variables) > 30:
|
||||
print(f" ... and {len(variables) - 30} more")
|
||||
return
|
||||
|
||||
all_results: Dict[str, List[ValidationResult]] = {}
|
||||
|
||||
# Validate specific files
|
||||
if args.files:
|
||||
results = []
|
||||
for file_path in args.files:
|
||||
path = Path(file_path)
|
||||
if path.exists():
|
||||
results.append(validate_file(path, args.verbose))
|
||||
else:
|
||||
result = ValidationResult(path)
|
||||
result.add_error("File not found")
|
||||
results.append(result)
|
||||
all_results['specified'] = results
|
||||
else:
|
||||
# Validate by type
|
||||
types_to_check = ASSET_DIRS.keys() if args.type == 'all' else [args.type]
|
||||
|
||||
for asset_type in types_to_check:
|
||||
if asset_type in ASSET_DIRS:
|
||||
results = validate_directory(ASSET_DIRS[asset_type], args.verbose)
|
||||
all_results[asset_type] = results
|
||||
|
||||
# Print results
|
||||
success = print_summary(all_results)
|
||||
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
218
.claude/skills/design-system/scripts/search-slides.py
Executable file
218
.claude/skills/design-system/scripts/search-slides.py
Executable file
@ -0,0 +1,218 @@
|
||||
#!/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
|
||||
)
|
||||
|
||||
|
||||
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()
|
||||
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Slide Token Validator (Legacy Wrapper)
|
||||
Now delegates to html-token-validator.py for unified HTML validation.
|
||||
|
||||
For new usage, prefer:
|
||||
python html-token-validator.py --type slides
|
||||
python html-token-validator.py --type infographics
|
||||
python html-token-validator.py # All HTML assets
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
UNIFIED_VALIDATOR = SCRIPT_DIR / 'html-token-validator.py'
|
||||
|
||||
|
||||
def main():
|
||||
"""Delegate to unified html-token-validator.py with --type slides."""
|
||||
args = sys.argv[1:]
|
||||
|
||||
# If no files specified, default to slides type
|
||||
if not args or all(arg.startswith('-') for arg in args):
|
||||
cmd = [sys.executable, str(UNIFIED_VALIDATOR), '--type', 'slides'] + args
|
||||
else:
|
||||
cmd = [sys.executable, str(UNIFIED_VALIDATOR)] + args
|
||||
|
||||
result = subprocess.run(cmd)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
453
.claude/skills/design-system/scripts/slide_search_core.py
Executable file
453
.claude/skills/design-system/scripts/slide_search_core.py
Executable file
@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Slide Search Core - BM25 search engine for slide design databases
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
from pathlib import Path
|
||||
from math import log
|
||||
from collections import defaultdict
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
MAX_RESULTS = 3
|
||||
|
||||
CSV_CONFIG = {
|
||||
"strategy": {
|
||||
"file": "slide-strategies.csv",
|
||||
"search_cols": ["strategy_name", "keywords", "goal", "audience", "narrative_arc"],
|
||||
"output_cols": ["strategy_name", "keywords", "slide_count", "structure", "goal", "audience", "tone", "narrative_arc", "sources"]
|
||||
},
|
||||
"layout": {
|
||||
"file": "slide-layouts.csv",
|
||||
"search_cols": ["layout_name", "keywords", "use_case", "recommended_for"],
|
||||
"output_cols": ["layout_name", "keywords", "use_case", "content_zones", "visual_weight", "cta_placement", "recommended_for", "avoid_for", "css_structure"]
|
||||
},
|
||||
"copy": {
|
||||
"file": "slide-copy.csv",
|
||||
"search_cols": ["formula_name", "keywords", "use_case", "emotion_trigger", "slide_type"],
|
||||
"output_cols": ["formula_name", "keywords", "components", "use_case", "example_template", "emotion_trigger", "slide_type", "source"]
|
||||
},
|
||||
"chart": {
|
||||
"file": "slide-charts.csv",
|
||||
"search_cols": ["chart_type", "keywords", "best_for", "when_to_use", "slide_context"],
|
||||
"output_cols": ["chart_type", "keywords", "best_for", "data_type", "when_to_use", "when_to_avoid", "max_categories", "slide_context", "css_implementation", "accessibility_notes"]
|
||||
}
|
||||
}
|
||||
|
||||
AVAILABLE_DOMAINS = list(CSV_CONFIG.keys())
|
||||
|
||||
|
||||
# ============ BM25 IMPLEMENTATION ============
|
||||
class BM25:
|
||||
"""BM25 ranking algorithm for text search"""
|
||||
|
||||
def __init__(self, k1=1.5, b=0.75):
|
||||
self.k1 = k1
|
||||
self.b = b
|
||||
self.corpus = []
|
||||
self.doc_lengths = []
|
||||
self.avgdl = 0
|
||||
self.idf = {}
|
||||
self.doc_freqs = defaultdict(int)
|
||||
self.N = 0
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Lowercase, split, remove punctuation, filter short words"""
|
||||
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
||||
return [w for w in text.split() if len(w) > 2]
|
||||
|
||||
def fit(self, documents):
|
||||
"""Build BM25 index from documents"""
|
||||
self.corpus = [self.tokenize(doc) for doc in documents]
|
||||
self.N = len(self.corpus)
|
||||
if self.N == 0:
|
||||
return
|
||||
self.doc_lengths = [len(doc) for doc in self.corpus]
|
||||
self.avgdl = sum(self.doc_lengths) / self.N
|
||||
|
||||
for doc in self.corpus:
|
||||
seen = set()
|
||||
for word in doc:
|
||||
if word not in seen:
|
||||
self.doc_freqs[word] += 1
|
||||
seen.add(word)
|
||||
|
||||
for word, freq in self.doc_freqs.items():
|
||||
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
||||
|
||||
def score(self, query):
|
||||
"""Score all documents against query"""
|
||||
query_tokens = self.tokenize(query)
|
||||
scores = []
|
||||
|
||||
for idx, doc in enumerate(self.corpus):
|
||||
score = 0
|
||||
doc_len = self.doc_lengths[idx]
|
||||
term_freqs = defaultdict(int)
|
||||
for word in doc:
|
||||
term_freqs[word] += 1
|
||||
|
||||
for token in query_tokens:
|
||||
if token in self.idf:
|
||||
tf = term_freqs[token]
|
||||
idf = self.idf[token]
|
||||
numerator = tf * (self.k1 + 1)
|
||||
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
||||
score += idf * numerator / denominator
|
||||
|
||||
scores.append((idx, score))
|
||||
|
||||
return sorted(scores, key=lambda x: x[1], reverse=True)
|
||||
|
||||
|
||||
# ============ SEARCH FUNCTIONS ============
|
||||
def _load_csv(filepath):
|
||||
"""Load CSV and return list of dicts"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
||||
"""Core search function using BM25"""
|
||||
if not filepath.exists():
|
||||
return []
|
||||
|
||||
data = _load_csv(filepath)
|
||||
|
||||
# Build documents from search columns
|
||||
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
||||
|
||||
# BM25 search
|
||||
bm25 = BM25()
|
||||
bm25.fit(documents)
|
||||
ranked = bm25.score(query)
|
||||
|
||||
# Get top results with score > 0
|
||||
results = []
|
||||
for idx, score in ranked[:max_results]:
|
||||
if score > 0:
|
||||
row = data[idx]
|
||||
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def detect_domain(query):
|
||||
"""Auto-detect the most relevant domain from query"""
|
||||
query_lower = query.lower()
|
||||
|
||||
domain_keywords = {
|
||||
"strategy": ["pitch", "deck", "investor", "yc", "seed", "series", "demo", "sales", "webinar",
|
||||
"conference", "board", "qbr", "all-hands", "duarte", "kawasaki", "structure"],
|
||||
"layout": ["slide", "layout", "grid", "column", "title", "hero", "section", "cta",
|
||||
"screenshot", "quote", "timeline", "comparison", "pricing", "team"],
|
||||
"copy": ["headline", "copy", "formula", "aida", "pas", "hook", "cta", "benefit",
|
||||
"objection", "proof", "testimonial", "urgency", "scarcity"],
|
||||
"chart": ["chart", "graph", "bar", "line", "pie", "funnel", "metrics", "data",
|
||||
"visualization", "kpi", "trend", "comparison", "heatmap", "gauge"]
|
||||
}
|
||||
|
||||
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
||||
best = max(scores, key=scores.get)
|
||||
return best if scores[best] > 0 else "strategy"
|
||||
|
||||
|
||||
def search(query, domain=None, max_results=MAX_RESULTS):
|
||||
"""Main search function with auto-domain detection"""
|
||||
if domain is None:
|
||||
domain = detect_domain(query)
|
||||
|
||||
config = CSV_CONFIG.get(domain, CSV_CONFIG["strategy"])
|
||||
filepath = DATA_DIR / config["file"]
|
||||
|
||||
if not filepath.exists():
|
||||
return {"error": f"File not found: {filepath}", "domain": domain}
|
||||
|
||||
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"query": query,
|
||||
"file": config["file"],
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
def search_all(query, max_results=2):
|
||||
"""Search across all domains for comprehensive results"""
|
||||
all_results = {}
|
||||
|
||||
for domain in AVAILABLE_DOMAINS:
|
||||
result = search(query, domain, max_results)
|
||||
if result.get("count", 0) > 0:
|
||||
all_results[domain] = result
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
# ============ CONTEXTUAL SEARCH (Premium Slide System) ============
|
||||
|
||||
# New CSV configurations for decision system
|
||||
DECISION_CSV_CONFIG = {
|
||||
"layout-logic": {
|
||||
"file": "slide-layout-logic.csv",
|
||||
"key_col": "goal"
|
||||
},
|
||||
"typography": {
|
||||
"file": "slide-typography.csv",
|
||||
"key_col": "content_type"
|
||||
},
|
||||
"color-logic": {
|
||||
"file": "slide-color-logic.csv",
|
||||
"key_col": "emotion"
|
||||
},
|
||||
"backgrounds": {
|
||||
"file": "slide-backgrounds.csv",
|
||||
"key_col": "slide_type"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _load_decision_csv(csv_type):
|
||||
"""Load a decision CSV and return as dict keyed by primary column."""
|
||||
config = DECISION_CSV_CONFIG.get(csv_type)
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
filepath = DATA_DIR / config["file"]
|
||||
if not filepath.exists():
|
||||
return {}
|
||||
|
||||
data = _load_csv(filepath)
|
||||
return {row[config["key_col"]]: row for row in data if config["key_col"] in row}
|
||||
|
||||
|
||||
def get_layout_for_goal(goal, previous_emotion=None):
|
||||
"""
|
||||
Get layout recommendation based on slide goal.
|
||||
Uses slide-layout-logic.csv for decision.
|
||||
"""
|
||||
layouts = _load_decision_csv("layout-logic")
|
||||
row = layouts.get(goal, layouts.get("features", {}))
|
||||
|
||||
result = dict(row) if row else {}
|
||||
|
||||
# Apply pattern-breaking logic
|
||||
if result.get("break_pattern") == "true" and previous_emotion:
|
||||
result["_pattern_break"] = True
|
||||
result["_contrast_with"] = previous_emotion
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_typography_for_slide(slide_type, has_metrics=False, has_quote=False):
|
||||
"""
|
||||
Get typography recommendation based on slide content.
|
||||
Uses slide-typography.csv for decision.
|
||||
"""
|
||||
typography = _load_decision_csv("typography")
|
||||
|
||||
if has_metrics:
|
||||
return typography.get("metric-callout", {})
|
||||
if has_quote:
|
||||
return typography.get("quote-block", {})
|
||||
|
||||
# Map slide types to typography
|
||||
type_map = {
|
||||
"hero": "hero-statement",
|
||||
"hook": "hero-statement",
|
||||
"title": "title-only",
|
||||
"problem": "subtitle-heavy",
|
||||
"agitation": "metric-callout",
|
||||
"solution": "subtitle-heavy",
|
||||
"features": "feature-grid",
|
||||
"proof": "metric-callout",
|
||||
"traction": "data-insight",
|
||||
"social": "quote-block",
|
||||
"testimonial": "testimonial",
|
||||
"pricing": "pricing",
|
||||
"team": "team",
|
||||
"cta": "cta-action",
|
||||
"comparison": "comparison",
|
||||
"timeline": "timeline",
|
||||
}
|
||||
|
||||
content_type = type_map.get(slide_type, "feature-grid")
|
||||
return typography.get(content_type, {})
|
||||
|
||||
|
||||
def get_color_for_emotion(emotion):
|
||||
"""
|
||||
Get color treatment based on emotional beat.
|
||||
Uses slide-color-logic.csv for decision.
|
||||
"""
|
||||
colors = _load_decision_csv("color-logic")
|
||||
return colors.get(emotion, colors.get("clarity", {}))
|
||||
|
||||
|
||||
def get_background_config(slide_type):
|
||||
"""
|
||||
Get background image configuration.
|
||||
Uses slide-backgrounds.csv for decision.
|
||||
"""
|
||||
backgrounds = _load_decision_csv("backgrounds")
|
||||
return backgrounds.get(slide_type, {})
|
||||
|
||||
|
||||
def should_use_full_bleed(slide_index, total_slides, emotion):
|
||||
"""
|
||||
Determine if slide should use full-bleed background.
|
||||
Premium decks use 2-3 full-bleed slides strategically.
|
||||
|
||||
Rules:
|
||||
1. Never consecutive full-bleed
|
||||
2. One in first third, one in middle, one at end
|
||||
3. Reserved for high-emotion beats (hope, urgency, fear)
|
||||
"""
|
||||
high_emotion_beats = ["hope", "urgency", "fear", "curiosity"]
|
||||
|
||||
if emotion not in high_emotion_beats:
|
||||
return False
|
||||
|
||||
if total_slides < 3:
|
||||
return False
|
||||
|
||||
third = total_slides // 3
|
||||
strategic_positions = [1, third, third * 2, total_slides - 1]
|
||||
|
||||
return slide_index in strategic_positions
|
||||
|
||||
|
||||
def calculate_pattern_break(slide_index, total_slides, previous_emotion=None):
|
||||
"""
|
||||
Determine if this slide should break the visual pattern.
|
||||
Used for emotional contrast (Duarte Sparkline technique).
|
||||
"""
|
||||
# Pattern breaks at strategic positions
|
||||
if total_slides < 5:
|
||||
return False
|
||||
|
||||
# Break at 1/3 and 2/3 points
|
||||
third = total_slides // 3
|
||||
if slide_index in [third, third * 2]:
|
||||
return True
|
||||
|
||||
# Break when switching between frustration and hope
|
||||
contrasting_emotions = {
|
||||
"frustration": ["hope", "relief"],
|
||||
"hope": ["frustration", "fear"],
|
||||
"fear": ["hope", "relief"],
|
||||
}
|
||||
|
||||
if previous_emotion in contrasting_emotions:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def search_with_context(query, slide_position=1, total_slides=9, previous_emotion=None):
|
||||
"""
|
||||
Enhanced search that considers deck context.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
slide_position: Current slide index (1-based)
|
||||
total_slides: Total slides in deck
|
||||
previous_emotion: Emotion of previous slide (for contrast)
|
||||
|
||||
Returns:
|
||||
Search results enriched with contextual recommendations
|
||||
"""
|
||||
# Get base results from existing BM25 search
|
||||
base_results = search_all(query, max_results=2)
|
||||
|
||||
# Detect likely slide goal from query
|
||||
goal = detect_domain(query.lower())
|
||||
if "problem" in query.lower():
|
||||
goal = "problem"
|
||||
elif "solution" in query.lower():
|
||||
goal = "solution"
|
||||
elif "cta" in query.lower() or "call to action" in query.lower():
|
||||
goal = "cta"
|
||||
elif "hook" in query.lower() or "title" in query.lower():
|
||||
goal = "hook"
|
||||
elif "traction" in query.lower() or "metric" in query.lower():
|
||||
goal = "traction"
|
||||
|
||||
# Enrich with contextual recommendations
|
||||
context = {
|
||||
"slide_position": slide_position,
|
||||
"total_slides": total_slides,
|
||||
"previous_emotion": previous_emotion,
|
||||
"inferred_goal": goal,
|
||||
}
|
||||
|
||||
# Get layout recommendation
|
||||
layout = get_layout_for_goal(goal, previous_emotion)
|
||||
if layout:
|
||||
context["recommended_layout"] = layout.get("layout_pattern")
|
||||
context["layout_direction"] = layout.get("direction")
|
||||
context["visual_weight"] = layout.get("visual_weight")
|
||||
context["use_background_image"] = layout.get("use_bg_image") == "true"
|
||||
|
||||
# Get typography recommendation
|
||||
typography = get_typography_for_slide(goal)
|
||||
if typography:
|
||||
context["typography"] = {
|
||||
"primary_size": typography.get("primary_size"),
|
||||
"secondary_size": typography.get("secondary_size"),
|
||||
"weight_contrast": typography.get("weight_contrast"),
|
||||
}
|
||||
|
||||
# Get color treatment
|
||||
emotion = layout.get("emotion", "clarity") if layout else "clarity"
|
||||
color = get_color_for_emotion(emotion)
|
||||
if color:
|
||||
context["color_treatment"] = {
|
||||
"background": color.get("background"),
|
||||
"text_color": color.get("text_color"),
|
||||
"accent_usage": color.get("accent_usage"),
|
||||
"card_style": color.get("card_style"),
|
||||
}
|
||||
|
||||
# Calculate pattern breaking
|
||||
context["should_break_pattern"] = calculate_pattern_break(
|
||||
slide_position, total_slides, previous_emotion
|
||||
)
|
||||
context["should_use_full_bleed"] = should_use_full_bleed(
|
||||
slide_position, total_slides, emotion
|
||||
)
|
||||
|
||||
# Get background config if needed
|
||||
if context.get("use_background_image"):
|
||||
bg_config = get_background_config(goal)
|
||||
if bg_config:
|
||||
context["background"] = {
|
||||
"image_category": bg_config.get("image_category"),
|
||||
"overlay_style": bg_config.get("overlay_style"),
|
||||
"search_keywords": bg_config.get("search_keywords"),
|
||||
}
|
||||
|
||||
# Suggested animation classes
|
||||
animation_map = {
|
||||
"hook": "animate-fade-up",
|
||||
"problem": "animate-fade-up",
|
||||
"agitation": "animate-count animate-stagger",
|
||||
"solution": "animate-scale",
|
||||
"features": "animate-stagger",
|
||||
"traction": "animate-chart animate-count",
|
||||
"proof": "animate-stagger-scale",
|
||||
"social": "animate-fade-up",
|
||||
"cta": "animate-pulse",
|
||||
}
|
||||
context["animation_class"] = animation_map.get(goal, "animate-fade-up")
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"context": context,
|
||||
"base_results": base_results,
|
||||
}
|
||||
251
.claude/skills/design-system/scripts/validate-tokens.cjs
Normal file
251
.claude/skills/design-system/scripts/validate-tokens.cjs
Normal file
@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate token usage in codebase
|
||||
* Finds hardcoded values that should use design tokens
|
||||
*
|
||||
* Usage:
|
||||
* node validate-tokens.cjs --dir src/
|
||||
* node validate-tokens.cjs --dir src/ --fix
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
dir: null,
|
||||
fix: false,
|
||||
ignore: ['node_modules', '.git', 'dist', 'build', '.next']
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--dir' || args[i] === '-d') {
|
||||
options.dir = args[++i];
|
||||
} else if (args[i] === '--fix') {
|
||||
options.fix = true;
|
||||
} else if (args[i] === '--ignore' || args[i] === '-i') {
|
||||
options.ignore.push(args[++i]);
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
Usage: node validate-tokens.cjs [options]
|
||||
|
||||
Options:
|
||||
-d, --dir <path> Directory to scan (required)
|
||||
--fix Show suggested fixes (no auto-fix)
|
||||
-i, --ignore <dir> Additional directories to ignore
|
||||
-h, --help Show this help
|
||||
|
||||
Checks for:
|
||||
- Hardcoded hex colors (#RGB, #RRGGBB)
|
||||
- Hardcoded pixel values (except 0, 1px)
|
||||
- Hardcoded rem values in CSS
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patterns to detect hardcoded values
|
||||
*/
|
||||
const patterns = {
|
||||
hexColor: {
|
||||
regex: /#([0-9A-Fa-f]{3}){1,2}\b/g,
|
||||
message: 'Hardcoded hex color',
|
||||
suggestion: 'Use var(--color-*) token'
|
||||
},
|
||||
rgbColor: {
|
||||
regex: /rgb\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)/gi,
|
||||
message: 'Hardcoded RGB color',
|
||||
suggestion: 'Use var(--color-*) token'
|
||||
},
|
||||
pixelValue: {
|
||||
regex: /:\s*(\d{2,})px/g, // 2+ digit px values
|
||||
message: 'Hardcoded pixel value',
|
||||
suggestion: 'Use var(--space-*) or var(--radius-*) token'
|
||||
},
|
||||
remValue: {
|
||||
regex: /:\s*\d+\.?\d*rem(?![^{]*\$value)/g, // rem not in token definition
|
||||
message: 'Hardcoded rem value',
|
||||
suggestion: 'Use var(--space-*) or var(--font-size-*) token'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* File extensions to scan
|
||||
*/
|
||||
const extensions = ['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.vue', '.svelte'];
|
||||
|
||||
/**
|
||||
* Files/patterns to skip
|
||||
*/
|
||||
const skipPatterns = [
|
||||
/\.min\.(css|js)$/,
|
||||
/tailwind\.config/,
|
||||
/globals\.css/, // Token definitions
|
||||
/tokens\.(css|json)/
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all files recursively
|
||||
*/
|
||||
function getFiles(dir, ignore, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!ignore.includes(entry.name)) {
|
||||
getFiles(fullPath, ignore, files);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file should be skipped
|
||||
*/
|
||||
function shouldSkip(filePath) {
|
||||
return skipPatterns.some(pattern => pattern.test(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan file for violations
|
||||
*/
|
||||
function scanFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const violations = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Skip comments
|
||||
if (line.trim().startsWith('//') || line.trim().startsWith('/*')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip lines that already use CSS variables
|
||||
if (line.includes('var(--')) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, pattern] of Object.entries(patterns)) {
|
||||
const matches = line.match(pattern.regex);
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
// Skip common exceptions
|
||||
if (name === 'hexColor' && ['#000', '#fff', '#FFF', '#000000', '#FFFFFF'].includes(match.toUpperCase())) {
|
||||
return; // Skip black/white, often intentional
|
||||
}
|
||||
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
column: line.indexOf(match) + 1,
|
||||
value: match,
|
||||
type: name,
|
||||
message: pattern.message,
|
||||
suggestion: pattern.suggestion,
|
||||
context: line.trim().substring(0, 80)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format violation report
|
||||
*/
|
||||
function formatReport(violations) {
|
||||
if (violations.length === 0) {
|
||||
return '✅ No token violations found';
|
||||
}
|
||||
|
||||
let report = `⚠️ Found ${violations.length} potential token violations:\n\n`;
|
||||
|
||||
// Group by file
|
||||
const byFile = {};
|
||||
violations.forEach(v => {
|
||||
if (!byFile[v.file]) byFile[v.file] = [];
|
||||
byFile[v.file].push(v);
|
||||
});
|
||||
|
||||
for (const [file, fileViolations] of Object.entries(byFile)) {
|
||||
report += `📁 ${file}\n`;
|
||||
fileViolations.forEach(v => {
|
||||
report += ` Line ${v.line}: ${v.message}\n`;
|
||||
report += ` Found: ${v.value}\n`;
|
||||
report += ` Suggestion: ${v.suggestion}\n`;
|
||||
report += ` Context: ${v.context}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
// Summary
|
||||
const byType = {};
|
||||
violations.forEach(v => {
|
||||
byType[v.type] = (byType[v.type] || 0) + 1;
|
||||
});
|
||||
|
||||
report += `\n📊 Summary:\n`;
|
||||
for (const [type, count] of Object.entries(byType)) {
|
||||
report += ` ${patterns[type].message}: ${count}\n`;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const options = parseArgs();
|
||||
|
||||
if (!options.dir) {
|
||||
console.error('Error: --dir is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dirPath = path.resolve(process.cwd(), options.dir);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
console.error(`Error: Directory not found: ${dirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Scanning ${dirPath} for token violations...\n`);
|
||||
|
||||
const files = getFiles(dirPath, options.ignore);
|
||||
const allViolations = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (shouldSkip(file)) continue;
|
||||
|
||||
const violations = scanFile(file);
|
||||
allViolations.push(...violations);
|
||||
}
|
||||
|
||||
console.log(formatReport(allViolations));
|
||||
|
||||
// Exit with error code if violations found
|
||||
if (allViolations.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@ -0,0 +1,143 @@
|
||||
{
|
||||
"$schema": "https://design-tokens.org/schema.json",
|
||||
"primitive": {
|
||||
"color": {
|
||||
"gray": {
|
||||
"50": { "$value": "#F9FAFB", "$type": "color" },
|
||||
"100": { "$value": "#F3F4F6", "$type": "color" },
|
||||
"200": { "$value": "#E5E7EB", "$type": "color" },
|
||||
"300": { "$value": "#D1D5DB", "$type": "color" },
|
||||
"400": { "$value": "#9CA3AF", "$type": "color" },
|
||||
"500": { "$value": "#6B7280", "$type": "color" },
|
||||
"600": { "$value": "#4B5563", "$type": "color" },
|
||||
"700": { "$value": "#374151", "$type": "color" },
|
||||
"800": { "$value": "#1F2937", "$type": "color" },
|
||||
"900": { "$value": "#111827", "$type": "color" },
|
||||
"950": { "$value": "#030712", "$type": "color" }
|
||||
},
|
||||
"blue": {
|
||||
"50": { "$value": "#EFF6FF", "$type": "color" },
|
||||
"500": { "$value": "#3B82F6", "$type": "color" },
|
||||
"600": { "$value": "#2563EB", "$type": "color" },
|
||||
"700": { "$value": "#1D4ED8", "$type": "color" },
|
||||
"800": { "$value": "#1E40AF", "$type": "color" }
|
||||
},
|
||||
"red": {
|
||||
"500": { "$value": "#EF4444", "$type": "color" },
|
||||
"600": { "$value": "#DC2626", "$type": "color" },
|
||||
"700": { "$value": "#B91C1C", "$type": "color" }
|
||||
},
|
||||
"green": {
|
||||
"500": { "$value": "#22C55E", "$type": "color" },
|
||||
"600": { "$value": "#16A34A", "$type": "color" }
|
||||
},
|
||||
"yellow": {
|
||||
"500": { "$value": "#EAB308", "$type": "color" }
|
||||
},
|
||||
"white": { "$value": "#FFFFFF", "$type": "color" }
|
||||
},
|
||||
"spacing": {
|
||||
"0": { "$value": "0", "$type": "dimension" },
|
||||
"1": { "$value": "0.25rem", "$type": "dimension" },
|
||||
"2": { "$value": "0.5rem", "$type": "dimension" },
|
||||
"3": { "$value": "0.75rem", "$type": "dimension" },
|
||||
"4": { "$value": "1rem", "$type": "dimension" },
|
||||
"5": { "$value": "1.25rem", "$type": "dimension" },
|
||||
"6": { "$value": "1.5rem", "$type": "dimension" },
|
||||
"8": { "$value": "2rem", "$type": "dimension" },
|
||||
"10": { "$value": "2.5rem", "$type": "dimension" },
|
||||
"12": { "$value": "3rem", "$type": "dimension" },
|
||||
"16": { "$value": "4rem", "$type": "dimension" }
|
||||
},
|
||||
"fontSize": {
|
||||
"xs": { "$value": "0.75rem", "$type": "dimension" },
|
||||
"sm": { "$value": "0.875rem", "$type": "dimension" },
|
||||
"base": { "$value": "1rem", "$type": "dimension" },
|
||||
"lg": { "$value": "1.125rem", "$type": "dimension" },
|
||||
"xl": { "$value": "1.25rem", "$type": "dimension" },
|
||||
"2xl": { "$value": "1.5rem", "$type": "dimension" },
|
||||
"3xl": { "$value": "1.875rem", "$type": "dimension" },
|
||||
"4xl": { "$value": "2.25rem", "$type": "dimension" }
|
||||
},
|
||||
"radius": {
|
||||
"none": { "$value": "0", "$type": "dimension" },
|
||||
"sm": { "$value": "0.125rem", "$type": "dimension" },
|
||||
"default": { "$value": "0.25rem", "$type": "dimension" },
|
||||
"md": { "$value": "0.375rem", "$type": "dimension" },
|
||||
"lg": { "$value": "0.5rem", "$type": "dimension" },
|
||||
"xl": { "$value": "0.75rem", "$type": "dimension" },
|
||||
"full": { "$value": "9999px", "$type": "dimension" }
|
||||
},
|
||||
"shadow": {
|
||||
"none": { "$value": "none", "$type": "shadow" },
|
||||
"sm": { "$value": "0 1px 2px 0 rgb(0 0 0 / 0.05)", "$type": "shadow" },
|
||||
"default": { "$value": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", "$type": "shadow" },
|
||||
"md": { "$value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", "$type": "shadow" },
|
||||
"lg": { "$value": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", "$type": "shadow" }
|
||||
},
|
||||
"duration": {
|
||||
"fast": { "$value": "150ms", "$type": "duration" },
|
||||
"normal": { "$value": "200ms", "$type": "duration" },
|
||||
"slow": { "$value": "300ms", "$type": "duration" }
|
||||
}
|
||||
},
|
||||
"semantic": {
|
||||
"color": {
|
||||
"background": { "$value": "{primitive.color.gray.50}", "$type": "color" },
|
||||
"foreground": { "$value": "{primitive.color.gray.900}", "$type": "color" },
|
||||
"primary": { "$value": "{primitive.color.blue.600}", "$type": "color" },
|
||||
"primary-hover": { "$value": "{primitive.color.blue.700}", "$type": "color" },
|
||||
"primary-foreground": { "$value": "{primitive.color.white}", "$type": "color" },
|
||||
"secondary": { "$value": "{primitive.color.gray.100}", "$type": "color" },
|
||||
"secondary-foreground": { "$value": "{primitive.color.gray.900}", "$type": "color" },
|
||||
"muted": { "$value": "{primitive.color.gray.100}", "$type": "color" },
|
||||
"muted-foreground": { "$value": "{primitive.color.gray.500}", "$type": "color" },
|
||||
"destructive": { "$value": "{primitive.color.red.600}", "$type": "color" },
|
||||
"destructive-foreground": { "$value": "{primitive.color.white}", "$type": "color" },
|
||||
"border": { "$value": "{primitive.color.gray.200}", "$type": "color" },
|
||||
"ring": { "$value": "{primitive.color.blue.500}", "$type": "color" }
|
||||
},
|
||||
"spacing": {
|
||||
"component": { "$value": "{primitive.spacing.4}", "$type": "dimension" },
|
||||
"section": { "$value": "{primitive.spacing.12}", "$type": "dimension" }
|
||||
}
|
||||
},
|
||||
"component": {
|
||||
"button": {
|
||||
"bg": { "$value": "{semantic.color.primary}", "$type": "color" },
|
||||
"fg": { "$value": "{semantic.color.primary-foreground}", "$type": "color" },
|
||||
"hover-bg": { "$value": "{semantic.color.primary-hover}", "$type": "color" },
|
||||
"padding-x": { "$value": "{primitive.spacing.4}", "$type": "dimension" },
|
||||
"padding-y": { "$value": "{primitive.spacing.2}", "$type": "dimension" },
|
||||
"radius": { "$value": "{primitive.radius.md}", "$type": "dimension" },
|
||||
"font-size": { "$value": "{primitive.fontSize.sm}", "$type": "dimension" }
|
||||
},
|
||||
"input": {
|
||||
"bg": { "$value": "{semantic.color.background}", "$type": "color" },
|
||||
"border": { "$value": "{semantic.color.border}", "$type": "color" },
|
||||
"focus-ring": { "$value": "{semantic.color.ring}", "$type": "color" },
|
||||
"padding-x": { "$value": "{primitive.spacing.3}", "$type": "dimension" },
|
||||
"padding-y": { "$value": "{primitive.spacing.2}", "$type": "dimension" },
|
||||
"radius": { "$value": "{primitive.radius.md}", "$type": "dimension" }
|
||||
},
|
||||
"card": {
|
||||
"bg": { "$value": "{primitive.color.white}", "$type": "color" },
|
||||
"border": { "$value": "{semantic.color.border}", "$type": "color" },
|
||||
"shadow": { "$value": "{primitive.shadow.default}", "$type": "shadow" },
|
||||
"padding": { "$value": "{primitive.spacing.6}", "$type": "dimension" },
|
||||
"radius": { "$value": "{primitive.radius.lg}", "$type": "dimension" }
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"semantic": {
|
||||
"color": {
|
||||
"background": { "$value": "{primitive.color.gray.950}", "$type": "color" },
|
||||
"foreground": { "$value": "{primitive.color.gray.50}", "$type": "color" },
|
||||
"secondary": { "$value": "{primitive.color.gray.800}", "$type": "color" },
|
||||
"muted": { "$value": "{primitive.color.gray.800}", "$type": "color" },
|
||||
"muted-foreground": { "$value": "{primitive.color.gray.400}", "$type": "color" },
|
||||
"border": { "$value": "{primitive.color.gray.800}", "$type": "color" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user