diff --git a/.cursor/rules/accessibility.mdc b/.cursor/rules/accessibility.mdc new file mode 100644 index 0000000..37ed949 --- /dev/null +++ b/.cursor/rules/accessibility.mdc @@ -0,0 +1,43 @@ +--- +description: Accessibility non-negotiables for all UI work +globs: app/**/*.tsx,components/**/*.tsx +alwaysApply: false +--- + +# Accessibility Rules (Amin Rayan) + +Follow `docs/design-system/accessibility.md`. Accessibility outranks visual experimentation. + +## Always + +- Preserve visible focus styles (`focus-visible:ring-*`); never remove outlines without a replacement. +- Provide accessible names for icon-only buttons (`aria-label`). +- Use semantic HTML (`button`, `a`, `label`, landmarks) before ARIA. +- Associate form controls with labels; set `aria-invalid` and error text when invalid. +- Honor `prefers-reduced-motion` for new animations (prefer `MotionReveal` / CSS reduced-motion hooks). +- Keep touch targets ≥ 40px for primary icon actions; inputs ≥ 36px (16px font on mobile). +- Write meaningful `alt` text for informative images; empty alt / `aria-hidden` for decorative ones. +- Verify contrast for text and interactive states in both light and dark themes. +- Ensure keyboard access for menus, sheets, dialogs (prefer existing Radix primitives). + +## Never + +- Rely on color alone to convey state. +- Use `outline-none` / `ring-0` without an equivalent focus treatment. +- Trap keyboard focus outside modal patterns. +- Ship motion that ignores reduced-motion preferences. +- Use placeholder-only fields without a visible label. +- Disable zoom or block paste for arbitrary reasons. + +## Quick checks before finishing + +1. Tab through the UI — can you reach and operate everything? +2. Toggle dark mode — is text still readable? +3. Icon buttons — do they announce a name? +4. Motion — does reduced motion disable entrance animations? + +## References + +- `docs/design-system/accessibility.md` +- `docs/design-system/forms.md` +- `docs/design-system/motion.md` diff --git a/.cursor/rules/design-system-effects.mdc b/.cursor/rules/design-system-effects.mdc new file mode 100644 index 0000000..6caf546 --- /dev/null +++ b/.cursor/rules/design-system-effects.mdc @@ -0,0 +1,59 @@ +--- +description: Visual effects must come from styles/design-system — page owns atmosphere, sections do not +globs: components/**/*.tsx, app/**/*.tsx +alwaysApply: false +--- + +# Design System Visual Effects + +Atmospheric effects are **page-owned**. Sections sit inside the shared ambient environment. + +## Architecture + +``` +body (flat base) +→ PageAtmosphere (gradient + grid + soft light + noise) +→ page-content / sections (transparent shells) +→ cards / panels (surface tokens) +``` + +Canonical modules live in `styles/design-system/`, imported from `app/globals.css`. + +| File | Responsibility | +| --- | --- | +| `tokens.css` | Brand, elevation, glow stops | +| `background.css` | Flat body base, CTA photo banner | +| `ambient.css` | `page-atmosphere*`, `hero-atmosphere`, timeline sizing tokens | +| `surfaces.css` | `section-shell`, transparent canvas | +| `borders.css` | `border-surface` | +| `shadows.css` | `shadow-surface-*`, `shadow-floating`, `shadow-overlay`, `shadow-hero` | +| `gradients.css` | Panel-only `bg-gradient-cta` (not section washes) | +| `glow.css` | Card/panel/button neon, `media-frame`, glow orbs | +| `grid.css` | Legacy section grid (prefer page grid) | + +## Allowed in sections + +```tsx +
+ … +
+``` + +- Section shells: `bg-section-canvas` (default), soft `bg-section-muted`, or dark-only `bg-section-lift` for brightness rhythm +- Surfaces: `border-surface`, `shadow-surface-sm|md`, `shadow-floating`, `shadow-hero`, `shadow-glow-sm|primary|lg|media`, `media-frame` +- CTA panels: `bg-gradient-cta`, `bg-cta-photo-banner` (content panels, not section backgrounds) +- Hero soft wash: `hero-atmosphere` only on the homepage hero +- Local emphasis: `local-highlight` / `local-highlight--soft` behind a card, image, title, or CTA panel — never as a section wash + +## Forbidden in sections + +- Owning page atmosphere (`section-pattern-grid`, `glow-orb`, `SectionAtmosphere`) +- Opaque `bg-background` / hard `bg-section-muted` slabs that restart the canvas +- Inline `bg-[radial-gradient(...)]`, custom `box-shadow`, `backdrop-filter`, or grid patterns +- Per-section lighting that begins/ends inside one block (`glow-orb` bands, large `bg-glow-primary` blobs) + +## References + +- `docs/design-system/gradients.md` +- `docs/design-system/sections.md` +- `components/page-atmosphere.tsx` diff --git a/.cursor/rules/frontend.mdc b/.cursor/rules/frontend.mdc new file mode 100644 index 0000000..a632766 --- /dev/null +++ b/.cursor/rules/frontend.mdc @@ -0,0 +1,53 @@ +--- +description: Frontend coding standards — tokens, reuse, dark mode, Next.js UI conventions +globs: app/**/*.{tsx,ts},components/**/*.{tsx,ts},lib/**/*.{ts,tsx} +alwaysApply: false +--- + +# Frontend Rules (Amin Rayan) + +Read and follow `docs/design-system/` — especially README, coding-rules, colors, typography, spacing, and quality-checklist. + +## Always + +- Follow the Design System docs as the single source of truth. +- Reuse design tokens from `app/globals.css` via semantic Tailwind classes (`bg-primary`, `text-muted-foreground`, `border-border`, …). +- Reuse `Container`, `layout` (`@/lib/layout`), `type` / `Typography` (`@/lib/typography`), `Button`, and `Card`. +- Support light and dark themes through CSS variables (never hardcode surfaces). +- Preserve responsive, mobile-first behavior. +- Preserve accessibility (focus rings, labels, keyboard, reduced motion). +- Prefer existing `components/ui/*` primitives over new ones. +- Use `cn()` for class composition. + +## Never + +- Create another content container or competing `max-w-*` section shell. +- Hardcode colors (hex/rgb/hsl) in component classNames. +- Invent spacing outside the project scale (4–96px tokens). +- Duplicate Button, Card, Input, or typography systems. +- Ship inconsistent radii, shadows, or hover timings. +- Introduce a second header/footer for marketing pages. +- Bypass the design system for “quick” one-off UI. + +## Patterns + +```tsx +import { Container } from "@/components/container"; +import { layout } from "@/lib/layout"; +import { type } from "@/lib/typography"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +
+ +

عنوان

+ +
+
+``` + +## References + +- `docs/design-system/README.md` +- `docs/design-system/coding-rules.md` +- `docs/design-system/quality-checklist.md` diff --git a/.cursor/rules/layout-container.mdc b/.cursor/rules/layout-container.mdc new file mode 100644 index 0000000..41a5c65 --- /dev/null +++ b/.cursor/rules/layout-container.mdc @@ -0,0 +1,45 @@ +--- +description: Global layout container rules for all page sections and blocks +globs: app/**/*.tsx,components/**/*.tsx +alwaysApply: false +--- + +# Layout & Container Rules + +Every imported or newly created block MUST follow the site's global layout container. + +Full documentation: `docs/design-system/layout.md`, `docs/design-system/container.md`, `docs/design-system/sections.md`. + +## Shared tokens + +Use `Container` from `@/components/container` and tokens from `@/lib/layout`: + +- `layout.container` / `` — `mx-auto w-full max-w-[76.8rem] px-6` +- `layout.sectionY` — vertical section rhythm (`py-16 md:py-20`; adjacent gap = 8rem / md:10rem) +- `layout.sectionTopWithHeader` — first section under the fixed header (`pt-28 pb-16 md:pt-32 md:pb-20`) +- `layout.scrollMargin` — for in-page anchors under the fixed header + +## Rules + +- Never let a block extend beyond the site's main content width (`max-w-[76.8rem]`). +- Always use the project's standard container width, max-width, and horizontal padding (`px-6`). +- Preserve consistent left and right alignment with all other sections. +- Do not use a different container size unless explicitly required by the design. +- Full-width backgrounds are allowed, but the content inside must remain constrained to ``. +- Maintain consistent spacing between sections via `layout.sectionY`. +- Prefer `section-shell` + optional `section-decor` / `section-content` for atmospheric sections. +- If a Shadcn/Tailark block has its own container or max-width, replace it with `` / `layout.container`. +- Remove any hardcoded width, max-width, or horizontal padding that conflicts with the site's layout. +- Before finishing a block, verify visual alignment with previous/next sections. + +## Pattern + +```tsx +
+ + {/* section content */} + +
+``` + +Inner text measure (`max-w-xl`, `max-w-3xl` on a heading/paragraph) is fine for readability — do not use those as section shells. diff --git a/.cursor/rules/shadcn.mdc b/.cursor/rules/shadcn.mdc new file mode 100644 index 0000000..6dd3cd8 --- /dev/null +++ b/.cursor/rules/shadcn.mdc @@ -0,0 +1,47 @@ +--- +description: Adapt Shadcn/Tailark imports to Amin Rayan design system — never ship vendor styles as-is +globs: components/**/*.tsx,components/ui/**/*.tsx +alwaysApply: false +--- + +# Shadcn / Tailark Adaptation Rules + +Any imported or regenerated Shadcn UI / Tailark / marketing block MUST be adapted to this project before it is considered done. See `docs/design-system/shadcn.md`. + +## Always + +- Replace vendor containers (`container`, `max-w-5xl`, foreign `px-*`) with `` / `layout.container`. +- Apply `layout.sectionY` on the outer section (plus `section-shell` / `bg-section-*` when appropriate). +- Remap colors to semantic tokens (`primary`, `background`, `foreground`, `muted-foreground`, `border`, …). +- Remap headings/body to `type.*` / ``. +- Wire CTAs through `@/components/ui/button`. +- Prefer `@/components/ui/card` for card-like panels. +- Align radius/shadow/hover/transition with existing primitives. +- Verify light and dark themes. +- Verify alignment against header/footer. +- Keep Radix accessibility behaviors intact. + +## Never allow + +- Random spacing, colors, typography, containers, or shadows from the vendor example. +- Purple / indigo / zinc marketing palettes left in place. +- Parallel button CSS or unstyled native buttons for primary CTAs. +- Nested competing max-width shells. +- New global CSS files for a single imported block without necessity. +- Shipping the block “to restyle later.” + +## Transform sketch + +```tsx +// Vendor → Project +// max-w-5xl px-4 py-24 → inside section with layout.sectionY +// bg-indigo-600 button → + + + + + + +Error message +``` diff --git a/.cursor/skills/design-system/references/tailwind-integration.md b/.cursor/skills/design-system/references/tailwind-integration.md new file mode 100644 index 0000000..c632788 --- /dev/null +++ b/.cursor/skills/design-system/references/tailwind-integration.md @@ -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) +
// 50% opacity +
// 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. diff --git a/.cursor/skills/design-system/references/token-architecture.md b/.cursor/skills/design-system/references/token-architecture.md new file mode 100644 index 0000000..e13ed2b --- /dev/null +++ b/.cursor/skills/design-system/references/token-architecture.md @@ -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" + } + } + } +} +``` diff --git a/.cursor/skills/design-system/scripts/embed-tokens.cjs b/.cursor/skills/design-system/scripts/embed-tokens.cjs new file mode 100644 index 0000000..419c104 --- /dev/null +++ b/.cursor/skills/design-system/scripts/embed-tokens.cjs @@ -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 `; + } 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); +} diff --git a/.cursor/skills/design-system/scripts/fetch-background.py b/.cursor/skills/design-system/scripts/fetch-background.py new file mode 100644 index 0000000..bcbd357 --- /dev/null +++ b/.cursor/skills/design-system/scripts/fetch-background.py @@ -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() diff --git a/.cursor/skills/design-system/scripts/generate-slide.py b/.cursor/skills/design-system/scripts/generate-slide.py new file mode 100644 index 0000000..2de390d --- /dev/null +++ b/.cursor/skills/design-system/scripts/generate-slide.py @@ -0,0 +1,770 @@ +#!/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 html import escape +from pathlib import Path +from datetime import datetime + + +def _e(value, default=''): + """HTML-escape a user-supplied value for safe embedding in HTML content.""" + return escape(str(value if value is not None else default)) + + +def _safe_url(url, default='#'): + """Validate and escape a URL for use in href attributes. + + Only allows http://, https://, #, and / schemes to prevent + javascript: URI injection (CWE-79). + """ + if url and str(url).strip().lower().startswith(('http://', 'https://', '#', '/')): + return escape(str(url), quote=True) + return default + +# 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 = ''' + + + + + {title} + + + + + + + + + + + +
+ {slides_content} +
+ + +''' + + +# ============ SLIDE GENERATORS ============ + +def generate_title_slide(data): + """Title slide with gradient headline""" + return f''' +
+
{_e(data.get('badge', 'Pitch Deck'))}
+

{_e(data.get('title', 'Your Title Here'))}

+

{_e(data.get('subtitle', 'Your compelling subtitle'))}

+ + +
+ ''' + + +def generate_problem_slide(data): + """Problem statement slide using PAS formula""" + return f''' +
+
The Problem
+

{_e(data.get('headline', 'The problem your audience faces'))}

+
+
+
01
+

{_e(data.get('pain_1_title', 'Pain Point 1'))}

+

{_e(data.get('pain_1_desc', 'Description of the first pain point'))}

+
+
+
02
+

{_e(data.get('pain_2_title', 'Pain Point 2'))}

+

{_e(data.get('pain_2_desc', 'Description of the second pain point'))}

+
+
+
03
+

{_e(data.get('pain_3_title', 'Pain Point 3'))}

+

{_e(data.get('pain_3_desc', 'Description of the third pain point'))}

+
+
+ +
+ ''' + + +def generate_solution_slide(data): + """Solution slide with feature highlights""" + return f''' +
+
The Solution
+

{_e(data.get('headline', 'How we solve this'))}

+
+
+
+
✓
+
+

{_e(data.get('feature_1_title', 'Feature 1'))}

+

{_e(data.get('feature_1_desc', 'Description of feature 1'))}

+
+
+
+
✓
+
+

{_e(data.get('feature_2_title', 'Feature 2'))}

+

{_e(data.get('feature_2_desc', 'Description of feature 2'))}

+
+
+
+
✓
+
+

{_e(data.get('feature_3_title', 'Feature 3'))}

+

{_e(data.get('feature_3_desc', 'Description of feature 3'))}

+
+
+
+
+
+
◆
+

Product screenshot or demo

+
+
+
+ +
+ ''' + + +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''' +
+
{_e(m.get('value', ''))}
+
{_e(m.get('label', ''))}
+
+ ''' for m in metrics[:4]]) + + return f''' +
+
Traction
+

{_e(data.get('headline', 'Our Growth'))}

+
+ {metrics_html} +
+ +
+ ''' + + +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''' +
+ {_e(b.get('display', str(b.get('value', 0)) + '%'))} + {_e(b.get('label', ''))} +
+ ''' for b in bars]) + + return f''' +
+
{_e(data.get('badge', 'Growth'))}
+

{_e(data.get('headline', 'Revenue Growth'))}

+
+
{_e(data.get('chart_title', 'Quarterly Revenue'))}
+
+ {bars_html} +
+
+ +
+ ''' + + +def generate_testimonial_slide(data): + """Social proof slide""" + return f''' +
+
What They Say
+
+

"{_e(data.get('quote', 'This product changed how we work. Incredible results.'))}"

+

{_e(data.get('author', 'Jane Doe'))}

+

{_e(data.get('role', 'CEO, Example Company'))}

+
+ +
+ ''' + + +def generate_cta_slide(data): + """Closing CTA slide""" + return f''' +
+

{_e(data.get('headline', 'Ready to get started?'))}

+

{_e(data.get('subheadline', 'Join thousands of teams already using our solution.'))}

