@getcoherent/cli 0.6.7 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
// src/commands/chat/plan-generator.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
// src/agents/design-constraints.ts
|
|
9
|
+
var DESIGN_THINKING = `
|
|
10
|
+
## DESIGN THINKING (answer internally BEFORE writing code)
|
|
11
|
+
|
|
12
|
+
1. PURPOSE \u2014 What is this page's job? (inform / convert / navigate / manage / onboard)
|
|
13
|
+
2. AUDIENCE \u2014 Who uses it? (developer / executive / consumer / admin)
|
|
14
|
+
3. MOOD \u2014 What should the user FEEL? (confident, excited, calm, focused, impressed)
|
|
15
|
+
4. FOCAL POINT \u2014 What ONE element draws the eye first on this page?
|
|
16
|
+
5. RHYTHM \u2014 Where should the page breathe (spacious) vs feel dense (packed with data)?
|
|
17
|
+
|
|
18
|
+
Then commit to:
|
|
19
|
+
- One DOMINANT visual element per page (hero image, large metric, gradient header, feature grid)
|
|
20
|
+
- One ACCENT technique per page (gradient text, colored icon containers, glass card, background pattern)
|
|
21
|
+
- One moment of CONTRAST (large vs small, dense vs spacious, dark vs light section)
|
|
22
|
+
|
|
23
|
+
DO NOT make every section look the same. Vary density, background treatment, and visual weight.
|
|
24
|
+
Every page should have a clear visual hierarchy \u2014 if you squint, the structure should still be obvious.
|
|
25
|
+
`;
|
|
26
|
+
var CORE_CONSTRAINTS = `
|
|
27
|
+
SHADCN/UI DESIGN CONSTRAINTS (MANDATORY \u2014 these rules produce professional UI):
|
|
28
|
+
|
|
29
|
+
TYPOGRAPHY (most impactful rules):
|
|
30
|
+
- Base text: text-sm (14px). NEVER use text-base (16px) as body text.
|
|
31
|
+
- Card/section titles: text-sm font-medium. NEVER text-lg or text-xl on card titles.
|
|
32
|
+
- Page title: text-2xl font-bold tracking-tight (only place for large text).
|
|
33
|
+
- Metric/KPI values: text-2xl font-bold (the ONLY other place for large text).
|
|
34
|
+
- Muted/secondary: text-sm text-muted-foreground (or text-xs text-muted-foreground).
|
|
35
|
+
- Create hierarchy through font WEIGHT (medium \u2192 semibold \u2192 bold), NOT font SIZE.
|
|
36
|
+
|
|
37
|
+
COLORS \u2014 ONLY SEMANTIC TOKENS (zero raw colors):
|
|
38
|
+
- Backgrounds: bg-background, bg-muted, bg-muted/50, bg-card, bg-primary, bg-secondary, bg-destructive.
|
|
39
|
+
- Text: text-foreground (default), text-muted-foreground, text-primary-foreground.
|
|
40
|
+
- Borders: border (no color suffix \u2014 uses CSS variable). border-b for headers.
|
|
41
|
+
- BANNED: bg-gray-*, bg-blue-*, bg-slate-*, text-gray-*, ANY raw Tailwind color. The validator REJECTS these.
|
|
42
|
+
|
|
43
|
+
SPACING (restricted palette \u2014 only multiples of 4px):
|
|
44
|
+
- Page content padding: p-4 lg:p-6. Gap between major sections: gap-6 md:gap-8.
|
|
45
|
+
- Gap inside a section: gap-4 md:gap-6. Card internal gap: gap-2. Max padding: p-6.
|
|
46
|
+
- Prefer gap-* over space-x-* / space-y-* at page/section level.
|
|
47
|
+
|
|
48
|
+
LAYOUT PATTERNS:
|
|
49
|
+
- Stats/KPI grid: grid gap-4 md:grid-cols-2 lg:grid-cols-4
|
|
50
|
+
- Card grid (3 col): grid gap-4 md:grid-cols-3
|
|
51
|
+
- Full-height page: min-h-svh (not min-h-screen)
|
|
52
|
+
- Centered form (login/signup): auth layout handles centering \u2014 just output div w-full max-w-md with Card inside
|
|
53
|
+
- Page content wrapper: flex flex-1 flex-col gap-4 p-4 lg:p-6
|
|
54
|
+
- Responsive: primary md: and lg:. Use sm:/xl: only when genuinely needed. Avoid 2xl:. NEVER arbitrary like min-[800px].
|
|
55
|
+
|
|
56
|
+
COMPONENT IMPORTS (mandatory \u2014 NEVER native HTML):
|
|
57
|
+
- NEVER use native <button>. Always: import { Button } from "@/components/ui/button".
|
|
58
|
+
- NEVER use native <input type="checkbox"> for toggles. Always: import { Checkbox } or { Switch }.
|
|
59
|
+
- NEVER use native <select>. Always: import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }.
|
|
60
|
+
- NEVER use native <input> alone. Always pair with Label.
|
|
61
|
+
- NEVER use native <table>. Always: import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell }.
|
|
62
|
+
SHADCN COMPONENTS ONLY (CRITICAL):
|
|
63
|
+
- ALWAYS import and use shadcn components: Button, Input, Label, Textarea, Badge, Checkbox, Switch, Select, Table, etc.
|
|
64
|
+
- Form field pattern: <div className="space-y-2"><Label htmlFor="x">Name</Label><Input id="x" /></div>
|
|
65
|
+
- Card with form: Card > CardHeader(title+description) > CardContent(form fields) > CardFooter(Button)
|
|
66
|
+
- Button variants: default, secondary, outline, ghost, destructive.
|
|
67
|
+
|
|
68
|
+
LINKS & INTERACTIVE STATES (consistency is critical):
|
|
69
|
+
- Text links (inline): text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground transition-colors
|
|
70
|
+
- ALL links on the SAME page MUST use the SAME style. Never mix underlined and non-underlined text links.
|
|
71
|
+
- ALL Button variants MUST have: hover: state, focus-visible:ring-2 focus-visible:ring-ring, active: state, disabled:opacity-50.
|
|
72
|
+
- ALL interactive elements MUST have visible hover and focus-visible states.
|
|
73
|
+
- CRITICAL: Every <Link> MUST have an href prop. Missing href causes runtime errors. Never use <Link className="..."> or <Button asChild><Link> without href.
|
|
74
|
+
- When shared components exist (@/components/shared/*), ALWAYS import and use them instead of re-implementing similar patterns inline.
|
|
75
|
+
|
|
76
|
+
ICONS:
|
|
77
|
+
- Size: ALWAYS size-4 (16px). Color: ALWAYS text-muted-foreground. Import: ALWAYS from lucide-react.
|
|
78
|
+
- ALWAYS add shrink-0 to icon className to prevent flex containers from squishing them.
|
|
79
|
+
|
|
80
|
+
ANTI-PATTERNS (NEVER DO):
|
|
81
|
+
- text-base as body text \u2192 use text-sm
|
|
82
|
+
- text-lg/xl on card titles \u2192 use text-sm font-medium
|
|
83
|
+
- Raw colors (bg-gray-100, text-blue-600) \u2192 use semantic tokens
|
|
84
|
+
- shadow-md or heavier \u2192 use shadow-sm or none
|
|
85
|
+
- padding > p-6 \u2192 max is p-6
|
|
86
|
+
- Raw <button> or <input> \u2192 always use Button, Input from @/components/ui/
|
|
87
|
+
- Mixing link styles on the same page \u2192 pick ONE style
|
|
88
|
+
- Interactive elements without hover/focus states
|
|
89
|
+
|
|
90
|
+
COMPONENT VARIANT RULES (CRITICAL):
|
|
91
|
+
- NEVER use <Button> with custom bg-*/text-* classes for navigation or tabs without variant="ghost".
|
|
92
|
+
The default Button variant sets bg-primary, so custom text-muted-foreground or bg-accent classes will conflict.
|
|
93
|
+
BAD: <Button className="text-muted-foreground hover:bg-accent">Tab</Button>
|
|
94
|
+
GOOD: <Button variant="ghost" className="text-muted-foreground">Tab</Button>
|
|
95
|
+
BEST: Use shadcn <Tabs> / <TabsList> / <TabsTrigger> for tab-style navigation.
|
|
96
|
+
- For sidebar navigation buttons, ALWAYS use variant="ghost" with active-state classes:
|
|
97
|
+
<Button variant="ghost" className={cn("w-full justify-start", isActive && "bg-accent font-medium")}>
|
|
98
|
+
- For filter toggle buttons, use variant={isActive ? 'default' : 'outline'} \u2014 NOT className toggling.
|
|
99
|
+
|
|
100
|
+
CONTENT (zero placeholders):
|
|
101
|
+
- NEVER: "Lorem ipsum", "Card content", "Description here"
|
|
102
|
+
- ALWAYS: Real, contextual content. Realistic metric names, values, dates.
|
|
103
|
+
`;
|
|
104
|
+
var DESIGN_QUALITY_COMMON = `
|
|
105
|
+
## DESIGN QUALITY \u2014 COMMON
|
|
106
|
+
|
|
107
|
+
### Typography Hierarchy
|
|
108
|
+
- Page headline (h1): text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-[1.1]
|
|
109
|
+
- Section titles (h2): text-2xl md:text-3xl font-bold
|
|
110
|
+
- Card titles (h3): text-sm font-semibold (never text-base or text-lg)
|
|
111
|
+
- Body text: text-sm text-muted-foreground leading-relaxed
|
|
112
|
+
- The SIZE DIFFERENCE between levels must be dramatic, not subtle
|
|
113
|
+
|
|
114
|
+
### Visual Depth & Layers
|
|
115
|
+
- Cards: bg-card border border-border/15 rounded-xl (not rounded-md)
|
|
116
|
+
- Cards on dark pages: bg-zinc-900/50 border-border/10 backdrop-blur-sm
|
|
117
|
+
- Cards MUST have hover state: hover:border-border/30 transition-colors
|
|
118
|
+
- Sections alternate between bg-background and bg-muted/5 for rhythm
|
|
119
|
+
- Section dividers: border-t border-border/10 (subtle, not heavy)
|
|
120
|
+
|
|
121
|
+
### Buttons with Icons
|
|
122
|
+
- Buttons containing text + icon: ALWAYS use inline-flex items-center gap-2 whitespace-nowrap
|
|
123
|
+
- Icon inside button: h-4 w-4 (never larger), placed AFTER text for arrows, BEFORE text for action icons
|
|
124
|
+
- NEVER let button content wrap to multiple lines \u2014 use whitespace-nowrap on the Button component
|
|
125
|
+
- CTA buttons: use the Button component, NEVER raw <button> or <a> styled as button
|
|
126
|
+
|
|
127
|
+
### Accent Color Discipline
|
|
128
|
+
- ONE accent color per page (primary or emerald-400)
|
|
129
|
+
- Use for: CTAs, terminal text, check icons, feature icon backgrounds, active states
|
|
130
|
+
- NEVER mix blue + purple + emerald on same page
|
|
131
|
+
- Badge: outline style (border-border/30 bg-transparent) not filled color
|
|
132
|
+
- Status icons: text-emerald-400 for positive, text-red-400 for negative
|
|
133
|
+
|
|
134
|
+
### Dark Theme Implementation
|
|
135
|
+
- html element: className="dark"
|
|
136
|
+
- Background: use CSS variables from globals.css dark section
|
|
137
|
+
- Text: text-foreground for primary, text-muted-foreground for secondary
|
|
138
|
+
- NEVER hardcode dark colors (bg-gray-900) \u2014 always use semantic tokens
|
|
139
|
+
- Cards and elevated elements: slightly lighter than background (bg-card or bg-zinc-900/50)
|
|
140
|
+
`;
|
|
141
|
+
var DESIGN_QUALITY_MARKETING = `
|
|
142
|
+
## DESIGN QUALITY \u2014 MARKETING PAGES
|
|
143
|
+
|
|
144
|
+
### Spacing Rhythm (3 distinct levels)
|
|
145
|
+
- Between sections: py-20 md:py-28 (generous)
|
|
146
|
+
- Within sections (title to content): mb-12 md:mb-16
|
|
147
|
+
- Within cards: p-6 (compact)
|
|
148
|
+
- Between cards in grid: gap-5 (tight)
|
|
149
|
+
- NEVER uniform spacing everywhere \u2014 contrast creates rhythm
|
|
150
|
+
|
|
151
|
+
### Hero headline
|
|
152
|
+
- Landing/marketing hero headline: text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-[1.05]
|
|
153
|
+
|
|
154
|
+
### Icons in Feature Cards
|
|
155
|
+
- Wrap in colored container: bg-primary/10 rounded-lg p-2.5
|
|
156
|
+
- Icon color: text-primary (or text-emerald-400 for dark themes)
|
|
157
|
+
- Icon size: h-5 w-5 inside the container
|
|
158
|
+
- NEVER bare icons floating in a card without container
|
|
159
|
+
|
|
160
|
+
### Terminal / Code Blocks
|
|
161
|
+
- Background: bg-zinc-950 (near-black, not gray)
|
|
162
|
+
- Border: border-border/10 rounded-xl
|
|
163
|
+
- Text: font-mono text-sm text-emerald-400
|
|
164
|
+
- Prompt: text-emerald-500 "$ " prefix (green dollar sign)
|
|
165
|
+
- Title bar (optional): flex with 3 dots (bg-zinc-700 rounded-full w-2.5 h-2.5) + title text-zinc-500 text-[11px]
|
|
166
|
+
- Copy button: text-zinc-500 hover:text-zinc-300
|
|
167
|
+
|
|
168
|
+
### Hero Section
|
|
169
|
+
- Minimum height: min-h-[80vh] flex items-center justify-center
|
|
170
|
+
- Content: centered, max-w-3xl, flex flex-col items-center text-center gap-8
|
|
171
|
+
- Badge above headline: small, outline, text-xs tracking-wide
|
|
172
|
+
- Gradient text on key phrase: bg-gradient-to-r from-white to-zinc-500 bg-clip-text text-transparent
|
|
173
|
+
- Subheadline: text-muted-foreground max-w-2xl text-base md:text-lg
|
|
174
|
+
- CTA row: flex items-center gap-4
|
|
175
|
+
|
|
176
|
+
### Comparison Sections (before/after, with/without)
|
|
177
|
+
- Two cards side by side: grid md:grid-cols-2 gap-6
|
|
178
|
+
- Negative card: neutral border, items with X icon text-red-400
|
|
179
|
+
- Positive card: accent border (border-emerald-500/20), items with Check icon text-emerald-400
|
|
180
|
+
- Header of each card: text-sm font-semibold uppercase tracking-wider
|
|
181
|
+
|
|
182
|
+
### Step/Process Sections
|
|
183
|
+
- Numbered steps: circle with border, number inside (w-10 h-10 rounded-full border border-border/30 text-sm)
|
|
184
|
+
- Label above: text-xs font-semibold tracking-widest uppercase text-muted-foreground
|
|
185
|
+
|
|
186
|
+
### Footer
|
|
187
|
+
- Minimal: border-t border-border/10, py-10
|
|
188
|
+
- Content: text-sm text-muted-foreground
|
|
189
|
+
- Links: hover:text-foreground transition-colors
|
|
190
|
+
- Layout: flex justify-between on desktop, stack on mobile
|
|
191
|
+
|
|
192
|
+
NEVER include app-style elements (sidebar widgets, data tables, filters) on marketing pages.
|
|
193
|
+
`;
|
|
194
|
+
var DESIGN_QUALITY_APP = `
|
|
195
|
+
## DESIGN QUALITY \u2014 APP PAGES
|
|
196
|
+
|
|
197
|
+
### Spacing
|
|
198
|
+
- gap-4 md:gap-6 between sections
|
|
199
|
+
- p-4 lg:p-6 content padding
|
|
200
|
+
- Within cards: p-4 to p-6 (compact)
|
|
201
|
+
- Between cards in grid: gap-4 (tight)
|
|
202
|
+
|
|
203
|
+
### Layout
|
|
204
|
+
- Data tables, card grids, filters, stat rows
|
|
205
|
+
- Page wrapper: flex flex-1 flex-col gap-4 p-4 lg:p-6
|
|
206
|
+
- Stats grid: grid gap-4 md:grid-cols-2 lg:grid-cols-4
|
|
207
|
+
- Content: functional, scannable, data-dense
|
|
208
|
+
|
|
209
|
+
### Toolbars & Filters
|
|
210
|
+
- Filter row: flex flex-wrap items-center gap-2 (plain div, NOT inside a Card)
|
|
211
|
+
- Search input: MUST use flex-1 to fill remaining horizontal space. NEVER fixed-width search.
|
|
212
|
+
- Filters/selects: fixed width (w-[180px] or auto), do NOT flex-grow
|
|
213
|
+
- On mobile (sm:): search full width, filters wrap to next line
|
|
214
|
+
- Do NOT wrap search/filter toolbars in Card components. They are plain flex rows above content.
|
|
215
|
+
|
|
216
|
+
NEVER include marketing sections (hero, pricing, testimonials) on app pages.
|
|
217
|
+
`;
|
|
218
|
+
var DESIGN_QUALITY_AUTH = `
|
|
219
|
+
## DESIGN QUALITY \u2014 AUTH PAGES
|
|
220
|
+
|
|
221
|
+
### Layout
|
|
222
|
+
- The auth layout ALREADY provides centering (flex items-center justify-center min-h-svh). Do NOT add your own centering wrapper or min-h-svh.
|
|
223
|
+
- Just output: <div className="w-full max-w-md"> containing a Card
|
|
224
|
+
- Card width: w-full max-w-md
|
|
225
|
+
- No navigation, no section containers, no sidebar
|
|
226
|
+
- Single focused form with clear CTA
|
|
227
|
+
- Card \u2192 CardHeader (title + description) \u2192 CardContent (form with space-y-4) \u2192 CardFooter (link to other auth page)
|
|
228
|
+
|
|
229
|
+
### Form Spacing
|
|
230
|
+
- Form fields inside CardContent: space-y-4 between field groups
|
|
231
|
+
- Ensure visible gap between the last input field and the submit button (use space-y-4 or space-y-6)
|
|
232
|
+
- Each field group (Label + Input): space-y-2
|
|
233
|
+
|
|
234
|
+
NEVER include navigation bars, sidebars, or multi-section layouts on auth pages.
|
|
235
|
+
`;
|
|
236
|
+
var DESIGN_QUALITY_CRITICAL = `
|
|
237
|
+
## CRITICAL CODE RULES (violations will be auto-corrected)
|
|
238
|
+
- Every lucide-react icon MUST have className="... shrink-0" to prevent flex squishing
|
|
239
|
+
- Button with asChild wrapping Link: the inner element MUST have className="inline-flex items-center gap-2"
|
|
240
|
+
- NEVER use raw Tailwind colors (bg-blue-500, text-gray-600). ONLY semantic tokens: bg-primary, text-muted-foreground, etc.
|
|
241
|
+
- <Link> and <a> MUST always have an href attribute. Never omit href.
|
|
242
|
+
- CardTitle: NEVER add text-xl, text-2xl, text-lg. CardTitle is text-sm font-medium by default.
|
|
243
|
+
`;
|
|
244
|
+
function getDesignQualityForType(type) {
|
|
245
|
+
switch (type) {
|
|
246
|
+
case "marketing":
|
|
247
|
+
return DESIGN_QUALITY_MARKETING + DESIGN_QUALITY_CRITICAL;
|
|
248
|
+
case "app":
|
|
249
|
+
return DESIGN_QUALITY_APP + DESIGN_QUALITY_CRITICAL;
|
|
250
|
+
case "auth":
|
|
251
|
+
return DESIGN_QUALITY_AUTH + DESIGN_QUALITY_CRITICAL;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function inferPageTypeFromRoute(route) {
|
|
255
|
+
const slug = route.replace(/^\//, "").split("/")[0] || "";
|
|
256
|
+
const authSlugs = /* @__PURE__ */ new Set([
|
|
257
|
+
"login",
|
|
258
|
+
"register",
|
|
259
|
+
"sign-up",
|
|
260
|
+
"signup",
|
|
261
|
+
"sign-in",
|
|
262
|
+
"signin",
|
|
263
|
+
"forgot-password",
|
|
264
|
+
"reset-password"
|
|
265
|
+
]);
|
|
266
|
+
const marketingSlugs = /* @__PURE__ */ new Set(["pricing", "features", "about", "blog", "contact", "terms", "privacy"]);
|
|
267
|
+
if (authSlugs.has(slug)) return "auth";
|
|
268
|
+
if (marketingSlugs.has(slug) || slug === "") return "marketing";
|
|
269
|
+
return "app";
|
|
270
|
+
}
|
|
271
|
+
var DESIGN_QUALITY = `${DESIGN_QUALITY_COMMON}
|
|
272
|
+
${DESIGN_QUALITY_MARKETING}`;
|
|
273
|
+
var VISUAL_DEPTH = `
|
|
274
|
+
## VISUAL DEPTH TECHNIQUES (pick 1-3 per page based on context)
|
|
275
|
+
|
|
276
|
+
### Gradient Techniques
|
|
277
|
+
- Key phrase emphasis: bg-gradient-to-r from-foreground to-foreground/60 bg-clip-text text-transparent
|
|
278
|
+
- Section background: bg-gradient-to-b from-background to-muted/30 (subtle, adds depth)
|
|
279
|
+
- Accent glow: bg-gradient-to-r from-primary/10 via-transparent to-primary/5
|
|
280
|
+
|
|
281
|
+
### Depth & Layering
|
|
282
|
+
- Glass cards (landing/hero): bg-card/80 backdrop-blur-sm border-border/40
|
|
283
|
+
- Floating accent blobs: absolute bg-primary/5 blur-3xl rounded-full -z-10 (behind content)
|
|
284
|
+
- Elevated cards on hover: hover:-translate-y-0.5 hover:shadow-md transition-all duration-200
|
|
285
|
+
- Section rhythm: alternate bg-background and bg-muted/5 between sections
|
|
286
|
+
|
|
287
|
+
### Micro-interactions (hover/focus only)
|
|
288
|
+
- Card hover lift: hover:-translate-y-0.5 transition-transform duration-200
|
|
289
|
+
- Icon container glow: group-hover:bg-primary/15 transition-colors
|
|
290
|
+
- Button shimmer: relative overflow-hidden + animated pseudo-element on hover
|
|
291
|
+
- Link underline reveal: underline-offset-4 decoration-transparent hover:decoration-foreground transition-all
|
|
292
|
+
|
|
293
|
+
### Context Budget (how many techniques to use)
|
|
294
|
+
- Dashboard / Settings / Admin: 0-1 techniques. Clean, functional, fast to scan.
|
|
295
|
+
- Landing / Marketing / Hero: 2-3 techniques. Impressive, memorable, worth scrolling.
|
|
296
|
+
- Product pages (pricing, features, about): 1-2 techniques. Professional with selective wow.
|
|
297
|
+
- Auth pages (login, signup): 0-1 techniques. Trustworthy, focused, minimal.
|
|
298
|
+
|
|
299
|
+
### What Makes a Page Memorable
|
|
300
|
+
Ask: "If someone sees this page for 3 seconds, what will they remember?"
|
|
301
|
+
- A landing page \u2192 the hero gradient and headline
|
|
302
|
+
- A dashboard \u2192 the bold stat numbers and clean data density
|
|
303
|
+
- A pricing page \u2192 the highlighted tier standing out from the rest
|
|
304
|
+
- A settings page \u2192 nothing flashy \u2014 that IS the correct answer
|
|
305
|
+
`;
|
|
306
|
+
var RULES_FORMS = `
|
|
307
|
+
FORM RULES:
|
|
308
|
+
- Label above Input (never beside on mobile; beside OK on desktop only for short fields).
|
|
309
|
+
- space-y-2 within field group (Label + Input + optional description).
|
|
310
|
+
- space-y-6 between field groups.
|
|
311
|
+
- CardFooter for form actions (Save, Cancel).
|
|
312
|
+
- Switch for boolean toggles; do NOT use Checkbox for on/off settings.
|
|
313
|
+
- Select for 3+ options; RadioGroup for 2\u20133 options.
|
|
314
|
+
- Two columns on desktop for related fields: md:grid-cols-2. Single column on mobile always.
|
|
315
|
+
|
|
316
|
+
RADIOGROUP:
|
|
317
|
+
- Use for 2-3 mutually exclusive options. For 4+, use Select.
|
|
318
|
+
- Pattern: <RadioGroup defaultValue="option1"><div className="flex items-center space-x-2"><RadioGroupItem value="option1" id="r1" /><Label htmlFor="r1">Option 1</Label></div></RadioGroup>
|
|
319
|
+
- Vertical layout by default (space-y-2). Horizontal only for 2 options.
|
|
320
|
+
- Label always to the right of the radio, using htmlFor.
|
|
321
|
+
|
|
322
|
+
FORM VALIDATION (inline errors):
|
|
323
|
+
- Error text: text-sm text-destructive, directly below the input (inside the space-y-2 group).
|
|
324
|
+
- Pattern: <div className="space-y-2"><Label htmlFor="email">Email</Label><Input id="email" className="border-destructive" /><p className="text-sm text-destructive">Please enter a valid email address.</p></div>
|
|
325
|
+
- Show errors after blur or on submit, not on every keystroke.
|
|
326
|
+
- Highlight the field: add border-destructive to the Input.
|
|
327
|
+
- Error summary (form level): <Alert variant="destructive"> at top of form listing all errors.
|
|
328
|
+
- NEVER use toast for form validation. NEVER use alert() or browser dialogs.
|
|
329
|
+
|
|
330
|
+
MULTI-STEP FORMS / STEPPER:
|
|
331
|
+
- Step indicator: <div className="flex items-center gap-2"> with numbered circles.
|
|
332
|
+
- Active step: bg-primary text-primary-foreground size-8 rounded-full flex items-center justify-center text-sm font-medium.
|
|
333
|
+
- Completed step: same as active but with CheckCircle icon instead of number.
|
|
334
|
+
- Upcoming step: bg-muted text-muted-foreground.
|
|
335
|
+
- Connector between steps: <div className="h-px flex-1 bg-border" /> (horizontal) or <div className="w-px h-8 bg-border mx-auto" /> (vertical).
|
|
336
|
+
- Navigation: "Back" (variant="outline") and "Next"/"Complete" (variant="default") buttons at bottom.
|
|
337
|
+
- Validate current step before allowing Next.
|
|
338
|
+
|
|
339
|
+
FILE UPLOAD / DROPZONE:
|
|
340
|
+
- Pattern: dashed border area with icon + text. <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-8 text-center hover:border-muted-foreground/50 transition-colors">
|
|
341
|
+
- Icon: <Upload className="size-8 text-muted-foreground mb-2" />
|
|
342
|
+
- Text: <p className="text-sm text-muted-foreground">Drag and drop or <span className="text-foreground underline cursor-pointer">browse</span></p>
|
|
343
|
+
- Accepted formats hint: <p className="text-xs text-muted-foreground mt-1">PNG, JPG up to 10MB</p>
|
|
344
|
+
- Active drag state: border-primary bg-primary/5.
|
|
345
|
+
|
|
346
|
+
MULTI-SELECT / TAG INPUT:
|
|
347
|
+
- Tags inside input area: <div className="flex flex-wrap gap-1 rounded-md border p-2">
|
|
348
|
+
- Each tag: <Badge variant="secondary" className="gap-1">{name}<X className="size-3 cursor-pointer" /></Badge>
|
|
349
|
+
- Input at the end: <Input className="flex-1 border-0 p-0 focus-visible:ring-0" placeholder="Add..." />
|
|
350
|
+
- Max display: show 5 tags, then "+N more" badge.
|
|
351
|
+
`;
|
|
352
|
+
var RULES_DATA_DISPLAY = `
|
|
353
|
+
DATA DISPLAY RULES:
|
|
354
|
+
|
|
355
|
+
STAT / METRIC CARDS:
|
|
356
|
+
- Pattern: Card > CardHeader(flex flex-row items-center justify-between space-y-0 pb-2) > CardTitle(text-sm font-medium) + Icon(size-4 text-muted-foreground) ; CardContent > metric(text-2xl font-bold) + change(text-xs text-muted-foreground).
|
|
357
|
+
- Grid: grid gap-4 md:grid-cols-2 lg:grid-cols-4.
|
|
358
|
+
- Trend up: text-emerald-600 (light) / text-emerald-400 (dark) \u2014 exception to semantic-only rule for trend indicators.
|
|
359
|
+
- Trend down: text-destructive.
|
|
360
|
+
- Trend icon: ArrowUp / ArrowDown className="size-3 inline mr-1".
|
|
361
|
+
- No actions on stat cards. Click entire card to drill down (if applicable).
|
|
362
|
+
|
|
363
|
+
TABLE ROW PATTERNS:
|
|
364
|
+
- Row hover: <TableRow className="hover:bg-muted/50"> on every data row.
|
|
365
|
+
- Action column: last column, right-aligned. Use DropdownMenu with MoreHorizontal icon trigger, NOT inline buttons.
|
|
366
|
+
- Action column header: <TableHead className="w-[50px]"></TableHead> (no label text).
|
|
367
|
+
- Sortable columns: <TableHead className="cursor-pointer select-none"> with ChevronDown/ChevronUp icon, size-4 ml-1.
|
|
368
|
+
- Selected row: bg-muted (single) or checkbox column for multi-select.
|
|
369
|
+
- Responsive: ALWAYS wrap Table in <div className="overflow-x-auto">.
|
|
370
|
+
- Empty table: <TableRow><TableCell colSpan={columns} className="h-24 text-center text-sm text-muted-foreground">No results.</TableCell></TableRow>
|
|
371
|
+
|
|
372
|
+
PAGINATION:
|
|
373
|
+
- Use shadcn Pagination component. Never build custom.
|
|
374
|
+
- Pattern: Previous + page numbers + Next. Show max 5 page numbers with ellipsis.
|
|
375
|
+
- Placement: below the list/table, centered. <div className="flex justify-center mt-4">
|
|
376
|
+
- For short lists (<20 items): no pagination. For feeds: "Load more" button (variant="outline" className="w-full").
|
|
377
|
+
|
|
378
|
+
EMPTY STATES:
|
|
379
|
+
- Pattern: <div className="flex flex-col items-center justify-center py-12 text-center">
|
|
380
|
+
- Icon: size-12 text-muted-foreground mb-4 (larger than normal icons \u2014 exception).
|
|
381
|
+
- Title: <h3 className="text-lg font-semibold">No projects yet</h3>
|
|
382
|
+
- Description: <p className="text-sm text-muted-foreground mt-1 max-w-sm">Create your first project to get started.</p>
|
|
383
|
+
- CTA: <Button className="mt-4">Create project</Button>
|
|
384
|
+
- Search empty: "No results for 'query'. Try different keywords." + clear search button.
|
|
385
|
+
- Filtered empty: "No items match your filters." + reset filters button.
|
|
386
|
+
|
|
387
|
+
DATA FORMATTING:
|
|
388
|
+
- Dates: use relative for recent ("2 hours ago", "Yesterday"), absolute for older ("Jan 26, 2026"). Never ISO format in UI.
|
|
389
|
+
- Numbers: use Intl.NumberFormat or toLocaleString(). 1,234 not 1234. Always include separator for 1000+.
|
|
390
|
+
- Currency: $1,234.56 format. Symbol before number. Two decimal places for amounts.
|
|
391
|
+
- Percentages: one decimal max. "+12.5%" with sign for changes.
|
|
392
|
+
|
|
393
|
+
STATUS INDICATORS (dot + text):
|
|
394
|
+
- Pattern: <div className="flex items-center gap-2"><div className="size-2 rounded-full bg-emerald-500" /><span className="text-sm">Active</span></div>
|
|
395
|
+
- Colors: bg-emerald-500 (active/online), bg-destructive (error/offline), bg-yellow-500 (warning), bg-muted-foreground (inactive).
|
|
396
|
+
- Alternative: use Badge variants for status in tables/lists (preferred over dots).
|
|
397
|
+
|
|
398
|
+
TREND INDICATORS:
|
|
399
|
+
- Up (positive): <span className="text-sm text-emerald-600 flex items-center"><ArrowUp className="size-3 mr-1" />12.5%</span>
|
|
400
|
+
- Down (negative): <span className="text-sm text-destructive flex items-center"><ArrowDown className="size-3 mr-1" />3.2%</span>
|
|
401
|
+
- Neutral: <span className="text-sm text-muted-foreground">0%</span>
|
|
402
|
+
- Always include arrow icon + sign (+ or -) + percentage.
|
|
403
|
+
|
|
404
|
+
TIMELINE / ACTIVITY LOG:
|
|
405
|
+
- Vertical layout: <div className="space-y-4"> with left border.
|
|
406
|
+
- Each entry: <div className="flex gap-4"><div className="flex flex-col items-center"><div className="size-2 rounded-full bg-primary" /><div className="flex-1 w-px bg-border" /></div><div className="flex-1 pb-4"><p className="text-sm font-medium">Event title</p><p className="text-xs text-muted-foreground">2 hours ago</p></div></div>
|
|
407
|
+
- Last item: no connector line (remove w-px div).
|
|
408
|
+
|
|
409
|
+
AVATAR GROUP (stacked):
|
|
410
|
+
- Pattern: <div className="flex -space-x-2"><Avatar className="ring-2 ring-background">...</Avatar>...</div>
|
|
411
|
+
- Max visible: 4 avatars, then <div className="flex size-8 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background">+3</div>
|
|
412
|
+
- Size: size-8 for all items in the stack. ring-2 ring-background to create visual separation.
|
|
413
|
+
|
|
414
|
+
TRUNCATION:
|
|
415
|
+
- Single line: className="truncate" (overflow-hidden text-ellipsis whitespace-nowrap).
|
|
416
|
+
- Multi-line: className="line-clamp-2" (or line-clamp-3). Needs Tailwind plugin or native CSS.
|
|
417
|
+
- When to truncate: card descriptions (2 lines), table cells (single line), list item subtitles (1-2 lines).
|
|
418
|
+
- Always set title={fullText} for accessibility on truncated text.
|
|
419
|
+
|
|
420
|
+
SEARCH INPUT:
|
|
421
|
+
- Pattern: <div className="relative"><Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /><Input placeholder="Search..." className="pl-9" /></div>
|
|
422
|
+
- Placement: top of the list/table it filters, max-w-sm.
|
|
423
|
+
- Clear button: X icon on right when value is not empty.
|
|
424
|
+
- Debounce: 300ms on keystroke. No search button.
|
|
425
|
+
`;
|
|
426
|
+
var RULES_NAVIGATION = `
|
|
427
|
+
NAVIGATION RULES:
|
|
428
|
+
|
|
429
|
+
NAVIGATION ACTIVE STATE (one pattern only):
|
|
430
|
+
- Sidebar nav: active item gets bg-accent text-accent-foreground font-medium. Inactive: text-muted-foreground hover:text-foreground hover:bg-accent.
|
|
431
|
+
- Top nav: active item gets text-foreground font-medium. Inactive: text-muted-foreground hover:text-foreground. No underline, no bg.
|
|
432
|
+
- Tab nav: use shadcn Tabs \u2014 default active styling.
|
|
433
|
+
- NEVER mix approaches. bg-accent for sidebar, font-weight for top nav.
|
|
434
|
+
|
|
435
|
+
BREADCRUMB:
|
|
436
|
+
- Use on any page 2+ levels deep in navigation hierarchy.
|
|
437
|
+
- Component: shadcn Breadcrumb. Never build custom.
|
|
438
|
+
- Pattern: <Breadcrumb><BreadcrumbList><BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>Current</BreadcrumbPage></BreadcrumbItem></BreadcrumbList></Breadcrumb>
|
|
439
|
+
- Current page: text-foreground (no link). Parent pages: text-muted-foreground with hover.
|
|
440
|
+
- Placement: top of page content, before page title.
|
|
441
|
+
- Max items: show first, last, up to 2 middle. Use BreadcrumbEllipsis for deeper paths.
|
|
442
|
+
|
|
443
|
+
IN-PAGE NAVIGATION (e.g. Settings tabs, Profile sections):
|
|
444
|
+
- For in-page navigation with <= 5 items, use shadcn Tabs (vertical orientation via orientation="vertical").
|
|
445
|
+
- Do NOT use the full Sidebar component for in-page navigation.
|
|
446
|
+
- Tabs variant for settings: left-side vertical tabs with TabsList + TabsContent.
|
|
447
|
+
- Pattern: <Tabs defaultValue="general" orientation="vertical" className="flex gap-6"><TabsList className="flex-col h-auto"><TabsTrigger value="general">General</TabsTrigger></TabsList><TabsContent value="general">...</TabsContent></Tabs>
|
|
448
|
+
|
|
449
|
+
SIDEBAR LAYOUT:
|
|
450
|
+
- Use shadcn Sidebar component (SidebarProvider, Sidebar, SidebarContent, SidebarMenu, etc.).
|
|
451
|
+
- Desktop: collapsible sidebar + main content. Mobile: Sheet from left triggered by SidebarTrigger.
|
|
452
|
+
- Sidebar structure: SidebarHeader (logo/brand) \u2192 SidebarContent (SidebarGroup with SidebarMenu) \u2192 SidebarFooter (user/settings).
|
|
453
|
+
- Each nav item: <SidebarMenuItem><SidebarMenuButton asChild isActive={active}><Link href="...">Label</Link></SidebarMenuButton></SidebarMenuItem>
|
|
454
|
+
- Active: add bg-accent text-accent-foreground font-medium.
|
|
455
|
+
- Sidebar collapse: hidden on mobile (md:flex), Sheet trigger visible (md:hidden).
|
|
456
|
+
|
|
457
|
+
RESPONSIVE SIDEBAR:
|
|
458
|
+
- Desktop (md+): sidebar visible, main content offset.
|
|
459
|
+
- Mobile (<md): sidebar hidden, hamburger menu in top bar. Opens Sheet from left with full nav.
|
|
460
|
+
- Trigger: <Button variant="ghost" size="icon" className="md:hidden"><Menu className="size-5" /></Button>
|
|
461
|
+
- Sheet closes on nav item click (route change).
|
|
462
|
+
|
|
463
|
+
NAVIGATION MENU (top nav with dropdowns):
|
|
464
|
+
- Use shadcn NavigationMenu for top nav with submenus.
|
|
465
|
+
- Max top-level items: 5-7. For more, group under dropdowns.
|
|
466
|
+
- Keep consistent with top nav active state (text-foreground font-medium for active).
|
|
467
|
+
`;
|
|
468
|
+
var RULES_OVERLAYS = `
|
|
469
|
+
OVERLAY / MODAL RULES:
|
|
470
|
+
|
|
471
|
+
DIALOG / MODAL:
|
|
472
|
+
- Small dialogs (confirm, delete): max-w-sm. One action + one cancel button.
|
|
473
|
+
- Standard dialogs (forms, details): max-w-md (default). Title + content + footer.
|
|
474
|
+
- Large dialogs (complex forms, previews): max-w-lg. Use sparingly.
|
|
475
|
+
- Internal layout: DialogHeader (DialogTitle + DialogDescription) \u2192 content with space-y-4 \u2192 DialogFooter (buttons right-aligned).
|
|
476
|
+
- Footer buttons: cancel on left (variant="outline"), primary action on right.
|
|
477
|
+
- NEVER use Dialog for success messages \u2014 use toast instead.
|
|
478
|
+
- Destructive: primary button is variant="destructive".
|
|
479
|
+
|
|
480
|
+
CONFIRMATION DIALOG (delete/destructive):
|
|
481
|
+
- Title: action-specific ("Delete project?" not "Are you sure?").
|
|
482
|
+
- Description: explain consequences. "This will permanently delete 'Project Alpha' and all its data. This action cannot be undone."
|
|
483
|
+
- Buttons: <Button variant="outline">Cancel</Button> <Button variant="destructive">Delete project</Button>
|
|
484
|
+
- NEVER auto-close. Wait for explicit user action.
|
|
485
|
+
- Pattern: DialogHeader(DialogTitle + DialogDescription) \u2192 DialogFooter(Cancel + Destructive).
|
|
486
|
+
|
|
487
|
+
DROPDOWN MENU:
|
|
488
|
+
- Item text: text-sm. Icon before text: size-4 mr-2.
|
|
489
|
+
- Group related items with DropdownMenuSeparator.
|
|
490
|
+
- Destructive item: className="text-destructive" at bottom, separated.
|
|
491
|
+
- NON-DESTRUCTIVE items: NEVER apply text color classes. Use default text-foreground. No text-amber, text-orange, text-yellow on menu items.
|
|
492
|
+
- Keyboard shortcut hint: <DropdownMenuShortcut>\u2318K</DropdownMenuShortcut>.
|
|
493
|
+
- Max items without scroll: 8. For more, use Command palette.
|
|
494
|
+
- Trigger: Button variant="ghost" size="icon" for icon-only, variant="outline" for labeled.
|
|
495
|
+
|
|
496
|
+
SHEET (SIDE PANEL):
|
|
497
|
+
- Use Sheet for: filters, mobile navigation, detail preview, secondary forms.
|
|
498
|
+
- Use Dialog for: confirmations, focused tasks, blocking actions.
|
|
499
|
+
- Default side: right. Left only for navigation drawers on mobile.
|
|
500
|
+
- Width: default (max-w-sm). Wider: className="w-[400px] sm:w-[540px]". Never full-width.
|
|
501
|
+
- Internal layout: SheetHeader \u2192 ScrollArea for content \u2192 SheetFooter for actions.
|
|
502
|
+
- Mobile nav: Sheet from left, close on route change.
|
|
503
|
+
|
|
504
|
+
POPOVER vs DROPDOWN vs DIALOG:
|
|
505
|
+
- Popover: small forms (1-3 fields), color pickers, date pickers, filters.
|
|
506
|
+
- DropdownMenu: action lists (Edit, Delete, Share).
|
|
507
|
+
- Dialog: focused tasks that need full attention.
|
|
508
|
+
- RULE: list of clickable items \u2192 DropdownMenu. Interactive controls \u2192 Popover. Complex/blocking \u2192 Dialog.
|
|
509
|
+
- Popover width: min-w-[200px] to max-w-[320px]. Never wider.
|
|
510
|
+
|
|
511
|
+
TOOLTIP:
|
|
512
|
+
- Use for icon-only buttons and truncated text. NEVER for critical information.
|
|
513
|
+
- Content: text-xs, max one line.
|
|
514
|
+
- Wrap the page or layout in a single <TooltipProvider>.
|
|
515
|
+
|
|
516
|
+
COMMAND PALETTE:
|
|
517
|
+
- Use shadcn Command (cmdk) for global search + actions. Trigger: \u2318K.
|
|
518
|
+
- Groups: <CommandGroup heading="Actions">, <CommandGroup heading="Pages">.
|
|
519
|
+
- Items: <CommandItem><Icon className="mr-2 size-4" />Label<CommandShortcut>\u2318N</CommandShortcut></CommandItem>
|
|
520
|
+
- Empty: <CommandEmpty>No results found.</CommandEmpty>
|
|
521
|
+
- Use when dropdown menu has 8+ items or app has 5+ pages to navigate.
|
|
522
|
+
|
|
523
|
+
DRAWER (mobile bottom sheet):
|
|
524
|
+
- Use Drawer (Vaul) as mobile alternative to Dialog. Pulls up from bottom.
|
|
525
|
+
- Use for mobile-only interactions: filters, confirmations, pickers.
|
|
526
|
+
- On desktop: fall back to Dialog or Popover.
|
|
527
|
+
- Handle: visible grab handle at top center.
|
|
528
|
+
`;
|
|
529
|
+
var RULES_FEEDBACK = `
|
|
530
|
+
FEEDBACK & STATUS RULES:
|
|
531
|
+
|
|
532
|
+
TOAST / NOTIFICATIONS:
|
|
533
|
+
- Use shadcn toast or Sonner. NEVER use browser alert()/confirm().
|
|
534
|
+
- Position: bottom-right (default). Never top-center.
|
|
535
|
+
- Duration: 3-5 seconds for success, persistent (manual dismiss) for errors.
|
|
536
|
+
- Success: toast({ description: "Changes saved" }) \u2014 no title, brief text.
|
|
537
|
+
- Error: toast({ variant: "destructive", title: "Error", description: "Could not save. Try again." }) \u2014 always title + description.
|
|
538
|
+
- Use toast for background actions (save, delete, copy). Inline text for form validation.
|
|
539
|
+
- NEVER use toast for critical info \u2014 use Alert or Dialog instead.
|
|
540
|
+
- Max one toast visible at a time.
|
|
541
|
+
|
|
542
|
+
ALERT / BANNER:
|
|
543
|
+
- Use shadcn Alert: <Alert><AlertTitle /><AlertDescription /></Alert>.
|
|
544
|
+
- Info: <Alert> (no variant). Error: <Alert variant="destructive">.
|
|
545
|
+
- Icon: AlertCircle for destructive, Info for default.
|
|
546
|
+
- Placement: top of the relevant section, full width.
|
|
547
|
+
- NEVER use Alert for success \u2014 use toast. NOT for form validation (those go inline).
|
|
548
|
+
|
|
549
|
+
SKELETON / LOADING:
|
|
550
|
+
- Text skeleton: h-4 rounded-md bg-muted animate-pulse. Vary widths: w-full, w-3/4, w-1/2.
|
|
551
|
+
- Card skeleton: <Card><CardHeader><div className="h-4 w-1/2 animate-pulse rounded-md bg-muted" /></CardHeader></Card>
|
|
552
|
+
- Avatar skeleton: <div className="size-8 animate-pulse rounded-full bg-muted" />
|
|
553
|
+
- ALWAYS skeleton matching content shape. NEVER centered spinner for page loads.
|
|
554
|
+
- Spinner (Loader2): only for inline actions. <Loader2 className="size-4 animate-spin" />
|
|
555
|
+
- Button loading: <Button disabled><Loader2 className="mr-2 size-4 animate-spin" />Saving...</Button>
|
|
556
|
+
|
|
557
|
+
PROGRESS BAR:
|
|
558
|
+
- Use shadcn Progress. Pattern: <Progress value={66} className="h-2" />
|
|
559
|
+
- Label: text-sm above or beside progress bar. Show percentage or "Step 2 of 3".
|
|
560
|
+
- Colors: default (bg-primary) for normal. For status: wrap in div and apply text color context.
|
|
561
|
+
- Use for: file uploads, multi-step processes, quotas. NEVER for page loads (use skeleton).
|
|
562
|
+
|
|
563
|
+
ERROR PAGES (404, 500):
|
|
564
|
+
- Centered layout: flex min-h-[50vh] flex-col items-center justify-center text-center.
|
|
565
|
+
- 404: <h1 className="text-4xl font-bold">404</h1><p className="text-muted-foreground mt-2">Page not found</p><Button className="mt-4" asChild><Link href="/">Go home</Link></Button>
|
|
566
|
+
- 500: same layout but "Something went wrong" + "Try again" button.
|
|
567
|
+
- NEVER leave error page without a CTA. Always provide a way back.
|
|
568
|
+
|
|
569
|
+
NOTIFICATION INDICATORS:
|
|
570
|
+
- Unread dot on nav icon: <div className="relative"><Bell className="size-4" /><div className="absolute -top-1 -right-1 size-2 rounded-full bg-destructive" /></div>
|
|
571
|
+
- Unread count badge: <div className="absolute -top-1.5 -right-1.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground">3</div>
|
|
572
|
+
- Max display: "9+" for counts above 9.
|
|
573
|
+
- Position: always top-right of the icon. Use relative + absolute positioning.
|
|
574
|
+
|
|
575
|
+
COPY TO CLIPBOARD:
|
|
576
|
+
- Button: <Button variant="ghost" size="sm" className="h-7 px-2 text-xs">Copy</Button>
|
|
577
|
+
- Feedback: swap icon from Copy to Check for 2 seconds, then revert.
|
|
578
|
+
- Pattern: onClick \u2192 navigator.clipboard.writeText(text) \u2192 setIcon("check") \u2192 setTimeout 2000 \u2192 setIcon("copy").
|
|
579
|
+
- Toast optional: only for non-obvious copies (e.g. API key). Not needed for code blocks.
|
|
580
|
+
`;
|
|
581
|
+
var RULES_CONTENT = `
|
|
582
|
+
CONTENT PAGE RULES:
|
|
583
|
+
|
|
584
|
+
PRICING CARDS:
|
|
585
|
+
- Grid: grid gap-6 md:grid-cols-3. Highlighted tier: ring-2 ring-primary.
|
|
586
|
+
- Card structure: CardHeader(tier name + price) \u2192 CardContent(feature list) \u2192 CardFooter(CTA).
|
|
587
|
+
- Price: <div className="text-3xl font-bold">$29<span className="text-sm font-normal text-muted-foreground">/month</span></div>
|
|
588
|
+
- Features: <ul className="space-y-2 text-sm"><li className="flex items-center gap-2"><Check className="size-4 text-primary" />Feature</li></ul>
|
|
589
|
+
- Popular badge: <Badge className="absolute -top-3 left-1/2 -translate-x-1/2">Popular</Badge> on the Card (relative positioning).
|
|
590
|
+
- CTA: primary variant on highlighted tier, outline on others.
|
|
591
|
+
|
|
592
|
+
PRICE DISPLAY (with discount):
|
|
593
|
+
- Old price: <span className="text-sm text-muted-foreground line-through">$49</span>
|
|
594
|
+
- New price: <span className="text-2xl font-bold">$29</span>
|
|
595
|
+
- Discount badge: <Badge variant="secondary">Save 40%</Badge> beside price.
|
|
596
|
+
- Always show both prices when discount is active.
|
|
597
|
+
|
|
598
|
+
HERO SECTION:
|
|
599
|
+
- Full width, centered text: <section className="flex flex-col items-center text-center py-16 md:py-24 gap-4">
|
|
600
|
+
- Headline: <h1 className="text-3xl md:text-5xl font-bold tracking-tight max-w-3xl">
|
|
601
|
+
- Subheadline: <p className="text-lg text-muted-foreground max-w-2xl">
|
|
602
|
+
- CTAs: <div className="flex gap-3"><Button size="lg">Primary</Button><Button variant="outline" size="lg">Secondary</Button></div>
|
|
603
|
+
- NEVER use text-6xl or larger. Max headline size: text-5xl on desktop.
|
|
604
|
+
|
|
605
|
+
FEATURE GRID:
|
|
606
|
+
- Grid: grid gap-6 md:grid-cols-3. Each feature: Card or plain div.
|
|
607
|
+
- Structure: Icon (size-8 text-primary mb-2) \u2192 title (text-sm font-semibold) \u2192 description (text-sm text-muted-foreground).
|
|
608
|
+
- Icon: in a muted circle: <div className="flex size-10 items-center justify-center rounded-lg bg-muted"><Icon className="size-5 text-primary" /></div>
|
|
609
|
+
|
|
610
|
+
TESTIMONIAL CARDS:
|
|
611
|
+
- Card with: quote (text-sm italic), author name (text-sm font-medium), role (text-xs text-muted-foreground), avatar (size-8).
|
|
612
|
+
- Author section: <div className="flex items-center gap-3 mt-4"><Avatar>...</Avatar><div><p className="text-sm font-medium">Name</p><p className="text-xs text-muted-foreground">Title, Company</p></div></div>
|
|
613
|
+
- Quote marks: optional, use text-muted-foreground opacity-50.
|
|
614
|
+
|
|
615
|
+
LANDING PAGE SECTIONS:
|
|
616
|
+
- Section spacing: py-16 md:py-24. Between sections: no extra gap (padding handles it).
|
|
617
|
+
- Section container: max-w-6xl mx-auto px-4.
|
|
618
|
+
- Section title: text-2xl md:text-3xl font-bold tracking-tight text-center mb-4.
|
|
619
|
+
- Section subtitle: text-muted-foreground text-center max-w-2xl mx-auto mb-8.
|
|
620
|
+
- Alternating layout: consider alternate background (bg-muted/50) every other section.
|
|
621
|
+
|
|
622
|
+
CHANGELOG / RELEASE NOTES:
|
|
623
|
+
- Each version: <div className="space-y-2"><div className="flex items-center gap-2"><Badge variant="outline">v2.1.0</Badge><span className="text-xs text-muted-foreground">Jan 26, 2026</span></div><ul className="space-y-1 text-sm pl-4">...</ul></div>
|
|
624
|
+
- Categories: use Badge variant to differentiate (default=New, secondary=Improved, destructive=Fixed).
|
|
625
|
+
`;
|
|
626
|
+
var RULES_CARDS_LAYOUT = `
|
|
627
|
+
CARD & LAYOUT RULES:
|
|
628
|
+
|
|
629
|
+
COMPONENT PATTERNS:
|
|
630
|
+
- Card shadow: NONE or shadow-sm. NEVER shadow-md/lg/xl.
|
|
631
|
+
- CardHeader for stat card: className="flex flex-row items-center justify-between space-y-0 pb-2"
|
|
632
|
+
- NO nested cards (card inside card). Max 2 levels: Card > content.
|
|
633
|
+
|
|
634
|
+
BADGE VARIANT MAPPING (consistent status colors):
|
|
635
|
+
- Success/active: <Badge variant="default">Active</Badge> (also: Paid, Verified, Online, Published)
|
|
636
|
+
- Neutral/info: <Badge variant="secondary">Pending</Badge> (also: Draft, In Progress, Scheduled)
|
|
637
|
+
- Attention/warning: <Badge variant="outline">Review</Badge> (also: Expiring, Low Stock)
|
|
638
|
+
- Error/destructive: <Badge variant="destructive">Failed</Badge> (also: Overdue, Declined, Cancelled)
|
|
639
|
+
- RULE: same semantic status = same Badge variant across ALL pages.
|
|
640
|
+
|
|
641
|
+
AVATAR STYLING:
|
|
642
|
+
- Default size: size-8. Profile headers: size-10. Never larger. Shape: always rounded-full.
|
|
643
|
+
- Fallback: text-xs font-medium, bg-muted text-muted-foreground.
|
|
644
|
+
- Always use shadcn Avatar: <Avatar><AvatarImage /><AvatarFallback>JD</AvatarFallback></Avatar>
|
|
645
|
+
|
|
646
|
+
SECTION HEADERS:
|
|
647
|
+
- Standard: <div className="flex items-center justify-between"><h2 className="text-lg font-semibold tracking-tight">Title</h2><Button variant="outline" size="sm">Action</Button></div>
|
|
648
|
+
- With description: add <p className="text-sm text-muted-foreground"> below h2, wrap in <div className="space-y-1">.
|
|
649
|
+
- NEVER use h3/h4 for top-level sections. h2 for sections, h3 for sub-sections.
|
|
650
|
+
|
|
651
|
+
BUTTON GROUPING & PLACEMENT:
|
|
652
|
+
- Multiple buttons: <div className="flex items-center gap-2">
|
|
653
|
+
- Order: secondary (outline/ghost) FIRST, primary LAST. Destructive always last.
|
|
654
|
+
- Page-level: <div className="flex items-center justify-between"> (title left, actions right).
|
|
655
|
+
- Card footer: <CardFooter className="flex justify-end gap-2">
|
|
656
|
+
- Dialog footer: <DialogFooter> \u2014 cancel then primary.
|
|
657
|
+
- Icon + text: <Button><Plus className="mr-2 size-4" />Add Item</Button>
|
|
658
|
+
- Icon-only: <Button variant="ghost" size="icon"> \u2014 always with Tooltip.
|
|
659
|
+
|
|
660
|
+
CARD ACTION PLACEMENT:
|
|
661
|
+
- Content cards: DropdownMenu with MoreHorizontal in top-right of CardHeader.
|
|
662
|
+
- Form cards: actions in CardFooter (Save, Cancel).
|
|
663
|
+
- Feature/marketing cards: single CTA button at bottom.
|
|
664
|
+
- Card with link: wrap in <Link> or onClick. Add hover:bg-accent/50 transition-colors.
|
|
665
|
+
- NEVER actions in BOTH CardHeader AND CardFooter.
|
|
666
|
+
|
|
667
|
+
SEPARATOR / DIVIDER:
|
|
668
|
+
- Between page sections: <Separator className="my-6" />.
|
|
669
|
+
- Between items in list: border-b on each item (className="border-b last:border-0 py-3"). NOT Separator.
|
|
670
|
+
- Between groups in DropdownMenu: <DropdownMenuSeparator />.
|
|
671
|
+
- NEVER use native <hr>. NEVER use divide-y.
|
|
672
|
+
|
|
673
|
+
TABS STYLING:
|
|
674
|
+
- Use shadcn Tabs. Never build custom tab UI.
|
|
675
|
+
- TabsContent: pt-4. Standalone: TabsList w-full md:w-auto. Max tabs visible: 5.
|
|
676
|
+
|
|
677
|
+
MAX-WIDTH CONTAINER:
|
|
678
|
+
- App pages with sidebar: no container needed (sidebar + main handles width).
|
|
679
|
+
- App pages without sidebar: main content max-w-4xl mx-auto.
|
|
680
|
+
- Landing/marketing pages: max-w-6xl mx-auto px-4.
|
|
681
|
+
- NEVER use container class inside app layouts \u2014 only for standalone pages.
|
|
682
|
+
|
|
683
|
+
SETTINGS PAGE LAYOUT:
|
|
684
|
+
- Two-column on desktop: left nav (w-48) + right content area. Single column on mobile.
|
|
685
|
+
- Left nav: list of text links, vertical. Active: font-medium text-foreground. Inactive: text-muted-foreground.
|
|
686
|
+
- Content area: Card for each settings section with CardHeader + CardContent + CardFooter.
|
|
687
|
+
- Danger zone: separate Card at bottom with destructive styling.
|
|
688
|
+
|
|
689
|
+
DASHBOARD GRID:
|
|
690
|
+
- Top row: stats (grid gap-4 md:grid-cols-2 lg:grid-cols-4).
|
|
691
|
+
- Middle: primary content (table, chart area). Full width or 2/3 + 1/3 split.
|
|
692
|
+
- Bottom: secondary content (recent activity, quick actions).
|
|
693
|
+
- Pattern: <main className="flex flex-1 flex-col gap-4 p-4 lg:p-6">
|
|
694
|
+
`;
|
|
695
|
+
var RULES_SHADCN_APIS = `
|
|
696
|
+
SHADCN COMPONENT API REFERENCE (use these exact patterns):
|
|
697
|
+
|
|
698
|
+
SIDEBAR:
|
|
699
|
+
- Wrap in <SidebarProvider>. Components: Sidebar, SidebarContent, SidebarHeader, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarTrigger.
|
|
700
|
+
- Nav items: <SidebarMenuItem><SidebarMenuButton asChild isActive={active}><Link href="...">Label</Link></SidebarMenuButton></SidebarMenuItem>
|
|
701
|
+
- NEVER use Button for sidebar nav \u2192 use SidebarMenuButton.
|
|
702
|
+
|
|
703
|
+
SELECT (Radix compound component):
|
|
704
|
+
- Pattern: <Select><SelectTrigger><SelectValue placeholder="..." /></SelectTrigger><SelectContent><SelectItem value="x">X</SelectItem></SelectContent></Select>
|
|
705
|
+
- NEVER use native <select>. Always use shadcn Select.
|
|
706
|
+
|
|
707
|
+
DROPDOWN MENU:
|
|
708
|
+
- Pattern: <DropdownMenu><DropdownMenuTrigger asChild><Button>Open</Button></DropdownMenuTrigger><DropdownMenuContent><DropdownMenuItem>Action</DropdownMenuItem></DropdownMenuContent></DropdownMenu>
|
|
709
|
+
- Destructive items: className="text-destructive focus:text-destructive"
|
|
710
|
+
- NEVER nest <button> inside trigger \u2192 use asChild.
|
|
711
|
+
|
|
712
|
+
SHEET (mobile panels, sidebars):
|
|
713
|
+
- Pattern: <Sheet><SheetTrigger asChild><Button>Open</Button></SheetTrigger><SheetContent side="right">...</SheetContent></Sheet>
|
|
714
|
+
- side: "top" | "right" | "bottom" | "left". Default "right".
|
|
715
|
+
|
|
716
|
+
DIALOG:
|
|
717
|
+
- Pattern: <Dialog><DialogTrigger asChild><Button>Open</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>Title</DialogTitle><DialogDescription>Desc</DialogDescription></DialogHeader>...</DialogContent></Dialog>
|
|
718
|
+
- ALWAYS include DialogTitle for accessibility.
|
|
719
|
+
|
|
720
|
+
COMMAND (command palette / search):
|
|
721
|
+
- Pattern: <Command><CommandInput placeholder="Search..." /><CommandList><CommandEmpty>No results</CommandEmpty><CommandGroup heading="Suggestions"><CommandItem>Item</CommandItem></CommandGroup></CommandList></Command>
|
|
722
|
+
|
|
723
|
+
ANTI-PATTERNS (NEVER DO):
|
|
724
|
+
- NEVER use <button> for sidebar navigation \u2192 SidebarMenuButton
|
|
725
|
+
- NEVER use native <select> \u2192 shadcn Select
|
|
726
|
+
- NEVER nest <button> inside a trigger \u2192 use asChild prop
|
|
727
|
+
- NEVER use custom dropdown \u2192 shadcn DropdownMenu
|
|
728
|
+
- NEVER build custom modal \u2192 shadcn Dialog
|
|
729
|
+
- NEVER use custom toast \u2192 shadcn Sonner (toast())
|
|
730
|
+
`;
|
|
731
|
+
var RULES_COMPONENTS_MISC = `
|
|
732
|
+
MISCELLANEOUS COMPONENT RULES:
|
|
733
|
+
|
|
734
|
+
ACCORDION:
|
|
735
|
+
- Use shadcn Accordion. Never build custom collapsible.
|
|
736
|
+
- Type: "single" for FAQ (one open at a time). "multiple" for settings/filters.
|
|
737
|
+
- Trigger text: text-sm font-medium. Content: text-sm text-muted-foreground.
|
|
738
|
+
- In Card: no border on AccordionItem (Card provides border). Standalone: built-in border-b.
|
|
739
|
+
|
|
740
|
+
SCROLLAREA:
|
|
741
|
+
- Use shadcn ScrollArea when content height is known and fixed (sidebar, dropdown, modal body).
|
|
742
|
+
- Use native overflow-y-auto for dynamic page content.
|
|
743
|
+
- Sidebar: <ScrollArea className="h-[calc(100vh-4rem)]">
|
|
744
|
+
- Dialog: <ScrollArea className="max-h-[60vh]"> for tall content.
|
|
745
|
+
|
|
746
|
+
CODE BLOCK / MONOSPACE:
|
|
747
|
+
- Inline: <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm">code</code>
|
|
748
|
+
- Block: <div className="rounded-md bg-muted px-4 py-3 font-mono text-sm">command</div>
|
|
749
|
+
- Copy button: <Button variant="ghost" size="sm" className="h-7 px-2 text-xs">Copy</Button> \u2014 always same style.
|
|
750
|
+
|
|
751
|
+
IMAGE / MEDIA CONTAINERS:
|
|
752
|
+
- Aspect ratio: aspect-video (16:9) for hero. aspect-square for avatars/thumbnails.
|
|
753
|
+
- Rounded: rounded-xl for hero. rounded-md for inline. rounded-full for avatars.
|
|
754
|
+
- Fallback: bg-muted with centered icon.
|
|
755
|
+
- Object fit: object-cover for photos. object-contain for logos.
|
|
756
|
+
|
|
757
|
+
CALENDAR / DATE PICKER:
|
|
758
|
+
- Use shadcn Calendar for date selection. Pair with Popover for date picker trigger.
|
|
759
|
+
- Display format: "Jan 26, 2026" in the trigger. Never raw ISO.
|
|
760
|
+
- Placeholder: "Pick a date".
|
|
761
|
+
|
|
762
|
+
TOGGLE / TOGGLEGROUP:
|
|
763
|
+
- Use for view switchers (grid/list, map/satellite). Not for boolean settings (use Switch).
|
|
764
|
+
- Pattern: <ToggleGroup type="single" value={view} onValueChange={setView}><ToggleGroupItem value="grid"><Grid className="size-4" /></ToggleGroupItem><ToggleGroupItem value="list"><List className="size-4" /></ToggleGroupItem></ToggleGroup>
|
|
765
|
+
- Size: default. Variant: outline.
|
|
766
|
+
|
|
767
|
+
DARK MODE TOGGLE:
|
|
768
|
+
- Pattern: <Button variant="ghost" size="icon" onClick={toggleTheme}><Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /><Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /></Button>
|
|
769
|
+
- Placement: top-right of navbar, or inside settings.
|
|
770
|
+
|
|
771
|
+
RATING / STARS:
|
|
772
|
+
- Pattern: 5 star icons. Filled: <Star className="size-4 fill-primary text-primary" />. Empty: <Star className="size-4 text-muted-foreground" />.
|
|
773
|
+
- Display only (non-interactive): no hover states needed.
|
|
774
|
+
- Interactive: add hover:text-primary cursor-pointer on each star.
|
|
775
|
+
|
|
776
|
+
Z-INDEX HIERARCHY:
|
|
777
|
+
- Content: z-0 (default). Sticky headers: z-10. Dropdowns/Popovers: z-50. Sheet/Dialog overlay: z-50 (shadcn default). Toast: z-[100].
|
|
778
|
+
- NEVER use arbitrary z-index values above z-50 except for toast (z-[100]).
|
|
779
|
+
|
|
780
|
+
ANIMATION / TRANSITIONS:
|
|
781
|
+
- Default transition: transition-colors for color changes. transition-all for size/position.
|
|
782
|
+
- Duration: 150ms (Tailwind default) for hover effects. 200-300ms for enter/exit animations.
|
|
783
|
+
- Easing: ease-in-out for most. ease-out for enter. ease-in for exit.
|
|
784
|
+
- NEVER animate on page load (no entrance animations). Animate only on user interaction.
|
|
785
|
+
- Allowed: hover effects, accordion open/close, dialog enter/exit, dropdown appear, toast slide in.
|
|
786
|
+
- BANNED: bouncing elements, parallax, auto-playing carousels, decorative animations that delay content.
|
|
787
|
+
`;
|
|
788
|
+
var INTERACTION_PATTERNS = `
|
|
789
|
+
## INTERACTION PATTERNS (mandatory)
|
|
790
|
+
|
|
791
|
+
### Loading & Latency
|
|
792
|
+
- NEVER show empty screen while loading. Always: skeleton OR spinner OR progress bar
|
|
793
|
+
- For operations >1s: show what's happening ("Saving changes...", "Loading dashboard...")
|
|
794
|
+
- For operations >3s: show progress or steps ("Step 2 of 3: Generating layout...")
|
|
795
|
+
- After completion: confirm success with brief feedback ("Changes saved" or toast/banner)
|
|
796
|
+
- Skeleton > spinner for page loads. Spinner > skeleton for inline actions (button submit)
|
|
797
|
+
|
|
798
|
+
### Feedback & Confirmation
|
|
799
|
+
- Every user action gets visible feedback:
|
|
800
|
+
- Button click \u2192 disabled state + loading indicator during processing
|
|
801
|
+
- Form submit \u2192 "Saving..." \u2192 "Saved \u2713" (or error message)
|
|
802
|
+
- Destructive action \u2192 confirmation dialog BEFORE execution, never after
|
|
803
|
+
- Toggle/switch \u2192 immediate visual change (optimistic UI)
|
|
804
|
+
- Success feedback: subtle (toast, inline text). Don't use modals for success.
|
|
805
|
+
- Error feedback: prominent, inline near the cause, with suggested fix
|
|
806
|
+
|
|
807
|
+
### Error Recovery
|
|
808
|
+
- Error messages: what happened + why + what to do next
|
|
809
|
+
\u2717 "Something went wrong"
|
|
810
|
+
\u2713 "Could not save changes \u2014 connection lost. Your changes are preserved. Try again."
|
|
811
|
+
- Always provide an action: "Try again", "Go back", "Contact support"
|
|
812
|
+
- For form errors: highlight specific fields, don't clear the form
|
|
813
|
+
- For page-level errors: show error boundary with retry button
|
|
814
|
+
- Never dead-end the user \u2014 every error state has a way out
|
|
815
|
+
|
|
816
|
+
### Empty States
|
|
817
|
+
- Every list, table, grid, feed MUST handle zero items:
|
|
818
|
+
- Friendly message (not "No data" \u2014 instead "No projects yet")
|
|
819
|
+
- Primary action ("Create your first project")
|
|
820
|
+
- Optional illustration or icon
|
|
821
|
+
- Search with no results: "No results for 'X'. Try different keywords." + clear search button
|
|
822
|
+
- Filtered with no results: "No items match your filters." + reset filters button
|
|
823
|
+
|
|
824
|
+
### Navigation & Transitions
|
|
825
|
+
- Current page clearly indicated in nav (active state)
|
|
826
|
+
- Page transitions: no jarring jumps. Content should appear smoothly
|
|
827
|
+
- Back navigation: always available on detail/child pages
|
|
828
|
+
- Breadcrumbs on pages 2+ levels deep
|
|
829
|
+
- After destructive action (delete): redirect to parent list, not empty detail page
|
|
830
|
+
|
|
831
|
+
### Handoff Patterns
|
|
832
|
+
- Data entered in one context must persist when moving to another
|
|
833
|
+
- "Unsaved changes" warning when navigating away from dirty form
|
|
834
|
+
- Copy/export actions: confirm what was copied/exported ("Copied to clipboard \u2713")
|
|
835
|
+
- External links: open in new tab, never navigate away without warning
|
|
836
|
+
`;
|
|
837
|
+
var CONTEXTUAL_CATEGORIES = [
|
|
838
|
+
{
|
|
839
|
+
keywords: /form|input|login|signup|sign.?up|register|settings|profile|password|email|field|validation|upload|stepper|step|wizard|radio|tag.?input|multi.?select/i,
|
|
840
|
+
rules: RULES_FORMS
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
keywords: /dashboard|table|list|stats|chart|data|analytics|metric|activity|timeline|report|pagination|paginate|search|filter|sort|empty.?state/i,
|
|
844
|
+
rules: RULES_DATA_DISPLAY
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
keywords: /nav|sidebar|menu|breadcrumb|header|footer|navigation|mobile.?menu|hamburger/i,
|
|
848
|
+
rules: RULES_NAVIGATION
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
keywords: /modal|dialog|dropdown|sheet|popover|confirm|delete|toast|notification|alert|error|loading|skeleton|progress|overlay|command.?palette|drawer/i,
|
|
852
|
+
rules: RULES_OVERLAYS
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
keywords: /modal|dialog|dropdown|sheet|popover|confirm|toast|notification|alert|error|loading|skeleton|progress|copy|clipboard/i,
|
|
856
|
+
rules: RULES_FEEDBACK
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
keywords: /landing|pricing|about|blog|testimonial|hero|feature|changelog|marketing|homepage|home.?page|price|plan|tier/i,
|
|
860
|
+
rules: RULES_CONTENT
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
keywords: /card|grid|layout|dashboard|badge|avatar|tab|section|separator|button|setting|stat|page/i,
|
|
864
|
+
rules: RULES_CARDS_LAYOUT
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
keywords: /accordion|scroll|code|image|calendar|date|toggle|dark.?mode|theme|rating|star|animation|z.?index/i,
|
|
868
|
+
rules: RULES_COMPONENTS_MISC
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
keywords: /sidebar|select|dropdown|sheet|dialog|modal|command|trigger|asChild|menu/i,
|
|
872
|
+
rules: RULES_SHADCN_APIS
|
|
873
|
+
}
|
|
874
|
+
];
|
|
875
|
+
function selectContextualRules(message) {
|
|
876
|
+
const matched = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const category of CONTEXTUAL_CATEGORIES) {
|
|
878
|
+
if (category.keywords.test(message)) {
|
|
879
|
+
matched.add(category.rules);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (matched.size === 0) {
|
|
883
|
+
return "";
|
|
884
|
+
}
|
|
885
|
+
return [...matched].join("\n");
|
|
886
|
+
}
|
|
887
|
+
var DESIGN_CONSTRAINTS = `${DESIGN_THINKING}
|
|
888
|
+
${CORE_CONSTRAINTS}
|
|
889
|
+
${DESIGN_QUALITY}
|
|
890
|
+
${VISUAL_DEPTH}
|
|
891
|
+
${RULES_FORMS}
|
|
892
|
+
${RULES_DATA_DISPLAY}
|
|
893
|
+
${RULES_NAVIGATION}
|
|
894
|
+
${RULES_OVERLAYS}
|
|
895
|
+
${RULES_FEEDBACK}
|
|
896
|
+
${RULES_CONTENT}
|
|
897
|
+
${RULES_CARDS_LAYOUT}
|
|
898
|
+
${RULES_COMPONENTS_MISC}`;
|
|
899
|
+
|
|
900
|
+
// src/commands/chat/plan-generator.ts
|
|
901
|
+
var LAYOUT_SYNONYMS = {
|
|
902
|
+
horizontal: "header",
|
|
903
|
+
top: "header",
|
|
904
|
+
nav: "header",
|
|
905
|
+
navbar: "header",
|
|
906
|
+
topbar: "header",
|
|
907
|
+
"top-bar": "header",
|
|
908
|
+
vertical: "sidebar",
|
|
909
|
+
left: "sidebar",
|
|
910
|
+
side: "sidebar",
|
|
911
|
+
drawer: "sidebar",
|
|
912
|
+
full: "both",
|
|
913
|
+
combined: "both",
|
|
914
|
+
empty: "none",
|
|
915
|
+
minimal: "none",
|
|
916
|
+
clean: "none"
|
|
917
|
+
};
|
|
918
|
+
var PAGE_TYPE_SYNONYMS = {
|
|
919
|
+
landing: "marketing",
|
|
920
|
+
public: "marketing",
|
|
921
|
+
home: "marketing",
|
|
922
|
+
website: "marketing",
|
|
923
|
+
static: "marketing",
|
|
924
|
+
application: "app",
|
|
925
|
+
dashboard: "app",
|
|
926
|
+
admin: "app",
|
|
927
|
+
panel: "app",
|
|
928
|
+
console: "app",
|
|
929
|
+
authentication: "auth",
|
|
930
|
+
login: "auth",
|
|
931
|
+
"log-in": "auth",
|
|
932
|
+
register: "auth",
|
|
933
|
+
signin: "auth",
|
|
934
|
+
"sign-in": "auth",
|
|
935
|
+
signup: "auth",
|
|
936
|
+
"sign-up": "auth"
|
|
937
|
+
};
|
|
938
|
+
var COMPONENT_TYPE_SYNONYMS = {
|
|
939
|
+
component: "widget",
|
|
940
|
+
ui: "widget",
|
|
941
|
+
element: "widget",
|
|
942
|
+
block: "widget",
|
|
943
|
+
"page-section": "section",
|
|
944
|
+
hero: "section",
|
|
945
|
+
feature: "section",
|
|
946
|
+
area: "section"
|
|
947
|
+
};
|
|
948
|
+
function normalizeEnum(synonyms) {
|
|
949
|
+
return (v) => {
|
|
950
|
+
const trimmed = v.trim().toLowerCase();
|
|
951
|
+
return synonyms[trimmed] ?? trimmed;
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
var RouteGroupSchema = z.object({
|
|
955
|
+
id: z.string(),
|
|
956
|
+
layout: z.string().transform(normalizeEnum(LAYOUT_SYNONYMS)).pipe(z.enum(["header", "sidebar", "both", "none"])),
|
|
957
|
+
pages: z.array(z.string())
|
|
958
|
+
});
|
|
959
|
+
var PlannedComponentSchema = z.object({
|
|
960
|
+
name: z.string(),
|
|
961
|
+
description: z.string().default(""),
|
|
962
|
+
props: z.string().default("{}"),
|
|
963
|
+
usedBy: z.array(z.string()).default([]),
|
|
964
|
+
type: z.string().transform(normalizeEnum(COMPONENT_TYPE_SYNONYMS)).pipe(z.enum(["section", "widget"])).catch("widget"),
|
|
965
|
+
shadcnDeps: z.array(z.string()).default([])
|
|
966
|
+
});
|
|
967
|
+
var PageNoteSchema = z.object({
|
|
968
|
+
type: z.string().transform(normalizeEnum(PAGE_TYPE_SYNONYMS)).pipe(z.enum(["marketing", "app", "auth"])),
|
|
969
|
+
sections: z.array(z.string()).default([]),
|
|
970
|
+
links: z.record(z.string()).optional()
|
|
971
|
+
});
|
|
972
|
+
var ArchitecturePlanSchema = z.object({
|
|
973
|
+
appName: z.string().optional(),
|
|
974
|
+
groups: z.array(RouteGroupSchema),
|
|
975
|
+
sharedComponents: z.array(PlannedComponentSchema).max(8).default([]),
|
|
976
|
+
pageNotes: z.record(z.string(), PageNoteSchema).default({})
|
|
977
|
+
});
|
|
978
|
+
function routeToKey(route) {
|
|
979
|
+
return route.replace(/^\//, "") || "home";
|
|
980
|
+
}
|
|
981
|
+
function getPageGroup(route, plan) {
|
|
982
|
+
return plan.groups.find((g) => g.pages.includes(route));
|
|
983
|
+
}
|
|
984
|
+
function getPageType(route, plan) {
|
|
985
|
+
return plan.pageNotes[routeToKey(route)]?.type ?? inferPageTypeFromRoute(route);
|
|
986
|
+
}
|
|
987
|
+
var PLAN_SYSTEM_PROMPT = `You are a UI architect. Given a list of pages for a web application, create a Component Architecture Plan as JSON.
|
|
988
|
+
|
|
989
|
+
Your task:
|
|
990
|
+
1. Group pages by navigation context (e.g., public marketing pages, authenticated app pages, auth flows)
|
|
991
|
+
2. Identify reusable UI components that appear on 2+ pages
|
|
992
|
+
3. Describe each page's sections and cross-page links
|
|
993
|
+
|
|
994
|
+
Rules:
|
|
995
|
+
- Each group gets a layout type: "header" (horizontal nav), "sidebar" (vertical nav), "both", or "none" (no nav)
|
|
996
|
+
- Shared components must be genuinely reusable (appear on 2+ pages). Do NOT create a shared component for patterns used on only one page.
|
|
997
|
+
- Page types: "marketing" (landing, features, pricing \u2014 spacious, section-based), "app" (dashboard, settings \u2014 compact, data-dense), "auth" (login, register \u2014 centered card form)
|
|
998
|
+
- Component props should be a TypeScript-like interface string
|
|
999
|
+
- shadcnDeps lists the shadcn/ui atoms the component will need (e.g., "card", "badge", "avatar")
|
|
1000
|
+
- Cross-page links: map link labels to target routes (e.g., {"Sign in": "/login"})
|
|
1001
|
+
- Maximum 8 shared components
|
|
1002
|
+
|
|
1003
|
+
Respond with EXACTLY this JSON structure (use these exact field names):
|
|
1004
|
+
|
|
1005
|
+
{
|
|
1006
|
+
"appName": "MyApp",
|
|
1007
|
+
"groups": [
|
|
1008
|
+
{ "id": "public", "layout": "header", "pages": ["/", "/pricing"] },
|
|
1009
|
+
{ "id": "app", "layout": "sidebar", "pages": ["/dashboard", "/settings"] },
|
|
1010
|
+
{ "id": "auth", "layout": "none", "pages": ["/login", "/register"] }
|
|
1011
|
+
],
|
|
1012
|
+
"sharedComponents": [
|
|
1013
|
+
{
|
|
1014
|
+
"name": "StatCard",
|
|
1015
|
+
"description": "Displays a single metric with label and value",
|
|
1016
|
+
"props": "{ label: string; value: string; icon?: React.ReactNode }",
|
|
1017
|
+
"usedBy": ["/dashboard", "/projects"],
|
|
1018
|
+
"type": "widget",
|
|
1019
|
+
"shadcnDeps": ["card"]
|
|
1020
|
+
}
|
|
1021
|
+
],
|
|
1022
|
+
"pageNotes": {
|
|
1023
|
+
"home": { "type": "marketing", "sections": ["Hero", "Features", "Pricing"], "links": { "Sign in": "/login" } },
|
|
1024
|
+
"dashboard": { "type": "app", "sections": ["Stats row", "Recent tasks", "Activity feed"] },
|
|
1025
|
+
"login": { "type": "auth", "sections": ["Login form"] }
|
|
1026
|
+
}
|
|
1027
|
+
}`;
|
|
1028
|
+
async function generateArchitecturePlan(pages, userMessage, aiProvider, layoutHint) {
|
|
1029
|
+
const userPrompt = `Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
1030
|
+
|
|
1031
|
+
User's request: "${userMessage}"
|
|
1032
|
+
|
|
1033
|
+
Navigation type requested: ${layoutHint || "auto-detect"}`;
|
|
1034
|
+
const warnings = [];
|
|
1035
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1036
|
+
try {
|
|
1037
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
1038
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
1039
|
+
if (parsed.success) return { plan: parsed.data, warnings };
|
|
1040
|
+
warnings.push(
|
|
1041
|
+
`Validation (attempt ${attempt + 1}): ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1042
|
+
);
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
warnings.push(`Error (attempt ${attempt + 1}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1045
|
+
if (attempt === 1) return { plan: null, warnings };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return { plan: null, warnings };
|
|
1049
|
+
}
|
|
1050
|
+
async function updateArchitecturePlan(existingPlan, newPages, userMessage, aiProvider) {
|
|
1051
|
+
const userPrompt = `Existing plan:
|
|
1052
|
+
${JSON.stringify(existingPlan, null, 2)}
|
|
1053
|
+
|
|
1054
|
+
New pages to integrate: ${newPages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
1055
|
+
|
|
1056
|
+
User's request: "${userMessage}"
|
|
1057
|
+
|
|
1058
|
+
Update the existing plan to include these new pages. Keep all existing groups, components, and pageNotes. Add the new pages to appropriate groups and add pageNotes for them.`;
|
|
1059
|
+
try {
|
|
1060
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
1061
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
1062
|
+
if (parsed.success) return parsed.data;
|
|
1063
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1064
|
+
console.warn(chalk.dim(` Plan update validation failed: ${issues}`));
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
console.warn(chalk.dim(` Plan update error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1067
|
+
}
|
|
1068
|
+
const merged = structuredClone(existingPlan);
|
|
1069
|
+
const largestGroup = merged.groups.reduce(
|
|
1070
|
+
(best, g) => g.pages.length > (best?.pages.length ?? 0) ? g : best,
|
|
1071
|
+
merged.groups[0]
|
|
1072
|
+
);
|
|
1073
|
+
for (const page of newPages) {
|
|
1074
|
+
const alreadyPlaced = merged.groups.some((g) => g.pages.includes(page.route));
|
|
1075
|
+
if (!alreadyPlaced && largestGroup) {
|
|
1076
|
+
largestGroup.pages.push(page.route);
|
|
1077
|
+
}
|
|
1078
|
+
const key = routeToKey(page.route);
|
|
1079
|
+
if (!merged.pageNotes[key]) {
|
|
1080
|
+
merged.pageNotes[key] = { type: "app", sections: [] };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return merged;
|
|
1084
|
+
}
|
|
1085
|
+
var cachedPlan = null;
|
|
1086
|
+
function savePlan(projectRoot, plan) {
|
|
1087
|
+
cachedPlan = null;
|
|
1088
|
+
const dir = resolve(projectRoot, ".coherent");
|
|
1089
|
+
mkdirSync(dir, { recursive: true });
|
|
1090
|
+
writeFileSync(resolve(dir, "plan.json"), JSON.stringify(plan, null, 2));
|
|
1091
|
+
}
|
|
1092
|
+
function loadPlan(projectRoot) {
|
|
1093
|
+
const planPath = resolve(projectRoot, ".coherent", "plan.json");
|
|
1094
|
+
if (cachedPlan?.path === planPath) return cachedPlan.plan;
|
|
1095
|
+
if (!existsSync(planPath)) return null;
|
|
1096
|
+
try {
|
|
1097
|
+
const raw = JSON.parse(readFileSync(planPath, "utf-8"));
|
|
1098
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
1099
|
+
if (!parsed.success) return null;
|
|
1100
|
+
cachedPlan = { path: planPath, plan: parsed.data };
|
|
1101
|
+
return parsed.data;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function toKebabCase(name) {
|
|
1107
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1108
|
+
}
|
|
1109
|
+
async function generateSharedComponentsFromPlan(plan, styleContext, projectRoot, aiProvider) {
|
|
1110
|
+
if (plan.sharedComponents.length === 0) return [];
|
|
1111
|
+
const componentSpecs = plan.sharedComponents.map(
|
|
1112
|
+
(c) => `- ${c.name}: ${c.description}. Props: ${c.props}. Type: ${c.type}. shadcn deps: ${c.shadcnDeps.join(", ") || "none"}`
|
|
1113
|
+
).join("\n");
|
|
1114
|
+
const prompt = `Generate React components as separate files. For EACH component below, return an add-page request with name and pageCode fields.
|
|
1115
|
+
|
|
1116
|
+
Components to generate:
|
|
1117
|
+
${componentSpecs}
|
|
1118
|
+
|
|
1119
|
+
Style context: ${styleContext || "default"}
|
|
1120
|
+
|
|
1121
|
+
Requirements:
|
|
1122
|
+
- Each component MUST have \`export default function ComponentName\`
|
|
1123
|
+
- Use shadcn/ui imports from @/components/ui/*
|
|
1124
|
+
- Use Tailwind CSS classes matching the style context
|
|
1125
|
+
- TypeScript with proper props interface
|
|
1126
|
+
- Each component is a standalone file
|
|
1127
|
+
|
|
1128
|
+
Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentName", pageCode: "..." } }, ...] }`;
|
|
1129
|
+
const results = [];
|
|
1130
|
+
try {
|
|
1131
|
+
const raw = await aiProvider.parseModification(prompt);
|
|
1132
|
+
const requests = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
1133
|
+
for (const comp of plan.sharedComponents) {
|
|
1134
|
+
const match = requests.find(
|
|
1135
|
+
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
1136
|
+
);
|
|
1137
|
+
const code = match?.changes?.pageCode;
|
|
1138
|
+
if (code && code.includes("export default")) {
|
|
1139
|
+
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
1140
|
+
results.push({ name: comp.name, code, file });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
} catch {
|
|
1144
|
+
for (const comp of plan.sharedComponents) {
|
|
1145
|
+
try {
|
|
1146
|
+
const singlePrompt = `Generate a React component: ${comp.name} \u2014 ${comp.description}. Props: ${comp.props}. shadcn deps: ${comp.shadcnDeps.join(", ") || "none"}. Style: ${styleContext || "default"}. Return { requests: [{ type: "add-page", changes: { name: "${comp.name}", pageCode: "..." } }] }`;
|
|
1147
|
+
const raw = await aiProvider.parseModification(singlePrompt);
|
|
1148
|
+
const requests = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
1149
|
+
const match = requests.find(
|
|
1150
|
+
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
1151
|
+
);
|
|
1152
|
+
const code = match?.changes?.pageCode;
|
|
1153
|
+
if (code && code.includes("export default")) {
|
|
1154
|
+
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
1155
|
+
results.push({ name: comp.name, code, file });
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
for (const comp of results) {
|
|
1162
|
+
const fullPath = resolve(projectRoot, comp.file);
|
|
1163
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
1164
|
+
await writeFile(fullPath, comp.code, "utf-8");
|
|
1165
|
+
}
|
|
1166
|
+
return results;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export {
|
|
1170
|
+
DESIGN_THINKING,
|
|
1171
|
+
CORE_CONSTRAINTS,
|
|
1172
|
+
getDesignQualityForType,
|
|
1173
|
+
inferPageTypeFromRoute,
|
|
1174
|
+
DESIGN_QUALITY,
|
|
1175
|
+
VISUAL_DEPTH,
|
|
1176
|
+
INTERACTION_PATTERNS,
|
|
1177
|
+
selectContextualRules,
|
|
1178
|
+
RouteGroupSchema,
|
|
1179
|
+
PlannedComponentSchema,
|
|
1180
|
+
PageNoteSchema,
|
|
1181
|
+
ArchitecturePlanSchema,
|
|
1182
|
+
routeToKey,
|
|
1183
|
+
getPageGroup,
|
|
1184
|
+
getPageType,
|
|
1185
|
+
generateArchitecturePlan,
|
|
1186
|
+
updateArchitecturePlan,
|
|
1187
|
+
savePlan,
|
|
1188
|
+
loadPlan,
|
|
1189
|
+
generateSharedComponentsFromPlan
|
|
1190
|
+
};
|