|
| 1 | +package dpr |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "path/filepath" |
| 7 | + "strconv" |
| 8 | + "strings" |
| 9 | +) |
| 10 | + |
| 11 | +// CursorRulesGenerator generates design_rules.mdc for AI agents |
| 12 | +type CursorRulesGenerator struct { |
| 13 | + projectRoot string |
| 14 | + data *DPRData |
| 15 | +} |
| 16 | + |
| 17 | +// NewCursorRulesGenerator creates a new cursor rules generator |
| 18 | +func NewCursorRulesGenerator(projectRoot string, data *DPRData) *CursorRulesGenerator { |
| 19 | + return &CursorRulesGenerator{ |
| 20 | + projectRoot: projectRoot, |
| 21 | + data: data, |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +// Generate creates the design_rules.mdc file |
| 26 | +func (crg *CursorRulesGenerator) Generate() error { |
| 27 | + rulesPath := filepath.Join(crg.projectRoot, ".doplan", "ai", "rules", "design_rules.mdc") |
| 28 | + |
| 29 | + // Ensure directory exists |
| 30 | + if err := os.MkdirAll(filepath.Dir(rulesPath), 0755); err != nil { |
| 31 | + return fmt.Errorf("failed to create ai/rules directory: %w", err) |
| 32 | + } |
| 33 | + |
| 34 | + content := crg.generateRulesContent() |
| 35 | + |
| 36 | + return os.WriteFile(rulesPath, []byte(content), 0644) |
| 37 | +} |
| 38 | + |
| 39 | +func (crg *CursorRulesGenerator) generateRulesContent() string { |
| 40 | + var content strings.Builder |
| 41 | + |
| 42 | + // Header |
| 43 | + content.WriteString("# Design Rules\n\n") |
| 44 | + content.WriteString("This file defines the design system rules that all AI agents MUST follow when working on this project.\n\n") |
| 45 | + content.WriteString("**Source:** Generated from DPR (Design Preferences & Requirements) questionnaire\n\n") |
| 46 | + content.WriteString("---\n\n") |
| 47 | + |
| 48 | + // Color Usage Rules |
| 49 | + content.WriteString("## Color Usage Rules\n\n") |
| 50 | + content.WriteString(crg.generateColorRules()) |
| 51 | + content.WriteString("\n\n") |
| 52 | + |
| 53 | + // Typography Rules |
| 54 | + content.WriteString("## Typography Rules\n\n") |
| 55 | + content.WriteString(crg.generateTypographyRules()) |
| 56 | + content.WriteString("\n\n") |
| 57 | + |
| 58 | + // Spacing Rules |
| 59 | + content.WriteString("## Spacing Rules\n\n") |
| 60 | + content.WriteString(crg.generateSpacingRules()) |
| 61 | + content.WriteString("\n\n") |
| 62 | + |
| 63 | + // Component Guidelines |
| 64 | + content.WriteString("## Component Guidelines\n\n") |
| 65 | + content.WriteString(crg.generateComponentGuidelines()) |
| 66 | + content.WriteString("\n\n") |
| 67 | + |
| 68 | + // Responsive Rules |
| 69 | + content.WriteString("## Responsive Rules\n\n") |
| 70 | + content.WriteString(crg.generateResponsiveRules()) |
| 71 | + content.WriteString("\n\n") |
| 72 | + |
| 73 | + // Accessibility Requirements |
| 74 | + content.WriteString("## Accessibility Requirements\n\n") |
| 75 | + content.WriteString(crg.generateAccessibilityRules()) |
| 76 | + content.WriteString("\n\n") |
| 77 | + |
| 78 | + // Code Style |
| 79 | + content.WriteString("## Code Style\n\n") |
| 80 | + content.WriteString(crg.generateCodeStyleRules()) |
| 81 | + content.WriteString("\n\n") |
| 82 | + |
| 83 | + return content.String() |
| 84 | +} |
| 85 | + |
| 86 | +func (crg *CursorRulesGenerator) generateColorRules() string { |
| 87 | + primary := crg.getAnswerString("color_primary", "#667eea") |
| 88 | + secondary := crg.getAnswerString("color_secondary", "#764ba2") |
| 89 | + |
| 90 | + return fmt.Sprintf(`### Primary Colors |
| 91 | +- **Primary Color:** %s - Use for primary actions, links, and brand elements |
| 92 | +- **Secondary Color:** %s - Use for secondary actions and accents |
| 93 | +- **DO NOT** use colors outside the design system |
| 94 | +- **DO NOT** create new colors without updating the design tokens |
| 95 | +
|
| 96 | +### Color Usage |
| 97 | +- Use semantic colors (success, warning, error, info) from design tokens |
| 98 | +- Ensure sufficient contrast ratios (WCAG AA minimum) |
| 99 | +- Test colors in both light and dark modes if applicable |
| 100 | +- Use neutral colors for backgrounds and text`, primary, secondary) |
| 101 | +} |
| 102 | + |
| 103 | +func (crg *CursorRulesGenerator) generateTypographyRules() string { |
| 104 | + style := crg.getAnswerString("typography_style", "Sans-serif") |
| 105 | + importance := crg.getAnswerInt("typography_importance", 3) |
| 106 | + |
| 107 | + rules := fmt.Sprintf(`### Typography System |
| 108 | +- **Font Style:** %s |
| 109 | +- **Importance Level:** %d/5 |
| 110 | +
|
| 111 | +### Type Scale |
| 112 | +Use the following font sizes from design tokens: |
| 113 | +- xs: 0.75rem (12px) |
| 114 | +- sm: 0.875rem (14px) |
| 115 | +- base: 1rem (16px) |
| 116 | +- lg: 1.125rem (18px) |
| 117 | +- xl: 1.25rem (20px) |
| 118 | +- 2xl: 1.5rem (24px) |
| 119 | +- 3xl: 1.875rem (30px) |
| 120 | +- 4xl: 2.25rem (36px) |
| 121 | +- 5xl: 3rem (48px) |
| 122 | +
|
| 123 | +### Line Heights |
| 124 | +- Use line-height values from design tokens |
| 125 | +- Headings: tight (1.25) |
| 126 | +- Body text: normal (1.5) |
| 127 | +- Long-form content: relaxed (1.625) |
| 128 | +
|
| 129 | +### Font Weights |
| 130 | +- Use font weights from design tokens |
| 131 | +- Headings: semibold (600) or bold (700) |
| 132 | +- Body text: normal (400) or medium (500)`, style, importance) |
| 133 | + |
| 134 | + return rules |
| 135 | +} |
| 136 | + |
| 137 | +func (crg *CursorRulesGenerator) generateSpacingRules() string { |
| 138 | + spacing := crg.getAnswerString("layout_spacing", "Moderate") |
| 139 | + |
| 140 | + return fmt.Sprintf(`### Spacing System |
| 141 | +- **Spacing Preference:** %s |
| 142 | +- **Base Unit:** Use spacing values from design-tokens.json |
| 143 | +- **DO NOT** use arbitrary spacing values |
| 144 | +- **DO NOT** use magic numbers (e.g., margin: 17px) |
| 145 | +
|
| 146 | +### Spacing Guidelines |
| 147 | +- Use spacing scale: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64 |
| 148 | +- Maintain consistent spacing rhythm |
| 149 | +- Use larger spacing for section separation |
| 150 | +- Use smaller spacing for related elements |
| 151 | +
|
| 152 | +### Tailwind Utilities |
| 153 | +When using Tailwind CSS, use spacing utilities: |
| 154 | +- p-4, m-4, gap-4, space-y-4, etc. |
| 155 | +- DO NOT use arbitrary values like p-[17px]`, spacing) |
| 156 | +} |
| 157 | + |
| 158 | +func (crg *CursorRulesGenerator) generateComponentGuidelines() string { |
| 159 | + style := crg.getAnswerString("components_style", "Elevated") |
| 160 | + interactivity := crg.getAnswerInt("components_interactivity", 3) |
| 161 | + |
| 162 | + return fmt.Sprintf(`### Component Style |
| 163 | +- **Style:** %s |
| 164 | +- **Interactivity Level:** %d/5 |
| 165 | +
|
| 166 | +### Component Rules |
| 167 | +- All components MUST follow the design system |
| 168 | +- Use design tokens for colors, spacing, typography |
| 169 | +- Maintain consistent styling across components |
| 170 | +- Ensure all components have proper states: |
| 171 | + - Default |
| 172 | + - Hover |
| 173 | + - Active/Focus |
| 174 | + - Disabled |
| 175 | + - Error (if applicable) |
| 176 | +
|
| 177 | +### Component Structure |
| 178 | +- Use consistent border radius from tokens |
| 179 | +- Apply shadows according to component style (%s) |
| 180 | +- Ensure proper focus indicators for accessibility |
| 181 | +- Use semantic HTML elements`, style, interactivity, style) |
| 182 | +} |
| 183 | + |
| 184 | +func (crg *CursorRulesGenerator) generateResponsiveRules() string { |
| 185 | + priority := crg.getAnswerString("responsive_priority", "Mobile, Desktop") |
| 186 | + approach := crg.getAnswerString("responsive_approach", "Mobile-first") |
| 187 | + |
| 188 | + return fmt.Sprintf(`### Responsive Strategy |
| 189 | +- **Device Priority:** %s |
| 190 | +- **Approach:** %s |
| 191 | +
|
| 192 | +### Breakpoints |
| 193 | +Use breakpoints from design tokens: |
| 194 | +- Mobile: 320px |
| 195 | +- Tablet: 768px |
| 196 | +- Desktop: 1024px |
| 197 | +- Large: 1440px |
| 198 | +
|
| 199 | +### Responsive Guidelines |
| 200 | +- Design %s |
| 201 | +- Test on all priority devices |
| 202 | +- Use fluid layouts where possible |
| 203 | +- Ensure touch-friendly interactions on mobile |
| 204 | +- Maintain usability across all screen sizes |
| 205 | +
|
| 206 | +### Tailwind Breakpoints |
| 207 | +When using Tailwind CSS: |
| 208 | +- sm: 640px (use sparingly) |
| 209 | +- md: 768px (tablet) |
| 210 | +- lg: 1024px (desktop) |
| 211 | +- xl: 1280px |
| 212 | +- 2xl: 1536px (large)`, priority, approach, approach) |
| 213 | +} |
| 214 | + |
| 215 | +func (crg *CursorRulesGenerator) generateAccessibilityRules() string { |
| 216 | + importance := crg.getAnswerInt("accessibility_importance", 4) |
| 217 | + requirements := crg.getAnswerString("accessibility_requirements", "") |
| 218 | + |
| 219 | + rules := fmt.Sprintf(`### Accessibility Priority |
| 220 | +- **Importance Level:** %d/5 |
| 221 | +
|
| 222 | +### WCAG Compliance |
| 223 | +- **Minimum:** WCAG 2.1 AA compliance |
| 224 | +- **Target:** WCAG 2.1 AAA where possible |
| 225 | +
|
| 226 | +### Accessibility Requirements |
| 227 | +- **Keyboard Navigation:** All interactive elements must be keyboard accessible |
| 228 | +- **Screen Readers:** Use semantic HTML and ARIA attributes appropriately |
| 229 | +- **Color Contrast:** Minimum 4.5:1 for normal text, 3:1 for large text |
| 230 | +- **Focus Indicators:** Visible focus indicators on all interactive elements |
| 231 | +- **Alt Text:** Provide meaningful alt text for images |
| 232 | +- **Form Labels:** All form inputs must have associated labels |
| 233 | +- **Error Messages:** Clear, accessible error messages |
| 234 | +- **Skip Links:** Provide skip navigation links for keyboard users |
| 235 | +- **ARIA Labels:** Use ARIA labels for complex interactions |
| 236 | +- **Reduced Motion:** Respect prefers-reduced-motion media query |
| 237 | +
|
| 238 | +### Semantic HTML |
| 239 | +- Use proper heading hierarchy (h1, h2, h3, etc.) |
| 240 | +- Use semantic elements (nav, main, article, section, etc.) |
| 241 | +- Use form elements correctly (input, label, fieldset, legend) |
| 242 | +- Use button for actions, not div or span |
| 243 | +
|
| 244 | +### Testing |
| 245 | +- Test with keyboard navigation (Tab, Enter, Space, Arrow keys) |
| 246 | +- Test with screen readers (NVDA, JAWS, VoiceOver) |
| 247 | +- Verify color contrast ratios using tools |
| 248 | +- Test focus indicators visibility |
| 249 | +- Test with zoom up to 200%% |
| 250 | +- Verify all content is accessible without mouse`, importance) |
| 251 | + |
| 252 | + if requirements != "" && requirements != "nil" { |
| 253 | + rules += fmt.Sprintf("\n\n### Specific Requirements\n%s", requirements) |
| 254 | + } |
| 255 | + |
| 256 | + return rules |
| 257 | +} |
| 258 | + |
| 259 | +func (crg *CursorRulesGenerator) generateCodeStyleRules() string { |
| 260 | + return `### Tailwind CSS Utilities |
| 261 | +When using Tailwind CSS, follow these patterns: |
| 262 | +
|
| 263 | +#### Colors |
| 264 | +- Use color utilities from design tokens: text-primary, bg-secondary |
| 265 | +- Use semantic colors: text-success, bg-error, etc. |
| 266 | +- DO NOT use arbitrary colors: text-[#ff0000] |
| 267 | +
|
| 268 | +#### Spacing |
| 269 | +- Use spacing scale: p-4, m-6, gap-8 |
| 270 | +- DO NOT use arbitrary spacing: p-[17px] |
| 271 | +
|
| 272 | +#### Typography |
| 273 | +- Use font size utilities: text-sm, text-lg, text-2xl |
| 274 | +- Use font weight utilities: font-medium, font-bold |
| 275 | +- Use line height utilities: leading-tight, leading-normal |
| 276 | +
|
| 277 | +#### Components |
| 278 | +- Use border radius from tokens: rounded-md, rounded-lg |
| 279 | +- Use shadows: shadow-sm, shadow-md, shadow-lg |
| 280 | +- Use consistent component patterns |
| 281 | +
|
| 282 | +### Code Organization |
| 283 | +- Group related styles together |
| 284 | +- Use consistent naming conventions |
| 285 | +- Comment complex styling decisions |
| 286 | +- Reference design tokens in comments when helpful` |
| 287 | +} |
| 288 | + |
| 289 | +func (crg *CursorRulesGenerator) getAnswerString(key string, defaultValue string) string { |
| 290 | + if val, ok := crg.data.Answers[key]; ok { |
| 291 | + if str, ok := val.(string); ok { |
| 292 | + return str |
| 293 | + } |
| 294 | + return fmt.Sprintf("%v", val) |
| 295 | + } |
| 296 | + return defaultValue |
| 297 | +} |
| 298 | + |
| 299 | +func (crg *CursorRulesGenerator) getAnswerInt(key string, defaultValue int) int { |
| 300 | + if val, ok := crg.data.Answers[key]; ok { |
| 301 | + if num, ok := val.(int); ok { |
| 302 | + return num |
| 303 | + } |
| 304 | + if str, ok := val.(string); ok { |
| 305 | + if num, err := strconv.Atoi(str); err == nil { |
| 306 | + return num |
| 307 | + } |
| 308 | + } |
| 309 | + } |
| 310 | + return defaultValue |
| 311 | +} |
| 312 | + |
0 commit comments