+ + +
+ ''' + + +# 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=escape(str(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() diff --git a/.cursor/skills/design-system/scripts/generate-tokens.cjs b/.cursor/skills/design-system/scripts/generate-tokens.cjs new file mode 100644 index 0000000..73cc7d7 --- /dev/null +++ b/.cursor/skills/design-system/scripts/generate-tokens.cjs @@ -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 Input JSON token file (required) + -o, --output Output file (default: stdout) + -f, --format 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(); diff --git a/.cursor/skills/design-system/scripts/html-token-validator.py b/.cursor/skills/design-system/scripts/html-token-validator.py new file mode 100644 index 0000000..a722498 --- /dev/null +++ b/.cursor/skills/design-system/scripts/html-token-validator.py @@ -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 '): + if verbose: + result.add_warning(f"Allowed in + + + + +
+ + +
+ + +
+
+

Title Slide

+

Subtitle or tagline

+
+
+ + + +
+ + + + + + + +``` + +## Chart.js Integration + +```html +
+ +
+ + +``` + +## Animation Classes + +```css +/* Fade Up */ +.animate-fade-up { + animation: fadeUp 0.6s ease-out forwards; + opacity: 0; +} +@keyframes fadeUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Count Animation */ +.animate-count { animation: countUp 1s ease-out forwards; } + +/* Scale */ +.animate-scale { + animation: scaleIn 0.5s ease-out forwards; +} +@keyframes scaleIn { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +/* Stagger Children */ +.animate-stagger > * { + opacity: 0; + animation: fadeUp 0.5s ease-out forwards; +} +.animate-stagger > *:nth-child(1) { animation-delay: 0.1s; } +.animate-stagger > *:nth-child(2) { animation-delay: 0.2s; } +.animate-stagger > *:nth-child(3) { animation-delay: 0.3s; } +.animate-stagger > *:nth-child(4) { animation-delay: 0.4s; } +``` + +## Background Images + +```html +
+
+
+ +
+
+``` + +## CSS Variables Reference + +| Variable | Usage | +|----------|-------| +| `--color-primary` | Brand primary (CTA, highlights) | +| `--color-background` | Slide background | +| `--color-secondary` | Secondary elements | +| `--primitive-gradient-primary` | Title gradients | +| `--typography-font-heading` | Headlines | +| `--typography-font-body` | Body text | diff --git a/.cursor/skills/design/references/slides-layout-patterns.md b/.cursor/skills/design/references/slides-layout-patterns.md new file mode 100644 index 0000000..e2b3849 --- /dev/null +++ b/.cursor/skills/design/references/slides-layout-patterns.md @@ -0,0 +1,137 @@ +# Layout Patterns + +25 slide layouts with CSS structures and animation classes. + +## Layout Selection by Use Case + +| Layout | Use Case | Animation | +|--------|----------|-----------| +| Title Slide | Opening/first impression | `animate-fade-up` | +| Problem Statement | Establish pain point | `animate-stagger` | +| Solution Overview | Introduce solution | `animate-scale` | +| Feature Grid | Show capabilities (3-6 cards) | `animate-stagger` | +| Metrics Dashboard | Display KPIs (3-4 metrics) | `animate-stagger-scale` | +| Comparison Table | Compare options | `animate-fade-up` | +| Timeline Flow | Show progression | `animate-stagger` | +| Team Grid | Introduce people | `animate-stagger` | +| Quote Testimonial | Customer endorsement | `animate-fade-up` | +| Two Column Split | Compare/contrast | `animate-fade-up` | +| Big Number Hero | Single powerful metric | `animate-count` | +| Product Screenshot | Show product UI | `animate-scale` | +| Pricing Cards | Present tiers | `animate-stagger` | +| CTA Closing | Drive action | `animate-pulse` | + +## CSS Structures + +### Title Slide +```css +.slide-title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} +``` + +### Two Column Split +```css +.slide-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: center; +} +@media (max-width: 768px) { + .slide-split { grid-template-columns: 1fr; gap: 24px; } +} +``` + +### Feature Grid (3 columns) +```css +.slide-features { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} +@media (max-width: 768px) { + .slide-features { grid-template-columns: repeat(2, 1fr); gap: 16px; } +} +@media (max-width: 480px) { + .slide-features { grid-template-columns: 1fr; } +} +``` + +### Metrics Dashboard (4 columns) +```css +.slide-metrics { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} +@media (max-width: 768px) { + .slide-metrics { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 480px) { + .slide-metrics { grid-template-columns: 1fr; } +} +``` + +## Component Variants + +### Card Styles +| Style | CSS Class | Use For | +|-------|-----------|---------| +| Icon Left | `.card-icon-left` | Features with icons | +| Accent Bar | `.card-accent-bar` | Highlighted features | +| Metric Card | `.card-metric` | Numbers/stats | +| Avatar Card | `.card-avatar` | Team members | +| Pricing Card | `.card-pricing` | Price tiers | + +### Metric Styles +| Style | Effect | +|-------|--------| +| `gradient-number` | Gradient text on numbers | +| `oversized` | Extra large (120px+) | +| `sparkline` | Small inline chart | +| `funnel-numbers` | Conversion stages | + +## Visual Treatments + +| Treatment | When to Use | +|-----------|-------------| +| `gradient-glow` | Title slides, CTAs | +| `subtle-border` | Problem statements | +| `icon-top` | Feature grids | +| `screenshot-shadow` | Product screenshots | +| `popular-highlight` | Pricing (scale 1.05) | +| `bg-overlay` | Background images | +| `contrast-pair` | Before/after | +| `logo-grayscale` | Client logos | + +## Search Commands + +```bash +# Find layout for specific use +python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout + +# Contextual recommendation +python .claude/skills/design-system/scripts/search-slides.py "traction slide" \ + --context --position 4 --total 10 +``` + +## Layout Decision Flow + +``` +1. What's the slide goal? + └─> Search layout-logic.csv + +2. What emotion should it trigger? + └─> Search color-logic.csv + +3. What's the content type? + └─> Search typography.csv + +4. Should it break pattern? + └─> Check position (1/3, 2/3) → Use full-bleed +``` diff --git a/.cursor/skills/design/references/slides-strategies.md b/.cursor/skills/design/references/slides-strategies.md new file mode 100644 index 0000000..e004fe1 --- /dev/null +++ b/.cursor/skills/design/references/slides-strategies.md @@ -0,0 +1,94 @@ +# Slide Strategies + +15 proven deck structures with emotion arcs. + +## Strategy Selection + +| Strategy | Slides | Goal | Audience | +|----------|--------|------|----------| +| YC Seed Deck | 10-12 | Raise seed funding | VCs | +| Guy Kawasaki | 10 | Pitch in 20 min | Investors | +| Series A | 12-15 | Raise Series A | Growth VCs | +| Product Demo | 5-8 | Demonstrate value | Prospects | +| Sales Pitch | 7-10 | Close deal | Qualified leads | +| Nancy Duarte Sparkline | Varies | Transform perspective | Any | +| Problem-Solution-Benefit | 3-5 | Quick persuasion | Time-pressed | +| QBR | 10-15 | Update stakeholders | Leadership | +| Team All-Hands | 8-12 | Align team | Employees | +| Conference Talk | 15-25 | Thought leadership | Attendees | +| Workshop | 20-40 | Teach skills | Learners | +| Case Study | 8-12 | Prove value | Prospects | +| Competitive Analysis | 6-10 | Strategic decisions | Internal | +| Board Meeting | 15-20 | Update board | Directors | +| Webinar | 20-30 | Generate leads | Registrants | + +## Common Structures + +### YC Seed Deck (10 slides) +1. Title/Hook +2. Problem +3. Solution +4. Traction +5. Market +6. Product +7. Business Model +8. Team +9. Financials +10. The Ask + +**Emotion arc:** curiosity→frustration→hope→confidence→trust→urgency + +### Sales Pitch (9 slides) +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 + Next Steps + +**Emotion arc:** connection→frustration→fear→hope→trust→confidence→urgency + +### Product Demo (6 slides) +1. Hook/Problem +2. Solution Overview +3. Live Demo/Screenshots +4. Key Features +5. Benefits/Pricing +6. CTA + +**Emotion arc:** curiosity→frustration→hope→confidence→urgency + +## Duarte Sparkline Pattern + +Alternate between "What Is" (current pain) and "What Could Be" (better future): + +``` +What Is → What Could Be → What Is → What Could Be → New Bliss +(pain) (hope) (pain) (hope) (resolution) +``` + +Pattern breaks at 1/3 and 2/3 positions create engagement peaks. + +## Search Commands + +```bash +# Find strategy by goal +python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy + +# Get emotion arc +python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json +``` + +## Matching Strategy to Context + +| Context | Recommended Strategy | +|---------|---------------------| +| Raising money | YC Seed, Series A, Guy Kawasaki | +| Selling product | Sales Pitch, Product Demo | +| Internal update | QBR, All-Hands, Board Meeting | +| Public speaking | Conference Talk, Workshop | +| Proving value | Case Study, Competitive Analysis | +| Lead generation | Webinar | diff --git a/.cursor/skills/design/references/slides.md b/.cursor/skills/design/references/slides.md new file mode 100644 index 0000000..748577d --- /dev/null +++ b/.cursor/skills/design/references/slides.md @@ -0,0 +1,42 @@ +# Slides Reference + +Strategic HTML presentation design with Chart.js data visualization, design tokens, responsive layouts, and copywriting formulas. + +## Usage + +Activate the `design` skill and specify slides task, e.g. "create a pitch deck". + +## Knowledge Base + +| Topic | File | Purpose | +|-------|------|---------| +| Creation Guide | `references/slides-create.md` | Step-by-step slide creation workflow | +| Layout Patterns | `references/slides-layout-patterns.md` | Slide layout templates and grid systems | +| HTML Template | `references/slides-html-template.md` | Base HTML structure for presentations | +| Copywriting | `references/slides-copywriting-formulas.md` | AIDA, PAS, FAB for slide content | +| Strategies | `references/slides-strategies.md` | Contextual strategies by presentation type | + +## When to Use + +- Marketing presentations and pitch decks +- Data-driven slides with Chart.js visualizations +- Strategic slide design with layout patterns +- Copywriting-optimized presentation content +- Investor decks, sales presentations, team updates + +## Key Features + +- **Chart.js Integration**: Bar, line, pie, doughnut, radar charts +- **Design Tokens**: Consistent spacing, colors, typography +- **Responsive**: Works on desktop and mobile +- **Copywriting**: Built-in AIDA, PAS, FAB formulas +- **Layout Patterns**: Hero, split, grid, comparison, timeline + +## Workflow + +1. Parse presentation type from user request +2. Load `references/slides-create.md` for creation guide +3. Select layout patterns from `references/slides-layout-patterns.md` +4. Apply copywriting formulas from `references/slides-copywriting-formulas.md` +5. Use HTML template from `references/slides-html-template.md` +6. Apply strategy from `references/slides-strategies.md` diff --git a/.cursor/skills/design/references/social-photos-design.md b/.cursor/skills/design/references/social-photos-design.md new file mode 100644 index 0000000..63f6545 --- /dev/null +++ b/.cursor/skills/design/references/social-photos-design.md @@ -0,0 +1,329 @@ +# Social Photos Design Guide + +Design social media images via HTML/CSS rendering + screenshot export. Orchestrates `ui-ux-pro-max`, `brand`, `design-system`, and `chrome-devtools` skills. + +## Platform Sizes + +| Platform | Type | Size (px) | Aspect | +|----------|------|-----------|--------| +| Instagram | Post | 1080 x 1080 | 1:1 | +| Instagram | Story/Reel | 1080 x 1920 | 9:16 | +| Instagram | Carousel | 1080 x 1350 | 4:5 | +| Facebook | Post | 1200 x 630 | ~1.9:1 | +| Facebook | Story | 1080 x 1920 | 9:16 | +| Twitter/X | Post | 1200 x 675 | 16:9 | +| Twitter/X | Card | 800 x 418 | ~1.91:1 | +| LinkedIn | Post | 1200 x 627 | ~1.91:1 | +| LinkedIn | Article | 1200 x 644 | ~1.86:1 | +| Pinterest | Pin | 1000 x 1500 | 2:3 | +| YouTube | Thumbnail | 1280 x 720 | 16:9 | +| TikTok | Cover | 1080 x 1920 | 9:16 | +| Threads | Post | 1080 x 1080 | 1:1 | + +## Workflow + +### Step 1: Activate Project Management + +Invoke `project-management` skill to create persistent TODO tasks via Claude's native task orchestration. Break down into: +- Requirement analysis task +- Idea generation task(s) +- HTML design task(s) — can parallelize per size/variant +- Screenshot export task(s) — can parallelize per file +- Report generation task + +Spawn parallel subagents for independent tasks (e.g., multiple HTML files for different sizes). + +### Step 2: Analyze Requirements + +Parse user input for: +- **Subject/topic** — what the social photo represents +- **Target platforms** — which sizes needed (default: Instagram Post 1:1 + Story 9:16) +- **Visual style** — minimalist, bold, gradient, photo-based, etc. +- **Brand context** — read from `docs/brand-guidelines.md` if exists +- **Content elements** — headline, subtext, CTA, images, icons +- **Quantity** — how many variations (default: 3) + +### Step 3: Generate Ideas + +Create 3-5 concept ideas that: +- Match the input prompt/requirements +- Consider platform-specific best practices +- Vary in composition, color, typography approach +- Align with brand guidelines if available + +Present ideas to user via `AskUserQuestion` for approval before designing. + +### Step 4: Design HTML Files + +Activate these skills in sequence: + +1. **`/ckm:brand`** — Extract brand colors, fonts, voice from user's project +2. **`/ckm:design-system`** — Get design tokens (spacing, typography scale, color palette) +3. **Randomly invoke ONE of:** `/ck:ui-ux-pro-max` OR `/ck:frontend-design` — for layout, hierarchy, visual balance. Pick one at random each run for design variety. + +For each approved idea + each target size, create an HTML file: + +``` +output/social-photos/ +├── idea-1-instagram-post-1080x1080.html +├── idea-1-instagram-story-1080x1920.html +├── idea-2-instagram-post-1080x1080.html +├── idea-2-instagram-story-1080x1920.html +└── ... +``` + +#### HTML Design Rules + +- **Viewport** — Set exact pixel dimensions matching target size +- **Self-contained** — Inline all CSS, embed fonts via Google Fonts CDN +- **No scrolling** — Everything fits in one viewport +- **High contrast** — Text readable at thumbnail size +- **Brand-aligned** — Use extracted brand colors/fonts +- **Safe zones** — Critical content within central 80% area +- **Typography** — Min 24px for headlines, min 16px for body at 1080px width +- **Visual hierarchy** — One focal point, clear reading flow + +#### HTML Template Structure + +```html + + + + + + + + + +
+ +
+ + +``` + +### Step 5: Screenshot Export + +Use Chrome headless, `chrome-devtools` skill, or Playwright/Puppeteer to capture exact-size screenshots. + +**IMPORTANT:** Always add a delay (3-5s) after page load for fonts/images to fully render before capture. + +#### Option A: Chrome Headless CLI (Recommended — zero dependencies) + +```bash +CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +DELAY=5 # seconds for fonts/images to load + +"$CHROME" \ + --headless \ + --disable-gpu \ + --no-sandbox \ + --hide-scrollbars \ + --window-size="${WIDTH},${HEIGHT}" \ + --virtual-time-budget=$((DELAY * 1000)) \ + --screenshot="output.png" \ + "file:///path/to/file.html" +``` + +Key flags: +- `--virtual-time-budget=5000` — waits 5s virtual time for assets (Google Fonts, images) to load +- `--hide-scrollbars` — prevents scrollbar artifacts in screenshots +- `--window-size=WxH` — sets exact pixel dimensions + +#### Option B: chrome-devtools skill + +Invoke `/chrome-devtools` with instructions to: +1. Open each HTML file in browser +2. Set viewport to exact target dimensions +3. Wait 3-5s for fonts/images to fully load +4. Screenshot full page to PNG +5. Save to `output/social-photos/exports/` + +#### Option C: Playwright script + +```javascript +const { chromium } = require('playwright'); + +async function captureScreenshots(htmlFiles) { + const browser = await chromium.launch(); + + for (const file of htmlFiles) { + const [width, height] = file.match(/(\d+)x(\d+)/).slice(1).map(Number); + + const page = await browser.newPage(); + await page.setViewportSize({ width, height }); + await page.goto(`file://${file}`, { waitUntil: 'networkidle' }); + // Wait for fonts/images to fully render + await page.waitForTimeout(3000); + + const outputPath = file.replace('.html', '.png').replace('social-photos/', 'social-photos/exports/'); + await page.screenshot({ path: outputPath, type: 'png' }); + await page.close(); + } + + await browser.close(); +} +``` + +#### Option D: Puppeteer script + +```javascript +const puppeteer = require('puppeteer'); + +async function captureScreenshots(htmlFiles) { + const browser = await puppeteer.launch(); + + for (const file of htmlFiles) { + const [width, height] = file.match(/(\d+)x(\d+)/).slice(1).map(Number); + + const page = await browser.newPage(); + await page.setViewport({ width, height, deviceScaleFactor: 2 }); // 2x for retina + await page.goto(`file://${file}`, { waitUntil: 'networkidle0' }); + // Wait for fonts/images to fully render + await new Promise(r => setTimeout(r, 3000)); + + const outputPath = file.replace('.html', '.png').replace('social-photos/', 'social-photos/exports/'); + await page.screenshot({ path: outputPath, type: 'png' }); + await page.close(); + } + + await browser.close(); +} +``` + +**IMPORTANT:** Use `deviceScaleFactor: 2` for retina-quality output (Puppeteer only). + +### Step 6: Verify & Fix Designs + +Use Chrome MCP or `chrome-devtools` skill to visually inspect each exported PNG: + +1. Open exported screenshots and check for layout/styling issues +2. Verify: fonts rendered correctly, colors match brand, text readable at thumbnail size +3. Check: no overflow, no cut-off content, safe zones respected, visual hierarchy clear +4. If issues found → fix HTML source → re-export screenshot → verify again +5. Repeat until all designs pass visual QA + +**Common issues to check:** +- Fonts not loaded (fallback to system fonts) +- Text overflow or clipping +- Elements outside safe zone (central 80%) +- Low contrast text (below WCAG AA 4.5:1) +- Misaligned elements or broken layouts + +### Step 7: Generate Summary Report + +Save report to `plans/reports/` with naming pattern from session hooks. + +Report structure: + +```markdown +# Social Photos Design Report + +## Overview +- Prompt/requirements: {original input} +- Platforms: {target platforms} +- Variations: {count} +- Style: {chosen style} + +## Ideas Generated +1. **{Idea name}** — {brief description, rationale} +2. ... + +## Design Decisions +- Color palette: {colors used, why} +- Typography: {fonts, sizes, why} +- Layout: {composition approach, why} +- Brand alignment: {how brand guidelines influenced design} + +## Output Files +| File | Size | Platform | Preview | +|------|------|----------|---------| +| exports/{filename}.png | {WxH} | {platform} | {description} | + +## Why This Works +- {Platform-specific reasoning} +- {Brand alignment reasoning} +- {Visual hierarchy reasoning} +- {Engagement potential reasoning} + +## Recommendations +- {A/B test suggestions} +- {Platform-specific tips} +- {Iteration opportunities} +``` + +### Step 8: Organize Output + +Invoke `assets-organizing` skill to organize all output files and reports: +- Move/copy exported PNGs to proper asset directories +- Ensure reports are in `plans/reports/` with correct naming +- Clean up intermediate HTML files if requested +- Tag outputs with metadata (platform, size, concept name) + +## Design Best Practices + +### Platform-Specific Tips + +- **Instagram** — Visual-first, minimal text (<20%), strong colors, lifestyle feel +- **Facebook** — Informative, can have more text, eye-catching in feed +- **Twitter/X** — Bold headlines, contrast for dark/light mode, clear message +- **LinkedIn** — Professional, clean, data-driven visuals, thought leadership +- **Pinterest** — Vertical format, text overlay on images, how-to style +- **YouTube** — Face close-ups perform best, bright colors, readable at small size +- **TikTok** — Trendy, energetic, bold typography, youth-oriented + +### Art Direction Styles (Reuse from Banner) + +| Style | Best For | Key Elements | +|-------|----------|--------------| +| Minimalist | SaaS, tech, luxury | Whitespace, single accent color, clean type | +| Bold Typography | Announcements, quotes | Large type, high contrast, minimal imagery | +| Gradient Mesh | Modern brands, apps | Fluid color transitions, floating elements | +| Photo-Based | Lifestyle, e-commerce | Hero image, subtle overlay, text on image | +| Geometric | Tech, fintech | Shapes, patterns, structured layouts | +| Glassmorphism | SaaS, modern apps | Frosted glass, blur effects, transparency | +| Flat Illustration | Education, health | Custom illustrations, friendly, approachable | +| Duotone | Creative, editorial | Two-color treatment on photos | +| Collage | Fashion, culture | Mixed media, overlapping elements | +| 3D/Isometric | Tech, product | Depth, shadows, modern perspective | + +### Color & Contrast + +- Ensure WCAG AA contrast ratio (4.5:1 min) for all text +- Test designs at 50% size to verify readability +- Consider platform dark/light mode compatibility +- Use brand primary color as dominant, secondary as accent + +### Typography Hierarchy + +| Element | Min Size (at 1080px) | Weight | +|---------|---------------------|--------| +| Headline | 48px | Bold/Black | +| Subheadline | 32px | Semibold | +| Body | 24px | Regular | +| Caption | 18px | Regular/Light | +| CTA | 28px | Bold | + +## Security & Scope + +This sub-skill handles social media image design only. Does NOT handle: +- Video content creation +- Animation/motion graphics +- Print production files (CMYK, bleed) +- Direct social media posting/scheduling +- AI image generation (use `ai-artist` skill for that) diff --git a/.cursor/skills/design/scripts/cip/core.py b/.cursor/skills/design/scripts/cip/core.py new file mode 100644 index 0000000..5e6b7a9 --- /dev/null +++ b/.cursor/skills/design/scripts/cip/core.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CIP Design Core - BM25 search engine for Corporate Identity Program design guidelines +""" + +import csv +import re +from pathlib import Path +from math import log +from collections import defaultdict + +# ============ CONFIGURATION ============ +DATA_DIR = Path(__file__).parent.parent.parent / "data" / "cip" +MAX_RESULTS = 3 + +CSV_CONFIG = { + "deliverable": { + "file": "deliverables.csv", + "search_cols": ["Deliverable", "Category", "Keywords", "Description", "Mockup Context"], + "output_cols": ["Deliverable", "Category", "Keywords", "Description", "Dimensions", "File Format", "Logo Placement", "Color Usage", "Typography Notes", "Mockup Context", "Best Practices", "Avoid"] + }, + "style": { + "file": "styles.csv", + "search_cols": ["Style Name", "Category", "Keywords", "Description", "Mood"], + "output_cols": ["Style Name", "Category", "Keywords", "Description", "Primary Colors", "Secondary Colors", "Typography", "Materials", "Finishes", "Mood", "Best For", "Avoid For"] + }, + "industry": { + "file": "industries.csv", + "search_cols": ["Industry", "Keywords", "CIP Style", "Mood"], + "output_cols": ["Industry", "Keywords", "CIP Style", "Primary Colors", "Secondary Colors", "Typography", "Key Deliverables", "Mood", "Best Practices", "Avoid"] + }, + "mockup": { + "file": "mockup-contexts.csv", + "search_cols": ["Context Name", "Category", "Keywords", "Scene Description"], + "output_cols": ["Context Name", "Category", "Keywords", "Scene Description", "Lighting", "Environment", "Props", "Camera Angle", "Background", "Style Notes", "Best For", "Prompt Modifiers"] + } +} + + +# ============ 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 = { + "deliverable": ["card", "letterhead", "envelope", "folder", "shirt", "cap", "badge", "signage", "vehicle", "car", "van", "stationery", "uniform", "merchandise", "packaging", "banner", "booth"], + "style": ["style", "minimal", "modern", "luxury", "vintage", "industrial", "elegant", "bold", "corporate", "organic", "playful"], + "industry": ["tech", "finance", "legal", "healthcare", "hospitality", "food", "fashion", "retail", "construction", "logistics"], + "mockup": ["mockup", "scene", "context", "photo", "shot", "lighting", "background", "studio", "lifestyle"] + } + + 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 "deliverable" + + +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["deliverable"]) + 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 and combine results""" + all_results = {} + for domain in CSV_CONFIG.keys(): + result = search(query, domain, max_results) + if result.get("results"): + all_results[domain] = result["results"] + return all_results + + +def get_cip_brief(brand_name, industry_query, style_query=None): + """Generate a comprehensive CIP brief for a brand""" + # Search industry + industry_results = search(industry_query, "industry", 1) + industry = industry_results.get("results", [{}])[0] if industry_results.get("results") else {} + + # Search style (use industry style if not specified) + style_query = style_query or industry.get("CIP Style", "corporate minimal") + style_results = search(style_query, "style", 1) + style = style_results.get("results", [{}])[0] if style_results.get("results") else {} + + # Get recommended deliverables for the industry + key_deliverables = industry.get("Key Deliverables", "").split() + deliverable_results = [] + for d in key_deliverables[:5]: + result = search(d, "deliverable", 1) + if result.get("results"): + deliverable_results.append(result["results"][0]) + + return { + "brand_name": brand_name, + "industry": industry, + "style": style, + "recommended_deliverables": deliverable_results, + "color_system": { + "primary": style.get("Primary Colors", industry.get("Primary Colors", "")), + "secondary": style.get("Secondary Colors", industry.get("Secondary Colors", "")) + }, + "typography": style.get("Typography", industry.get("Typography", "")), + "materials": style.get("Materials", ""), + "finishes": style.get("Finishes", "") + } diff --git a/.cursor/skills/design/scripts/cip/generate.py b/.cursor/skills/design/scripts/cip/generate.py new file mode 100644 index 0000000..0be632f --- /dev/null +++ b/.cursor/skills/design/scripts/cip/generate.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CIP Design Generator - Generate corporate identity mockups using Gemini Nano Banana + +Uses Gemini's native image generation (Nano Banana Flash/Pro) for high-quality mockups. +Supports text-and-image-to-image generation for using actual brand logos. + +- gemini-2.5-flash-image: Fast generation, cost-effective (default) +- gemini-3-pro-image-preview: Pro quality, 4K text rendering + +Image Editing (text-and-image-to-image): + When --logo is provided, the script uses Gemini's image editing capability + to incorporate the actual logo into CIP mockups instead of generating one. +""" + +import argparse +import json +import os +import sys +from pathlib import Path +from datetime import datetime + +# Add parent directory for imports +sys.path.insert(0, str(Path(__file__).parent)) +from core import search, get_cip_brief + +# Model options +MODELS = { + "flash": "gemini-2.5-flash-image", # Nano Banana Flash - fast, default + "pro": "gemini-3-pro-image-preview" # Nano Banana Pro - quality, 4K text +} +DEFAULT_MODEL = "flash" + + +def load_logo_image(logo_path): + """Load logo image using PIL for Gemini image editing""" + try: + from PIL import Image + except ImportError: + print("Error: pillow package not installed.") + print("Install with: pip install pillow") + return None + + logo_path = Path(logo_path) + if not logo_path.exists(): + print(f"Error: Logo file not found: {logo_path}") + return None + + try: + img = Image.open(logo_path) + # Convert to RGB if necessary (Gemini works best with RGB) + if img.mode in ('RGBA', 'P'): + # Create white background for transparent images + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'RGBA': + background.paste(img, mask=img.split()[3]) # Use alpha channel as mask + else: + background.paste(img) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + return img + except Exception as e: + print(f"Error loading logo: {e}") + return None + +# Load environment variables +def load_env(): + """Load environment variables from .env files""" + env_paths = [ + Path(__file__).parent.parent.parent / ".env", + Path.home() / ".claude" / "skills" / ".env", + Path.home() / ".claude" / ".env" + ] + for env_path in env_paths: + if env_path.exists(): + with open(env_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + if key not in os.environ: + os.environ[key] = value.strip('"\'') + +load_env() + + +def build_cip_prompt(deliverable, brand_name, style=None, industry=None, mockup=None, use_logo_image=False): + """Build an optimized prompt for CIP mockup generation + + Args: + deliverable: Type of deliverable (business card, letterhead, etc.) + brand_name: Name of the brand + style: Design style preference + industry: Industry for style recommendations + mockup: Mockup context override + use_logo_image: If True, prompt is optimized for image editing with logo + """ + + # Get deliverable details + deliverable_info = search(deliverable, "deliverable", 1) + deliverable_data = deliverable_info.get("results", [{}])[0] if deliverable_info.get("results") else {} + + # Get style details + style_info = search(style or "corporate minimal", "style", 1) if style else {} + style_data = style_info.get("results", [{}])[0] if style_info.get("results") else {} + + # Get industry details + industry_info = search(industry or "technology", "industry", 1) if industry else {} + industry_data = industry_info.get("results", [{}])[0] if industry_info.get("results") else {} + + # Get mockup context + mockup_context = deliverable_data.get("Mockup Context", "clean professional") + if mockup: + mockup_info = search(mockup, "mockup", 1) + if mockup_info.get("results"): + mockup_data = mockup_info["results"][0] + mockup_context = mockup_data.get("Scene Description", mockup_context) + + # Build prompt components + deliverable_name = deliverable_data.get("Deliverable", deliverable) + description = deliverable_data.get("Description", "") + dimensions = deliverable_data.get("Dimensions", "") + logo_placement = deliverable_data.get("Logo Placement", "center") + + style_name = style_data.get("Style Name", style or "corporate") + primary_colors = style_data.get("Primary Colors", industry_data.get("Primary Colors", "#0F172A #FFFFFF")) + typography = style_data.get("Typography", industry_data.get("Typography", "clean sans-serif")) + materials = style_data.get("Materials", "premium quality") + finishes = style_data.get("Finishes", "professional") + + mood = style_data.get("Mood", industry_data.get("Mood", "professional")) + + # Construct the prompt - different for image editing vs pure generation + if use_logo_image: + # Image editing prompt: instructs to USE the provided logo image + prompt_parts = [ + f"Create a professional corporate identity mockup photograph of a {deliverable_name}", + f"Use the EXACT logo from the provided image - do NOT modify or recreate the logo", + f"The logo MUST appear exactly as shown in the input image", + f"Place the logo on the {deliverable_name} at: {logo_placement}", + f"Brand name: '{brand_name}'", + f"{description}" if description else "", + f"Design style: {style_name}", + f"Color scheme matching the logo colors", + f"Materials: {materials} with {finishes} finish", + f"Setting: {mockup_context}", + f"Mood: {mood}", + "Photorealistic product photography", + "Soft natural lighting, professional studio quality", + "8K resolution, sharp details" + ] + else: + # Pure text-to-image prompt + prompt_parts = [ + f"Professional corporate identity mockup photograph", + f"showing {deliverable_name} for brand '{brand_name}'", + f"{description}" if description else "", + f"{style_name} design style", + f"using colors {primary_colors}", + f"{typography} typography", + f"logo placement: {logo_placement}", + f"{materials} materials with {finishes} finish", + f"{mockup_context} setting", + f"{mood} mood", + "photorealistic product photography", + "soft natural lighting", + "high quality professional shot", + "8k resolution detailed" + ] + + prompt = ", ".join([p for p in prompt_parts if p]) + + return { + "prompt": prompt, + "deliverable": deliverable_name, + "style": style_name, + "brand": brand_name, + "colors": primary_colors, + "mockup_context": mockup_context, + "logo_placement": logo_placement + } + + +def generate_with_nano_banana(prompt_data, output_dir=None, model_key="flash", aspect_ratio="1:1", logo_image=None): + """Generate image using Gemini Nano Banana (native image generation) + + Supports two modes: + 1. Text-to-image: Pure prompt-based generation (logo_image=None) + 2. Image editing: Text-and-image-to-image using provided logo (logo_image=PIL.Image) + + Models: + - flash: gemini-2.5-flash-image (fast, cost-effective) - DEFAULT + - pro: gemini-3-pro-image-preview (quality, 4K text rendering) + + Args: + prompt_data: Dict with prompt, deliverable, brand, etc. + output_dir: Output directory for generated images + model_key: 'flash' or 'pro' + aspect_ratio: Output aspect ratio (1:1, 16:9, etc.) + logo_image: PIL.Image object of the brand logo for image editing mode + """ + try: + from google import genai + from google.genai import types + except ImportError: + print("Error: google-genai package not installed.") + print("Install with: pip install google-genai") + return None + + api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY") + if not api_key: + print("Error: GEMINI_API_KEY or GOOGLE_API_KEY not set") + return None + + client = genai.Client(api_key=api_key) + + prompt = prompt_data["prompt"] + model_name = MODELS.get(model_key, MODELS[DEFAULT_MODEL]) + + # Determine mode + mode = "image-editing" if logo_image else "text-to-image" + + print(f"\n🎨 Generating CIP mockup...") + print(f" Mode: {mode}") + print(f" Deliverable: {prompt_data['deliverable']}") + print(f" Brand: {prompt_data['brand']}") + print(f" Style: {prompt_data['style']}") + print(f" Model: {model_name}") + print(f" Context: {prompt_data['mockup_context']}") + if logo_image: + print(f" Logo: Using provided image ({logo_image.size[0]}x{logo_image.size[1]})") + + try: + # Build contents: either just prompt or [prompt, image] for image editing + if logo_image: + # Image editing mode: pass both prompt and logo image + contents = [prompt, logo_image] + else: + # Text-to-image mode: just the prompt + contents = prompt + + # Use generate_content with response_modalities=['IMAGE'] for Nano Banana + response = client.models.generate_content( + model=model_name, + contents=contents, + config=types.GenerateContentConfig( + response_modalities=['IMAGE'], # Uppercase required + image_config=types.ImageConfig( + aspect_ratio=aspect_ratio + ) + ) + ) + + # Extract image from response + if response.candidates and response.candidates[0].content.parts: + for part in response.candidates[0].content.parts: + if hasattr(part, 'inline_data') and part.inline_data: + # Save image + output_dir = output_dir or Path.cwd() + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + brand_slug = prompt_data["brand"].lower().replace(" ", "-") + deliverable_slug = prompt_data["deliverable"].lower().replace(" ", "-") + filename = f"{brand_slug}-{deliverable_slug}-{timestamp}.png" + filepath = output_dir / filename + + image_data = part.inline_data.data + with open(filepath, "wb") as f: + f.write(image_data) + + print(f"\n✅ Generated: {filepath}") + return str(filepath) + + print("No image generated in response") + return None + + except Exception as e: + print(f"Error generating image: {e}") + return None + + +def generate_cip_set(brand_name, industry, style=None, deliverables=None, output_dir=None, model_key="flash", logo_path=None, aspect_ratio="1:1"): + """Generate a complete CIP set for a brand + + Args: + brand_name: Brand name to generate for + industry: Industry type for style recommendations + style: Optional specific style override + deliverables: List of deliverables to generate (default: core set) + output_dir: Output directory for images + model_key: 'flash' (fast) or 'pro' (quality) + logo_path: Path to brand logo image for image editing mode + aspect_ratio: Output aspect ratio + """ + + # Load logo image if provided + logo_image = None + if logo_path: + logo_image = load_logo_image(logo_path) + if not logo_image: + print("Warning: Could not load logo, falling back to text-to-image mode") + + # Get CIP brief for the brand + brief = get_cip_brief(brand_name, industry, style) + + # Default deliverables if not specified + if not deliverables: + deliverables = ["business card", "letterhead", "office signage", "vehicle", "polo shirt"] + + results = [] + for deliverable in deliverables: + prompt_data = build_cip_prompt( + deliverable=deliverable, + brand_name=brand_name, + style=brief.get("style", {}).get("Style Name"), + industry=industry, + use_logo_image=(logo_image is not None) + ) + + filepath = generate_with_nano_banana( + prompt_data, + output_dir, + model_key=model_key, + aspect_ratio=aspect_ratio, + logo_image=logo_image + ) + if filepath: + results.append({ + "deliverable": deliverable, + "filepath": filepath, + "prompt": prompt_data["prompt"] + }) + + return results + + +def check_logo_required(brand_name, skip_prompt=False): + """Check if logo is required and suggest logo-design skill if not provided + + Returns: + str: 'continue' to proceed without logo, 'generate' to use logo-design skill, 'exit' to abort + """ + if skip_prompt: + return 'continue' + + print(f"\n⚠️ No logo image provided for '{brand_name}'") + print(" Without a logo, AI will generate its own interpretation of the brand logo.") + print("") + print(" Options:") + print(" 1. Continue without logo (AI-generated logo interpretation)") + print(" 2. Generate a logo first using 'logo-design' skill") + print(" 3. Exit and provide a logo path with --logo") + print("") + + try: + choice = input(" Enter choice [1/2/3] (default: 1): ").strip() + if choice == '2': + return 'generate' + elif choice == '3': + return 'exit' + return 'continue' + except (EOFError, KeyboardInterrupt): + return 'continue' + + +def main(): + parser = argparse.ArgumentParser( + description="Generate CIP mockups using Gemini Nano Banana", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Generate with brand logo (RECOMMENDED) + python generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card" + + # Generate CIP set with logo + python generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set + + # Generate without logo (AI interprets brand) + python generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt + + # Generate with Pro model (higher quality, 4K text) + python generate.py --brand "TechFlow" --logo logo.png --deliverable "business card" --model pro + + # Specify output directory and aspect ratio + python generate.py --brand "MyBrand" --logo logo.png --deliverable "vehicle" --output ./mockups --ratio 16:9 + +Models: + flash (default): gemini-2.5-flash-image - Fast, cost-effective + pro: gemini-3-pro-image-preview - Quality, 4K text rendering + +Image Editing Mode: + When --logo is provided, uses Gemini's text-and-image-to-image capability + to incorporate your ACTUAL logo into the CIP mockups. + """ + ) + + parser.add_argument("--brand", "-b", required=True, help="Brand name") + parser.add_argument("--logo", "-l", help="Path to brand logo image (enables image editing mode)") + parser.add_argument("--deliverable", "-d", help="Single deliverable to generate") + parser.add_argument("--deliverables", help="Comma-separated list of deliverables") + parser.add_argument("--industry", "-i", default="technology", help="Industry type") + parser.add_argument("--style", "-s", help="Design style") + parser.add_argument("--mockup", "-m", help="Mockup context") + parser.add_argument("--set", action="store_true", help="Generate full CIP set") + parser.add_argument("--output", "-o", help="Output directory") + parser.add_argument("--model", default="flash", choices=["flash", "pro"], help="Model: flash (fast) or pro (quality)") + parser.add_argument("--ratio", default="1:1", help="Aspect ratio (1:1, 16:9, 4:3, etc.)") + parser.add_argument("--prompt-only", action="store_true", help="Only show prompt, don't generate") + parser.add_argument("--json", "-j", action="store_true", help="Output as JSON") + parser.add_argument("--no-logo-prompt", action="store_true", help="Skip logo prompt, proceed without logo") + + args = parser.parse_args() + + # Check if logo is provided, prompt user if not + logo_image = None + if args.logo: + logo_image = load_logo_image(args.logo) + if not logo_image: + print("Error: Could not load logo image") + sys.exit(1) + elif not args.prompt_only: + # No logo provided - ask user what to do + action = check_logo_required(args.brand, skip_prompt=args.no_logo_prompt) + if action == 'generate': + print("\n💡 To generate a logo, use the logo-design skill:") + print(f" python ~/.claude/skills/design/scripts/logo/generate.py --brand \"{args.brand}\" --industry \"{args.industry}\"") + print("\n Then re-run this command with --logo ") + sys.exit(0) + elif action == 'exit': + print("\n Provide logo with: --logo /path/to/your/logo.png") + sys.exit(0) + # else: continue without logo + + use_logo = logo_image is not None + + if args.set or args.deliverables: + # Generate multiple deliverables + deliverables = args.deliverables.split(",") if args.deliverables else None + + if args.prompt_only: + results = [] + deliverables = deliverables or ["business card", "letterhead", "office signage", "vehicle", "polo shirt"] + for d in deliverables: + prompt_data = build_cip_prompt(d, args.brand, args.style, args.industry, args.mockup, use_logo_image=use_logo) + results.append(prompt_data) + if args.json: + print(json.dumps(results, indent=2)) + else: + for r in results: + print(f"\n{r['deliverable']}:\n{r['prompt']}\n") + else: + results = generate_cip_set( + args.brand, args.industry, args.style, deliverables, args.output, + model_key=args.model, logo_path=args.logo, aspect_ratio=args.ratio + ) + if args.json: + print(json.dumps(results, indent=2)) + else: + print(f"\n✅ Generated {len(results)} CIP mockups") + else: + # Generate single deliverable + deliverable = args.deliverable or "business card" + prompt_data = build_cip_prompt(deliverable, args.brand, args.style, args.industry, args.mockup, use_logo_image=use_logo) + + if args.prompt_only: + if args.json: + print(json.dumps(prompt_data, indent=2)) + else: + print(f"\nPrompt:\n{prompt_data['prompt']}") + else: + filepath = generate_with_nano_banana( + prompt_data, args.output, model_key=args.model, + aspect_ratio=args.ratio, logo_image=logo_image + ) + if args.json: + print(json.dumps({"filepath": filepath, **prompt_data}, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/design/scripts/cip/render-html.py b/.cursor/skills/design/scripts/cip/render-html.py new file mode 100644 index 0000000..34e3782 --- /dev/null +++ b/.cursor/skills/design/scripts/cip/render-html.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CIP HTML Presentation Renderer + +Generates a professional HTML presentation from CIP mockup images +with detailed descriptions, concepts, and brand guidelines. +""" + +import argparse +import json +import os +import sys +import base64 +from pathlib import Path +from datetime import datetime + +# Add parent directory for imports +sys.path.insert(0, str(Path(__file__).parent)) +from core import search, get_cip_brief + +# Deliverable descriptions for presentation +DELIVERABLE_INFO = { + "business card": { + "title": "Business Card", + "concept": "First impression touchpoint for professional networking", + "purpose": "Creates memorable brand recall during business exchanges", + "specs": "Standard 3.5 x 2 inches, premium paper stock" + }, + "letterhead": { + "title": "Letterhead", + "concept": "Official correspondence identity", + "purpose": "Establishes credibility and professionalism in written communications", + "specs": "A4/Letter size, digital and print versions" + }, + "document template": { + "title": "Document Template", + "concept": "Branded document system for internal and external use", + "purpose": "Ensures consistent brand representation across all documents", + "specs": "Multiple formats: Word, PDF, Google Docs compatible" + }, + "reception signage": { + "title": "Reception Signage", + "concept": "Brand presence in physical office environment", + "purpose": "Creates strong first impression for visitors and reinforces brand identity", + "specs": "3D dimensional letters, backlit LED options, premium materials" + }, + "office signage": { + "title": "Office Signage", + "concept": "Wayfinding and brand presence system", + "purpose": "Guides visitors while maintaining consistent brand experience", + "specs": "Modular system with directional and informational signs" + }, + "polo shirt": { + "title": "Polo Shirt", + "concept": "Professional team apparel", + "purpose": "Creates unified team identity and brand ambassadorship", + "specs": "Premium pique cotton, embroidered logo on left chest" + }, + "t-shirt": { + "title": "T-Shirt", + "concept": "Casual brand apparel", + "purpose": "Extends brand reach through everyday wear and promotional events", + "specs": "High-quality cotton, screen print or embroidery options" + }, + "vehicle": { + "title": "Vehicle Branding", + "concept": "Mobile brand advertising", + "purpose": "Transforms fleet into moving billboards for maximum visibility", + "specs": "Partial or full wrap, vinyl graphics, weather-resistant" + }, + "van": { + "title": "Van Branding", + "concept": "Commercial vehicle identity", + "purpose": "Professional fleet presence for service and delivery operations", + "specs": "Full wrap design, high-visibility contact information" + }, + "car": { + "title": "Car Branding", + "concept": "Executive vehicle identity", + "purpose": "Professional presence for corporate and sales teams", + "specs": "Subtle branding, door panels and rear window" + }, + "envelope": { + "title": "Envelope", + "concept": "Branded mail correspondence", + "purpose": "Extends brand identity to all outgoing mail", + "specs": "DL, C4, C5 sizes with logo placement" + }, + "folder": { + "title": "Presentation Folder", + "concept": "Document organization with brand identity", + "purpose": "Professional presentation of proposals and materials", + "specs": "A4/Letter pocket folder with die-cut design" + } +} + + +def get_image_base64(image_path): + """Convert image to base64 for embedding in HTML""" + try: + with open(image_path, "rb") as f: + return base64.b64encode(f.read()).decode('utf-8') + except Exception as e: + print(f"Warning: Could not load image {image_path}: {e}") + return None + + +def get_deliverable_info(filename): + """Extract deliverable type from filename and get info""" + filename_lower = filename.lower() + for key, info in DELIVERABLE_INFO.items(): + if key.replace(" ", "-") in filename_lower or key.replace(" ", "_") in filename_lower: + return info + # Default info + return { + "title": filename.replace("-", " ").replace("_", " ").title(), + "concept": "Brand identity application", + "purpose": "Extends brand presence across touchpoints", + "specs": "Custom specifications" + } + + +def generate_html(brand_name, industry, images_dir, output_path=None, style=None): + """Generate HTML presentation from CIP images""" + + images_dir = Path(images_dir) + if not images_dir.exists(): + print(f"Error: Directory not found: {images_dir}") + return None + + # Get all PNG images + images = sorted(images_dir.glob("*.png")) + if not images: + print(f"Error: No PNG images found in {images_dir}") + return None + + # Get CIP brief for brand info + brief = get_cip_brief(brand_name, industry, style) + style_info = brief.get("style", {}) + industry_info = brief.get("industry", {}) + + # Build HTML + html_parts = [f''' + + + + + {brand_name} - Corporate Identity Program + + + +
+

