@a13xu/lucid 1.9.5 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/build/database.d.ts +32 -0
- package/build/database.js +38 -0
- package/build/index.js +282 -1
- package/build/instance.d.ts +9 -0
- package/build/instance.js +78 -0
- package/build/tools/e2e.d.ts +13 -0
- package/build/tools/e2e.js +109 -0
- package/build/tools/plan.d.ts +75 -0
- package/build/tools/plan.js +148 -0
- package/build/tools/webdev/accessibility-audit.d.ts +23 -0
- package/build/tools/webdev/accessibility-audit.js +214 -0
- package/build/tools/webdev/api-client.d.ts +24 -0
- package/build/tools/webdev/api-client.js +167 -0
- package/build/tools/webdev/design-tokens.d.ts +18 -0
- package/build/tools/webdev/design-tokens.js +375 -0
- package/build/tools/webdev/generate-component.d.ts +18 -0
- package/build/tools/webdev/generate-component.js +123 -0
- package/build/tools/webdev/index.d.ts +10 -0
- package/build/tools/webdev/index.js +10 -0
- package/build/tools/webdev/perf-hints.d.ts +24 -0
- package/build/tools/webdev/perf-hints.js +247 -0
- package/build/tools/webdev/responsive-layout.d.ts +18 -0
- package/build/tools/webdev/responsive-layout.js +229 -0
- package/build/tools/webdev/scaffold-page.d.ts +18 -0
- package/build/tools/webdev/scaffold-page.js +137 -0
- package/build/tools/webdev/security-scan.d.ts +23 -0
- package/build/tools/webdev/security-scan.js +247 -0
- package/build/tools/webdev/seo-meta.d.ts +24 -0
- package/build/tools/webdev/seo-meta.js +114 -0
- package/build/tools/webdev/test-generator.d.ts +18 -0
- package/build/tools/webdev/test-generator.js +269 -0
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const PerfHintsSchema = z.object({
|
|
6
|
+
code: z.string().describe("Component or page source code to analyze"),
|
|
7
|
+
framework: z
|
|
8
|
+
.enum(["react", "vue", "nuxt", "vanilla"])
|
|
9
|
+
.describe("Frontend framework"),
|
|
10
|
+
context: z
|
|
11
|
+
.enum(["component", "page", "layout"])
|
|
12
|
+
.describe("File role in the application"),
|
|
13
|
+
});
|
|
14
|
+
const RULES = [
|
|
15
|
+
// LCP rules
|
|
16
|
+
{
|
|
17
|
+
id: "lcp-no-priority-img",
|
|
18
|
+
metric: "LCP",
|
|
19
|
+
impact: "high",
|
|
20
|
+
frameworks: ["react", "all"],
|
|
21
|
+
contexts: ["page", "layout"],
|
|
22
|
+
// Next.js Image without priority on hero images
|
|
23
|
+
pattern: /<Image\b(?![^>]*\bpriority\b)[^>]*(?:hero|banner|above-fold|lcp)[^>]*>/gi,
|
|
24
|
+
message: "Hero/LCP Image missing `priority` prop — delays LCP",
|
|
25
|
+
fix: "Add priority to the above-the-fold <Image> component: <Image priority ... />",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "lcp-img-no-dimensions",
|
|
29
|
+
metric: "LCP",
|
|
30
|
+
impact: "high",
|
|
31
|
+
frameworks: ["all"],
|
|
32
|
+
contexts: ["all"],
|
|
33
|
+
// <img> without width and height attributes
|
|
34
|
+
pattern: /<img\b(?![^>]*\bwidth\b)[^>]*>/gi,
|
|
35
|
+
message: "<img> missing width/height — browser cannot reserve layout space, causing reflow",
|
|
36
|
+
fix: "Always add explicit width and height to <img> to prevent layout shifts and help LCP",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "lcp-large-background",
|
|
40
|
+
metric: "LCP",
|
|
41
|
+
impact: "medium",
|
|
42
|
+
frameworks: ["all"],
|
|
43
|
+
contexts: ["page", "layout"],
|
|
44
|
+
pattern: /background-image\s*:\s*url\s*\(/gi,
|
|
45
|
+
message: "CSS background-image is not preloadable — consider using <img> for LCP element",
|
|
46
|
+
fix: "Use <img> instead of background-image for above-the-fold hero images so browsers can preload them",
|
|
47
|
+
},
|
|
48
|
+
// CLS rules
|
|
49
|
+
{
|
|
50
|
+
id: "cls-no-aspect-ratio",
|
|
51
|
+
metric: "CLS",
|
|
52
|
+
impact: "high",
|
|
53
|
+
frameworks: ["all"],
|
|
54
|
+
contexts: ["all"],
|
|
55
|
+
// img/video without aspect-ratio or explicit dimensions
|
|
56
|
+
pattern: /<(?:img|video|iframe)\b(?![^>]*(?:width|aspect-ratio))[^>]*>/gi,
|
|
57
|
+
message: "Media element without dimensions/aspect-ratio causes layout shift (CLS)",
|
|
58
|
+
fix: "Add width + height attrs or aspect-ratio CSS to reserve space before media loads",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "cls-dynamic-inject",
|
|
62
|
+
metric: "CLS",
|
|
63
|
+
impact: "high",
|
|
64
|
+
frameworks: ["all"],
|
|
65
|
+
contexts: ["page", "layout"],
|
|
66
|
+
// Ad slots, banners injected dynamically
|
|
67
|
+
pattern: /document\.body\.(?:append|prepend|insertBefore)|\.insertAdjacentElement\s*\(\s*["'](?:beforebegin|afterbegin)/g,
|
|
68
|
+
message: "Dynamic DOM insertion at top of page causes CLS",
|
|
69
|
+
fix: "Reserve space for dynamically injected content (ads, banners) with a min-height placeholder",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "cls-font-swap",
|
|
73
|
+
metric: "CLS",
|
|
74
|
+
impact: "medium",
|
|
75
|
+
frameworks: ["all"],
|
|
76
|
+
contexts: ["page", "layout"],
|
|
77
|
+
pattern: /@font-face\b(?![^}]*font-display)/gi,
|
|
78
|
+
message: "@font-face without font-display can cause text layout shift (FOUT/FOIT)",
|
|
79
|
+
fix: "Add font-display: swap (or optional) to @font-face rules",
|
|
80
|
+
},
|
|
81
|
+
// INP rules
|
|
82
|
+
{
|
|
83
|
+
id: "inp-heavy-click-handler",
|
|
84
|
+
metric: "INP",
|
|
85
|
+
impact: "high",
|
|
86
|
+
frameworks: ["react", "vue", "all"],
|
|
87
|
+
contexts: ["component", "all"],
|
|
88
|
+
// Synchronous loops or complex operations in event handlers
|
|
89
|
+
pattern: /(?:onClick|@click|v-on:click)\s*=\s*\{[^}]*(?:for\s*\(|while\s*\(|\.forEach\s*\()/g,
|
|
90
|
+
message: "Potentially heavy computation in click handler — can block main thread and hurt INP",
|
|
91
|
+
fix: "Move expensive work off click handler: use setTimeout, requestIdleCallback, or Web Worker",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "inp-missing-memo",
|
|
95
|
+
metric: "INP",
|
|
96
|
+
impact: "medium",
|
|
97
|
+
frameworks: ["react"],
|
|
98
|
+
contexts: ["component"],
|
|
99
|
+
// Large lists without useMemo/memo
|
|
100
|
+
pattern: /\.map\s*\(\s*\([^)]*\)\s*=>/g,
|
|
101
|
+
message: "Array .map() in render — if list is large, wrap with useMemo to avoid re-renders",
|
|
102
|
+
fix: "const items = useMemo(() => data.map(...), [data]); — prevents recalculation on unrelated renders",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "inp-missing-computed",
|
|
106
|
+
metric: "INP",
|
|
107
|
+
impact: "medium",
|
|
108
|
+
frameworks: ["vue", "nuxt"],
|
|
109
|
+
contexts: ["component"],
|
|
110
|
+
// .filter/.reduce in template expressions
|
|
111
|
+
pattern: /\{\{[^}]*\.(?:filter|reduce|sort|map)\s*\(/g,
|
|
112
|
+
message: "Array transform in template expression runs on every render — use computed()",
|
|
113
|
+
fix: "Move .filter()/.map()/.sort() to a computed property: const filtered = computed(() => items.value.filter(...))",
|
|
114
|
+
},
|
|
115
|
+
// FCP / TTFB rules
|
|
116
|
+
{
|
|
117
|
+
id: "ttfb-fetch-in-render",
|
|
118
|
+
metric: "TTFB",
|
|
119
|
+
impact: "high",
|
|
120
|
+
frameworks: ["react", "vue", "nuxt", "vanilla"],
|
|
121
|
+
contexts: ["component", "page"],
|
|
122
|
+
// fetch() or axios() called directly in component body (not in useEffect/onMounted/setup)
|
|
123
|
+
pattern: /(?:^|\n)\s*(?:const|let)\s+\w+\s*=\s*(?:await\s+)?(?:fetch|axios)\s*\(/gm,
|
|
124
|
+
message: "Data fetching at component root level — can block rendering and inflate TTFB",
|
|
125
|
+
fix: "Move data fetching into useEffect (React), onMounted/setup (Vue), or use SSR data fetching (useFetch in Nuxt, getServerSideProps in Next)",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "fcp-render-blocking",
|
|
129
|
+
metric: "FCP",
|
|
130
|
+
impact: "high",
|
|
131
|
+
frameworks: ["all"],
|
|
132
|
+
contexts: ["page", "layout"],
|
|
133
|
+
// Synchronous scripts in <head>
|
|
134
|
+
pattern: /<script\b(?![^>]*(?:async|defer|type\s*=\s*["']module["']))[^>]*src=/gi,
|
|
135
|
+
message: "Render-blocking synchronous <script> in document — delays FCP",
|
|
136
|
+
fix: "Add defer or async attribute: <script defer src=...> or <script type=\"module\" src=...>",
|
|
137
|
+
},
|
|
138
|
+
// General perf rules
|
|
139
|
+
{
|
|
140
|
+
id: "general-console-log",
|
|
141
|
+
metric: "General",
|
|
142
|
+
impact: "low",
|
|
143
|
+
frameworks: ["all"],
|
|
144
|
+
contexts: ["all"],
|
|
145
|
+
pattern: /console\.log\s*\(/g,
|
|
146
|
+
message: "console.log() left in production code — minor serialization overhead",
|
|
147
|
+
fix: "Remove console.log calls or guard with: if (process.env.NODE_ENV !== 'production') console.log(...)",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "general-inline-style-object",
|
|
151
|
+
metric: "CLS",
|
|
152
|
+
impact: "low",
|
|
153
|
+
frameworks: ["react"],
|
|
154
|
+
contexts: ["component"],
|
|
155
|
+
// Recreated style objects on every render
|
|
156
|
+
pattern: /style\s*=\s*\{\s*\{/g,
|
|
157
|
+
message: "Inline style object recreated on every render — extract to constant or use className",
|
|
158
|
+
fix: "Move to a const outside the component: const styles = { ... }; or use a CSS class",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "general-large-import",
|
|
162
|
+
metric: "FCP",
|
|
163
|
+
impact: "medium",
|
|
164
|
+
frameworks: ["all"],
|
|
165
|
+
contexts: ["all"],
|
|
166
|
+
// Import of whole large libraries
|
|
167
|
+
pattern: /import\s+\*\s+as\s+\w+\s+from\s+["'](?:lodash|moment|rxjs)["']/g,
|
|
168
|
+
message: "Whole library import (lodash/moment/rxjs) increases bundle size — tree-shake",
|
|
169
|
+
fix: "Import only what you need: import { debounce } from 'lodash-es' or import debounce from 'lodash/debounce'",
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Analyzer
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
function analyzePerf(code, framework, context) {
|
|
176
|
+
const lines = code.split("\n");
|
|
177
|
+
const issues = [];
|
|
178
|
+
for (const rule of RULES) {
|
|
179
|
+
const fw = rule.frameworks;
|
|
180
|
+
if (!fw.includes("all") && !fw.includes(framework))
|
|
181
|
+
continue;
|
|
182
|
+
const ctx = rule.contexts;
|
|
183
|
+
if (!ctx.includes("all") && !ctx.includes(context))
|
|
184
|
+
continue;
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
const line = lines[i];
|
|
187
|
+
if (/^\s*\/\/|^\s*\*/.test(line))
|
|
188
|
+
continue;
|
|
189
|
+
const re = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", "") + "g");
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = re.exec(line)) !== null) {
|
|
192
|
+
issues.push({
|
|
193
|
+
line: i + 1,
|
|
194
|
+
metric: rule.metric,
|
|
195
|
+
impact: rule.impact,
|
|
196
|
+
message: rule.message,
|
|
197
|
+
fix: rule.fix,
|
|
198
|
+
});
|
|
199
|
+
if (match.index === re.lastIndex)
|
|
200
|
+
re.lastIndex++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const metricOrder = { LCP: 0, CLS: 1, INP: 2, FCP: 3, TTFB: 4, General: 5 };
|
|
205
|
+
const impactOrder = { high: 0, medium: 1, low: 2 };
|
|
206
|
+
issues.sort((a, b) => metricOrder[a.metric] - metricOrder[b.metric] ||
|
|
207
|
+
impactOrder[a.impact] - impactOrder[b.impact] ||
|
|
208
|
+
a.line - b.line);
|
|
209
|
+
return issues;
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Formatter
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
const IMPACT_ICON = { high: "🔴", medium: "🟠", low: "🔵" };
|
|
215
|
+
const METRIC_EMOJI = {
|
|
216
|
+
LCP: "🖼️", CLS: "📐", INP: "⚡", FCP: "🎨", TTFB: "🌐", General: "⚙️",
|
|
217
|
+
};
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Handler
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Example call:
|
|
222
|
+
// handlePerfHints({ code: '<img src="hero.jpg">', framework: "react", context: "page" })
|
|
223
|
+
export function handlePerfHints(args) {
|
|
224
|
+
const { code, framework, context } = args;
|
|
225
|
+
const issues = analyzePerf(code, framework, context);
|
|
226
|
+
if (issues.length === 0) {
|
|
227
|
+
return (`✅ No performance issues detected (${framework}, ${context} context).\n\n` +
|
|
228
|
+
`💡 Tip: Run Lighthouse or web-vitals in the browser for runtime CWV measurement. ` +
|
|
229
|
+
`Static analysis cannot detect runtime bottlenecks like long tasks, large images, or slow fonts.`);
|
|
230
|
+
}
|
|
231
|
+
const counts = { high: 0, medium: 0, low: 0 };
|
|
232
|
+
for (const i of issues)
|
|
233
|
+
counts[i.impact]++;
|
|
234
|
+
const lines = [
|
|
235
|
+
`⚡ Performance Hints — ${framework} ${context} (Core Web Vitals)`,
|
|
236
|
+
`Found ${issues.length} issue(s): 🔴 ${counts.high} high 🟠 ${counts.medium} medium 🔵 ${counts.low} low`,
|
|
237
|
+
``,
|
|
238
|
+
];
|
|
239
|
+
for (const issue of issues) {
|
|
240
|
+
lines.push(`${IMPACT_ICON[issue.impact]} Line ${issue.line} — ${METRIC_EMOJI[issue.metric]} ${issue.metric}`, ` ${issue.message}`, ` Fix: ${issue.fix}`, ``);
|
|
241
|
+
}
|
|
242
|
+
lines.push(`💡 Reasoning: Analyzed ${context} for Core Web Vitals issues (LCP, CLS, INP) and general ` +
|
|
243
|
+
`performance anti-patterns. High-impact issues should be fixed first. ` +
|
|
244
|
+
`Run Lighthouse (Chrome DevTools) for real-world CWV scores and use ` +
|
|
245
|
+
`https://web.dev/measure/ for field data.`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ResponsiveLayoutSchema: z.ZodObject<{
|
|
3
|
+
description: z.ZodString;
|
|
4
|
+
framework: z.ZodEnum<["tailwind", "css-grid", "flexbox"]>;
|
|
5
|
+
breakpoints: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
container: z.ZodDefault<z.ZodOptional<z.ZodEnum<["full", "centered", "sidebar"]>>>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
description: string;
|
|
9
|
+
framework: "tailwind" | "css-grid" | "flexbox";
|
|
10
|
+
breakpoints: string[];
|
|
11
|
+
container: "full" | "centered" | "sidebar";
|
|
12
|
+
}, {
|
|
13
|
+
description: string;
|
|
14
|
+
framework: "tailwind" | "css-grid" | "flexbox";
|
|
15
|
+
breakpoints: string[];
|
|
16
|
+
container?: "full" | "centered" | "sidebar" | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleResponsiveLayout(args: z.infer<typeof ResponsiveLayoutSchema>): string;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const ResponsiveLayoutSchema = z.object({
|
|
6
|
+
description: z.string().describe("Wireframe description (e.g. 'sidebar left, main content, right panel')"),
|
|
7
|
+
framework: z
|
|
8
|
+
.enum(["tailwind", "css-grid", "flexbox"])
|
|
9
|
+
.describe("CSS framework/technique to use"),
|
|
10
|
+
breakpoints: z
|
|
11
|
+
.array(z.string())
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(5)
|
|
14
|
+
.describe("Breakpoint names to handle (e.g. ['mobile', 'tablet', 'desktop'])"),
|
|
15
|
+
container: z
|
|
16
|
+
.enum(["full", "centered", "sidebar"])
|
|
17
|
+
.optional()
|
|
18
|
+
.default("centered")
|
|
19
|
+
.describe("Layout container type"),
|
|
20
|
+
});
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Layout builders
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
function buildTailwindLayout(description, breakpoints, container) {
|
|
25
|
+
const hasMobile = breakpoints.some((b) => /mobile|sm|xs/i.test(b));
|
|
26
|
+
const hasTablet = breakpoints.some((b) => /tablet|md/i.test(b));
|
|
27
|
+
const hasDesktop = breakpoints.some((b) => /desktop|lg|xl/i.test(b));
|
|
28
|
+
const colsClass = container === "sidebar"
|
|
29
|
+
? `grid-cols-1${hasTablet ? " md:grid-cols-[260px_1fr]" : ""}${hasDesktop ? " lg:grid-cols-[280px_1fr_240px]" : ""}`
|
|
30
|
+
: `grid-cols-1${hasTablet ? " md:grid-cols-2" : ""}${hasDesktop ? " lg:grid-cols-3" : ""}`;
|
|
31
|
+
const containerClass = container === "full"
|
|
32
|
+
? "w-full"
|
|
33
|
+
: container === "sidebar"
|
|
34
|
+
? "max-w-screen-xl mx-auto px-4"
|
|
35
|
+
: "max-w-screen-lg mx-auto px-4 sm:px-6 lg:px-8";
|
|
36
|
+
const isSidebar = container === "sidebar";
|
|
37
|
+
const code = `{/* ${description} */}
|
|
38
|
+
<div class="${containerClass}">
|
|
39
|
+
|
|
40
|
+
{/* Mobile-first responsive ${isSidebar ? "sidebar layout" : "grid"} */}
|
|
41
|
+
<div class="grid ${colsClass} gap-6 py-8">
|
|
42
|
+
|
|
43
|
+
${isSidebar ? `{/* Sidebar — hidden on mobile, visible md+ */}
|
|
44
|
+
<aside class="${hasMobile ? "hidden md:block " : ""}sticky top-4 h-fit">
|
|
45
|
+
<nav>
|
|
46
|
+
{/* Sidebar navigation */}
|
|
47
|
+
</nav>
|
|
48
|
+
</aside>
|
|
49
|
+
|
|
50
|
+
{/* Main content */}
|
|
51
|
+
<main class="min-w-0">
|
|
52
|
+
{/* Primary content */}
|
|
53
|
+
</main>
|
|
54
|
+
|
|
55
|
+
${hasDesktop ? `{/* Right panel — visible lg+ */}
|
|
56
|
+
<aside class="hidden lg:block space-y-6">
|
|
57
|
+
{/* Right panel widgets */}
|
|
58
|
+
</aside>` : ""}` : `{/* Grid cells — adjust col-span as needed */}
|
|
59
|
+
<section class="col-span-1${hasDesktop ? " lg:col-span-2" : ""}">
|
|
60
|
+
{/* Main content */}
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<aside class="${hasMobile ? "col-span-1" : ""}${hasTablet ? " md:col-span-1" : ""}">
|
|
64
|
+
{/* Secondary content */}
|
|
65
|
+
</aside>`}
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
</div>`;
|
|
69
|
+
return code;
|
|
70
|
+
}
|
|
71
|
+
function buildCssGridLayout(description, breakpoints, container) {
|
|
72
|
+
const hasMobile = breakpoints.some((b) => /mobile|sm|xs/i.test(b));
|
|
73
|
+
const hasTablet = breakpoints.some((b) => /tablet|md/i.test(b));
|
|
74
|
+
const hasDesktop = breakpoints.some((b) => /desktop|lg|xl/i.test(b));
|
|
75
|
+
const isSidebar = container === "sidebar";
|
|
76
|
+
const html = `<!-- ${description} -->
|
|
77
|
+
<div class="page-container">
|
|
78
|
+
<div class="layout-grid">
|
|
79
|
+
${isSidebar ? `<aside class="sidebar">
|
|
80
|
+
<!-- Sidebar -->
|
|
81
|
+
</aside>
|
|
82
|
+
<main class="main-content">
|
|
83
|
+
<!-- Main content -->
|
|
84
|
+
</main>
|
|
85
|
+
<aside class="right-panel">
|
|
86
|
+
<!-- Right panel -->
|
|
87
|
+
</aside>` : `<section class="primary">
|
|
88
|
+
<!-- Primary content -->
|
|
89
|
+
</section>
|
|
90
|
+
<aside class="secondary">
|
|
91
|
+
<!-- Secondary content -->
|
|
92
|
+
</aside>`}
|
|
93
|
+
</div>
|
|
94
|
+
</div>`;
|
|
95
|
+
const mobileGrid = isSidebar
|
|
96
|
+
? `grid-template-columns: 1fr;\n grid-template-areas:\n "main"\n "sidebar"\n "right";`
|
|
97
|
+
: `grid-template-columns: 1fr;\n grid-template-areas:\n "primary"\n "secondary";`;
|
|
98
|
+
const tabletGrid = isSidebar
|
|
99
|
+
? `grid-template-columns: 260px 1fr;\n grid-template-areas:\n "sidebar main"\n "sidebar right";`
|
|
100
|
+
: `grid-template-columns: 2fr 1fr;\n grid-template-areas:\n "primary secondary";`;
|
|
101
|
+
const desktopGrid = isSidebar
|
|
102
|
+
? `grid-template-columns: 280px 1fr 240px;\n grid-template-areas: "sidebar main right";`
|
|
103
|
+
: `grid-template-columns: repeat(3, 1fr);\n grid-template-areas: "primary primary secondary";`;
|
|
104
|
+
const css = `.page-container {
|
|
105
|
+
width: 100%;
|
|
106
|
+
max-width: ${container === "full" ? "100%" : "1200px"};
|
|
107
|
+
margin: 0 auto;
|
|
108
|
+
padding: 0 1rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.layout-grid {
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: 1.5rem;
|
|
114
|
+
/* Mobile — single column */
|
|
115
|
+
${mobileGrid}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
${hasTablet ? `@media (min-width: 768px) {
|
|
119
|
+
.layout-grid {
|
|
120
|
+
${tabletGrid}
|
|
121
|
+
}
|
|
122
|
+
}` : ""}
|
|
123
|
+
|
|
124
|
+
${hasDesktop ? `@media (min-width: 1024px) {
|
|
125
|
+
.layout-grid {
|
|
126
|
+
${desktopGrid}
|
|
127
|
+
}
|
|
128
|
+
}` : ""}
|
|
129
|
+
|
|
130
|
+
${isSidebar ? `.sidebar { grid-area: sidebar; }
|
|
131
|
+
.main-content { grid-area: main; }
|
|
132
|
+
.right-panel { grid-area: right; }` : `.primary { grid-area: primary; }
|
|
133
|
+
.secondary { grid-area: secondary; }`}`;
|
|
134
|
+
return { html, css };
|
|
135
|
+
}
|
|
136
|
+
function buildFlexboxLayout(description, breakpoints, container) {
|
|
137
|
+
const hasTablet = breakpoints.some((b) => /tablet|md/i.test(b));
|
|
138
|
+
const hasDesktop = breakpoints.some((b) => /desktop|lg|xl/i.test(b));
|
|
139
|
+
const isSidebar = container === "sidebar";
|
|
140
|
+
const html = `<!-- ${description} -->
|
|
141
|
+
<div class="page-container">
|
|
142
|
+
<div class="flex-layout">
|
|
143
|
+
${isSidebar ? `<aside class="flex-sidebar">
|
|
144
|
+
<!-- Sidebar -->
|
|
145
|
+
</aside>
|
|
146
|
+
<main class="flex-main">
|
|
147
|
+
<!-- Main content -->
|
|
148
|
+
</main>` : `<section class="flex-primary">
|
|
149
|
+
<!-- Primary content -->
|
|
150
|
+
</section>
|
|
151
|
+
<aside class="flex-secondary">
|
|
152
|
+
<!-- Secondary content -->
|
|
153
|
+
</aside>`}
|
|
154
|
+
</div>
|
|
155
|
+
</div>`;
|
|
156
|
+
const css = `.page-container {
|
|
157
|
+
max-width: ${container === "full" ? "100%" : "1200px"};
|
|
158
|
+
margin: 0 auto;
|
|
159
|
+
padding: 0 1rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Mobile first: stacked */
|
|
163
|
+
.flex-layout {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 1.5rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
${hasTablet || hasDesktop ? `@media (min-width: ${hasTablet ? "768px" : "1024px"}) {
|
|
170
|
+
.flex-layout {
|
|
171
|
+
flex-direction: row;
|
|
172
|
+
align-items: flex-start;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
${isSidebar ? `.flex-sidebar {
|
|
176
|
+
flex: 0 0 260px;
|
|
177
|
+
position: sticky;
|
|
178
|
+
top: 1rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.flex-main {
|
|
182
|
+
flex: 1 1 0;
|
|
183
|
+
min-width: 0; /* prevent overflow */
|
|
184
|
+
}` : `.flex-primary {
|
|
185
|
+
flex: 2 1 0;
|
|
186
|
+
min-width: 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.flex-secondary {
|
|
190
|
+
flex: 1 1 0;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}`}
|
|
193
|
+
}` : ""}`;
|
|
194
|
+
return { html, css };
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Handler
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Example call:
|
|
200
|
+
// handleResponsiveLayout({ description: "sidebar left, main content area, right widgets panel", framework: "tailwind", breakpoints: ["mobile", "tablet", "desktop"], container: "sidebar" })
|
|
201
|
+
export function handleResponsiveLayout(args) {
|
|
202
|
+
const { description, framework, breakpoints, container } = args;
|
|
203
|
+
const c = container ?? "centered";
|
|
204
|
+
const lines = [
|
|
205
|
+
`✅ Responsive layout: ${framework}`,
|
|
206
|
+
`📐 Breakpoints: ${breakpoints.join(", ")} | Container: ${c}`,
|
|
207
|
+
``,
|
|
208
|
+
];
|
|
209
|
+
if (framework === "tailwind") {
|
|
210
|
+
const code = buildTailwindLayout(description, breakpoints, c);
|
|
211
|
+
lines.push("```jsx", code, "```");
|
|
212
|
+
}
|
|
213
|
+
else if (framework === "css-grid") {
|
|
214
|
+
const { html, css } = buildCssGridLayout(description, breakpoints, c);
|
|
215
|
+
lines.push("```html", html, "```", "", "```css", css, "```");
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// flexbox
|
|
219
|
+
const { html, css } = buildFlexboxLayout(description, breakpoints, c);
|
|
220
|
+
lines.push("```html", html, "```", "", "```css", css, "```");
|
|
221
|
+
}
|
|
222
|
+
lines.push(``, `💡 Reasoning: Mobile-first ${framework} layout for "${description}". ` +
|
|
223
|
+
`Container is "${c}". Breakpoints detected: ${breakpoints.join(", ")}. ` +
|
|
224
|
+
(framework === "tailwind"
|
|
225
|
+
? "Tailwind responsive prefixes (sm:/md:/lg:) handle breakpoints. "
|
|
226
|
+
: "CSS custom properties can replace hardcoded values. ") +
|
|
227
|
+
"Fill in actual content within each section. Add overflow: hidden to prevent content spillage.");
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ScaffoldPageSchema: z.ZodObject<{
|
|
3
|
+
page_name: z.ZodString;
|
|
4
|
+
framework: z.ZodEnum<["nuxt", "next", "vue"]>;
|
|
5
|
+
sections: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
seo_title: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
framework: "vue" | "nuxt" | "next";
|
|
9
|
+
page_name: string;
|
|
10
|
+
sections: string[];
|
|
11
|
+
seo_title?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
framework: "vue" | "nuxt" | "next";
|
|
14
|
+
page_name: string;
|
|
15
|
+
sections: string[];
|
|
16
|
+
seo_title?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleScaffoldPage(args: z.infer<typeof ScaffoldPageSchema>): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const ScaffoldPageSchema = z.object({
|
|
6
|
+
page_name: z.string().describe("Page name (e.g. 'About', 'Dashboard', 'ProductDetail')"),
|
|
7
|
+
framework: z.enum(["nuxt", "next", "vue"]).describe("Target framework"),
|
|
8
|
+
sections: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(10)
|
|
12
|
+
.describe("Page sections (e.g. ['hero', 'features', 'pricing', 'footer'])"),
|
|
13
|
+
seo_title: z.string().optional().describe("Optional SEO title for the page"),
|
|
14
|
+
});
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function toPascalCase(str) {
|
|
19
|
+
return str
|
|
20
|
+
.replace(/[^a-zA-Z0-9\s]/g, "")
|
|
21
|
+
.split(/\s+/)
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
function buildNuxtPage(pageName, sections, seoTitle) {
|
|
27
|
+
const componentName = toPascalCase(pageName) + "Page";
|
|
28
|
+
const sectionComponents = sections.map((s) => {
|
|
29
|
+
const name = toPascalCase(s);
|
|
30
|
+
return ` <!-- ${name} section -->\n <section id="${s.toLowerCase()}" class="py-16">\n <h2 class="text-2xl font-bold">{{ /* ${name} heading */ }}</h2>\n <!-- TODO: ${name} content -->\n </section>`;
|
|
31
|
+
});
|
|
32
|
+
return `<script setup lang="ts">
|
|
33
|
+
// ${componentName}
|
|
34
|
+
|
|
35
|
+
useHead({
|
|
36
|
+
title: "${seoTitle}",
|
|
37
|
+
meta: [
|
|
38
|
+
{ name: "description", content: "TODO: add page description" },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// TODO: fetch page data
|
|
43
|
+
// const { data } = await useFetch("/api/${pageName.toLowerCase()}")
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<main>
|
|
48
|
+
${sectionComponents.join("\n\n")}
|
|
49
|
+
</main>
|
|
50
|
+
</template>`;
|
|
51
|
+
}
|
|
52
|
+
function buildNextPage(pageName, sections, seoTitle) {
|
|
53
|
+
const componentName = toPascalCase(pageName) + "Page";
|
|
54
|
+
const sectionComponents = sections.map((s) => {
|
|
55
|
+
const name = toPascalCase(s);
|
|
56
|
+
return ` {/* ${name} */}\n <section id="${s.toLowerCase()}" className="py-16">\n <h2 className="text-2xl font-bold">{/* ${name} heading */}</h2>\n {/* TODO: ${name} content */}\n </section>`;
|
|
57
|
+
});
|
|
58
|
+
return `import type { Metadata } from "next";
|
|
59
|
+
|
|
60
|
+
export const metadata: Metadata = {
|
|
61
|
+
title: "${seoTitle}",
|
|
62
|
+
description: "TODO: add page description",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default function ${componentName}() {
|
|
66
|
+
return (
|
|
67
|
+
<main>
|
|
68
|
+
${sectionComponents.join("\n\n")}
|
|
69
|
+
</main>
|
|
70
|
+
);
|
|
71
|
+
}`;
|
|
72
|
+
}
|
|
73
|
+
function buildVuePage(pageName, sections, seoTitle) {
|
|
74
|
+
const componentName = toPascalCase(pageName) + "Page";
|
|
75
|
+
const sectionComponents = sections.map((s) => {
|
|
76
|
+
const name = toPascalCase(s);
|
|
77
|
+
return ` <!-- ${name} section -->\n <section :id="'${s.toLowerCase()}'" class="py-16">\n <h2 class="text-2xl font-bold"><!-- ${name} heading --></h2>\n <!-- TODO: ${name} content -->\n </section>`;
|
|
78
|
+
});
|
|
79
|
+
return `<script setup lang="ts">
|
|
80
|
+
// ${componentName}
|
|
81
|
+
|
|
82
|
+
// TODO: set page title via your router meta or vue-meta
|
|
83
|
+
// useTitle("${seoTitle}")
|
|
84
|
+
|
|
85
|
+
// TODO: fetch page data
|
|
86
|
+
// const pageData = ref(null)
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<main>
|
|
91
|
+
${sectionComponents.join("\n\n")}
|
|
92
|
+
</main>
|
|
93
|
+
</template>`;
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Handler
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Example call:
|
|
99
|
+
// handleScaffoldPage({ page_name: "About", framework: "nuxt", sections: ["hero", "team", "contact"], seo_title: "About Us" })
|
|
100
|
+
export function handleScaffoldPage(args) {
|
|
101
|
+
const { page_name, framework, sections, seo_title } = args;
|
|
102
|
+
const seoTitle = seo_title ?? page_name;
|
|
103
|
+
const safeName = toPascalCase(page_name) || "Page";
|
|
104
|
+
let code;
|
|
105
|
+
let filename;
|
|
106
|
+
let lang;
|
|
107
|
+
switch (framework) {
|
|
108
|
+
case "nuxt":
|
|
109
|
+
code = buildNuxtPage(page_name, sections, seoTitle);
|
|
110
|
+
filename = `pages/${page_name.toLowerCase()}.vue`;
|
|
111
|
+
lang = "vue";
|
|
112
|
+
break;
|
|
113
|
+
case "next":
|
|
114
|
+
code = buildNextPage(page_name, sections, seoTitle);
|
|
115
|
+
filename = `app/${page_name.toLowerCase()}/page.tsx`;
|
|
116
|
+
lang = "tsx";
|
|
117
|
+
break;
|
|
118
|
+
default: // vue
|
|
119
|
+
code = buildVuePage(page_name, sections, seoTitle);
|
|
120
|
+
filename = `views/${safeName}View.vue`;
|
|
121
|
+
lang = "vue";
|
|
122
|
+
}
|
|
123
|
+
const lines = [
|
|
124
|
+
`✅ Page scaffold: ${safeName}`,
|
|
125
|
+
`📄 Filename: ${filename}`,
|
|
126
|
+
`🔧 Framework: ${framework} | Sections: ${sections.join(", ")}`,
|
|
127
|
+
``,
|
|
128
|
+
"```" + lang,
|
|
129
|
+
code,
|
|
130
|
+
"```",
|
|
131
|
+
``,
|
|
132
|
+
`💡 Reasoning: Generated a ${framework} page with ${sections.length} section(s). ` +
|
|
133
|
+
`SEO title set to "${seoTitle}". ` +
|
|
134
|
+
`Each section has a placeholder — implement content and connect to your data layer.`,
|
|
135
|
+
];
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SecurityScanSchema: z.ZodObject<{
|
|
3
|
+
code: z.ZodString;
|
|
4
|
+
language: z.ZodEnum<["javascript", "typescript", "html", "vue"]>;
|
|
5
|
+
context: z.ZodEnum<["frontend", "backend", "api"]>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
code: string;
|
|
8
|
+
language: "javascript" | "typescript" | "vue" | "html";
|
|
9
|
+
context: "frontend" | "backend" | "api";
|
|
10
|
+
}, {
|
|
11
|
+
code: string;
|
|
12
|
+
language: "javascript" | "typescript" | "vue" | "html";
|
|
13
|
+
context: "frontend" | "backend" | "api";
|
|
14
|
+
}>;
|
|
15
|
+
export type SecSeverity = "critical" | "high" | "medium" | "low";
|
|
16
|
+
export interface SecurityIssue {
|
|
17
|
+
line: number;
|
|
18
|
+
severity: SecSeverity;
|
|
19
|
+
category: string;
|
|
20
|
+
message: string;
|
|
21
|
+
remediation: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function handleSecurityScan(args: z.infer<typeof SecurityScanSchema>): string;
|