{brand_name}

+

Corporate Identity Program

+
+
+
Industry
+
{industry_info.get("Industry", industry.title())}
+
+
+
Style
+
{style_info.get("Style Name", "Corporate")}
+
+
+
Mood
+
{style_info.get("Mood", "Professional")}
+
+
+
Deliverables
+
{len(images)} Items
+
+
+
+ +
+

Brand Applications

+

+ Comprehensive identity system designed to maintain consistency + across all brand touchpoints and communications. +

+'''] + + # Add each deliverable + for i, image_path in enumerate(images): + info = get_deliverable_info(image_path.stem) + img_base64 = get_image_base64(image_path) + + if img_base64: + img_src = f"data:image/png;base64,{img_base64}" + else: + img_src = str(image_path) + + html_parts.append(f''' +
+
+ {info['title']} +
+
+

{info['title']}

+

{info['concept']}

+

{info['purpose']}

+ {info['specs']} +
+
+''') + + # Close HTML + html_parts.append(f''' +
+ +
+

{brand_name} Corporate Identity Program

+

Generated on {datetime.now().strftime("%B %d, %Y")}

+

Powered by CIP Design Skill

+
+ + +''') + + html_content = "".join(html_parts) + + # Save HTML + output_path = output_path or images_dir / f"{brand_name.lower().replace(' ', '-')}-cip-presentation.html" + output_path = Path(output_path) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(html_content) + + print(f"✅ HTML presentation generated: {output_path}") + return str(output_path) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate HTML presentation from CIP mockups", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Generate HTML from CIP images directory + python render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip + + # Specify output path + python render-html.py --brand "TopGroup" --industry "consulting" --images ./cip --output presentation.html + """ + ) + + parser.add_argument("--brand", "-b", required=True, help="Brand name") + parser.add_argument("--industry", "-i", default="technology", help="Industry type") + parser.add_argument("--style", "-s", help="Design style") + parser.add_argument("--images", required=True, help="Directory containing CIP mockup images") + parser.add_argument("--output", "-o", help="Output HTML file path") + + args = parser.parse_args() + + generate_html( + brand_name=args.brand, + industry=args.industry, + images_dir=args.images, + output_path=args.output, + style=args.style + ) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/design/scripts/cip/search.py b/.cursor/skills/design/scripts/cip/search.py new file mode 100644 index 0000000..894f484 --- /dev/null +++ b/.cursor/skills/design/scripts/cip/search.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CIP Design Search CLI - Search corporate identity design guidelines +""" + +import argparse +import json +import sys +from pathlib import Path + +# Add parent directory for imports +sys.path.insert(0, str(Path(__file__).parent)) +from core import search, search_all, get_cip_brief, CSV_CONFIG + + +def format_results(results, domain): + """Format search results for display""" + if not results: + return "No results found." + + output = [] + for i, item in enumerate(results, 1): + output.append(f"\n{'='*60}") + output.append(f"Result {i}:") + for key, value in item.items(): + if value: + output.append(f" {key}: {value}") + return "\n".join(output) + + +def format_brief(brief): + """Format CIP brief for display""" + output = [] + output.append(f"\n{'='*60}") + output.append(f"CIP DESIGN BRIEF: {brief['brand_name']}") + output.append(f"{'='*60}") + + if brief.get("industry"): + output.append(f"\n📊 INDUSTRY: {brief['industry'].get('Industry', 'N/A')}") + output.append(f" Style: {brief['industry'].get('CIP Style', 'N/A')}") + output.append(f" Mood: {brief['industry'].get('Mood', 'N/A')}") + + if brief.get("style"): + output.append(f"\n🎨 DESIGN STYLE: {brief['style'].get('Style Name', 'N/A')}") + output.append(f" Description: {brief['style'].get('Description', 'N/A')}") + output.append(f" Materials: {brief['style'].get('Materials', 'N/A')}") + output.append(f" Finishes: {brief['style'].get('Finishes', 'N/A')}") + + if brief.get("color_system"): + output.append(f"\n🎯 COLOR SYSTEM:") + output.append(f" Primary: {brief['color_system'].get('primary', 'N/A')}") + output.append(f" Secondary: {brief['color_system'].get('secondary', 'N/A')}") + + output.append(f"\n✏️ TYPOGRAPHY: {brief.get('typography', 'N/A')}") + + if brief.get("recommended_deliverables"): + output.append(f"\n📦 RECOMMENDED DELIVERABLES:") + for d in brief["recommended_deliverables"]: + output.append(f" • {d.get('Deliverable', 'N/A')}: {d.get('Description', '')[:60]}...") + + return "\n".join(output) + + +def main(): + parser = argparse.ArgumentParser( + description="Search CIP design guidelines", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Search deliverables + python search.py "business card" + + # Search specific domain + python search.py "luxury elegant" --domain style + + # Generate CIP brief + python search.py "tech startup" --cip-brief -b "TechFlow" + + # Search all domains + python search.py "corporate professional" --all + + # JSON output + python search.py "vehicle branding" --json + """ + ) + + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), + help="Search domain (auto-detected if not specified)") + parser.add_argument("--max", "-m", type=int, default=3, help="Max results (default: 3)") + parser.add_argument("--all", "-a", action="store_true", help="Search all domains") + parser.add_argument("--cip-brief", "-c", action="store_true", help="Generate CIP brief") + parser.add_argument("--brand", "-b", default="BrandName", help="Brand name for CIP brief") + parser.add_argument("--style", "-s", help="Style override for CIP brief") + parser.add_argument("--json", "-j", action="store_true", help="Output as JSON") + + args = parser.parse_args() + + if args.cip_brief: + brief = get_cip_brief(args.brand, args.query, args.style) + if args.json: + print(json.dumps(brief, indent=2)) + else: + print(format_brief(brief)) + elif args.all: + results = search_all(args.query, args.max) + if args.json: + print(json.dumps(results, indent=2)) + else: + for domain, items in results.items(): + print(f"\n{'#'*60}") + print(f"# {domain.upper()}") + print(format_results(items, domain)) + else: + result = search(args.query, args.domain, args.max) + if args.json: + print(json.dumps(result, indent=2)) + else: + print(f"\nDomain: {result['domain']}") + print(f"Query: {result['query']}") + print(f"Results: {result['count']}") + print(format_results(result.get("results", []), result["domain"])) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/design/scripts/icon/generate.py b/.cursor/skills/design/scripts/icon/generate.py new file mode 100644 index 0000000..5e00fa9 --- /dev/null +++ b/.cursor/skills/design/scripts/icon/generate.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Icon Generation Script using Gemini 3.1 Pro Preview API +Generates SVG icons via text generation (SVG is XML text format) + +Model: gemini-3.1-pro-preview - best thinking, token efficiency, factual consistency + +Usage: + python generate.py --prompt "settings gear icon" --style outlined + python generate.py --prompt "shopping cart" --style filled --color "#6366F1" + python generate.py --name "dashboard" --category navigation --style duotone + python generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons + python generate.py --prompt "user profile" --sizes "16,24,32,48" +""" + +import argparse +import json +import os +import re +import sys +import time +from pathlib import Path +from datetime import datetime + + +def load_env(): + """Load .env files in priority order""" + env_paths = [ + Path(__file__).parent.parent.parent / ".env", + Path.home() / ".claude" / "skills" / ".env", + Path.home() / ".claude" / ".env" + ] + for env_path in env_paths: + if env_path.exists(): + with open(env_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + if key not in os.environ: + os.environ[key] = value.strip('"\'') + +load_env() + +try: + from google import genai + from google.genai import types +except ImportError: + print("Error: google-genai package not installed.") + print("Install with: pip install google-genai") + sys.exit(1) + + +# ============ CONFIGURATION ============ +GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") +MODEL = "gemini-3.1-pro-preview" + +# Icon styles with SVG-specific instructions +ICON_STYLES = { + "outlined": "outlined stroke icons, 2px stroke width, no fill, clean open paths", + "filled": "solid filled icons, no stroke, flat color fills, bold shapes", + "duotone": "duotone style with primary color at full opacity and secondary color at 30% opacity, layered shapes", + "thin": "thin line icons, 1px or 1.5px stroke width, delicate minimalist lines", + "bold": "bold thick line icons, 3px stroke width, heavy weight, impactful", + "rounded": "rounded icons with round line caps and joins, soft corners, friendly feel", + "sharp": "sharp angular icons, square line caps and mitered joins, precise edges", + "flat": "flat design icons, solid fills, no gradients or shadows, geometric simplicity", + "gradient": "linear or radial gradient fills, modern vibrant color transitions", + "glassmorphism": "glassmorphism style with semi-transparent fills, blur backdrop effect simulation, frosted glass", + "pixel": "pixel art style icons on a grid, retro 8-bit aesthetic, crisp edges", + "hand-drawn": "hand-drawn sketch style, slightly irregular strokes, organic feel, imperfect lines", + "isometric": "isometric 3D projection, 30-degree angles, dimensional depth", + "glyph": "simple glyph style, single solid shape, minimal detail, pictogram", + "animated-ready": "animated-ready SVG with named groups and IDs for CSS/JS animation targets", +} + +ICON_CATEGORIES = { + "navigation": "arrows, menus, hamburger, chevrons, home, back, forward, breadcrumb", + "action": "edit, delete, save, download, upload, share, copy, paste, print, search", + "communication": "email, chat, phone, video call, notification, bell, message bubble", + "media": "play, pause, stop, skip, volume, microphone, camera, image, gallery", + "file": "document, folder, archive, attachment, cloud, database, storage", + "user": "person, group, avatar, profile, settings, lock, key, shield", + "commerce": "cart, bag, wallet, credit card, receipt, tag, gift, store", + "data": "chart, graph, analytics, dashboard, table, filter, sort, calendar", + "development": "code, terminal, bug, git, API, server, database, deploy", + "social": "heart, star, thumbs up, bookmark, flag, trophy, badge, crown", + "weather": "sun, moon, cloud, rain, snow, wind, thunder, temperature", + "map": "pin, location, compass, globe, route, directions, map marker", +} + +# SVG generation prompt template +SVG_PROMPT_TEMPLATE = """Generate a clean, production-ready SVG icon. + +Requirements: +- Output ONLY valid SVG code, nothing else +- ViewBox: "0 0 {viewbox} {viewbox}" +- Use currentColor for strokes/fills (inherits CSS color) +- No embedded fonts or text elements unless specifically requested +- No raster images or external references +- Optimized paths with minimal nodes +- Accessible: include element with icon description +{style_instructions} +{color_instructions} +{size_instructions} + +Icon to generate: {prompt} + +Output the SVG code only, wrapped in ```svg``` code block.""" + +SVG_BATCH_PROMPT_TEMPLATE = """Generate {count} distinct SVG icon variations for: {prompt} + +Requirements for EACH icon: +- Output ONLY valid SVG code +- ViewBox: "0 0 {viewbox} {viewbox}" +- Use currentColor for strokes/fills (inherits CSS color) +- No embedded fonts, raster images, or external references +- Optimized paths with minimal nodes +- Include <title> element with icon description +{style_instructions} +{color_instructions} + +Generate {count} different visual interpretations. Output each SVG in a separate ```svg``` code block. +Label each variation (e.g., "Variation 1: [brief description]").""" + + +def extract_svgs(text): + """Extract SVG code blocks from model response""" + svgs = [] + + # Try ```svg code blocks first + pattern = r'```svg\s*\n(.*?)```' + matches = re.findall(pattern, text, re.DOTALL) + if matches: + svgs.extend(matches) + + # Fallback: try ```xml code blocks + if not svgs: + pattern = r'```xml\s*\n(.*?)```' + matches = re.findall(pattern, text, re.DOTALL) + svgs.extend(matches) + + # Fallback: try bare <svg> tags + if not svgs: + pattern = r'(<svg[^>]*>.*?</svg>)' + matches = re.findall(pattern, text, re.DOTALL) + svgs.extend(matches) + + # Clean up extracted SVGs + cleaned = [] + for svg in svgs: + svg = svg.strip() + if not svg.startswith('<svg'): + # Try to find <svg> within the extracted text + match = re.search(r'(<svg[^>]*>.*?</svg>)', svg, re.DOTALL) + if match: + svg = match.group(1) + else: + continue + cleaned.append(svg) + + return cleaned + + +def apply_color(svg_code, color): + """Replace currentColor with specific color if provided""" + if color: + # Replace currentColor with the specified color + svg_code = svg_code.replace('currentColor', color) + # If no currentColor was present, add fill/stroke color + if color not in svg_code: + svg_code = svg_code.replace('<svg', f'<svg color="{color}"', 1) + return svg_code + + +def apply_viewbox_size(svg_code, size): + """Adjust SVG viewBox to target size""" + if size: + # Update width/height attributes if present + svg_code = re.sub(r'width="[^"]*"', f'width="{size}"', svg_code) + svg_code = re.sub(r'height="[^"]*"', f'height="{size}"', svg_code) + # Add width/height if not present + if 'width=' not in svg_code: + svg_code = svg_code.replace('<svg', f'<svg width="{size}" height="{size}"', 1) + return svg_code + + +def generate_icon(prompt, style=None, category=None, name=None, + color=None, size=24, output_path=None, viewbox=24): + """Generate a single SVG icon using Gemini 3.1 Pro Preview""" + + if not GEMINI_API_KEY: + print("Error: GEMINI_API_KEY not set") + print("Set it with: export GEMINI_API_KEY='your-key'") + return None + + client = genai.Client(api_key=GEMINI_API_KEY) + + # Build style instructions + style_instructions = "" + if style and style in ICON_STYLES: + style_instructions = f"- Style: {ICON_STYLES[style]}" + + # Build color instructions + color_instructions = "- Use currentColor for all strokes and fills" + if color: + color_instructions = f"- Use color: {color} for primary elements, currentColor for secondary" + + # Build size instructions + size_instructions = f"- Design for {size}px display size, optimize detail level accordingly" + + # Build final prompt + icon_prompt = prompt + if category and category in ICON_CATEGORIES: + icon_prompt = f"{prompt} (category: {ICON_CATEGORIES[category]})" + if name: + icon_prompt = f"'{name}' icon: {icon_prompt}" + + full_prompt = SVG_PROMPT_TEMPLATE.format( + prompt=icon_prompt, + viewbox=viewbox, + style_instructions=style_instructions, + color_instructions=color_instructions, + size_instructions=size_instructions + ) + + print(f"Generating icon with {MODEL}...") + print(f"Prompt: {prompt}") + if style: + print(f"Style: {style}") + print() + + try: + response = client.models.generate_content( + model=MODEL, + contents=full_prompt, + config=types.GenerateContentConfig( + temperature=0.7, + max_output_tokens=4096, + ) + ) + + # Extract SVG from response + response_text = response.text if hasattr(response, 'text') else "" + if not response_text: + for part in response.candidates[0].content.parts: + if hasattr(part, 'text') and part.text: + response_text += part.text + + svgs = extract_svgs(response_text) + + if not svgs: + print("No valid SVG generated. Model response:") + print(response_text[:500]) + return None + + svg_code = svgs[0] + + # Apply color if specified + svg_code = apply_color(svg_code, color) + + # Apply size + svg_code = apply_viewbox_size(svg_code, size) + + # Determine output path + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + slug = name or prompt.split()[0] if prompt else "icon" + slug = re.sub(r'[^a-zA-Z0-9_-]', '_', slug.lower()) + style_suffix = f"_{style}" if style else "" + output_path = f"{slug}{style_suffix}_{timestamp}.svg" + + # Save SVG + with open(output_path, "w", encoding="utf-8") as f: + f.write(svg_code) + + print(f"Icon saved to: {output_path}") + return output_path + + except Exception as e: + print(f"Error generating icon: {e}") + return None + + +def generate_batch(prompt, count, output_dir, style=None, color=None, + viewbox=24, name=None): + """Generate multiple icon variations""" + + if not GEMINI_API_KEY: + print("Error: GEMINI_API_KEY not set") + return [] + + client = genai.Client(api_key=GEMINI_API_KEY) + os.makedirs(output_dir, exist_ok=True) + + # Build instructions + style_instructions = "" + if style and style in ICON_STYLES: + style_instructions = f"- Style: {ICON_STYLES[style]}" + + color_instructions = "- Use currentColor for all strokes and fills" + if color: + color_instructions = f"- Use color: {color} for primary elements" + + full_prompt = SVG_BATCH_PROMPT_TEMPLATE.format( + prompt=prompt, + count=count, + viewbox=viewbox, + style_instructions=style_instructions, + color_instructions=color_instructions + ) + + print(f"\n{'='*60}") + print(f" BATCH ICON GENERATION") + print(f" Model: {MODEL}") + print(f" Prompt: {prompt}") + print(f" Variants: {count}") + print(f" Output: {output_dir}") + print(f"{'='*60}\n") + + try: + response = client.models.generate_content( + model=MODEL, + contents=full_prompt, + config=types.GenerateContentConfig( + temperature=0.9, + max_output_tokens=16384, + ) + ) + + response_text = response.text if hasattr(response, 'text') else "" + if not response_text: + for part in response.candidates[0].content.parts: + if hasattr(part, 'text') and part.text: + response_text += part.text + + svgs = extract_svgs(response_text) + + if not svgs: + print("No valid SVGs generated.") + print(response_text[:500]) + return [] + + results = [] + slug = name or re.sub(r'[^a-zA-Z0-9_-]', '_', prompt.split()[0].lower()) + style_suffix = f"_{style}" if style else "" + + for i, svg_code in enumerate(svgs[:count]): + svg_code = apply_color(svg_code, color) + filename = f"{slug}{style_suffix}_{i+1:02d}.svg" + filepath = os.path.join(output_dir, filename) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(svg_code) + + results.append(filepath) + print(f" [{i+1}/{len(svgs[:count])}] Saved: {filename}") + + print(f"\n{'='*60}") + print(f" BATCH COMPLETE: {len(results)}/{count} icons generated") + print(f"{'='*60}\n") + + return results + + except Exception as e: + print(f"Error generating icons: {e}") + return [] + + +def generate_sizes(prompt, sizes, style=None, color=None, output_dir=None, name=None): + """Generate same icon at multiple sizes""" + if output_dir is None: + output_dir = "." + os.makedirs(output_dir, exist_ok=True) + + results = [] + slug = name or re.sub(r'[^a-zA-Z0-9_-]', '_', prompt.split()[0].lower()) + style_suffix = f"_{style}" if style else "" + + for size in sizes: + print(f"Generating {size}px variant...") + filename = f"{slug}{style_suffix}_{size}px.svg" + filepath = os.path.join(output_dir, filename) + + result = generate_icon( + prompt=prompt, + style=style, + color=color, + size=size, + output_path=filepath, + viewbox=size + ) + + if result: + results.append(result) + + time.sleep(1) + + return results + + +def main(): + parser = argparse.ArgumentParser( + description="Generate SVG icons using Gemini 3.1 Pro Preview" + ) + parser.add_argument("--prompt", "-p", type=str, help="Icon description") + parser.add_argument("--name", "-n", type=str, help="Icon name (for filename)") + parser.add_argument("--style", "-s", choices=list(ICON_STYLES.keys()), + help="Icon style") + parser.add_argument("--category", "-c", choices=list(ICON_CATEGORIES.keys()), + help="Icon category for context") + parser.add_argument("--color", type=str, + help="Primary color (hex, e.g. #6366F1). Default: currentColor") + parser.add_argument("--size", type=int, default=24, + help="Icon size in px (default: 24)") + parser.add_argument("--viewbox", type=int, default=24, + help="SVG viewBox size (default: 24)") + parser.add_argument("--output", "-o", type=str, help="Output file path") + parser.add_argument("--output-dir", type=str, help="Output directory for batch") + parser.add_argument("--batch", type=int, + help="Number of icon variants to generate") + parser.add_argument("--sizes", type=str, + help="Comma-separated sizes (e.g. '16,24,32,48')") + parser.add_argument("--list-styles", action="store_true", + help="List available icon styles") + parser.add_argument("--list-categories", action="store_true", + help="List available icon categories") + + args = parser.parse_args() + + if args.list_styles: + print("Available icon styles:") + for style, desc in ICON_STYLES.items(): + print(f" {style}: {desc[:70]}...") + return + + if args.list_categories: + print("Available icon categories:") + for cat, desc in ICON_CATEGORIES.items(): + print(f" {cat}: {desc}") + return + + if not args.prompt and not args.name: + parser.error("Either --prompt or --name is required") + + prompt = args.prompt or args.name + + # Multi-size mode + if args.sizes: + sizes = [int(s.strip()) for s in args.sizes.split(",")] + generate_sizes( + prompt=prompt, + sizes=sizes, + style=args.style, + color=args.color, + output_dir=args.output_dir or "./icons", + name=args.name + ) + # Batch mode + elif args.batch: + output_dir = args.output_dir or "./icons" + generate_batch( + prompt=prompt, + count=args.batch, + output_dir=output_dir, + style=args.style, + color=args.color, + viewbox=args.viewbox, + name=args.name + ) + # Single icon + else: + generate_icon( + prompt=prompt, + style=args.style, + category=args.category, + name=args.name, + color=args.color, + size=args.size, + output_path=args.output, + viewbox=args.viewbox + ) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/design/scripts/logo/core.py b/.cursor/skills/design/scripts/logo/core.py new file mode 100644 index 0000000..b3de30c --- /dev/null +++ b/.cursor/skills/design/scripts/logo/core.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Logo Design Core - BM25 search engine for logo design guidelines +""" + +import csv +import re +from pathlib import Path +from math import log +from collections import defaultdict + +# ============ CONFIGURATION ============ +DATA_DIR = Path(__file__).parent.parent.parent / "data" / "logo" +MAX_RESULTS = 3 + +CSV_CONFIG = { + "style": { + "file": "styles.csv", + "search_cols": ["Style Name", "Category", "Keywords", "Best For"], + "output_cols": ["Style Name", "Category", "Keywords", "Primary Colors", "Secondary Colors", "Typography", "Effects", "Best For", "Avoid For", "Complexity", "Era"] + }, + "color": { + "file": "colors.csv", + "search_cols": ["Palette Name", "Category", "Keywords", "Psychology", "Best For"], + "output_cols": ["Palette Name", "Category", "Keywords", "Primary Hex", "Secondary Hex", "Accent Hex", "Background Hex", "Text Hex", "Psychology", "Best For", "Avoid For"] + }, + "industry": { + "file": "industries.csv", + "search_cols": ["Industry", "Keywords", "Recommended Styles", "Mood"], + "output_cols": ["Industry", "Keywords", "Recommended Styles", "Primary Colors", "Typography", "Common Symbols", "Mood", "Best Practices", "Avoid"] + } +} + + +# ============ 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 = { + "style": ["style", "minimalist", "vintage", "modern", "retro", "geometric", "abstract", "emblem", "badge", "wordmark", "mascot", "luxury", "playful", "corporate"], + "color": ["color", "palette", "hex", "#", "rgb", "blue", "red", "green", "gold", "warm", "cool", "vibrant", "pastel"], + "industry": ["tech", "healthcare", "finance", "legal", "restaurant", "food", "fashion", "beauty", "education", "sports", "fitness", "real estate", "crypto", "gaming"] + } + + 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 "style" + + +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["style"]) + 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 and combine results""" + all_results = {} + for domain in CSV_CONFIG.keys(): + result = search(query, domain, max_results) + if result.get("results"): + all_results[domain] = result["results"] + return all_results diff --git a/.cursor/skills/design/scripts/logo/generate.py b/.cursor/skills/design/scripts/logo/generate.py new file mode 100644 index 0000000..e33f8db --- /dev/null +++ b/.cursor/skills/design/scripts/logo/generate.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Logo Generation Script using Gemini Nano Banana API +Uses Gemini 2.5 Flash Image and Gemini 3 Pro Image Preview models + +Models: +- Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency +- Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning + +Usage: + python generate.py --prompt "tech startup logo minimalist blue" + python generate.py --prompt "coffee shop vintage badge" --style vintage --output logo.png + python generate.py --brand "TechFlow" --industry tech --style minimalist + python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model + +Batch mode (generates multiple variants): + python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro +""" + +import argparse +import os +import sys +import time +from pathlib import Path +from datetime import datetime + +# Load environment variables +def load_env(): + """Load .env files in priority order""" + env_paths = [ + Path(__file__).parent.parent.parent / ".env", + Path.home() / ".claude" / "skills" / ".env", + Path.home() / ".claude" / ".env" + ] + + for env_path in env_paths: + if env_path.exists(): + with open(env_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + if key not in os.environ: + os.environ[key] = value.strip('"\'') + +load_env() + +try: + from google import genai + from google.genai import types +except ImportError: + print("Error: google-genai package not installed.") + print("Install with: pip install google-genai") + sys.exit(1) + + +# ============ CONFIGURATION ============ +GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") + +# Gemini "Nano Banana" model configurations for image generation +GEMINI_FLASH = "gemini-2.5-flash-image" # Nano Banana: fast, high-volume, low-latency +GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quality, advanced reasoning + +# Supported aspect ratios +ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] +DEFAULT_ASPECT_RATIO = "1:1" # Square is ideal for logos + +# Logo-specific prompt templates +LOGO_PROMPT_TEMPLATE = """Generate a professional logo image: {prompt} + +Style requirements: +- Clean vector-style illustration suitable for a logo +- Simple, scalable design that works at any size +- Clear silhouette and recognizable shape +- Professional quality suitable for business use +- Centered composition on plain white or transparent background +- No text unless specifically requested +- High contrast and clear edges +- Square format, perfectly centered +- Output as a clean, high-quality logo image +""" + +STYLE_MODIFIERS = { + "minimalist": "minimalist, simple geometric shapes, clean lines, lots of white space, single color or limited palette", + "vintage": "vintage, retro, badge style, distressed texture, heritage feel, warm earth tones", + "modern": "modern, sleek, gradient colors, tech-forward, innovative feel", + "luxury": "luxury, elegant, gold accents, refined, premium feel, serif typography", + "playful": "playful, fun, colorful, friendly, approachable, rounded shapes", + "corporate": "corporate, professional, trustworthy, stable, conservative colors", + "organic": "organic, natural, flowing lines, earth tones, sustainable feel", + "geometric": "geometric, abstract, mathematical precision, symmetrical", + "hand-drawn": "hand-drawn, artisan, sketch-like, authentic, imperfect lines", + "3d": "3D, dimensional, depth, shadows, isometric perspective", + "abstract": "abstract mark, conceptual, symbolic, non-literal representation, artistic interpretation", + "lettermark": "lettermark, single letter or initials, typographic, monogram style, distinctive character", + "wordmark": "wordmark, logotype, custom typography, brand name as logo, distinctive lettering", + "emblem": "emblem, badge, crest style, enclosed design, traditional, authoritative feel", + "mascot": "mascot, character, friendly face, personified, memorable figure", + "gradient": "gradient, color transition, vibrant, modern digital feel, smooth color flow", + "lineart": "line art, single stroke, continuous line, elegant simplicity, wire-frame style", + "negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion" +} + +INDUSTRY_PROMPTS = { + "tech": "technology company, digital, innovative, modern, circuit-like elements", + "healthcare": "healthcare, medical, caring, trust, cross or heart symbol", + "finance": "financial services, stable, trustworthy, growth, upward elements", + "food": "food and beverage, appetizing, warm colors, welcoming", + "fashion": "fashion brand, elegant, stylish, refined, artistic", + "fitness": "fitness and sports, dynamic, energetic, powerful, movement", + "eco": "eco-friendly, sustainable, natural, green, leaf or earth elements", + "education": "education, knowledge, growth, learning, book or cap symbol", + "real-estate": "real estate, property, home, roof or building silhouette", + "creative": "creative agency, artistic, unique, expressive, colorful" +} + + +def enhance_prompt(base_prompt, style=None, industry=None, brand_name=None): + """Enhance the logo prompt with style and industry modifiers""" + prompt_parts = [base_prompt] + + if style and style in STYLE_MODIFIERS: + prompt_parts.append(STYLE_MODIFIERS[style]) + + if industry and industry in INDUSTRY_PROMPTS: + prompt_parts.append(INDUSTRY_PROMPTS[industry]) + + if brand_name: + prompt_parts.insert(0, f"Logo for '{brand_name}':") + + combined = ", ".join(prompt_parts) + return LOGO_PROMPT_TEMPLATE.format(prompt=combined) + + +def generate_logo(prompt, style=None, industry=None, brand_name=None, + output_path=None, use_pro=False, aspect_ratio=None): + """Generate a logo using Gemini models with image generation + + Args: + aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4" + Default is "1:1" (square) for logos. + """ + + if not GEMINI_API_KEY: + print("Error: GEMINI_API_KEY not set") + print("Set it with: export GEMINI_API_KEY='your-key'") + return None + + # Initialize client + client = genai.Client(api_key=GEMINI_API_KEY) + + # Enhance the prompt + full_prompt = enhance_prompt(prompt, style, industry, brand_name) + + # Select model + model = GEMINI_PRO if use_pro else GEMINI_FLASH + model_label = "Nano Banana Pro (gemini-3-pro-image-preview)" if use_pro else "Nano Banana (gemini-2.5-flash-image)" + + # Set aspect ratio (default to 1:1 for logos) + ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO + + print(f"Generating logo with {model_label}...") + print(f"Aspect ratio: {ratio}") + print(f"Prompt: {full_prompt[:150]}...") + print() + + try: + # Generate image using Gemini with image generation capability + response = client.models.generate_content( + model=model, + contents=full_prompt, + config=types.GenerateContentConfig( + response_modalities=["IMAGE", "TEXT"], + image_config=types.ImageConfig( + aspect_ratio=ratio + ), + safety_settings=[ + types.SafetySetting( + category="HARM_CATEGORY_HATE_SPEECH", + threshold="BLOCK_LOW_AND_ABOVE" + ), + types.SafetySetting( + category="HARM_CATEGORY_DANGEROUS_CONTENT", + threshold="BLOCK_LOW_AND_ABOVE" + ), + types.SafetySetting( + category="HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold="BLOCK_LOW_AND_ABOVE" + ), + types.SafetySetting( + category="HARM_CATEGORY_HARASSMENT", + threshold="BLOCK_LOW_AND_ABOVE" + ), + ] + ) + ) + + # Extract image from response + image_data = None + for part in response.candidates[0].content.parts: + if hasattr(part, 'inline_data') and part.inline_data: + if part.inline_data.mime_type.startswith('image/'): + image_data = part.inline_data.data + break + + if not image_data: + print("No image generated. The model may not have produced an image.") + print("Try a different prompt or check if the model supports image generation.") + return None + + # Determine output path + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo" + output_path = f"{brand_slug}_{timestamp}.png" + + # Save image + with open(output_path, "wb") as f: + f.write(image_data) + + print(f"Logo saved to: {output_path}") + return output_path + + except Exception as e: + print(f"Error generating logo: {e}") + return None + + +def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_context=None, aspect_ratio=None): + """Generate multiple logo variants with different styles""" + + # Select appropriate styles for batch generation + batch_styles = [ + ("minimalist", "Clean, simple geometric shape with minimal details"), + ("modern", "Sleek gradient with tech-forward aesthetic"), + ("geometric", "Abstract geometric patterns, mathematical precision"), + ("gradient", "Vibrant color transitions, modern digital feel"), + ("abstract", "Conceptual symbolic representation"), + ("lettermark", "Stylized letter 'U' as monogram"), + ("negative-space", "Clever use of negative space, hidden meaning"), + ("lineart", "Single stroke continuous line design"), + ("3d", "Dimensional design with depth and shadows"), + ] + + # Ensure output directory exists + os.makedirs(output_dir, exist_ok=True) + + results = [] + model_label = "Pro" if use_pro else "Flash" + ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO + + print(f"\n{'='*60}") + print(f" BATCH LOGO GENERATION: {brand_name}") + print(f" Model: Nano Banana {model_label}") + print(f" Aspect Ratio: {ratio}") + print(f" Variants: {count}") + print(f" Output: {output_dir}") + print(f"{'='*60}\n") + + for i in range(min(count, len(batch_styles))): + style_key, style_desc = batch_styles[i] + + # Build enhanced prompt with brand context + enhanced_prompt = f"{prompt}, {style_desc}" + if brand_context: + enhanced_prompt = f"{brand_context}, {enhanced_prompt}" + + # Generate filename + filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i+1:02d}.png" + output_path = os.path.join(output_dir, filename) + + print(f"[{i+1}/{count}] Generating {style_key} variant...") + + result = generate_logo( + prompt=enhanced_prompt, + style=style_key, + industry="tech", + brand_name=brand_name, + output_path=output_path, + use_pro=use_pro, + aspect_ratio=aspect_ratio + ) + + if result: + results.append(result) + print(f" ✓ Saved: {filename}\n") + else: + print(f" ✗ Failed: {style_key}\n") + + # Rate limiting between requests + if i < count - 1: + time.sleep(2) + + print(f"\n{'='*60}") + print(f" BATCH COMPLETE: {len(results)}/{count} logos generated") + print(f"{'='*60}\n") + + return results + + +def main(): + parser = argparse.ArgumentParser(description="Generate logos using Gemini Nano Banana models") + parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt") + parser.add_argument("--brand", "-b", type=str, help="Brand name") + parser.add_argument("--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style") + parser.add_argument("--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type") + parser.add_argument("--output", "-o", type=str, help="Output file path") + parser.add_argument("--output-dir", type=str, help="Output directory for batch generation") + parser.add_argument("--batch", type=int, help="Number of logo variants to generate (batch mode)") + parser.add_argument("--brand-context", type=str, help="Additional brand context for prompts") + parser.add_argument("--pro", action="store_true", help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality") + parser.add_argument("--aspect-ratio", "-r", choices=ASPECT_RATIOS, default=DEFAULT_ASPECT_RATIO, + help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)") + parser.add_argument("--list-styles", action="store_true", help="List available styles") + parser.add_argument("--list-industries", action="store_true", help="List available industries") + + args = parser.parse_args() + + if args.list_styles: + print("Available styles:") + for style, desc in STYLE_MODIFIERS.items(): + print(f" {style}: {desc[:60]}...") + return + + if args.list_industries: + print("Available industries:") + for industry, desc in INDUSTRY_PROMPTS.items(): + print(f" {industry}: {desc[:60]}...") + return + + if not args.prompt and not args.brand: + parser.error("Either --prompt or --brand is required") + + prompt = args.prompt or "professional logo" + + # Batch mode + if args.batch: + output_dir = args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos" + generate_batch( + prompt=prompt, + brand_name=args.brand or "Logo", + count=args.batch, + output_dir=output_dir, + use_pro=args.pro, + brand_context=args.brand_context, + aspect_ratio=args.aspect_ratio + ) + else: + generate_logo( + prompt=prompt, + style=args.style, + industry=args.industry, + brand_name=args.brand, + output_path=args.output, + use_pro=args.pro, + aspect_ratio=args.aspect_ratio + ) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/design/scripts/logo/search.py b/.cursor/skills/design/scripts/logo/search.py new file mode 100644 index 0000000..e843753 --- /dev/null +++ b/.cursor/skills/design/scripts/logo/search.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Logo Design Search - CLI for searching logo design guidelines +Usage: python search.py "<query>" [--domain <domain>] [--max-results 3] + python search.py "<query>" --design-brief [-p "Brand Name"] + +Domains: style, color, industry +""" + +import argparse +from core import CSV_CONFIG, MAX_RESULTS, search, search_all + + +def format_output(result): + """Format results for Claude consumption (token-optimized)""" + if "error" in result: + return f"Error: {result['error']}" + + output = [] + output.append(f"## Logo Design Search Results") + output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") + output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") + + for i, row in enumerate(result['results'], 1): + output.append(f"### Result {i}") + for key, value in row.items(): + value_str = str(value) + if len(value_str) > 300: + value_str = value_str[:300] + "..." + output.append(f"- **{key}:** {value_str}") + output.append("") + + return "\n".join(output) + + +def generate_design_brief(query, brand_name=None): + """Generate a comprehensive logo design brief based on query""" + results = search_all(query, max_results=2) + + output = [] + output.append("=" * 60) + if brand_name: + output.append(f" LOGO DESIGN BRIEF: {brand_name.upper()}") + else: + output.append(" LOGO DESIGN BRIEF") + output.append("=" * 60) + output.append(f" Query: {query}") + output.append("=" * 60) + output.append("") + + # Industry recommendations + if "industry" in results: + output.append("## INDUSTRY ANALYSIS") + for r in results["industry"]: + output.append(f"**Industry:** {r.get('Industry', 'N/A')}") + output.append(f"- Recommended Styles: {r.get('Recommended Styles', 'N/A')}") + output.append(f"- Colors: {r.get('Primary Colors', 'N/A')}") + output.append(f"- Typography: {r.get('Typography', 'N/A')}") + output.append(f"- Symbols: {r.get('Common Symbols', 'N/A')}") + output.append(f"- Mood: {r.get('Mood', 'N/A')}") + output.append(f"- Best Practices: {r.get('Best Practices', 'N/A')}") + output.append(f"- Avoid: {r.get('Avoid', 'N/A')}") + output.append("") + + # Style recommendations + if "style" in results: + output.append("## STYLE RECOMMENDATIONS") + for r in results["style"]: + output.append(f"**{r.get('Style Name', 'N/A')}** ({r.get('Category', 'N/A')})") + output.append(f"- Colors: {r.get('Primary Colors', 'N/A')} | {r.get('Secondary Colors', 'N/A')}") + output.append(f"- Typography: {r.get('Typography', 'N/A')}") + output.append(f"- Effects: {r.get('Effects', 'N/A')}") + output.append(f"- Best For: {r.get('Best For', 'N/A')}") + output.append(f"- Complexity: {r.get('Complexity', 'N/A')}") + output.append("") + + # Color recommendations + if "color" in results: + output.append("## COLOR PALETTE OPTIONS") + for r in results["color"]: + output.append(f"**{r.get('Palette Name', 'N/A')}**") + output.append(f"- Primary: {r.get('Primary Hex', 'N/A')}") + output.append(f"- Secondary: {r.get('Secondary Hex', 'N/A')}") + output.append(f"- Accent: {r.get('Accent Hex', 'N/A')}") + output.append(f"- Background: {r.get('Background Hex', 'N/A')}") + output.append(f"- Psychology: {r.get('Psychology', 'N/A')}") + output.append("") + + output.append("=" * 60) + return "\n".join(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Logo Design Search") + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain") + parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") + parser.add_argument("--json", action="store_true", help="Output as JSON") + parser.add_argument("--design-brief", "-db", action="store_true", help="Generate comprehensive design brief") + parser.add_argument("--brand-name", "-p", type=str, default=None, help="Brand name for design brief") + + args = parser.parse_args() + + if args.design_brief: + result = generate_design_brief(args.query, args.brand_name) + print(result) + else: + result = search(args.query, args.domain, args.max_results) + if args.json: + import json + print(json.dumps(result, indent=2, ensure_ascii=False)) + else: + print(format_output(result)) diff --git a/.cursor/skills/slides/SKILL.md b/.cursor/skills/slides/SKILL.md new file mode 100644 index 0000000..38750ff --- /dev/null +++ b/.cursor/skills/slides/SKILL.md @@ -0,0 +1,40 @@ +--- +name: slides +description: Create strategic HTML presentations with Chart.js, design tokens, responsive layouts, copywriting formulas, and contextual slide strategies. +argument-hint: "[topic] [slide-count]" +metadata: + author: claudekit + version: "1.0.0" +--- + +# Slides + +Strategic HTML presentation design with data visualization. + +## When to Use + +- Marketing presentations and pitch decks +- Data-driven slides with Chart.js +- Strategic slide design with layout patterns +- Copywriting-optimized presentation content + +## Subcommands + +| Subcommand | Description | Reference | +|------------|-------------|-----------| +| `create` | Create strategic presentation slides | `references/create.md` | + +## References (Knowledge Base) + +| Topic | File | +|-------|------| +| Layout Patterns | `references/layout-patterns.md` | +| HTML Template | `references/html-template.md` | +| Copywriting Formulas | `references/copywriting-formulas.md` | +| Slide Strategies | `references/slide-strategies.md` | + +## Routing + +1. Parse subcommand from `$ARGUMENTS` (first word) +2. Load corresponding `references/{subcommand}.md` +3. Execute with remaining arguments diff --git a/.cursor/skills/slides/references/copywriting-formulas.md b/.cursor/skills/slides/references/copywriting-formulas.md new file mode 100644 index 0000000..ecf2875 --- /dev/null +++ b/.cursor/skills/slides/references/copywriting-formulas.md @@ -0,0 +1,84 @@ +# Copywriting Formulas + +25 formulas for persuasive slide copy. + +## Core Formulas + +### PAS (Problem-Agitate-Solution) +**Use:** Problem slides, pain points +**Components:** Problem → Agitate → Solution +**Template:** "[Pain point]? Every [time frame], [consequence]. [Solution] fixes this." + +### AIDA (Attention-Interest-Desire-Action) +**Use:** CTAs, closing slides +**Components:** Attention → Interest → Desire → Action +**Template:** "[Bold statement]. [Benefit detail]. [Social proof]. [CTA]." + +### FAB (Features-Advantages-Benefits) +**Use:** Feature slides, product showcases +**Components:** Feature → Advantage → Benefit +**Template:** "[Feature] lets you [advantage], so you can [benefit]." + +### Cost of Inaction +**Use:** Agitation slides, urgency +**Components:** Status Quo → Loss → Time Decay +**Template:** "Without [solution], you're losing [amount] every [timeframe]." + +### Before-After-Bridge +**Use:** Transformation slides, case studies +**Components:** Before → After → Bridge +**Template:** "[Pain point before]. [Desired state after]. [Your solution] is the bridge." + +## Formula-to-Slide Mapping + +| Slide Type | Primary Formula | Emotion | +|------------|-----------------|---------| +| Title/Hook | AIDA, Hook | curiosity | +| Problem | PAS, Agitate | frustration | +| Cost/Risk | Cost of Inaction | fear | +| Solution | FAB, BAB | hope | +| Features | FAB | confidence | +| Traction | Proof Stack | trust | +| Social Proof | Testimonial | trust | +| Pricing | Value Stack | confidence | +| CTA | AIDA, Urgency | urgency | + +## Headline Patterns + +### Power Words +- "Stop [bad thing]" +- "Get [desired result] in [timeframe]" +- "The [adjective] way to [action]" +- "Why [audience] choose [product]" +- "[Number] ways to [achieve goal]" + +### Contrast Patterns +- "[Old way] is dead. Meet [new way]." +- "Don't [bad action]. Instead, [good action]." +- "From [pain point] to [benefit]." + +### Social Proof Patterns +- "[Number]+ [users/companies] trust [product]" +- "Join [notable company] and [notable company]" +- "As seen in [publication]" + +## Search Commands + +```bash +# Find formula for slide type +python .claude/skills/design-system/scripts/search-slides.py "problem agitation" -d copy + +# Get emotion-appropriate formula +python .claude/skills/design-system/scripts/search-slides.py "urgency cta" -d copy +``` + +## Quick Reference + +| Need | Use Formula | +|------|------------| +| Create urgency | Cost of Inaction, Scarcity | +| Build trust | Social Proof, Testimonial | +| Show value | FAB, Value Stack | +| Drive action | AIDA, CTA | +| Tell story | BAB, Story Arc | +| Present data | Proof Stack | diff --git a/.cursor/skills/slides/references/create.md b/.cursor/skills/slides/references/create.md new file mode 100644 index 0000000..55b79a1 --- /dev/null +++ b/.cursor/skills/slides/references/create.md @@ -0,0 +1,4 @@ +Invoke `slides` skill to create persuasive HTML slides using design tokens, Chart.js, and the slide knowledge database. + +## Task +<task>$ARGUMENTS</task> diff --git a/.cursor/skills/slides/references/html-template.md b/.cursor/skills/slides/references/html-template.md new file mode 100644 index 0000000..8b5a178 --- /dev/null +++ b/.cursor/skills/slides/references/html-template.md @@ -0,0 +1,295 @@ +# HTML Slide Template + +Complete HTML structure with navigation, tokens, and Chart.js integration. + +## Base Structure + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Presentation Title + + + + + +
+ + +
+ + +
+
+

Title Slide

+

Subtitle or tagline

+
+
+ + + +
+ + + + + + + +``` + +## Chart.js Integration + +```html +
+ +
+ + +``` + +## Animation Classes + +```css +/* Fade Up */ +.animate-fade-up { + animation: fadeUp 0.6s ease-out forwards; + opacity: 0; +} +@keyframes fadeUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Count Animation */ +.animate-count { animation: countUp 1s ease-out forwards; } + +/* Scale */ +.animate-scale { + animation: scaleIn 0.5s ease-out forwards; +} +@keyframes scaleIn { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +/* Stagger Children */ +.animate-stagger > * { + opacity: 0; + animation: fadeUp 0.5s ease-out forwards; +} +.animate-stagger > *:nth-child(1) { animation-delay: 0.1s; } +.animate-stagger > *:nth-child(2) { animation-delay: 0.2s; } +.animate-stagger > *:nth-child(3) { animation-delay: 0.3s; } +.animate-stagger > *:nth-child(4) { animation-delay: 0.4s; } +``` + +## Background Images + +```html +
+
+
+ +
+
+``` + +## CSS Variables Reference + +| Variable | Usage | +|----------|-------| +| `--color-primary` | Brand primary (CTA, highlights) | +| `--color-background` | Slide background | +| `--color-secondary` | Secondary elements | +| `--primitive-gradient-primary` | Title gradients | +| `--typography-font-heading` | Headlines | +| `--typography-font-body` | Body text | diff --git a/.cursor/skills/slides/references/layout-patterns.md b/.cursor/skills/slides/references/layout-patterns.md new file mode 100644 index 0000000..e2b3849 --- /dev/null +++ b/.cursor/skills/slides/references/layout-patterns.md @@ -0,0 +1,137 @@ +# Layout Patterns + +25 slide layouts with CSS structures and animation classes. + +## Layout Selection by Use Case + +| Layout | Use Case | Animation | +|--------|----------|-----------| +| Title Slide | Opening/first impression | `animate-fade-up` | +| Problem Statement | Establish pain point | `animate-stagger` | +| Solution Overview | Introduce solution | `animate-scale` | +| Feature Grid | Show capabilities (3-6 cards) | `animate-stagger` | +| Metrics Dashboard | Display KPIs (3-4 metrics) | `animate-stagger-scale` | +| Comparison Table | Compare options | `animate-fade-up` | +| Timeline Flow | Show progression | `animate-stagger` | +| Team Grid | Introduce people | `animate-stagger` | +| Quote Testimonial | Customer endorsement | `animate-fade-up` | +| Two Column Split | Compare/contrast | `animate-fade-up` | +| Big Number Hero | Single powerful metric | `animate-count` | +| Product Screenshot | Show product UI | `animate-scale` | +| Pricing Cards | Present tiers | `animate-stagger` | +| CTA Closing | Drive action | `animate-pulse` | + +## CSS Structures + +### Title Slide +```css +.slide-title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} +``` + +### Two Column Split +```css +.slide-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: center; +} +@media (max-width: 768px) { + .slide-split { grid-template-columns: 1fr; gap: 24px; } +} +``` + +### Feature Grid (3 columns) +```css +.slide-features { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} +@media (max-width: 768px) { + .slide-features { grid-template-columns: repeat(2, 1fr); gap: 16px; } +} +@media (max-width: 480px) { + .slide-features { grid-template-columns: 1fr; } +} +``` + +### Metrics Dashboard (4 columns) +```css +.slide-metrics { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} +@media (max-width: 768px) { + .slide-metrics { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 480px) { + .slide-metrics { grid-template-columns: 1fr; } +} +``` + +## Component Variants + +### Card Styles +| Style | CSS Class | Use For | +|-------|-----------|---------| +| Icon Left | `.card-icon-left` | Features with icons | +| Accent Bar | `.card-accent-bar` | Highlighted features | +| Metric Card | `.card-metric` | Numbers/stats | +| Avatar Card | `.card-avatar` | Team members | +| Pricing Card | `.card-pricing` | Price tiers | + +### Metric Styles +| Style | Effect | +|-------|--------| +| `gradient-number` | Gradient text on numbers | +| `oversized` | Extra large (120px+) | +| `sparkline` | Small inline chart | +| `funnel-numbers` | Conversion stages | + +## Visual Treatments + +| Treatment | When to Use | +|-----------|-------------| +| `gradient-glow` | Title slides, CTAs | +| `subtle-border` | Problem statements | +| `icon-top` | Feature grids | +| `screenshot-shadow` | Product screenshots | +| `popular-highlight` | Pricing (scale 1.05) | +| `bg-overlay` | Background images | +| `contrast-pair` | Before/after | +| `logo-grayscale` | Client logos | + +## Search Commands + +```bash +# Find layout for specific use +python .claude/skills/design-system/scripts/search-slides.py "metrics dashboard" -d layout + +# Contextual recommendation +python .claude/skills/design-system/scripts/search-slides.py "traction slide" \ + --context --position 4 --total 10 +``` + +## Layout Decision Flow + +``` +1. What's the slide goal? + └─> Search layout-logic.csv + +2. What emotion should it trigger? + └─> Search color-logic.csv + +3. What's the content type? + └─> Search typography.csv + +4. Should it break pattern? + └─> Check position (1/3, 2/3) → Use full-bleed +``` diff --git a/.cursor/skills/slides/references/slide-strategies.md b/.cursor/skills/slides/references/slide-strategies.md new file mode 100644 index 0000000..e004fe1 --- /dev/null +++ b/.cursor/skills/slides/references/slide-strategies.md @@ -0,0 +1,94 @@ +# Slide Strategies + +15 proven deck structures with emotion arcs. + +## Strategy Selection + +| Strategy | Slides | Goal | Audience | +|----------|--------|------|----------| +| YC Seed Deck | 10-12 | Raise seed funding | VCs | +| Guy Kawasaki | 10 | Pitch in 20 min | Investors | +| Series A | 12-15 | Raise Series A | Growth VCs | +| Product Demo | 5-8 | Demonstrate value | Prospects | +| Sales Pitch | 7-10 | Close deal | Qualified leads | +| Nancy Duarte Sparkline | Varies | Transform perspective | Any | +| Problem-Solution-Benefit | 3-5 | Quick persuasion | Time-pressed | +| QBR | 10-15 | Update stakeholders | Leadership | +| Team All-Hands | 8-12 | Align team | Employees | +| Conference Talk | 15-25 | Thought leadership | Attendees | +| Workshop | 20-40 | Teach skills | Learners | +| Case Study | 8-12 | Prove value | Prospects | +| Competitive Analysis | 6-10 | Strategic decisions | Internal | +| Board Meeting | 15-20 | Update board | Directors | +| Webinar | 20-30 | Generate leads | Registrants | + +## Common Structures + +### YC Seed Deck (10 slides) +1. Title/Hook +2. Problem +3. Solution +4. Traction +5. Market +6. Product +7. Business Model +8. Team +9. Financials +10. The Ask + +**Emotion arc:** curiosity→frustration→hope→confidence→trust→urgency + +### Sales Pitch (9 slides) +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 + Next Steps + +**Emotion arc:** connection→frustration→fear→hope→trust→confidence→urgency + +### Product Demo (6 slides) +1. Hook/Problem +2. Solution Overview +3. Live Demo/Screenshots +4. Key Features +5. Benefits/Pricing +6. CTA + +**Emotion arc:** curiosity→frustration→hope→confidence→urgency + +## Duarte Sparkline Pattern + +Alternate between "What Is" (current pain) and "What Could Be" (better future): + +``` +What Is → What Could Be → What Is → What Could Be → New Bliss +(pain) (hope) (pain) (hope) (resolution) +``` + +Pattern breaks at 1/3 and 2/3 positions create engagement peaks. + +## Search Commands + +```bash +# Find strategy by goal +python .claude/skills/design-system/scripts/search-slides.py "investor pitch" -d strategy + +# Get emotion arc +python .claude/skills/design-system/scripts/search-slides.py "series a funding" -d strategy --json +``` + +## Matching Strategy to Context + +| Context | Recommended Strategy | +|---------|---------------------| +| Raising money | YC Seed, Series A, Guy Kawasaki | +| Selling product | Sales Pitch, Product Demo | +| Internal update | QBR, All-Hands, Board Meeting | +| Public speaking | Conference Talk, Workshop | +| Proving value | Case Study, Competitive Analysis | +| Lead generation | Webinar | diff --git a/.cursor/skills/ui-styling/LICENSE.txt b/.cursor/skills/ui-styling/LICENSE.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/.cursor/skills/ui-styling/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.cursor/skills/ui-styling/SKILL.md b/.cursor/skills/ui-styling/SKILL.md new file mode 100644 index 0000000..5824efe --- /dev/null +++ b/.cursor/skills/ui-styling/SKILL.md @@ -0,0 +1,324 @@ +--- +name: ui-styling +description: Create beautiful, accessible user interfaces with shadcn/ui components (built on Radix UI + Tailwind), Tailwind CSS utility-first styling, and canvas-based visual designs. Use when building user interfaces, implementing design systems, creating responsive layouts, adding accessible components (dialogs, dropdowns, forms, tables), customizing themes and colors, implementing dark mode, generating visual designs and posters, or establishing consistent styling patterns across applications. +argument-hint: "[component or layout]" +license: MIT +metadata: + author: claudekit + version: "1.0.0" +--- + +# UI Styling Skill + +Comprehensive skill for creating beautiful, accessible user interfaces combining shadcn/ui components, Tailwind CSS utility styling, and canvas-based visual design systems. + +## Reference + +- shadcn/ui: https://ui.shadcn.com/llms.txt +- Tailwind CSS: https://tailwindcss.com/docs + +## When to Use This Skill + +Use when: +- Building UI with React-based frameworks (Next.js, Vite, Remix, Astro) +- Implementing accessible components (dialogs, forms, tables, navigation) +- Styling with utility-first CSS approach +- Creating responsive, mobile-first layouts +- Implementing dark mode and theme customization +- Building design systems with consistent tokens +- Generating visual designs, posters, or brand materials +- Rapid prototyping with immediate visual feedback +- Adding complex UI patterns (data tables, charts, command palettes) + +## Core Stack + +### Component Layer: shadcn/ui +- Pre-built accessible components via Radix UI primitives +- Copy-paste distribution model (components live in your codebase) +- TypeScript-first with full type safety +- Composable primitives for complex UIs +- CLI-based installation and management + +### Styling Layer: Tailwind CSS +- Utility-first CSS framework +- Build-time processing with zero runtime overhead +- Mobile-first responsive design +- Consistent design tokens (colors, spacing, typography) +- Automatic dead code elimination + +### Visual Design Layer: Canvas +- Museum-quality visual compositions +- Philosophy-driven design approach +- Sophisticated visual communication +- Minimal text, maximum visual impact +- Systematic patterns and refined aesthetics + +## Quick Start + +### Component + Styling Setup + +**Install shadcn/ui with Tailwind:** +```bash +npx shadcn@latest init +``` + +CLI prompts for framework, TypeScript, paths, and theme preferences. This configures both shadcn/ui and Tailwind CSS. + +**Add components:** +```bash +npx shadcn@latest add button card dialog form +``` + +**Use components with utility styling:** +```tsx +import { Button } from "@/components/ui/button" +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" + +export function Dashboard() { + return ( +
+ + + Analytics + + +

View your metrics

+ +
+
+
+ ) +} +``` + +### Alternative: Tailwind-Only Setup + +**Vite projects:** +```bash +npm install -D tailwindcss @tailwindcss/vite +``` + +```javascript +// vite.config.ts +import tailwindcss from '@tailwindcss/vite' +export default { plugins: [tailwindcss()] } +``` + +```css +/* src/index.css */ +@import "tailwindcss"; +``` + +## Component Library Guide + +**Comprehensive component catalog with usage patterns, installation, and composition examples.** + +See: `references/shadcn-components.md` + +Covers: +- Form & input components (Button, Input, Select, Checkbox, Date Picker, Form validation) +- Layout & navigation (Card, Tabs, Accordion, Navigation Menu) +- Overlays & dialogs (Dialog, Drawer, Popover, Toast, Command) +- Feedback & status (Alert, Progress, Skeleton) +- Display components (Table, Data Table, Avatar, Badge) + +## Theme & Customization + +**Theme configuration, CSS variables, dark mode implementation, and component customization.** + +See: `references/shadcn-theming.md` + +Covers: +- Dark mode setup with next-themes +- CSS variable system +- Color customization and palettes +- Component variant customization +- Theme toggle implementation + +## Accessibility Patterns + +**ARIA patterns, keyboard navigation, screen reader support, and accessible component usage.** + +See: `references/shadcn-accessibility.md` + +Covers: +- Radix UI accessibility features +- Keyboard navigation patterns +- Focus management +- Screen reader announcements +- Form validation accessibility + +## Tailwind Utilities + +**Core utility classes for layout, spacing, typography, colors, borders, and shadows.** + +See: `references/tailwind-utilities.md` + +Covers: +- Layout utilities (Flexbox, Grid, positioning) +- Spacing system (padding, margin, gap) +- Typography (font sizes, weights, alignment, line height) +- Colors and backgrounds +- Borders and shadows +- Arbitrary values for custom styling + +## Responsive Design + +**Mobile-first breakpoints, responsive utilities, and adaptive layouts.** + +See: `references/tailwind-responsive.md` + +Covers: +- Mobile-first approach +- Breakpoint system (sm, md, lg, xl, 2xl) +- Responsive utility patterns +- Container queries +- Max-width queries +- Custom breakpoints + +## Tailwind Customization + +**Config file structure, custom utilities, plugins, and theme extensions.** + +See: `references/tailwind-customization.md` + +Covers: +- @theme directive for custom tokens +- Custom colors and fonts +- Spacing and breakpoint extensions +- Custom utility creation +- Custom variants +- Layer organization (@layer base, components, utilities) +- Apply directive for component extraction + +## Visual Design System + +**Canvas-based design philosophy, visual communication principles, and sophisticated compositions.** + +See: `references/canvas-design-system.md` + +Covers: +- Design philosophy approach +- Visual communication over text +- Systematic patterns and composition +- Color, form, and spatial design +- Minimal text integration +- Museum-quality execution +- Multi-page design systems + +## Utility Scripts + +**Python automation for component installation and configuration generation.** + +### shadcn_add.py +Add shadcn/ui components with dependency handling: +```bash +python scripts/shadcn_add.py button card dialog +``` + +### tailwind_config_gen.py +Generate tailwind.config.js with custom theme: +```bash +python scripts/tailwind_config_gen.py --colors brand:blue --fonts display:Inter +``` + +## Best Practices + +1. **Component Composition**: Build complex UIs from simple, composable primitives +2. **Utility-First Styling**: Use Tailwind classes directly; extract components only for true repetition +3. **Mobile-First Responsive**: Start with mobile styles, layer responsive variants +4. **Accessibility-First**: Leverage Radix UI primitives, add focus states, use semantic HTML +5. **Design Tokens**: Use consistent spacing scale, color palettes, typography system +6. **Dark Mode Consistency**: Apply dark variants to all themed elements +7. **Performance**: Leverage automatic CSS purging, avoid dynamic class names +8. **TypeScript**: Use full type safety for better DX +9. **Visual Hierarchy**: Let composition guide attention, use spacing and color intentionally +10. **Expert Craftsmanship**: Every detail matters - treat UI as a craft + +## Reference Navigation + +**Component Library** +- `references/shadcn-components.md` - Complete component catalog +- `references/shadcn-theming.md` - Theming and customization +- `references/shadcn-accessibility.md` - Accessibility patterns + +**Styling System** +- `references/tailwind-utilities.md` - Core utility classes +- `references/tailwind-responsive.md` - Responsive design +- `references/tailwind-customization.md` - Configuration and extensions + +**Visual Design** +- `references/canvas-design-system.md` - Design philosophy and canvas workflows + +**Automation** +- `scripts/shadcn_add.py` - Component installation +- `scripts/tailwind_config_gen.py` - Config generation + +## Common Patterns + +**Form with validation:** +```tsx +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" + +const schema = z.object({ + email: z.string().email(), + password: z.string().min(8) +}) + +export function LoginForm() { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { email: "", password: "" } + }) + + return ( +
+ + ( + + Email + + + + + + )} /> + + + + ) +} +``` + +**Responsive layout with dark mode:** +```tsx +
+
+
+ + +

+ Content +

+
+
+
+
+
+``` + +## Resources + +- shadcn/ui Docs: https://ui.shadcn.com +- Tailwind CSS Docs: https://tailwindcss.com +- Radix UI: https://radix-ui.com +- Tailwind UI: https://tailwindui.com +- Headless UI: https://headlessui.com +- v0 (AI UI Generator): https://v0.dev diff --git a/.cursor/skills/ui-styling/references/canvas-design-system.md b/.cursor/skills/ui-styling/references/canvas-design-system.md new file mode 100644 index 0000000..6f99bee --- /dev/null +++ b/.cursor/skills/ui-styling/references/canvas-design-system.md @@ -0,0 +1,320 @@ +# Canvas Design System + +Visual design philosophy, systematic composition, and sophisticated visual communication. + +## Design Philosophy Approach + +Canvas design operates through two-phase process: + +### Phase 1: Design Philosophy Creation + +Create visual philosophy - aesthetic movement expressed through form, space, color, composition. Not layouts or templates, but pure visual philosophy. + +**What is created:** Design manifesto emphasizing: +- Visual expression over text +- Spatial communication +- Artistic interpretation +- Minimal words as visual accent + +**Philosophy structure (4-6 paragraphs):** +- Space and form principles +- Color and material approach +- Scale and rhythm guidance +- Composition and balance rules +- Visual hierarchy system + +### Phase 2: Visual Expression + +Express philosophy through canvas artifacts: +- 90% visual design +- 10% essential text +- Museum-quality execution +- Systematic patterns +- Sophisticated composition + +## Core Principles + +### 1. Visual Communication First + +Information lives in design, not paragraphs. Express ideas through: +- Color zones and fields +- Geometric precision +- Spatial relationships +- Visual weight and tension +- Form and structure + +### 2. Minimal Text Integration + +Text as rare, powerful gesture: +- Never paragraphs +- Only essential words +- Integrated into visual architecture +- Small labels, huge impact +- Typography as visual element + +### 3. Expert Craftsmanship + +Work must appear: +- Meticulously crafted +- Labored over with care +- Product of countless hours +- From absolute top of field +- Master-level execution + +### 4. Systematic Patterns + +Use scientific visual language: +- Repeating patterns +- Perfect shapes +- Dense accumulation of marks +- Layered elements +- Patient repetition rewards sustained viewing + +## Design Movement Examples + +### Concrete Poetry +**Philosophy:** Communication through monumental form and bold geometry. + +**Expression:** +- Massive color blocks +- Sculptural typography (huge words, tiny labels) +- Brutalist spatial divisions +- Polish poster energy meets Le Corbusier +- Ideas through visual weight and spatial tension +- Text as rare, powerful gesture + +### Chromatic Language +**Philosophy:** Color as primary information system. + +**Expression:** +- Geometric precision +- Color zones create meaning +- Typography minimal - small sans-serif labels +- Josef Albers' interaction meets data visualization +- Information encoded spatially and chromatically +- Words only anchor what color shows + +### Analog Meditation +**Philosophy:** Quiet visual contemplation through texture and breathing room. + +**Expression:** +- Paper grain, ink bleeds +- Vast negative space +- Photography and illustration dominate +- Typography whispered (small, restrained) +- Japanese photobook aesthetic +- Images breathe across pages +- Text appears sparingly - short phrases only + +### Organic Systems +**Philosophy:** Natural clustering and modular growth patterns. + +**Expression:** +- Rounded forms +- Organic arrangements +- Color from nature through architecture +- Information through visual diagrams +- Spatial relationships and iconography +- Text only for key labels floating in space +- Composition tells story through spatial orchestration + +### Geometric Silence +**Philosophy:** Pure order and restraint. + +**Expression:** +- Grid-based precision +- Bold photography or stark graphics +- Dramatic negative space +- Typography precise but minimal +- Small essential text, large quiet zones +- Swiss formalism meets Brutalist material honesty +- Structure communicates, not words +- Every alignment from countless refinements + +## Implementation Guidelines + +### Subtle Reference Integration + +Embed conceptual DNA without announcing: +- Niche reference woven invisibly +- Those who know feel it intuitively +- Others experience masterful abstract composition +- Like jazz musician quoting another song +- Sophisticated, never literal +- Reference enhances depth quietly + +### Color Approach + +**Intentional palette:** +- Limited colors (2-5) +- Cohesive system +- Purposeful relationships +- oklch color space for precision +- Each shade carries meaning + +**Example palette:** +``` +--color-primary: oklch(0.55 0.22 264) +--color-accent: oklch(0.75 0.18 45) +--color-neutral: oklch(0.90 0.02 264) +--color-dark: oklch(0.25 0.15 264) +``` + +### Typography System + +**Thin fonts preferred:** +- Light weights (200-300) +- Clean sans-serifs +- Geometric precision +- Small sizes for labels +- Large sizes for impact moments + +**Font integration:** +- Search `./canvas-fonts` directory +- Download needed fonts +- Bring typography onto canvas +- Part of art, not typeset digitally + +### Composition Rules + +**Systematic approach:** +- Repeating patterns establish rhythm +- Perfect geometric shapes +- Clinical typography +- Reference markers suggest imaginary discipline +- Dense accumulation builds meaning +- Layered patterns reward attention + +**Spacing discipline:** +- Nothing falls off page +- Nothing overlaps +- Every element within canvas boundaries +- Proper margins non-negotiable +- Breathing room and clear separation +- Professional execution mandatory + +### Canvas Boundaries + +**Technical specs:** +- Single page default (multi-page when requested) +- PDF or PNG output +- High resolution +- Clean margins +- Contained composition +- Flawless formatting + +## Multi-Page Design Systems + +When creating multiple pages: + +### Approach +- Treat first page as single page in coffee table book +- Create more pages along same philosophy +- Distinctly different but cohesive +- Pages tell story tastefully +- Full creative freedom + +### Consistency Elements +- Shared color palette +- Consistent typography system +- Related compositional approach +- Visual language continuity +- Philosophical thread throughout + +### Variation Strategy +- Unique twist per page +- Different focal points +- Varied spatial arrangements +- Complementary patterns +- Progressive visual narrative + +## Execution Checklist + +Before finalizing: + +- [ ] Philosophy guides every decision +- [ ] 90% visual, 10% text maximum +- [ ] Text minimal and integrated +- [ ] Nothing overlaps or falls off page +- [ ] Margins and spacing pristine +- [ ] Composition cohesive with art +- [ ] Appears meticulously crafted +- [ ] Master-level execution evident +- [ ] Sophisticated, never amateur +- [ ] Could be displayed in museum +- [ ] Proves undeniable expertise +- [ ] Formatting flawless +- [ ] Every detail perfect + +## Quality Standards + +### What to Avoid +- Cartoony aesthetics +- Amateur execution +- Text-heavy composition +- Random placement +- Overlapping elements +- Inconsistent spacing +- Obvious AI generation +- Lack of refinement + +### What to Achieve +- Museum quality +- Magazine worthy +- Art object status +- Countless hours appearance +- Top-of-field craftsmanship +- Philosophical coherence +- Visual sophistication +- Systematic precision + +## Refinement Process + +### Initial Pass +Create based on philosophy and principles. + +### Second Pass (Critical) +- Don't add more graphics +- Refine what exists +- Make extremely crisp +- Respect minimalism philosophy +- Increase cohesion with art +- Make existing elements more artistic +- Polish rather than expand + +### Final Verification +User already said: "It isn't perfect enough. Must be pristine, masterpiece of craftsmanship, as if about to be displayed in museum." + +Apply this standard before delivery. + +## Output Format + +**Required files:** +1. Design philosophy (.md file) +2. Visual expression (.pdf or .png) + +**Philosophy file contains:** +- Movement name +- 4-6 paragraph philosophy +- Visual principles +- Execution guidance + +**Canvas file contains:** +- Visual interpretation +- Minimal text +- Systematic composition +- Expert-level execution + +## Use Cases + +Apply canvas design for: +- Brand identity systems +- Poster designs +- Visual manifestos +- Design system documentation +- Art pieces and compositions +- Conceptual visual frameworks +- Editorial design +- Exhibition materials +- Coffee table books +- Design philosophy demonstrations diff --git a/.cursor/skills/ui-styling/references/shadcn-accessibility.md b/.cursor/skills/ui-styling/references/shadcn-accessibility.md new file mode 100644 index 0000000..d1cef4d --- /dev/null +++ b/.cursor/skills/ui-styling/references/shadcn-accessibility.md @@ -0,0 +1,471 @@ +# shadcn/ui Accessibility Patterns + +ARIA patterns, keyboard navigation, screen reader support, and accessible component usage. + +## Foundation: Radix UI Primitives + +shadcn/ui built on Radix UI primitives - unstyled, accessible components following WAI-ARIA design patterns. + +Benefits: +- Keyboard navigation built-in +- Screen reader announcements +- Focus management +- ARIA attributes automatically applied +- Tested against accessibility standards + +## Keyboard Navigation + +### Focus Management + +**Focus visible states:** +```tsx + +``` + +**Skip to content:** +```tsx + + Skip to content + + +
+ {/* Content */} +
+``` + +### Dialog/Modal Navigation + +Dialogs trap focus automatically via Radix Dialog primitive: + +```tsx +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog" + + + Open + + {/* Focus trapped here */} + {/* Auto-focused */} + + {/* Esc to close, Tab to navigate */} + + +``` + +Features: +- Focus trapped within dialog +- Esc key closes +- Tab cycles through focusable elements +- Focus returns to trigger on close + +### Dropdown/Menu Navigation + +```tsx +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" + + + Open + + Profile + Settings + Logout + + +``` + +Keyboard shortcuts: +- `Space/Enter`: Open menu +- `Arrow Up/Down`: Navigate items +- `Esc`: Close menu +- `Tab`: Close and move focus + +### Command Palette Navigation + +```tsx +import { Command } from "@/components/ui/command" + + + + + + Calendar + Search + + + +``` + +Features: +- Type to filter +- Arrow keys to navigate +- Enter to select +- Esc to close + +## Screen Reader Support + +### Semantic HTML + +Use proper HTML elements: + +```tsx +// Good: Semantic HTML + + + +// Avoid: Div soup +
Click me
+``` + +### ARIA Labels + +**Label interactive elements:** +```tsx + + + +``` + +**Describe elements:** +```tsx + +

+ This action permanently deletes your account and cannot be undone +

+``` + +### Screen Reader Only Text + +Use `sr-only` class for screen reader only content: + +```tsx + + +// CSS for sr-only +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} +``` + +### Live Regions + +Announce dynamic content: + +```tsx +
+ {message} +
+ +// For urgent updates +
+ {error} +
+``` + +Toast component includes live region: +```tsx +const { toast } = useToast() + +toast({ + title: "Success", + description: "Profile updated" +}) +// Announced to screen readers automatically +``` + +## Form Accessibility + +### Labels and Descriptions + +**Always label inputs:** +```tsx +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" + +
+ + +
+``` + +**Add descriptions:** +```tsx +import { FormDescription, FormMessage } from "@/components/ui/form" + + + Username + + + + + Your public display name + + {/* Error messages */} + +``` + +### Error Handling + +Announce errors to screen readers: + +```tsx + ( + + Email + + + + + + )} +/> +``` + +### Required Fields + +Indicate required fields: + +```tsx + + +``` + +### Fieldset and Legend + +Group related fields: + +```tsx +
+ + Contact Information + +
+ + +
+
+``` + +## Component-Specific Patterns + +### Accordion + +```tsx +import { Accordion } from "@/components/ui/accordion" + + + + + {/* Includes aria-expanded, aria-controls automatically */} + Is it accessible? + + + {/* Hidden when collapsed, announced when expanded */} + Yes. Follows WAI-ARIA design pattern. + + + +``` + +### Tabs + +```tsx +import { Tabs } from "@/components/ui/tabs" + + + + {/* Arrow keys navigate, Space/Enter activates */} + Account + Password + + + {/* Hidden unless selected, aria-labelledby links to trigger */} + Account content + + +``` + +### Select + +```tsx +import { Select } from "@/components/ui/select" + + +``` + +### Checkbox and Radio + +```tsx +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" + +
+ + +
+

+ You agree to our Terms of Service and Privacy Policy +

+``` + +### Alert + +```tsx +import { Alert } from "@/components/ui/alert" + + + {/* Announced immediately to screen readers */} + Error + + Your session has expired + + +``` + +## Color Contrast + +Ensure sufficient contrast between text and background. + +**WCAG Requirements:** +- **AA**: 4.5:1 for normal text, 3:1 for large text +- **AAA**: 7:1 for normal text, 4.5:1 for large text + +**Check defaults:** +```tsx +// Good: High contrast +

Text

+ +// Avoid: Low contrast +

Hard to read

+``` + +**Muted text:** +```tsx +// Use semantic muted foreground +

+ Secondary text with accessible contrast +

+``` + +## Focus Indicators + +Always provide visible focus indicators: + +**Default focus ring:** +```tsx + +``` + +**Custom focus styles:** +```tsx + + Link + +``` + +**Don't remove focus styles:** +```tsx +// Avoid + + +// Use focus-visible instead + +``` + +## Motion and Animation + +Respect reduced motion preference: + +```css +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +``` + +In components: +```tsx +
+ Respects user preference +
+``` + +## Testing Checklist + +- [ ] All interactive elements keyboard accessible +- [ ] Focus indicators visible +- [ ] Screen reader announces all content correctly +- [ ] Form errors announced and associated +- [ ] Color contrast meets WCAG AA +- [ ] Semantic HTML used +- [ ] ARIA labels provided for icon-only buttons +- [ ] Modal/dialog focus trap works +- [ ] Dropdown/select keyboard navigable +- [ ] Live regions announce updates +- [ ] Respects reduced motion preference +- [ ] Works with browser zoom up to 200% +- [ ] Tab order logical +- [ ] Skip links provided for navigation + +## Tools + +**Testing tools:** +- Lighthouse accessibility audit +- axe DevTools browser extension +- NVDA/JAWS screen readers +- Keyboard-only navigation testing +- Color contrast checkers (Contrast Ratio, WebAIM) + +**Automated testing:** +```bash +npm install -D @axe-core/react +``` + +```tsx +import { useEffect } from 'react' + +if (process.env.NODE_ENV === 'development') { + import('@axe-core/react').then((axe) => { + axe.default(React, ReactDOM, 1000) + }) +} +``` diff --git a/.cursor/skills/ui-styling/references/shadcn-components.md b/.cursor/skills/ui-styling/references/shadcn-components.md new file mode 100644 index 0000000..b6c60b3 --- /dev/null +++ b/.cursor/skills/ui-styling/references/shadcn-components.md @@ -0,0 +1,424 @@ +# shadcn/ui Component Reference + +Complete catalog of shadcn/ui components with usage patterns and installation. + +## Installation + +**Add specific components:** +```bash +npx shadcn@latest add button +npx shadcn@latest add button card dialog # Multiple +npx shadcn@latest add --all # All components +``` + +Components install to `components/ui/` with automatic dependency management. + +## Form & Input Components + +### Button +```tsx +import { Button } from "@/components/ui/button" + + + + + + +``` + +Variants: `default | destructive | outline | secondary | ghost | link` +Sizes: `default | sm | lg | icon` + +### Input +```tsx +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +
+ + +
+``` + +### Form (with React Hook Form + Zod) +```tsx +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" + +const schema = z.object({ + username: z.string().min(2).max(50), + email: z.string().email() +}) + +function ProfileForm() { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { username: "", email: "" } + }) + + return ( +
+ + ( + + Username + + + + + + )} /> + + + + ) +} +``` + +### Select +```tsx +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + + +``` + +### Checkbox +```tsx +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" + +
+ + +
+``` + +### Radio Group +```tsx +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label" + + +
+ + +
+
+ + +
+
+``` + +### Textarea +```tsx +import { Textarea } from "@/components/ui/textarea" + + +``` + +### Custom Plugin + +```javascript +// tailwind.config.js +const plugin = require('tailwindcss/plugin') + +export default { + plugins: [ + plugin(function({ addUtilities, addComponents, theme }) { + // Add utilities + addUtilities({ + '.text-shadow': { + textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + }, + '.text-shadow-lg': { + textShadow: '4px 4px 8px rgba(0, 0, 0, 0.2)', + }, + }) + + // Add components + addComponents({ + '.card-custom': { + backgroundColor: theme('colors.white'), + borderRadius: theme('borderRadius.lg'), + padding: theme('spacing.6'), + boxShadow: theme('boxShadow.md'), + }, + }) + }), + ], +} +``` + +## Configuration Examples + +### Complete Tailwind Config + +```javascript +// tailwind.config.ts +import type { Config } from 'tailwindcss' + +const config: Config = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + brand: { + 50: '#f0f9ff', + 500: '#3b82f6', + 900: '#1e3a8a', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + display: ['Playfair Display', 'serif'], + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + '128': '32rem', + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "slide-in": { + "0%": { transform: "translateX(-100%)" }, + "100%": { transform: "translateX(0)" }, + }, + }, + animation: { + "slide-in": "slide-in 0.5s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} + +export default config +``` + +## Dark Mode Configuration + +```javascript +// tailwind.config.js +export default { + darkMode: ["class"], // or "media" for automatic + // ... +} +``` + +**Usage:** +```html + + +
+ Responds to .dark class +
+ + + +
+ Responds to system preference automatically +
+``` + +## Content Configuration + +Specify files to scan for classes: + +```javascript +// tailwind.config.js +export default { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./app/**/*.{js,jsx,ts,tsx}", + "./components/**/*.{js,jsx,ts,tsx}", + "./pages/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +### Safelist + +Preserve dynamic classes: + +```javascript +export default { + safelist: [ + 'bg-red-500', + 'bg-green-500', + 'bg-blue-500', + { + pattern: /bg-(red|green|blue)-(100|500|900)/, + }, + ], +} +``` + +## Best Practices + +1. **Use @theme for simple customizations**: Prefer CSS-based customization +2. **Extract components sparingly**: Use @apply only for truly repeated patterns +3. **Leverage design tokens**: Define custom tokens in @theme +4. **Layer organization**: Keep base, components, and utilities separate +5. **Plugin for complex logic**: Use plugins for advanced customizations +6. **Test dark mode**: Ensure custom colors work in both themes +7. **Document custom utilities**: Add comments explaining custom classes +8. **Semantic naming**: Use descriptive names (primary not blue) diff --git a/.cursor/skills/ui-styling/references/tailwind-responsive.md b/.cursor/skills/ui-styling/references/tailwind-responsive.md new file mode 100644 index 0000000..f252e18 --- /dev/null +++ b/.cursor/skills/ui-styling/references/tailwind-responsive.md @@ -0,0 +1,382 @@ +# Tailwind CSS Responsive Design + +Mobile-first breakpoints, responsive utilities, and adaptive layouts. + +## Mobile-First Approach + +Tailwind uses mobile-first responsive design. Base styles apply to all screen sizes, then use breakpoint prefixes to override at larger sizes. + +```html + +
+
Item 1
+
Item 2
+
Item 3
+
Item 4
+
+``` + +## Breakpoint System + +**Default breakpoints:** + +| Prefix | Min Width | CSS Media Query | +|--------|-----------|-----------------| +| `sm:` | 640px | `@media (min-width: 640px)` | +| `md:` | 768px | `@media (min-width: 768px)` | +| `lg:` | 1024px | `@media (min-width: 1024px)` | +| `xl:` | 1280px | `@media (min-width: 1280px)` | +| `2xl:` | 1536px | `@media (min-width: 1536px)` | + +## Responsive Patterns + +### Layout Changes + +```html + +
+
Left
+
Right
+
+ + +
+
Item 1
+
Item 2
+
Item 3
+
+``` + +### Visibility + +```html + + + + +
+ Mobile only content +
+ + +
Mobile menu
+ +``` + +### Typography + +```html + +

+ Heading scales with screen size +

+ +

+ Body text scales appropriately +

+``` + +### Spacing + +```html + +
+ More padding on larger screens +
+ + +
+
Item 1
+
Item 2
+
+``` + +### Width + +```html + +
+ Responsive width +
+ + +
+ Centered with responsive max width +
+``` + +## Common Responsive Layouts + +### Sidebar Layout + +```html +
+ + + + +
+ Main content +
+
+``` + +### Card Grid + +```html +
+
Card 1
+
Card 2
+
Card 3
+
Card 4
+
+``` + +### Hero Section + +```html +
+
+
+
+

+ Hero Title +

+

+ Hero description +

+ +
+
+ +
+
+
+
+``` + +### Navigation + +```html + +``` + +## Max-Width Queries + +Apply styles only below certain breakpoint using `max-*:` prefix: + +```html + +
+ Centered on mobile/tablet, left-aligned on desktop +
+ + +
+ Hidden only on mobile +
+``` + +Available: `max-sm:` `max-md:` `max-lg:` `max-xl:` `max-2xl:` + +## Range Queries + +Apply styles between breakpoints: + +```html + +
+ Visible only on tablets +
+ + +
+ 2 columns on tablet, 4 on extra large +
+``` + +## Container Queries + +Style elements based on parent container width: + +```html +
+
+ Responds to parent width, not viewport +
+
+``` + +Container query breakpoints: `@sm:` `@md:` `@lg:` `@xl:` `@2xl:` + +## Custom Breakpoints + +Define custom breakpoints in theme: + +```css +@theme { + --breakpoint-3xl: 120rem; /* 1920px */ + --breakpoint-tablet: 48rem; /* 768px */ +} +``` + +```html +
+ Uses custom breakpoints +
+``` + +## Responsive State Variants + +Combine responsive with hover/focus: + +```html + + + + + + Link + +``` + +## Best Practices + +### 1. Mobile-First Design + +Start with mobile styles, add complexity at larger breakpoints: + +```html + +
+ + +
+``` + +### 2. Consistent Breakpoint Usage + +Use same breakpoints across related elements: + +```html +
+ Spacing scales with layout +
+``` + +### 3. Test at Breakpoint Boundaries + +Test at exact breakpoint widths (640px, 768px, 1024px, etc.) to catch edge cases. + +### 4. Use Container for Content Width + +```html +
+
+ Content with consistent max width +
+
+``` + +### 5. Progressive Enhancement + +Ensure core functionality works on mobile, enhance for larger screens: + +```html + +
+ +
+ Content +
+
+``` + +### 6. Avoid Too Many Breakpoints + +Use 2-3 breakpoints per element for maintainability: + +```html + +
+ + +
+``` + +## Common Responsive Utilities + +### Responsive Display + +```html +
+ Changes display type per breakpoint +
+``` + +### Responsive Position + +```html +
+ Positioned differently per breakpoint +
+``` + +### Responsive Order + +```html +
+
First on desktop
+
First on mobile
+
+``` + +### Responsive Overflow + +```html +
+ Scrollable on mobile, expanded on desktop +
+``` + +## Testing Checklist + +- [ ] Test at 320px (small mobile) +- [ ] Test at 640px (mobile breakpoint) +- [ ] Test at 768px (tablet breakpoint) +- [ ] Test at 1024px (desktop breakpoint) +- [ ] Test at 1280px (large desktop breakpoint) +- [ ] Test landscape orientation +- [ ] Verify touch targets (min 44x44px) +- [ ] Check text readability at all sizes +- [ ] Verify navigation works on mobile +- [ ] Test with browser zoom diff --git a/.cursor/skills/ui-styling/references/tailwind-utilities.md b/.cursor/skills/ui-styling/references/tailwind-utilities.md new file mode 100644 index 0000000..7b7b123 --- /dev/null +++ b/.cursor/skills/ui-styling/references/tailwind-utilities.md @@ -0,0 +1,455 @@ +# Tailwind CSS Utility Reference + +Core utility classes for layout, spacing, typography, colors, borders, and shadows. + +## Layout Utilities + +### Display + +```html +
Block
+
Inline Block
+
Inline
+
Flexbox
+
Inline Flex
+
Grid
+
Inline Grid
+ +``` + +### Flexbox + +**Container:** +```html +
Row (default)
+
Column
+
Reverse row
+
Reverse column
+``` + +**Justify (main axis):** +```html +
Start
+
Center
+
End
+
Space between
+
Space around
+
Space evenly
+``` + +**Align (cross axis):** +```html +
Start
+
Center
+
End
+
Baseline
+
Stretch
+``` + +**Gap:** +```html +
All sides
+
X and Y
+``` + +**Wrap:** +```html +
Wrap
+
No wrap
+``` + +### Grid + +**Columns:** +```html +
1 column
+
2 columns
+
3 columns
+
4 columns
+
12 columns
+
Custom
+``` + +**Rows:** +```html +
3 rows
+
Custom
+``` + +**Span:** +```html +
Span 2 columns
+
Span 3 rows
+``` + +**Gap:** +```html +
All sides
+
X and Y
+``` + +### Positioning + +```html +
Static (default)
+
Relative
+
Absolute
+
Fixed
+
Sticky
+ + +
Top right
+
All sides 0
+
Left/right 4
+
Top/bottom 8
+``` + +### Z-Index + +```html +
z-index: 0
+
z-index: 10
+
z-index: 20
+
z-index: 50
+``` + +## Spacing Utilities + +### Padding + +```html +
All sides
+
Left and right
+
Top and bottom
+
Top
+
Right
+
Bottom
+
Left
+``` + +### Margin + +```html +
All sides
+
Center horizontally
+
Top and bottom
+
Top
+
Negative top
+
Push to right
+``` + +### Space Between + +```html +
Horizontal spacing
+
Vertical spacing
+``` + +### Spacing Scale + +- `0`: 0px +- `px`: 1px +- `0.5`: 0.125rem (2px) +- `1`: 0.25rem (4px) +- `2`: 0.5rem (8px) +- `3`: 0.75rem (12px) +- `4`: 1rem (16px) +- `6`: 1.5rem (24px) +- `8`: 2rem (32px) +- `12`: 3rem (48px) +- `16`: 4rem (64px) +- `24`: 6rem (96px) + +## Typography + +### Font Size + +```html +

Extra small (12px)

+

Small (14px)

+

Base (16px)

+

Large (18px)

+

XL (20px)

+

2XL (24px)

+

3XL (30px)

+

4XL (36px)

+

5XL (48px)

+``` + +### Font Weight + +```html +

Thin (100)

+

Light (300)

+

Normal (400)

+

Medium (500)

+

Semibold (600)

+

Bold (700)

+

Black (900)

+``` + +### Text Alignment + +```html +

Left

+

Center

+

Right

+

Justify

+``` + +### Line Height + +```html +

1

+

1.25

+

1.5

+

1.75

+

2

+``` + +### Combined Font Utilities + +```html +

+ Font size 4xl with tight line height +

+``` + +### Text Transform + +```html +

UPPERCASE

+

lowercase

+

Capitalize

+

Normal

+``` + +### Text Decoration + +```html +

Underline

+

Line through

+

No underline

+``` + +### Text Overflow + +```html +

Truncate with ellipsis...

+

Clamp to 3 lines...

+

Ellipsis

+``` + +## Colors + +### Text Colors + +```html +

Black

+

White

+

Gray 500

+

Red 600

+

Blue 500

+

Green 600

+``` + +### Background Colors + +```html +
White
+
Gray 100
+
Blue 500
+
Red 600
+``` + +### Color Scale + +Each color has 11 shades (50-950): +- `50`: Lightest +- `100-400`: Light variations +- `500`: Base color +- `600-800`: Dark variations +- `950`: Darkest + +### Opacity Modifiers + +```html +
75% opacity
+
30% opacity
+
87% opacity
+``` + +### Gradients + +```html +
+ Left to right gradient +
+
+ With via color +
+``` + +Directions: `to-t | to-tr | to-r | to-br | to-b | to-bl | to-l | to-tl` + +## Borders + +### Border Width + +```html +
1px all sides
+
2px all sides
+
Top only
+
Right 4px
+
Bottom 2px
+
Left only
+
No border
+``` + +### Border Color + +```html +
Gray
+
Blue
+
Red with opacity
+``` + +### Border Radius + +```html +
0.25rem
+
0.375rem
+
0.5rem
+
0.75rem
+
1rem
+
9999px
+ + +
Top corners
+
Bottom right
+``` + +### Border Style + +```html +
Solid
+
Dashed
+
Dotted
+``` + +## Shadows + +```html +
Small
+
Default
+
Medium
+
Large
+
Extra large
+
2XL
+
No shadow
+``` + +### Colored Shadows + +```html +
Blue shadow
+``` + +## Width & Height + +### Width + +```html +
100%
+
50%
+
33.333%
+
16rem
+
500px
+
100vw
+ + +
min-width: 0
+
max-width: 28rem
+
max-width: 1280px
+``` + +### Height + +```html +
100%
+
100vh
+
16rem
+
500px
+ + +
min-height: 100vh
+
max-height: 24rem
+``` + +## Arbitrary Values + +Use square brackets for custom values: + +```html + +
Custom padding
+
Custom position
+ + +
Hex color
+
RGB
+ + +
Custom width
+
Custom font size
+ + +
CSS var
+ + +
Custom grid
+``` + +## Aspect Ratio + +```html +
1:1
+
16:9
+
4:3
+``` + +## Overflow + +```html +
Auto scroll
+
Hidden
+
Always scroll
+
Horizontal scroll
+
No vertical scroll
+``` + +## Opacity + +```html +
0%
+
50%
+
75%
+
100%
+``` + +## Cursor + +```html +
Pointer
+
Wait
+
Not allowed
+
Default
+``` + +## User Select + +```html +
No select
+
Text selectable
+
Select all
+``` diff --git a/.cursor/skills/ui-styling/scripts/requirements.txt b/.cursor/skills/ui-styling/scripts/requirements.txt new file mode 100644 index 0000000..75f72ca --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/requirements.txt @@ -0,0 +1,17 @@ +# UI Styling Skill Dependencies +# Python 3.10+ required + +# No Python package dependencies - uses only standard library + +# Testing dependencies (dev) +pytest>=8.0.0 +pytest-cov>=4.1.0 +pytest-mock>=3.12.0 + +# Note: This skill works with shadcn/ui and Tailwind CSS +# Requires Node.js and package managers: +# - Node.js 18+: https://nodejs.org/ +# - npm (comes with Node.js) +# +# shadcn/ui CLI is installed per-project: +# npx shadcn-ui@latest init diff --git a/.cursor/skills/ui-styling/scripts/shadcn_add.py b/.cursor/skills/ui-styling/scripts/shadcn_add.py new file mode 100644 index 0000000..6168df7 --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/shadcn_add.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +shadcn/ui Component Installer + +Add shadcn/ui components to project with automatic dependency handling. +Wraps shadcn CLI for programmatic component installation. +""" + +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import List, Optional + + +class ShadcnInstaller: + """Handle shadcn/ui component installation.""" + + def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False): + """ + Initialize installer. + + Args: + project_root: Project root directory (default: current directory) + dry_run: If True, show actions without executing + """ + self.project_root = project_root or Path.cwd() + self.dry_run = dry_run + self.components_json = self.project_root / "components.json" + + def check_shadcn_config(self) -> bool: + """ + Check if shadcn is initialized in project. + + Returns: + True if components.json exists + """ + return self.components_json.exists() + + def get_installed_components(self) -> List[str]: + """ + Get list of already installed components. + + Returns: + List of installed component names + """ + if not self.check_shadcn_config(): + return [] + + try: + with open(self.components_json) as f: + config = json.load(f) + + components_dir = self.project_root / config.get("aliases", {}).get( + "components", "components" + ).replace("@/", "") + ui_dir = components_dir / "ui" + + if not ui_dir.exists(): + return [] + + return [f.stem for f in ui_dir.glob("*.tsx") if f.is_file()] + except (json.JSONDecodeError, KeyError, OSError): + return [] + + def _get_shadcn_version(self) -> str: + """Read shadcn version from project package.json; fall back to a pinned default.""" + pkg_json = self.project_root / "package.json" + if pkg_json.exists(): + try: + pkg = json.loads(pkg_json.read_text()) + for section in ("dependencies", "devDependencies"): + version = pkg.get(section, {}).get("shadcn") + if version: + return version.lstrip("^~>=<").split()[0] + except (json.JSONDecodeError, KeyError): + pass + return "2.3.0" # pinned fallback; update when newer stable release is needed + + def add_components( + self, components: List[str], overwrite: bool = False + ) -> tuple[bool, str]: + """ + Add shadcn/ui components. + + Args: + components: List of component names to add + overwrite: If True, overwrite existing components + + Returns: + Tuple of (success, message) + """ + if not components: + return False, "No components specified" + + if not self.check_shadcn_config(): + return ( + False, + "shadcn not initialized. Run 'npx shadcn@latest init' first", + ) + + # Check which components already exist + installed = self.get_installed_components() + already_installed = [c for c in components if c in installed] + + if already_installed and not overwrite: + return ( + False, + f"Components already installed: {', '.join(already_installed)}. " + "Use --overwrite to reinstall", + ) + + # Build command + shadcn_version = self._get_shadcn_version() + cmd = ["npx", f"shadcn@{shadcn_version}", "add"] + components + + if overwrite: + cmd.append("--overwrite") + + if self.dry_run: + return True, f"Would run: {' '.join(cmd)}" + + # Execute command + try: + result = subprocess.run( + cmd, + cwd=self.project_root, + capture_output=True, + text=True, + check=True, + ) + + success_msg = f"Successfully added components: {', '.join(components)}" + if result.stdout: + success_msg += f"\n\nOutput:\n{result.stdout}" + + return True, success_msg + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to add components: {e.stderr or e.stdout or str(e)}" + return False, error_msg + except FileNotFoundError: + return False, "npx not found. Ensure Node.js is installed" + + def add_all_components(self, overwrite: bool = False) -> tuple[bool, str]: + """ + Add all available shadcn/ui components. + + Args: + overwrite: If True, overwrite existing components + + Returns: + Tuple of (success, message) + """ + if not self.check_shadcn_config(): + return ( + False, + "shadcn not initialized. Run 'npx shadcn@latest init' first", + ) + + shadcn_version = self._get_shadcn_version() + cmd = ["npx", f"shadcn@{shadcn_version}", "add", "--all"] + + if overwrite: + cmd.append("--overwrite") + + if self.dry_run: + return True, f"Would run: {' '.join(cmd)}" + + try: + result = subprocess.run( + cmd, + cwd=self.project_root, + capture_output=True, + text=True, + check=True, + ) + + success_msg = "Successfully added all components" + if result.stdout: + success_msg += f"\n\nOutput:\n{result.stdout}" + + return True, success_msg + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to add all components: {e.stderr or e.stdout or str(e)}" + return False, error_msg + except FileNotFoundError: + return False, "npx not found. Ensure Node.js is installed" + + def list_installed(self) -> tuple[bool, str]: + """ + List installed components. + + Returns: + Tuple of (success, message with component list) + """ + if not self.check_shadcn_config(): + return False, "shadcn not initialized" + + installed = self.get_installed_components() + + if not installed: + return True, "No components installed" + + return True, f"Installed components:\n" + "\n".join(f" - {c}" for c in sorted(installed)) + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Add shadcn/ui components to your project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Add single component + python shadcn_add.py button + + # Add multiple components + python shadcn_add.py button card dialog + + # Add all components + python shadcn_add.py --all + + # Overwrite existing components + python shadcn_add.py button --overwrite + + # Dry run (show what would be done) + python shadcn_add.py button card --dry-run + + # List installed components + python shadcn_add.py --list + """, + ) + + parser.add_argument( + "components", + nargs="*", + help="Component names to add (e.g., button, card, dialog)", + ) + + parser.add_argument( + "--all", + action="store_true", + help="Add all available components", + ) + + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing components", + ) + + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without executing", + ) + + parser.add_argument( + "--list", + action="store_true", + help="List installed components", + ) + + parser.add_argument( + "--project-root", + type=Path, + help="Project root directory (default: current directory)", + ) + + args = parser.parse_args() + + # Initialize installer + installer = ShadcnInstaller( + project_root=args.project_root, + dry_run=args.dry_run, + ) + + # Handle list command + if args.list: + success, message = installer.list_installed() + print(message) + sys.exit(0 if success else 1) + + # Handle add all command + if args.all: + success, message = installer.add_all_components(overwrite=args.overwrite) + print(message) + sys.exit(0 if success else 1) + + # Handle add specific components + if not args.components: + parser.print_help() + sys.exit(1) + + success, message = installer.add_components( + args.components, + overwrite=args.overwrite, + ) + + print(message) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/ui-styling/scripts/tailwind_config_gen.py b/.cursor/skills/ui-styling/scripts/tailwind_config_gen.py new file mode 100644 index 0000000..093c625 --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/tailwind_config_gen.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +""" +Tailwind CSS Configuration Generator + +Generate tailwind.config.js/ts with custom theme configuration. +Supports colors, fonts, spacing, breakpoints, and plugin recommendations. +""" + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional + +# Valid npm package name pattern: optional @scope/, then package name with +# optional subpath. Only allows alphanumeric, hyphens, dots, underscores, +# and forward slashes — no quotes, parens, or semicolons. +_VALID_PLUGIN_NAME = re.compile(r'^(@[a-zA-Z0-9_-]+/)?[a-zA-Z0-9_-]+(/[a-zA-Z0-9_.-]+)*$') + + +class TailwindConfigGenerator: + """Generate Tailwind CSS configuration files.""" + + def __init__( + self, + typescript: bool = True, + framework: str = "react", + output_path: Optional[Path] = None, + ): + """ + Initialize generator. + + Args: + typescript: If True, generate .ts config, else .js + framework: Framework name (react, vue, svelte, nextjs) + output_path: Output file path (default: auto-detect) + """ + self.typescript = typescript + self.framework = framework + self.output_path = output_path or self._default_output_path() + self.config: Dict[str, Any] = self._base_config() + + def _default_output_path(self) -> Path: + """Determine default output path.""" + ext = "ts" if self.typescript else "js" + return Path.cwd() / f"tailwind.config.{ext}" + + def _base_config(self) -> Dict[str, Any]: + """Create base configuration structure.""" + return { + "darkMode": ["class"], + "content": self._default_content_paths(), + "theme": { + "extend": {} + }, + "plugins": [] + } + + def _default_content_paths(self) -> List[str]: + """Get default content paths for framework.""" + paths = { + "react": [ + "./src/**/*.{js,jsx,ts,tsx}", + "./index.html", + ], + "vue": [ + "./src/**/*.{vue,js,ts,jsx,tsx}", + "./index.html", + ], + "svelte": [ + "./src/**/*.{svelte,js,ts}", + "./src/app.html", + ], + "nextjs": [ + "./app/**/*.{js,ts,jsx,tsx}", + "./pages/**/*.{js,ts,jsx,tsx}", + "./components/**/*.{js,ts,jsx,tsx}", + ], + } + return paths.get(self.framework, paths["react"]) + + def add_colors(self, colors: Dict[str, str]) -> None: + """ + Add custom colors to theme. + + Args: + colors: Dict of color_name: color_value + Value can be hex (#3b82f6) or variable (hsl(var(--primary))) + """ + if "colors" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["colors"] = {} + + self.config["theme"]["extend"]["colors"].update(colors) + + def add_color_palette(self, name: str, base_color: str) -> None: + """ + Add full color palette (50-950 shades) for a base color. + + Args: + name: Color name (e.g., 'brand', 'primary') + base_color: Base color in oklch format or hex + """ + # For simplicity, use CSS variable approach + if "colors" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["colors"] = {} + + self.config["theme"]["extend"]["colors"][name] = { + "50": f"var(--color-{name}-50)", + "100": f"var(--color-{name}-100)", + "200": f"var(--color-{name}-200)", + "300": f"var(--color-{name}-300)", + "400": f"var(--color-{name}-400)", + "500": f"var(--color-{name}-500)", + "600": f"var(--color-{name}-600)", + "700": f"var(--color-{name}-700)", + "800": f"var(--color-{name}-800)", + "900": f"var(--color-{name}-900)", + "950": f"var(--color-{name}-950)", + } + + def add_fonts(self, fonts: Dict[str, List[str]]) -> None: + """ + Add custom font families. + + Args: + fonts: Dict of font_type: [font_names] + e.g., {'sans': ['Inter', 'system-ui', 'sans-serif']} + """ + if "fontFamily" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["fontFamily"] = {} + + self.config["theme"]["extend"]["fontFamily"].update(fonts) + + def add_spacing(self, spacing: Dict[str, str]) -> None: + """ + Add custom spacing values. + + Args: + spacing: Dict of name: value + e.g., {'18': '4.5rem', 'navbar': '4rem'} + """ + if "spacing" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["spacing"] = {} + + self.config["theme"]["extend"]["spacing"].update(spacing) + + def add_breakpoints(self, breakpoints: Dict[str, str]) -> None: + """ + Add custom breakpoints. + + Args: + breakpoints: Dict of name: width + e.g., {'3xl': '1920px', 'tablet': '768px'} + """ + if "screens" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["screens"] = {} + + self.config["theme"]["extend"]["screens"].update(breakpoints) + + def add_plugins(self, plugins: List[str]) -> None: + """ + Add plugin requirements. + + Args: + plugins: List of plugin names + e.g., ['@tailwindcss/typography', '@tailwindcss/forms'] + """ + for plugin in plugins: + if plugin not in self.config["plugins"]: + self.config["plugins"].append(plugin) + + def recommend_plugins(self) -> List[str]: + """ + Get plugin recommendations based on configuration. + + Returns: + List of recommended plugin package names + """ + recommendations = [] + + # Always recommend animation plugin + recommendations.append("tailwindcss-animate") + + # Framework-specific recommendations + if self.framework == "nextjs": + recommendations.append("@tailwindcss/typography") + + return recommendations + + def generate_config_string(self) -> str: + """ + Generate configuration file content. + + Returns: + Configuration file as string + """ + if self.typescript: + return self._generate_typescript() + return self._generate_javascript() + + def _generate_typescript(self) -> str: + """Generate TypeScript configuration.""" + plugins_str = self._format_plugins() + + config_json = json.dumps(self.config, indent=2) + + # Remove plugin array from JSON (we'll add it with require()) + config_obj = self.config.copy() + config_obj.pop("plugins", None) + config_json = json.dumps(config_obj, indent=2) + + return f"""import type {{ Config }} from 'tailwindcss' + +const config: Config = {{ +{self._indent_json(config_json, 1)}, + plugins: [{plugins_str}], +}} + +export default config +""" + + def _generate_javascript(self) -> str: + """Generate JavaScript configuration.""" + plugins_str = self._format_plugins() + + config_obj = self.config.copy() + config_obj.pop("plugins", None) + config_json = json.dumps(config_obj, indent=2) + + return f"""/** @type {{import('tailwindcss').Config}} */ +module.exports = {{ +{self._indent_json(config_json, 1)}, + plugins: [{plugins_str}], +}} +""" + + def _format_plugins(self) -> str: + """Format plugins array for config. + + Validates each plugin name against a strict allowlist pattern + to prevent code injection via crafted require() statements + (see: CWE-94). + """ + if not self.config["plugins"]: + return "" + + plugin_requires = [] + for plugin in self.config["plugins"]: + if not _VALID_PLUGIN_NAME.match(plugin): + raise ValueError( + f"Invalid plugin name: {plugin!r}. " + "Plugin names must be valid npm package names " + "(e.g. '@tailwindcss/typography')." + ) + plugin_requires.append(f"require('{plugin}')") + return ", ".join(plugin_requires) + + def _indent_json(self, json_str: str, level: int) -> str: + """Add indentation to JSON string.""" + indent = " " * level + lines = json_str.split("\n") + # Skip first and last lines (braces) + indented = [indent + line for line in lines[1:-1]] + return "\n".join(indented) + + def write_config(self) -> tuple[bool, str]: + """ + Write configuration to file. + + Returns: + Tuple of (success, message) + """ + try: + config_content = self.generate_config_string() + + self.output_path.write_text(config_content) + + return True, f"Configuration written to {self.output_path}" + + except OSError as e: + return False, f"Failed to write config: {e}" + + def validate_config(self) -> tuple[bool, str]: + """ + Validate configuration. + + Returns: + Tuple of (valid, message) + """ + # Check content paths exist + if not self.config["content"]: + return False, "No content paths specified" + + # Check if extending empty theme + if not self.config["theme"]["extend"]: + return True, "Warning: No theme extensions defined" + + return True, "Configuration valid" + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Generate Tailwind CSS configuration", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Generate TypeScript config for Next.js + python tailwind_config_gen.py --framework nextjs + + # Generate JavaScript config with custom colors + python tailwind_config_gen.py --js --colors brand:#3b82f6 accent:#8b5cf6 + + # Add custom fonts + python tailwind_config_gen.py --fonts display:"Playfair Display,serif" + + # Add custom spacing and breakpoints + python tailwind_config_gen.py --spacing navbar:4rem --breakpoints 3xl:1920px + + # Add recommended plugins + python tailwind_config_gen.py --plugins + """, + ) + + parser.add_argument( + "--framework", + choices=["react", "vue", "svelte", "nextjs"], + default="react", + help="Target framework (default: react)", + ) + + parser.add_argument( + "--js", + action="store_true", + help="Generate JavaScript config instead of TypeScript", + ) + + parser.add_argument( + "--output", + type=Path, + help="Output file path", + ) + + parser.add_argument( + "--colors", + nargs="*", + metavar="NAME:VALUE", + help="Custom colors (e.g., brand:#3b82f6)", + ) + + parser.add_argument( + "--fonts", + nargs="*", + metavar="TYPE:FAMILY", + help="Custom fonts (e.g., sans:'Inter,system-ui')", + ) + + parser.add_argument( + "--spacing", + nargs="*", + metavar="NAME:VALUE", + help="Custom spacing (e.g., navbar:4rem)", + ) + + parser.add_argument( + "--breakpoints", + nargs="*", + metavar="NAME:WIDTH", + help="Custom breakpoints (e.g., 3xl:1920px)", + ) + + parser.add_argument( + "--plugins", + action="store_true", + help="Add recommended plugins", + ) + + parser.add_argument( + "--validate-only", + action="store_true", + help="Validate config without writing file", + ) + + args = parser.parse_args() + + # Initialize generator + generator = TailwindConfigGenerator( + typescript=not args.js, + framework=args.framework, + output_path=args.output, + ) + + # Add custom colors + if args.colors: + colors = {} + for color_spec in args.colors: + try: + name, value = color_spec.split(":", 1) + colors[name] = value + except ValueError: + print(f"Invalid color spec: {color_spec}", file=sys.stderr) + sys.exit(1) + generator.add_colors(colors) + + # Add custom fonts + if args.fonts: + fonts = {} + for font_spec in args.fonts: + try: + font_type, family = font_spec.split(":", 1) + fonts[font_type] = [f.strip().strip("'\"") for f in family.split(",")] + except ValueError: + print(f"Invalid font spec: {font_spec}", file=sys.stderr) + sys.exit(1) + generator.add_fonts(fonts) + + # Add custom spacing + if args.spacing: + spacing = {} + for spacing_spec in args.spacing: + try: + name, value = spacing_spec.split(":", 1) + spacing[name] = value + except ValueError: + print(f"Invalid spacing spec: {spacing_spec}", file=sys.stderr) + sys.exit(1) + generator.add_spacing(spacing) + + # Add custom breakpoints + if args.breakpoints: + breakpoints = {} + for bp_spec in args.breakpoints: + try: + name, width = bp_spec.split(":", 1) + breakpoints[name] = width + except ValueError: + print(f"Invalid breakpoint spec: {bp_spec}", file=sys.stderr) + sys.exit(1) + generator.add_breakpoints(breakpoints) + + # Add recommended plugins + if args.plugins: + recommended = generator.recommend_plugins() + generator.add_plugins(recommended) + print(f"Added recommended plugins: {', '.join(recommended)}") + print("\nInstall with:") + print(f" npm install -D {' '.join(recommended)}") + + # Validate + valid, message = generator.validate_config() + if not valid: + print(f"Validation failed: {message}", file=sys.stderr) + sys.exit(1) + + if message.startswith("Warning"): + print(message) + + # Validate only mode + if args.validate_only: + print("Configuration valid") + print("\nGenerated config:") + print(generator.generate_config_string()) + sys.exit(0) + + # Write config + success, message = generator.write_config() + print(message) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/ui-styling/scripts/tests/coverage-ui.json b/.cursor/skills/ui-styling/scripts/tests/coverage-ui.json new file mode 100644 index 0000000..2a20568 --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/tests/coverage-ui.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.11.0", "timestamp": "2025-11-05T00:57:08.005243", "branch_coverage": false, "show_contexts": false}, "files": {"shadcn_add.py": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 28, 29, 30, 32, 39, 41, 48, 49, 51, 52, 53, 55, 58, 60, 63, 67, 80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129, 131, 141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168, 176, 183, 184, 186, 188, 189, 191, 194, 291], "summary": {"covered_lines": 70, "num_statements": 103, "percent_covered": 67.96116504854369, "percent_covered_display": "68", "missing_lines": 33, "excluded_lines": 0}, "missing_lines": [61, 64, 65, 150, 170, 171, 172, 173, 174, 196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288, 292], "excluded_lines": [], "functions": {"ShadcnInstaller.__init__": {"executed_lines": [28, 29, 30], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.check_shadcn_config": {"executed_lines": [39], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.get_installed_components": {"executed_lines": [48, 49, 51, 52, 53, 55, 58, 60, 63], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [61, 64, 65], "excluded_lines": []}, "ShadcnInstaller.add_components": {"executed_lines": [80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.add_all_components": {"executed_lines": [141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168], "summary": {"covered_lines": 12, "num_statements": 18, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [150, 170, 171, 172, 173, 174], "excluded_lines": []}, "ShadcnInstaller.list_installed": {"executed_lines": [183, 184, 186, 188, 189, 191], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 23, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 23, "excluded_lines": 0}, "missing_lines": [196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 32, 41, 67, 131, 176, 194, 291], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [292], "excluded_lines": []}}, "classes": {"ShadcnInstaller": {"executed_lines": [28, 29, 30, 39, 48, 49, 51, 52, 53, 55, 58, 60, 63, 80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129, 141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168, 183, 184, 186, 188, 189, 191], "summary": {"covered_lines": 55, "num_statements": 64, "percent_covered": 85.9375, "percent_covered_display": "86", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [61, 64, 65, 150, 170, 171, 172, 173, 174], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 32, 41, 67, 131, 176, 194, 291], "summary": {"covered_lines": 15, "num_statements": 39, "percent_covered": 38.46153846153846, "percent_covered_display": "38", "missing_lines": 24, "excluded_lines": 0}, "missing_lines": [196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288, 292], "excluded_lines": []}}}, "tailwind_config_gen.py": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 33, 34, 35, 36, 38, 40, 41, 43, 45, 54, 56, 75, 77, 85, 86, 88, 90, 99, 100, 102, 116, 124, 125, 127, 129, 137, 138, 140, 142, 150, 151, 153, 155, 163, 164, 165, 167, 174, 177, 180, 181, 183, 185, 192, 193, 194, 196, 198, 200, 203, 204, 205, 207, 217, 219, 221, 222, 223, 225, 232, 234, 235, 237, 240, 242, 244, 245, 247, 248, 250, 257, 258, 260, 262, 264, 265, 267, 275, 276, 279, 280, 285, 455], "summary": {"covered_lines": 90, "num_statements": 164, "percent_covered": 54.8780487804878, "percent_covered_display": "55", "missing_lines": 74, "excluded_lines": 0}, "missing_lines": [282, 287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452, 456], "excluded_lines": [], "functions": {"TailwindConfigGenerator.__init__": {"executed_lines": [33, 34, 35, 36], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._default_output_path": {"executed_lines": [40, 41], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._base_config": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._default_content_paths": {"executed_lines": [56, 75], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_colors": {"executed_lines": [85, 86, 88], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_color_palette": {"executed_lines": [99, 100, 102], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_fonts": {"executed_lines": [124, 125, 127], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_spacing": {"executed_lines": [137, 138, 140], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_breakpoints": {"executed_lines": [150, 151, 153], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_plugins": {"executed_lines": [163, 164, 165], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.recommend_plugins": {"executed_lines": [174, 177, 180, 181, 183], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.generate_config_string": {"executed_lines": [192, 193, 194], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._generate_typescript": {"executed_lines": [198, 200, 203, 204, 205, 207], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._generate_javascript": {"executed_lines": [219, 221, 222, 223, 225], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._format_plugins": {"executed_lines": [234, 235, 237, 240], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._indent_json": {"executed_lines": [244, 245, 247, 248], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.write_config": {"executed_lines": [257, 258, 260, 262, 264, 265], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.validate_config": {"executed_lines": [275, 276, 279, 280], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [282], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 72, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 72, "excluded_lines": 0}, "missing_lines": [287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 38, 43, 54, 77, 90, 116, 129, 142, 155, 167, 185, 196, 217, 232, 242, 250, 267, 285, 455], "summary": {"covered_lines": 26, "num_statements": 27, "percent_covered": 96.29629629629629, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [456], "excluded_lines": []}}, "classes": {"TailwindConfigGenerator": {"executed_lines": [33, 34, 35, 36, 40, 41, 45, 56, 75, 85, 86, 88, 99, 100, 102, 124, 125, 127, 137, 138, 140, 150, 151, 153, 163, 164, 165, 174, 177, 180, 181, 183, 192, 193, 194, 198, 200, 203, 204, 205, 207, 219, 221, 222, 223, 225, 234, 235, 237, 240, 244, 245, 247, 248, 257, 258, 260, 262, 264, 265, 275, 276, 279, 280], "summary": {"covered_lines": 64, "num_statements": 65, "percent_covered": 98.46153846153847, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [282], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 38, 43, 54, 77, 90, 116, 129, 142, 155, 167, 185, 196, 217, 232, 242, 250, 267, 285, 455], "summary": {"covered_lines": 26, "num_statements": 99, "percent_covered": 26.262626262626263, "percent_covered_display": "26", "missing_lines": 73, "excluded_lines": 0}, "missing_lines": [287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452, 456], "excluded_lines": []}}}, "tests/test_shadcn_add.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 23, 24, 27, 28, 39, 40, 42, 44, 46, 47, 48, 50, 52, 53, 55, 57, 58, 60, 62, 63, 65, 67, 68, 70, 72, 73, 74, 76, 78, 81, 82, 84, 85, 87, 89, 91, 92, 93, 95, 97, 98, 100, 101, 103, 105, 106, 108, 109, 111, 113, 114, 116, 117, 119, 120, 121, 123, 125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144, 146, 148, 149, 151, 152, 153, 154, 156, 157, 159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178, 180, 181, 183, 187, 188, 190, 191, 193, 194, 196, 198, 199, 201, 202, 204, 206, 207, 209, 210, 212, 214, 215, 217, 218, 219, 221, 222, 224, 229, 230, 232, 233, 236, 237, 239, 241, 242, 244, 245, 247, 249, 250, 252, 253, 255, 257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 153, "num_statements": 153, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"TestShadcnInstaller.temp_project": {"executed_lines": [23, 24, 27, 28, 39, 40, 42], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_default_project_root": {"executed_lines": [46, 47, 48], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_custom_project_root": {"executed_lines": [52, 53], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_dry_run": {"executed_lines": [57, 58], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_check_shadcn_config_exists": {"executed_lines": [62, 63], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_check_shadcn_config_not_exists": {"executed_lines": [67, 68], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_empty": {"executed_lines": [72, 73, 74], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_with_files": {"executed_lines": [78, 81, 82, 84, 85, 87], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_no_config": {"executed_lines": [91, 92, 93], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_no_components": {"executed_lines": [97, 98, 100, 101], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_no_config": {"executed_lines": [105, 106, 108, 109], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_already_installed": {"executed_lines": [113, 114, 116, 117, 119, 120, 121], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_with_overwrite": {"executed_lines": [125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_dry_run": {"executed_lines": [148, 149, 151, 152, 153, 154], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_success": {"executed_lines": [159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_subprocess_error": {"executed_lines": [183, 187, 188, 190, 191], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_npx_not_found": {"executed_lines": [196, 198, 199, 201, 202], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_no_config": {"executed_lines": [206, 207, 209, 210], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_dry_run": {"executed_lines": [214, 215, 217, 218, 219], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_success": {"executed_lines": [224, 229, 230, 232, 233, 236, 237], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_no_config": {"executed_lines": [241, 242, 244, 245], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_empty": {"executed_lines": [249, 250, 252, 253], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_with_components": {"executed_lines": [257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 44, 50, 55, 60, 65, 70, 76, 89, 95, 103, 111, 123, 146, 156, 157, 180, 181, 193, 194, 204, 212, 221, 222, 239, 247, 255], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestShadcnInstaller": {"executed_lines": [23, 24, 27, 28, 39, 40, 42, 46, 47, 48, 52, 53, 57, 58, 62, 63, 67, 68, 72, 73, 74, 78, 81, 82, 84, 85, 87, 91, 92, 93, 97, 98, 100, 101, 105, 106, 108, 109, 113, 114, 116, 117, 119, 120, 121, 125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144, 148, 149, 151, 152, 153, 154, 159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178, 183, 187, 188, 190, 191, 196, 198, 199, 201, 202, 206, 207, 209, 210, 214, 215, 217, 218, 219, 224, 229, 230, 232, 233, 236, 237, 241, 242, 244, 245, 249, 250, 252, 253, 257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 116, "num_statements": 116, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 44, 50, 55, 60, 65, 70, 76, 89, 95, 103, 111, 123, 146, 156, 157, 180, 181, 193, 194, 204, 212, 221, 222, 239, 247, 255], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "tests/test_tailwind_config_gen.py": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 19, 20, 21, 23, 25, 26, 28, 30, 31, 32, 34, 36, 37, 39, 41, 42, 44, 46, 47, 48, 50, 52, 53, 55, 56, 57, 58, 59, 61, 63, 64, 66, 67, 69, 71, 72, 74, 75, 76, 78, 80, 81, 83, 85, 87, 88, 92, 94, 95, 96, 98, 100, 102, 103, 105, 106, 107, 109, 111, 112, 114, 116, 117, 118, 119, 120, 122, 124, 125, 129, 131, 132, 133, 135, 137, 138, 142, 144, 145, 146, 148, 150, 151, 155, 157, 158, 159, 161, 163, 164, 165, 167, 168, 170, 172, 173, 174, 176, 177, 179, 181, 182, 184, 185, 187, 189, 190, 192, 194, 196, 197, 199, 200, 201, 203, 205, 206, 208, 209, 211, 213, 214, 215, 217, 218, 220, 222, 223, 224, 226, 227, 229, 231, 232, 234, 236, 238, 239, 241, 243, 244, 246, 248, 251, 253, 254, 256, 258, 259, 261, 263, 264, 265, 267, 269, 270, 271, 273, 275, 276, 277, 279, 281, 283, 285, 286, 288, 290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315, 317, 319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 201, "num_statements": 201, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"TestTailwindConfigGenerator.test_init_default_typescript": {"executed_lines": [19, 20, 21], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_init_javascript": {"executed_lines": [25, 26], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_init_framework": {"executed_lines": [30, 31, 32], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_output_path_typescript": {"executed_lines": [36, 37], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_output_path_javascript": {"executed_lines": [41, 42], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_custom_output_path": {"executed_lines": [46, 47, 48], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_base_config_structure": {"executed_lines": [52, 53, 55, 56, 57, 58, 59], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_react": {"executed_lines": [63, 64, 66, 67], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_nextjs": {"executed_lines": [71, 72, 74, 75, 76], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_vue": {"executed_lines": [80, 81, 83], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_colors": {"executed_lines": [87, 88, 92, 94, 95, 96], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_colors_multiple_times": {"executed_lines": [100, 102, 103, 105, 106, 107], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_color_palette": {"executed_lines": [111, 112, 114, 116, 117, 118, 119, 120], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_fonts": {"executed_lines": [124, 125, 129, 131, 132, 133], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_spacing": {"executed_lines": [137, 138, 142, 144, 145, 146], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_breakpoints": {"executed_lines": [150, 151, 155, 157, 158, 159], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_plugins": {"executed_lines": [163, 164, 165, 167, 168], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_plugins_no_duplicates": {"executed_lines": [172, 173, 174, 176, 177], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_recommend_plugins": {"executed_lines": [181, 182, 184, 185], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_recommend_plugins_nextjs": {"executed_lines": [189, 190, 192], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_typescript_config": {"executed_lines": [196, 197, 199, 200, 201], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_javascript_config": {"executed_lines": [205, 206, 208, 209], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_config_with_colors": {"executed_lines": [213, 214, 215, 217, 218], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_config_with_plugins": {"executed_lines": [222, 223, 224, 226, 227], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_valid": {"executed_lines": [231, 232, 234], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_no_content": {"executed_lines": [238, 239, 241, 243, 244], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_empty_theme": {"executed_lines": [248, 251, 253, 254], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config": {"executed_lines": [258, 259, 261, 263, 264, 265], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config_creates_content": {"executed_lines": [269, 270, 271, 273, 275, 276, 277], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config_invalid_path": {"executed_lines": [281, 283, 285, 286], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_full_configuration_typescript": {"executed_lines": [290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_full_configuration_javascript": {"executed_lines": [319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 23, 28, 34, 39, 44, 50, 61, 69, 78, 85, 98, 109, 122, 135, 148, 161, 170, 179, 187, 194, 203, 211, 220, 229, 236, 246, 256, 267, 279, 288, 317], "summary": {"covered_lines": 38, "num_statements": 38, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestTailwindConfigGenerator": {"executed_lines": [19, 20, 21, 25, 26, 30, 31, 32, 36, 37, 41, 42, 46, 47, 48, 52, 53, 55, 56, 57, 58, 59, 63, 64, 66, 67, 71, 72, 74, 75, 76, 80, 81, 83, 87, 88, 92, 94, 95, 96, 100, 102, 103, 105, 106, 107, 111, 112, 114, 116, 117, 118, 119, 120, 124, 125, 129, 131, 132, 133, 137, 138, 142, 144, 145, 146, 150, 151, 155, 157, 158, 159, 163, 164, 165, 167, 168, 172, 173, 174, 176, 177, 181, 182, 184, 185, 189, 190, 192, 196, 197, 199, 200, 201, 205, 206, 208, 209, 213, 214, 215, 217, 218, 222, 223, 224, 226, 227, 231, 232, 234, 238, 239, 241, 243, 244, 248, 251, 253, 254, 258, 259, 261, 263, 264, 265, 269, 270, 271, 273, 275, 276, 277, 281, 283, 285, 286, 290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315, 319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 163, "num_statements": 163, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 23, 28, 34, 39, 44, 50, 61, 69, 78, 85, 98, 109, 122, 135, 148, 161, 170, 179, 187, 194, 203, 211, 220, 229, 236, 246, 256, 267, 279, 288, 317], "summary": {"covered_lines": 38, "num_statements": 38, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}}, "totals": {"covered_lines": 514, "num_statements": 621, "percent_covered": 82.76972624798712, "percent_covered_display": "83", "missing_lines": 107, "excluded_lines": 0}} \ No newline at end of file diff --git a/.cursor/skills/ui-styling/scripts/tests/requirements.txt b/.cursor/skills/ui-styling/scripts/tests/requirements.txt new file mode 100644 index 0000000..3a0f66d --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/tests/requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.4.0 +pytest-cov>=4.1.0 +pytest-mock>=3.11.1 diff --git a/.cursor/skills/ui-styling/scripts/tests/test_shadcn_add.py b/.cursor/skills/ui-styling/scripts/tests/test_shadcn_add.py new file mode 100644 index 0000000..03c8f31 --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/tests/test_shadcn_add.py @@ -0,0 +1,266 @@ +"""Tests for shadcn_add.py""" + +import json +import subprocess +from pathlib import Path +from unittest.mock import MagicMock, mock_open, patch + +import pytest + +# Add parent directory to path for imports +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from shadcn_add import ShadcnInstaller + + +class TestShadcnInstaller: + """Test ShadcnInstaller class.""" + + @pytest.fixture + def temp_project(self, tmp_path): + """Create temporary project structure.""" + project_root = tmp_path / "test-project" + project_root.mkdir() + + # Create components.json + components_json = project_root / "components.json" + components_json.write_text( + json.dumps({ + "style": "new-york", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } + }) + ) + + # Create components directory + ui_dir = project_root / "components" / "ui" + ui_dir.mkdir(parents=True) + + return project_root + + def test_init_default_project_root(self): + """Test initialization with default project root.""" + installer = ShadcnInstaller() + assert installer.project_root == Path.cwd() + assert installer.dry_run is False + + def test_init_custom_project_root(self, tmp_path): + """Test initialization with custom project root.""" + installer = ShadcnInstaller(project_root=tmp_path) + assert installer.project_root == tmp_path + + def test_init_dry_run(self): + """Test initialization with dry run mode.""" + installer = ShadcnInstaller(dry_run=True) + assert installer.dry_run is True + + def test_check_shadcn_config_exists(self, temp_project): + """Test checking for existing shadcn config.""" + installer = ShadcnInstaller(project_root=temp_project) + assert installer.check_shadcn_config() is True + + def test_check_shadcn_config_not_exists(self, tmp_path): + """Test checking for non-existent shadcn config.""" + installer = ShadcnInstaller(project_root=tmp_path) + assert installer.check_shadcn_config() is False + + def test_get_installed_components_empty(self, temp_project): + """Test getting installed components when none exist.""" + installer = ShadcnInstaller(project_root=temp_project) + installed = installer.get_installed_components() + assert installed == [] + + def test_get_installed_components_with_files(self, temp_project): + """Test getting installed components when files exist.""" + ui_dir = temp_project / "components" / "ui" + + # Create component files + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + (ui_dir / "card.tsx").write_text("export const Card = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + installed = installer.get_installed_components() + + assert sorted(installed) == ["button", "card"] + + def test_get_installed_components_no_config(self, tmp_path): + """Test getting installed components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + installed = installer.get_installed_components() + assert installed == [] + + def test_add_components_no_components(self, temp_project): + """Test adding components with empty list.""" + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components([]) + + assert success is False + assert "No components specified" in message + + def test_add_components_no_config(self, tmp_path): + """Test adding components without shadcn config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.add_components(["button"]) + + assert success is False + assert "not initialized" in message + + def test_add_components_already_installed(self, temp_project): + """Test adding components that are already installed.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "already installed" in message + assert "button" in message + + def test_add_components_with_overwrite(self, temp_project): + """Test adding components with overwrite flag.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + stdout="Component added successfully", + returncode=0 + ) + + success, message = installer.add_components(["button"], overwrite=True) + + assert success is True + assert "Successfully added" in message + mock_run.assert_called_once() + + # Verify --overwrite flag was passed + call_args = mock_run.call_args[0][0] + assert "--overwrite" in call_args + + def test_add_components_dry_run(self, temp_project): + """Test adding components in dry run mode.""" + installer = ShadcnInstaller(project_root=temp_project, dry_run=True) + success, message = installer.add_components(["button", "card"]) + + assert success is True + assert "Would run:" in message + assert "button" in message + assert "card" in message + + @patch("subprocess.run") + def test_add_components_success(self, mock_run, temp_project): + """Test successful component addition.""" + mock_run.return_value = MagicMock( + stdout="Components added successfully", + stderr="", + returncode=0 + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button", "card"]) + + assert success is True + assert "Successfully added" in message + assert "button" in message + assert "card" in message + + # Verify correct command was called + mock_run.assert_called_once() + call_args = mock_run.call_args[0][0] + assert call_args[:3] == ["npx", "shadcn@latest", "add"] + assert "button" in call_args + assert "card" in call_args + + @patch("subprocess.run") + def test_add_components_subprocess_error(self, mock_run, temp_project): + """Test component addition with subprocess error.""" + mock_run.side_effect = subprocess.CalledProcessError( + 1, "cmd", stderr="Error occurred" + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "Failed to add" in message + + @patch("subprocess.run") + def test_add_components_npx_not_found(self, mock_run, temp_project): + """Test component addition when npx is not found.""" + mock_run.side_effect = FileNotFoundError() + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "npx not found" in message + + def test_add_all_components_no_config(self, tmp_path): + """Test adding all components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.add_all_components() + + assert success is False + assert "not initialized" in message + + def test_add_all_components_dry_run(self, temp_project): + """Test adding all components in dry run mode.""" + installer = ShadcnInstaller(project_root=temp_project, dry_run=True) + success, message = installer.add_all_components() + + assert success is True + assert "Would run:" in message + assert "--all" in message + + @patch("subprocess.run") + def test_add_all_components_success(self, mock_run, temp_project): + """Test successful addition of all components.""" + mock_run.return_value = MagicMock( + stdout="All components added", + returncode=0 + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_all_components() + + assert success is True + assert "Successfully added all" in message + + # Verify --all flag was passed + call_args = mock_run.call_args[0][0] + assert "--all" in call_args + + def test_list_installed_no_config(self, tmp_path): + """Test listing installed components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.list_installed() + + assert success is False + assert "not initialized" in message + + def test_list_installed_empty(self, temp_project): + """Test listing installed components when none exist.""" + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.list_installed() + + assert success is True + assert "No components installed" in message + + def test_list_installed_with_components(self, temp_project): + """Test listing installed components when they exist.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + (ui_dir / "card.tsx").write_text("export const Card = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.list_installed() + + assert success is True + assert "button" in message + assert "card" in message diff --git a/.cursor/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py b/.cursor/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py new file mode 100644 index 0000000..2facdbd --- /dev/null +++ b/.cursor/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py @@ -0,0 +1,394 @@ +"""Tests for tailwind_config_gen.py""" + +import shutil +import subprocess +from pathlib import Path + +import pytest + +# Add parent directory to path for imports +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from tailwind_config_gen import TailwindConfigGenerator + + +class TestTailwindConfigGenerator: + """Test TailwindConfigGenerator class.""" + + def test_init_default_typescript(self): + """Test initialization with default settings.""" + generator = TailwindConfigGenerator() + assert generator.typescript is True + assert generator.framework == "react" + + def test_init_javascript(self): + """Test initialization for JavaScript config.""" + generator = TailwindConfigGenerator(typescript=False) + assert generator.typescript is False + + def test_init_framework(self): + """Test initialization with different frameworks.""" + for framework in ["react", "vue", "svelte", "nextjs"]: + generator = TailwindConfigGenerator(framework=framework) + assert generator.framework == framework + + def test_default_output_path_typescript(self): + """Test default output path for TypeScript.""" + generator = TailwindConfigGenerator(typescript=True) + assert generator.output_path.name == "tailwind.config.ts" + + def test_default_output_path_javascript(self): + """Test default output path for JavaScript.""" + generator = TailwindConfigGenerator(typescript=False) + assert generator.output_path.name == "tailwind.config.js" + + def test_custom_output_path(self, tmp_path): + """Test custom output path.""" + custom_path = tmp_path / "custom-config.ts" + generator = TailwindConfigGenerator(output_path=custom_path) + assert generator.output_path == custom_path + + def test_base_config_structure(self): + """Test base configuration structure.""" + generator = TailwindConfigGenerator() + config = generator.config + + assert "darkMode" in config + assert "content" in config + assert "theme" in config + assert "plugins" in config + assert "extend" in config["theme"] + + def test_default_content_paths_react(self): + """Test default content paths for React.""" + generator = TailwindConfigGenerator(framework="react") + paths = generator.config["content"] + + assert any("src/**/*.{js,jsx,ts,tsx}" in p for p in paths) + assert any("index.html" in p for p in paths) + + def test_default_content_paths_nextjs(self): + """Test default content paths for Next.js.""" + generator = TailwindConfigGenerator(framework="nextjs") + paths = generator.config["content"] + + assert any("app/**" in p for p in paths) + assert any("pages/**" in p for p in paths) + assert any("components/**" in p for p in paths) + + def test_default_content_paths_vue(self): + """Test default content paths for Vue.""" + generator = TailwindConfigGenerator(framework="vue") + paths = generator.config["content"] + + assert any("vue" in p for p in paths) + + def test_add_colors(self): + """Test adding custom colors.""" + generator = TailwindConfigGenerator() + colors = { + "brand": "#3b82f6", + "accent": "#8b5cf6" + } + generator.add_colors(colors) + + assert "colors" in generator.config["theme"]["extend"] + assert generator.config["theme"]["extend"]["colors"]["brand"] == "#3b82f6" + assert generator.config["theme"]["extend"]["colors"]["accent"] == "#8b5cf6" + + def test_add_colors_multiple_times(self): + """Test adding colors multiple times.""" + generator = TailwindConfigGenerator() + + generator.add_colors({"brand": "#3b82f6"}) + generator.add_colors({"accent": "#8b5cf6"}) + + colors = generator.config["theme"]["extend"]["colors"] + assert "brand" in colors + assert "accent" in colors + + def test_add_color_palette(self): + """Test adding full color palette.""" + generator = TailwindConfigGenerator() + generator.add_color_palette("brand", "#3b82f6") + + brand = generator.config["theme"]["extend"]["colors"]["brand"] + + assert isinstance(brand, dict) + assert "50" in brand + assert "500" in brand + assert "950" in brand + assert "var(--color-brand" in brand["500"] + + def test_add_fonts(self): + """Test adding custom fonts.""" + generator = TailwindConfigGenerator() + fonts = { + "sans": ["Inter", "system-ui", "sans-serif"], + "display": ["Playfair Display", "serif"] + } + generator.add_fonts(fonts) + + font_family = generator.config["theme"]["extend"]["fontFamily"] + assert font_family["sans"] == ["Inter", "system-ui", "sans-serif"] + assert font_family["display"] == ["Playfair Display", "serif"] + + def test_add_spacing(self): + """Test adding custom spacing.""" + generator = TailwindConfigGenerator() + spacing = { + "18": "4.5rem", + "navbar": "4rem" + } + generator.add_spacing(spacing) + + spacing_config = generator.config["theme"]["extend"]["spacing"] + assert spacing_config["18"] == "4.5rem" + assert spacing_config["navbar"] == "4rem" + + def test_add_breakpoints(self): + """Test adding custom breakpoints.""" + generator = TailwindConfigGenerator() + breakpoints = { + "3xl": "1920px", + "tablet": "768px" + } + generator.add_breakpoints(breakpoints) + + screens = generator.config["theme"]["extend"]["screens"] + assert screens["3xl"] == "1920px" + assert screens["tablet"] == "768px" + + def test_add_plugins(self): + """Test adding plugins.""" + generator = TailwindConfigGenerator() + plugins = ["@tailwindcss/typography", "@tailwindcss/forms"] + generator.add_plugins(plugins) + + assert "@tailwindcss/typography" in generator.config["plugins"] + assert "@tailwindcss/forms" in generator.config["plugins"] + + def test_add_plugins_no_duplicates(self): + """Test that adding same plugin twice doesn't duplicate.""" + generator = TailwindConfigGenerator() + generator.add_plugins(["@tailwindcss/typography"]) + generator.add_plugins(["@tailwindcss/typography"]) + + count = generator.config["plugins"].count("@tailwindcss/typography") + assert count == 1 + + def test_recommend_plugins(self): + """Test plugin recommendations.""" + generator = TailwindConfigGenerator() + recommendations = generator.recommend_plugins() + + assert isinstance(recommendations, list) + assert "tailwindcss-animate" in recommendations + + def test_recommend_plugins_nextjs(self): + """Test plugin recommendations for Next.js.""" + generator = TailwindConfigGenerator(framework="nextjs") + recommendations = generator.recommend_plugins() + + assert "@tailwindcss/typography" in recommendations + + def test_generate_typescript_config(self): + """Test generating TypeScript configuration.""" + generator = TailwindConfigGenerator(typescript=True) + config = generator.generate_config_string() + + assert "import type { Config } from 'tailwindcss'" in config + assert "const config: Config" in config + assert "export default config" in config + + def test_generate_javascript_config(self): + """Test generating JavaScript configuration.""" + generator = TailwindConfigGenerator(typescript=False) + config = generator.generate_config_string() + + assert "module.exports" in config + assert "@type" in config + + def test_generate_config_with_colors(self): + """Test generating config with custom colors.""" + generator = TailwindConfigGenerator() + generator.add_colors({"brand": "#3b82f6"}) + config = generator.generate_config_string() + + assert "colors" in config + assert "brand" in config + + def test_generate_config_with_plugins(self): + """Test generating config with plugins.""" + generator = TailwindConfigGenerator() + generator.add_plugins(["tailwindcss-animate"]) + config = generator.generate_config_string() + + assert "plugins:" in config + assert "require('tailwindcss-animate')" in config + + def test_validate_config_valid(self): + """Test validating valid configuration.""" + generator = TailwindConfigGenerator() + valid, message = generator.validate_config() + + assert valid is True + + def test_validate_config_no_content(self): + """Test validating config with no content paths.""" + generator = TailwindConfigGenerator() + generator.config["content"] = [] + + valid, message = generator.validate_config() + + assert valid is False + assert "No content paths" in message + + def test_validate_config_empty_theme(self): + """Test validating config with empty theme extensions.""" + generator = TailwindConfigGenerator() + # Default has empty theme.extend + + valid, message = generator.validate_config() + + assert valid is True + assert "Warning" in message + + def test_write_config(self, tmp_path): + """Test writing configuration to file.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator(output_path=output_path) + + success, message = generator.write_config() + + assert success is True + assert output_path.exists() + assert "written to" in message + + def test_write_config_creates_content(self, tmp_path): + """Test that written config contains expected content.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator(output_path=output_path) + generator.add_colors({"brand": "#3b82f6"}) + + generator.write_config() + + content = output_path.read_text() + assert "import type { Config }" in content + assert "brand" in content + + def test_write_config_invalid_path(self): + """Test writing config to invalid path.""" + generator = TailwindConfigGenerator(output_path=Path("/invalid/path/config.ts")) + + success, message = generator.write_config() + + assert success is False + assert "Failed to write" in message + + def test_full_configuration_typescript(self, tmp_path): + """Test generating complete TypeScript configuration.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator( + typescript=True, + framework="nextjs", + output_path=output_path + ) + + # Add various customizations + generator.add_colors({"brand": "#3b82f6", "accent": "#8b5cf6"}) + generator.add_fonts({"sans": ["Inter", "sans-serif"]}) + generator.add_spacing({"navbar": "4rem"}) + generator.add_breakpoints({"3xl": "1920px"}) + generator.add_plugins(["tailwindcss-animate"]) + + success, _ = generator.write_config() + assert success is True + + content = output_path.read_text() + + # Verify all customizations are present + assert "brand" in content + assert "accent" in content + assert "Inter" in content + assert "navbar" in content + assert "3xl" in content + assert "tailwindcss-animate" in content + + def test_full_configuration_javascript(self, tmp_path): + """Test generating complete JavaScript configuration.""" + output_path = tmp_path / "tailwind.config.js" + generator = TailwindConfigGenerator( + typescript=False, + framework="react", + output_path=output_path + ) + + generator.add_colors({"primary": "#3b82f6"}) + generator.add_plugins(["@tailwindcss/forms"]) + + success, _ = generator.write_config() + assert success is True + + content = output_path.read_text() + + assert "module.exports" in content + assert "primary" in content + assert "@tailwindcss/forms" in content + + +def _strip_to_object(config_str: str) -> str: + """Reduce a generated TS/JS config to a bare assignable object so it can be + handed to `node --check` without a TypeScript loader.""" + lines = [] + for line in config_str.splitlines(): + if line.startswith("import type"): + continue + if line.strip() == "export default config": + continue + line = line.replace("const config: Config =", "const config =") + line = line.replace("module.exports =", "const config =") + lines.append(line) + return "\n".join(lines) + + +class TestGeneratedConfigIsValidJs: + """Regression guard for the missing-comma bug between the ``theme`` block and + ``plugins`` that produced syntactically invalid config files. The data-shape + tests above all passed while the emitted string was unparseable, so these + tests validate the serialized output itself.""" + + @pytest.mark.parametrize("typescript", [True, False]) + def test_property_before_plugins_is_comma_terminated(self, typescript): + """The property preceding ``plugins`` must end with a comma (pure-Python + check, so the regression is caught even where node is unavailable).""" + generator = TailwindConfigGenerator(typescript=typescript) + generator.add_colors({"brand": "#6366F1"}) + generator.add_breakpoints({"3xl": "1920px"}) + config = generator.generate_config_string() + + assert "}\n plugins:" not in config, "missing comma before plugins" + assert "},\n plugins:" in config + + @pytest.mark.parametrize("typescript", [True, False]) + def test_node_check_parses_generated_config(self, typescript, tmp_path): + """The emitted config parses as valid JS via ``node --check``.""" + node = shutil.which("node") + if not node: + pytest.skip("node not available") + + generator = TailwindConfigGenerator(typescript=typescript) + generator.add_colors({"brand": "#6366F1", "accent": "#10B981"}) + generator.add_fonts({"sans": ["Inter"]}) + generator.add_breakpoints({"3xl": "1920px"}) + generator.add_plugins(["tailwindcss-animate"]) + + snippet = _strip_to_object(generator.generate_config_string()) + path = tmp_path / "config.cjs" + path.write_text(snippet) + + result = subprocess.run( + [node, "--check", str(path)], capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr diff --git a/.cursor/skills/ui-ux-pro-max/SKILL.md b/.cursor/skills/ui-ux-pro-max/SKILL.md new file mode 100644 index 0000000..da9a22d --- /dev/null +++ b/.cursor/skills/ui-ux-pro-max/SKILL.md @@ -0,0 +1,383 @@ +--- +name: ui-ux-pro-max +description: Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. +--- +# ui-ux-pro-max + +Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations. + +# Prerequisites + +The bundled scripts require Python 3 (standard library only — no third-party packages, no network access). Check if it is available: + +```bash +python3 --version || python --version +``` + +If Python is not installed, **do not install it yourself**. Stop and ask the user to install Python 3 using their preferred method (e.g. from [python.org](https://www.python.org/downloads/) or their OS package manager), then continue once it is available. Never run package-manager or system-modifying commands (`sudo`, `brew`, `apt`, `winget`, etc.) on the user's machine for this skill. + +If the user prefers not to install Python, skip the CLI searches and rely on the Quick Reference sections above. + +> **Note:** On Windows, use `python` instead of `python3` to run scripts (e.g., `python scripts/search.py` instead of `python3 scripts/search.py`). + +--- + +## How to Use This Skill + +Use this skill when the user requests any of the following: + +| Scenario | Trigger Examples | Start From | +|----------|-----------------|------------| +| **New project / page** | "做一个 landing page"、"Build a dashboard" | Step 1 → Step 2 (design system) | +| **New component** | "Create a pricing card"、"Add a modal" | Step 3 (domain search: style, ux) | +| **Choose style / color / font** | "What style fits a fintech app?"、"推荐配色" | Step 2 (design system) | +| **Review existing UI** | "Review this page for UX issues"、"检查无障碍" | Quick Reference checklist above | +| **Fix a UI bug** | "Button hover is broken"、"Layout shifts on load" | Quick Reference → relevant section | +| **Improve / optimize** | "Make this faster"、"Improve mobile experience" | Step 3 (domain search: ux, react) | +| **Implement dark mode** | "Add dark mode support" | Step 3 (domain: style "dark mode") | +| **Add charts / data viz** | "Add an analytics dashboard chart" | Step 3 (domain: chart) | +| **Stack best practices** | "React performance tips"、"SwiftUI navigation" | Step 4 (stack search) | + +Follow this workflow: + +### Step 1: Analyze User Requirements + +Extract key information from user request: +- **Product type**: Entertainment (social, video, music, gaming), Tool (scanner, editor, converter), Productivity (task manager, notes, calendar), or hybrid +- **Target audience**: C-end consumer users; consider age group, usage context (commute, leisure, work) +- **Style keywords**: playful, vibrant, minimal, dark mode, content-first, immersive, etc. +- **Stack**: React Native (this project's only tech stack) + +### Step 2: Generate Design System (REQUIRED) + +**Always start with `--design-system`** to get comprehensive recommendations with reasoning: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py " " --design-system [-p "Project Name"] +``` + +This command: +1. Searches domains in parallel (product, style, color, landing, typography) +2. Applies reasoning rules from `ui-reasoning.csv` to select best matches +3. Returns complete design system: pattern, style, colors, typography, effects +4. Includes anti-patterns to avoid + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa" +``` + +### Step 2b: Persist Design System (Master + Overrides Pattern) + +To save the design system for **hierarchical retrieval across sessions**, add `--persist`: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" +``` + +This creates: +- `design-system/MASTER.md` — Global Source of Truth with all design rules +- `design-system/pages/` — Folder for page-specific overrides + +**With page-specific override:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" --page "dashboard" +``` + +This also creates: +- `design-system/pages/dashboard.md` — Page-specific deviations from Master + +**How hierarchical retrieval works:** +1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md` +2. If the page file exists, its rules **override** the Master file +3. If not, use `design-system/MASTER.md` exclusively + +**Context-aware retrieval prompt:** +``` +I am building the [Page Name] page. Please read design-system/MASTER.md. +Also check if design-system/pages/[page-name].md exists. +If the page file exists, prioritize its rules. +If not, use the Master rules exclusively. +Now, generate the code... +``` + +### Step 2c: Design Dials (optional) + +Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` + +| Dial | Low (1-3) | Mid (4-7) | High (8-10) | +|------|-----------|-----------|-------------| +| `--variance` | Centered / minimal (biases toward Minimalism-style categories) | Balanced / modern | Bold / asymmetric (biases toward Brutalism, Bento Grids) | +| `--motion` | Subtle micro-interactions | Standard scroll/stagger motion | Complex choreography (pin, Flip, SplitText) | +| `--density` | Spacious (24-96px spacing scale) | Standard (16-64px, current default) | Dense/dashboard (8-32px spacing scale) | + +- `--motion` attaches a ready-to-use GSAP snippet (with framework notes, Do/Don't, and performance notes) pulled from `--domain gsap`, matched to the resolved tier (Subtle/Standard/Complex). +- `--density` overrides the `--space-*` CSS variable table in the ASCII/markdown/MASTER.md output — use it for dashboards (high) vs. marketing pages (low) without hand-editing tokens. +- Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change). + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console" +``` + +### Step 3: Supplement with Detailed Searches (as needed) + +After getting the design system, use domain searches to get additional details: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --domain [-n ] +``` + +**When to use detailed searches:** + +| Need | Domain | Example | +|------|--------|---------| +| Product type patterns | `product` | `--domain product "entertainment social"` | +| More style options | `style` | `--domain style "glassmorphism dark"` | +| Color palettes | `color` | `--domain color "entertainment vibrant"` | +| Font pairings | `typography` | `--domain typography "playful modern"` | +| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | +| UX best practices | `ux` | `--domain ux "animation accessibility"` | +| Landing structure | `landing` | `--domain landing "hero social-proof"` | +| React Native perf | `react` | `--domain react "rerender memo list"` | +| App interface a11y | `web` | `--domain web "accessibilityLabel touch safe-areas"` | +| AI prompt / CSS keywords | `prompt` | `--domain prompt "minimalism"` | + +### Step 4: Stack Guidelines + +Get implementation-specific best practices for the user's stack: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --stack +``` + +--- + +## Search Reference + +### Available Domains + +| Domain | Use For | Example Keywords | +|--------|---------|------------------| +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `gsap` | GSAP animation skeletons by intensity tier | scroll reveal, stagger, magnetic cursor, page transition | +| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | +| `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | +| `prompt` | AI prompts, CSS keywords | (style name) | + +### Available Stacks + +| Stack | Focus | +|-------|-------| +| `react-native` | Components, Navigation, Lists | +| `javafx` | Enterprise desktop apps, AtlantaFX themes, FXML, CSS, Controls, Binding, Threading, Packaging | + +**JavaFX enterprise examples:** + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "atlantafx primer enterprise theme" --stack javafx +python3 skills/ui-ux-pro-max/scripts/search.py "enterprise tableview density permission" --stack javafx +``` + +--- + +## Example Workflow + +**User request:** "Make an AI search homepage。" + +### Step 1: Analyze Requirements +- Product type: Tool (AI search engine) +- Target audience: C-end users looking for fast, intelligent search +- Style keywords: modern, minimal, content-first, dark mode +- Stack: React Native + +### Step 2: Generate Design System (REQUIRED) + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "AI search tool modern minimal" --design-system -p "AI Search" +``` + +**Output:** Complete design system with pattern, style, colors, typography, effects, and anti-patterns. + +### Step 3: Supplement with Detailed Searches (as needed) + +```bash +# Get style options for a modern tool product +python3 skills/ui-ux-pro-max/scripts/search.py "minimalism dark mode" --domain style + +# Get UX best practices for search interaction and loading +python3 skills/ui-ux-pro-max/scripts/search.py "search loading animation" --domain ux +``` + +### Step 4: Stack Guidelines + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "list performance navigation" --stack react-native +``` + +**Then:** Synthesize design system + detailed searches and implement the design. + +--- + +## Output Formats + +The `--design-system` flag supports two output formats: + +```bash +# ASCII box (default) - best for terminal display +python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system + +# Markdown - best for documentation +python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown +``` + +--- + +## Tips for Better Results + +### Query Strategy + +- Use **multi-dimensional keywords** — combine product + industry + tone + density: `"entertainment social vibrant content-dense"` not just `"app"` +- Try different keywords for the same need: `"playful neon"` → `"vibrant dark"` → `"content-first minimal"` +- Use `--design-system` first for full recommendations, then `--domain` to deep-dive any dimension you're unsure about +- Add `--stack ` for implementation-specific guidance when the target stack is known + +### Common Sticking Points + +| Problem | What to Do | +|---------|------------| +| Can't decide on style/color | Re-run `--design-system` with different keywords | +| Dark mode contrast issues | Quick Reference §6: `color-dark-mode` + `color-accessible-pairs` | +| Animations feel unnatural | Quick Reference §7: `spring-physics` + `easing` + `exit-faster-than-enter` | +| Form UX is poor | Quick Reference §8: `inline-validation` + `error-clarity` + `focus-management` | +| Navigation feels confusing | Quick Reference §9: `nav-hierarchy` + `bottom-nav-limit` + `back-behavior` | +| Layout breaks on small screens | Quick Reference §5: `mobile-first` + `breakpoint-consistency` | +| Performance / jank | Quick Reference §3: `virtualize-lists` + `main-thread-budget` + `debounce-throttle` | + +### Pre-Delivery Checklist + +- Run `--domain ux "animation accessibility z-index loading"` as a UX validation pass before implementation +- Run through Quick Reference **§1–§3** (CRITICAL + HIGH) as a final review +- Test on 375px (small phone) and landscape orientation +- Verify behavior with **reduced-motion** enabled and **Dynamic Type** at largest size +- Check dark mode contrast independently (don't assume light mode values work) +- Confirm all touch targets ≥44pt and no content hidden behind safe areas + +--- + +## Common Rules for Professional UI + +These are frequently overlooked issues that make UI look unprofessional: +Scope notice: The rules below are for App UI (iOS/Android/React Native/Flutter), not desktop-web interaction patterns. + +### Icons & Visual Elements + +- 默认图标库使用 **Phosphor (`@phosphor-icons/react`)**。`src/ui-ux-pro-max/data/icons.csv` 中列出的只是常用推荐图标,不是完整集合。 +- 当推荐表中找不到合适的图标时: + - **优先继续从 Phosphor 的完整图标集中选择任何语义更贴切的图标**; + - 如果 Phosphor 也没有理想选项,可以使用 **Heroicons (`@heroicons/react`)** 作为备选,注意保持风格一致(线性/填充、笔画粗细、圆角风格)。 + +| Rule | Standard | Avoid | Why It Matters | +|------|----------|--------|----------------| +| **No Emoji as Structural Icons** | Use vector-based icons (e.g., Phosphor `@phosphor-icons/react`, Heroicons `@heroicons/react`, react-native-vector-icons, @expo/vector-icons). | Using emojis (🎨 🚀 ⚙️) for navigation, settings, or system controls. | Emojis are font-dependent, inconsistent across platforms, and cannot be controlled via design tokens. | +| **Vector-Only Assets** | Use SVG or platform vector icons that scale cleanly and support theming. | Raster PNG icons that blur or pixelate. | Ensures scalability, crisp rendering, and dark/light mode adaptability. | +| **Stable Interaction States** | Use color, opacity, or elevation transitions for press states without changing layout bounds. | Layout-shifting transforms that move surrounding content or trigger visual jitter. | Prevents unstable interactions and preserves smooth motion/perceived quality on mobile. | +| **Correct Brand Logos** | Use official brand assets and follow their usage guidelines (spacing, color, clear space). | Guessing logo paths, recoloring unofficially, or modifying proportions. | Prevents brand misuse and ensures legal/platform compliance. | +| **Consistent Icon Sizing** | Define icon sizes as design tokens (e.g., icon-sm, icon-md = 24pt, icon-lg). | Mixing arbitrary values like 20pt / 24pt / 28pt randomly. | Maintains rhythm and visual hierarchy across the interface. | +| **Stroke Consistency** | Use a consistent stroke width within the same visual layer (e.g., 1.5px or 2px). | Mixing thick and thin stroke styles arbitrarily. | Inconsistent strokes reduce perceived polish and cohesion. | +| **Filled vs Outline Discipline** | Use one icon style per hierarchy level. | Mixing filled and outline icons at the same hierarchy level. | Maintains semantic clarity and stylistic coherence. | +| **Touch Target Minimum** | Minimum 44×44pt interactive area (use hitSlop if icon is smaller). | Small icons without expanded tap area. | Meets accessibility and platform usability standards. | +| **Icon Alignment** | Align icons to text baseline and maintain consistent padding. | Misaligned icons or inconsistent spacing around them. | Prevents subtle visual imbalance that reduces perceived quality. | +| **Icon Contrast** | Follow WCAG contrast standards: 4.5:1 for small elements, 3:1 minimum for larger UI glyphs. | Low-contrast icons that blend into the background. | Ensures accessibility in both light and dark modes. | + + +### Interaction (App) + +| Rule | Do | Don't | +|------|----|----- | +| **Tap feedback** | Provide clear pressed feedback (ripple/opacity/elevation) within 80-150ms | No visual response on tap | +| **Animation timing** | Keep micro-interactions around 150-300ms with platform-native easing | Instant transitions or slow animations (>500ms) | +| **Accessibility focus** | Ensure screen reader focus order matches visual order and labels are descriptive | Unlabeled controls or confusing focus traversal | +| **Disabled state clarity** | Use disabled semantics (`disabled`/native disabled props), reduced emphasis, and no tap action | Controls that look tappable but do nothing | +| **Touch target minimum** | Keep tap areas >=44x44pt (iOS) or >=48x48dp (Android), expand hit area when icon is smaller | Tiny tap targets or icon-only hit areas without padding | +| **Gesture conflict prevention** | Keep one primary gesture per region and avoid nested tap/drag conflicts | Overlapping gestures causing accidental actions | +| **Semantic native controls** | Prefer native interactive primitives (`Button`, `Pressable`, platform equivalents) with proper accessibility roles | Generic containers used as primary controls without semantics | + +### Light/Dark Mode Contrast + +| Rule | Do | Don't | +|------|----|----- | +| **Surface readability (light)** | Keep cards/surfaces clearly separated from background with sufficient opacity/elevation | Overly transparent surfaces that blur hierarchy | +| **Text contrast (light)** | Maintain body text contrast >=4.5:1 against light surfaces | Low-contrast gray body text | +| **Text contrast (dark)** | Maintain primary text contrast >=4.5:1 and secondary text >=3:1 on dark surfaces | Dark mode text that blends into background | +| **Border and divider visibility** | Ensure separators are visible in both themes (not just light mode) | Theme-specific borders disappearing in one mode | +| **State contrast parity** | Keep pressed/focused/disabled states equally distinguishable in light and dark themes | Defining interaction states for one theme only | +| **Token-driven theming** | Use semantic color tokens mapped per theme across app surfaces/text/icons | Hardcoded per-screen hex values | +| **Scrim and modal legibility** | Use a modal scrim strong enough to isolate foreground content (typically 40-60% black) | Weak scrim that leaves background visually competing | + +### Layout & Spacing + +| Rule | Do | Don't | +|------|----|----- | +| **Safe-area compliance** | Respect top/bottom safe areas for all fixed headers, tab bars, and CTA bars | Placing fixed UI under notch, status bar, or gesture area | +| **System bar clearance** | Add spacing for status/navigation bars and gesture home indicator | Let tappable content collide with OS chrome | +| **Consistent content width** | Keep predictable content width per device class (phone/tablet) | Mixing arbitrary widths between screens | +| **8dp spacing rhythm** | Use a consistent 4/8dp spacing system for padding/gaps/section spacing | Random spacing increments with no rhythm | +| **Readable text measure** | Keep long-form text readable on large devices (avoid edge-to-edge paragraphs on tablets) | Full-width long text that hurts readability | +| **Section spacing hierarchy** | Define clear vertical rhythm tiers (e.g., 16/24/32/48) by hierarchy | Similar UI levels with inconsistent spacing | +| **Adaptive gutters by breakpoint** | Increase horizontal insets on larger widths and in landscape | Same narrow gutter on all device sizes/orientations | +| **Scroll and fixed element coexistence** | Add bottom/top content insets so lists are not hidden behind fixed bars | Scroll content obscured by sticky headers/footers | + +--- + +## Pre-Delivery Checklist + +Before delivering UI code, verify these items: +Scope notice: This checklist is for App UI (iOS/Android/React Native/Flutter). + +### Visual Quality +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons come from a consistent icon family and style +- [ ] Official brand assets are used with correct proportions and clear space +- [ ] Pressed-state visuals do not shift layout bounds or cause jitter +- [ ] Semantic theme tokens are used consistently (no ad-hoc per-screen hardcoded colors) + +### Interaction +- [ ] All tappable elements provide clear pressed feedback (ripple/opacity/elevation) +- [ ] Touch targets meet minimum size (>=44x44pt iOS, >=48x48dp Android) +- [ ] Micro-interaction timing stays in the 150-300ms range with native-feeling easing +- [ ] Disabled states are visually clear and non-interactive +- [ ] Screen reader focus order matches visual order, and interactive labels are descriptive +- [ ] Gesture regions avoid nested/conflicting interactions (tap/drag/back-swipe conflicts) + +### Light/Dark Mode +- [ ] Primary text contrast >=4.5:1 in both light and dark mode +- [ ] Secondary text contrast >=3:1 in both light and dark mode +- [ ] Dividers/borders and interaction states are distinguishable in both modes +- [ ] Modal/drawer scrim opacity is strong enough to preserve foreground legibility (typically 40-60% black) +- [ ] Both themes are tested before delivery (not inferred from a single theme) + +### Layout +- [ ] Safe areas are respected for headers, tab bars, and bottom CTA bars +- [ ] Scroll content is not hidden behind fixed/sticky bars +- [ ] Verified on small phone, large phone, and tablet (portrait + landscape) +- [ ] Horizontal insets/gutters adapt correctly by device size and orientation +- [ ] 4/8dp spacing rhythm is maintained across component, section, and page levels +- [ ] Long-form text measure remains readable on larger devices (no edge-to-edge paragraphs) + +### Accessibility +- [ ] All meaningful images/icons have accessibility labels +- [ ] Form fields have labels, hints, and clear error messages +- [ ] Color is not the only indicator +- [ ] Reduced motion and dynamic text size are supported without layout breakage +- [ ] Accessibility traits/roles/states (selected, disabled, expanded) are announced correctly diff --git a/.cursor/skills/ui-ux-pro-max/data/app-interface.csv b/.cursor/skills/ui-ux-pro-max/data/app-interface.csv new file mode 100644 index 0000000..f34c3cd --- /dev/null +++ b/.cursor/skills/ui-ux-pro-max/data/app-interface.csv @@ -0,0 +1,31 @@ +No,Category,Issue,Keywords,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity +1,Accessibility,Icon Button Labels,icon button accessibilityLabel,iOS/Android/React Native,Icon-only buttons must expose an accessible label,Set accessibilityLabel or label prop on icon buttons,Icon buttons without accessible names,"","",Critical +2,Accessibility,Form Control Labels,form input label accessibilityLabel,iOS/Android/React Native,All inputs must have a visible label and an accessibility label,Pair Text label with input and set accessibilityLabel,Inputs with placeholder only,"Email","",Critical +3,Accessibility,Role & Traits,accessibilityRole accessibilityTraits,iOS/Android/React Native,Interactive elements must expose correct roles/traits,Use accessibilityRole/button/link/checkbox etc.,Rely on generic views with no roles,"Submit","Submit",High +4,Accessibility,Dynamic Updates,accessibilityLiveRegion announce,iOS/Android/React Native,Async status updates should be announced to screen readers,Use accessibilityLiveRegion or announceForAccessibility,Update text silently with no announcement,"{status}","{status}",Medium +5,Accessibility,Decorative Icons,accessible={false} importantForAccessibility,iOS/Android/React Native,Decorative icons should be hidden from screen readers,Mark decorative icons as not accessible,Have screen reader read every icon,"","",Medium +6,Touch,Touch Target Size,touch 44x44 hitSlop,iOS/Android/React Native,Primary touch targets must be at least 44x44pt,Increase hitSlop or padding to meet minimum,Small icons with tiny touch area,"","",Critical +7,Touch,Touch Spacing,touch spacing gap 8px,iOS/Android/React Native,Adjacent touch targets need enough spacing,Keep at least 8dp spacing between touchables,Cluster many buttons with no gap,"