@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,75 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { Statements } from "../database.js";
|
|
4
|
+
export declare const PlanCreateSchema: z.ZodObject<{
|
|
5
|
+
title: z.ZodString;
|
|
6
|
+
description: z.ZodString;
|
|
7
|
+
user_story: z.ZodString;
|
|
8
|
+
tasks: z.ZodArray<z.ZodObject<{
|
|
9
|
+
title: z.ZodString;
|
|
10
|
+
description: z.ZodString;
|
|
11
|
+
test_criteria: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
description: string;
|
|
14
|
+
title: string;
|
|
15
|
+
test_criteria: string;
|
|
16
|
+
}, {
|
|
17
|
+
description: string;
|
|
18
|
+
title: string;
|
|
19
|
+
test_criteria: string;
|
|
20
|
+
}>, "many">;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
description: string;
|
|
23
|
+
title: string;
|
|
24
|
+
user_story: string;
|
|
25
|
+
tasks: {
|
|
26
|
+
description: string;
|
|
27
|
+
title: string;
|
|
28
|
+
test_criteria: string;
|
|
29
|
+
}[];
|
|
30
|
+
}, {
|
|
31
|
+
description: string;
|
|
32
|
+
title: string;
|
|
33
|
+
user_story: string;
|
|
34
|
+
tasks: {
|
|
35
|
+
description: string;
|
|
36
|
+
title: string;
|
|
37
|
+
test_criteria: string;
|
|
38
|
+
}[];
|
|
39
|
+
}>;
|
|
40
|
+
export declare const PlanListSchema: z.ZodObject<{
|
|
41
|
+
status: z.ZodDefault<z.ZodOptional<z.ZodEnum<["active", "completed", "abandoned", "all"]>>>;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
status: "all" | "active" | "completed" | "abandoned";
|
|
44
|
+
}, {
|
|
45
|
+
status?: "all" | "active" | "completed" | "abandoned" | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
export declare const PlanGetSchema: z.ZodObject<{
|
|
48
|
+
plan_id: z.ZodNumber;
|
|
49
|
+
}, "strip", z.ZodTypeAny, {
|
|
50
|
+
plan_id: number;
|
|
51
|
+
}, {
|
|
52
|
+
plan_id: number;
|
|
53
|
+
}>;
|
|
54
|
+
export declare const PlanUpdateTaskSchema: z.ZodObject<{
|
|
55
|
+
task_id: z.ZodNumber;
|
|
56
|
+
status: z.ZodEnum<["pending", "in_progress", "done", "blocked"]>;
|
|
57
|
+
note: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
status: "blocked" | "done" | "pending" | "in_progress";
|
|
60
|
+
task_id: number;
|
|
61
|
+
note?: string | undefined;
|
|
62
|
+
}, {
|
|
63
|
+
status: "blocked" | "done" | "pending" | "in_progress";
|
|
64
|
+
task_id: number;
|
|
65
|
+
note?: string | undefined;
|
|
66
|
+
}>;
|
|
67
|
+
type PlanCreateArgs = z.infer<typeof PlanCreateSchema>;
|
|
68
|
+
type PlanListArgs = z.infer<typeof PlanListSchema>;
|
|
69
|
+
type PlanGetArgs = z.infer<typeof PlanGetSchema>;
|
|
70
|
+
type PlanUpdateTaskArgs = z.infer<typeof PlanUpdateTaskSchema>;
|
|
71
|
+
export declare function handlePlanCreate(db: Database.Database, stmts: Statements, args: PlanCreateArgs): string;
|
|
72
|
+
export declare function handlePlanList(stmts: Statements, args: PlanListArgs): string;
|
|
73
|
+
export declare function handlePlanGet(stmts: Statements, args: PlanGetArgs): string;
|
|
74
|
+
export declare function handlePlanUpdateTask(stmts: Statements, args: PlanUpdateTaskArgs): string;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Zod schemas
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const PlanCreateSchema = z.object({
|
|
6
|
+
title: z.string().min(1),
|
|
7
|
+
description: z.string().min(1),
|
|
8
|
+
user_story: z.string().min(1).describe("As a [user], I want [goal], so that [benefit]"),
|
|
9
|
+
tasks: z.array(z.object({
|
|
10
|
+
title: z.string().min(1),
|
|
11
|
+
description: z.string().min(1),
|
|
12
|
+
test_criteria: z.string().min(1),
|
|
13
|
+
})).min(1).max(20),
|
|
14
|
+
});
|
|
15
|
+
export const PlanListSchema = z.object({
|
|
16
|
+
status: z.enum(["active", "completed", "abandoned", "all"]).optional().default("active"),
|
|
17
|
+
});
|
|
18
|
+
export const PlanGetSchema = z.object({
|
|
19
|
+
plan_id: z.number().int().positive(),
|
|
20
|
+
});
|
|
21
|
+
export const PlanUpdateTaskSchema = z.object({
|
|
22
|
+
task_id: z.number().int().positive(),
|
|
23
|
+
status: z.enum(["pending", "in_progress", "done", "blocked"]),
|
|
24
|
+
note: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const STATUS_ICONS = {
|
|
30
|
+
pending: "⬜",
|
|
31
|
+
in_progress: "🔄",
|
|
32
|
+
done: "✅",
|
|
33
|
+
blocked: "🚫",
|
|
34
|
+
};
|
|
35
|
+
const PLAN_STATUS_ICONS = {
|
|
36
|
+
active: "active",
|
|
37
|
+
completed: "completed",
|
|
38
|
+
abandoned: "abandoned",
|
|
39
|
+
};
|
|
40
|
+
function progressBar(done, total) {
|
|
41
|
+
if (total === 0)
|
|
42
|
+
return "░".repeat(10);
|
|
43
|
+
const filled = Math.round((done / total) * 10);
|
|
44
|
+
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Handlers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export function handlePlanCreate(db, stmts, args) {
|
|
50
|
+
const { title, description, user_story, tasks } = args;
|
|
51
|
+
const planId = db.transaction(() => {
|
|
52
|
+
const result = stmts.insertPlan.run(title, description, user_story);
|
|
53
|
+
const id = result.lastInsertRowid;
|
|
54
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
55
|
+
const t = tasks[i];
|
|
56
|
+
stmts.insertPlanTask.run(id, i + 1, t.title, t.description, t.test_criteria);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
})();
|
|
60
|
+
const lines = [
|
|
61
|
+
`[PLAN #${planId} active] ${title}`,
|
|
62
|
+
`User Story: ${user_story}`,
|
|
63
|
+
``,
|
|
64
|
+
];
|
|
65
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
66
|
+
lines.push(`[TASK ${i + 1} #${planId * 100 + i + 1} pending] ${tasks[i].title}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push(``, `Progress: 0/${tasks.length} done`);
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
export function handlePlanList(stmts, args) {
|
|
72
|
+
const { status } = args;
|
|
73
|
+
const all = stmts.getAllPlans.all();
|
|
74
|
+
const filtered = status === "all" ? all : all.filter(p => p.status === status);
|
|
75
|
+
if (filtered.length === 0) {
|
|
76
|
+
return `No ${status === "all" ? "" : status + " "}plans found.`;
|
|
77
|
+
}
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (const plan of filtered) {
|
|
80
|
+
const tasks = stmts.getTasksByPlanId.all(plan.id);
|
|
81
|
+
const doneCount = tasks.filter(t => t.status === "done").length;
|
|
82
|
+
const label = PLAN_STATUS_ICONS[plan.status] ?? plan.status;
|
|
83
|
+
lines.push(`[#${plan.id} ${label}] ${plan.title} — ${doneCount}/${tasks.length} tasks done`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
export function handlePlanGet(stmts, args) {
|
|
88
|
+
const plan = stmts.getPlanById.get(args.plan_id);
|
|
89
|
+
if (!plan)
|
|
90
|
+
return `Error: Plan #${args.plan_id} not found.`;
|
|
91
|
+
const tasks = stmts.getTasksByPlanId.all(plan.id);
|
|
92
|
+
const doneCount = tasks.filter(t => t.status === "done").length;
|
|
93
|
+
const total = tasks.length;
|
|
94
|
+
const bar = progressBar(doneCount, total);
|
|
95
|
+
const label = PLAN_STATUS_ICONS[plan.status] ?? plan.status;
|
|
96
|
+
const lines = [
|
|
97
|
+
`[PLAN #${plan.id} | ${label}] ${plan.title}`,
|
|
98
|
+
`User Story: ${plan.user_story}`,
|
|
99
|
+
`Progress: ${doneCount}/${total} done ${bar}`,
|
|
100
|
+
``,
|
|
101
|
+
];
|
|
102
|
+
for (const task of tasks) {
|
|
103
|
+
const icon = STATUS_ICONS[task.status] ?? "❓";
|
|
104
|
+
lines.push(`[${task.seq}] ${icon} ${task.status} — ${task.title}`);
|
|
105
|
+
lines.push(` Desc: ${task.description}`);
|
|
106
|
+
lines.push(` Test: ${task.test_criteria}`);
|
|
107
|
+
let parsedNotes = [];
|
|
108
|
+
try {
|
|
109
|
+
parsedNotes = JSON.parse(task.notes);
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ }
|
|
112
|
+
for (const n of parsedNotes) {
|
|
113
|
+
const date = new Date(n.ts * 1000).toISOString().slice(0, 10);
|
|
114
|
+
lines.push(` Note: ${date} — ${n.text}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(``);
|
|
117
|
+
}
|
|
118
|
+
return lines.join("\n").trimEnd();
|
|
119
|
+
}
|
|
120
|
+
export function handlePlanUpdateTask(stmts, args) {
|
|
121
|
+
const { task_id, status, note } = args;
|
|
122
|
+
const task = stmts.getTaskById.get(task_id);
|
|
123
|
+
if (!task)
|
|
124
|
+
return `Error: Task #${task_id} not found.`;
|
|
125
|
+
let notes = [];
|
|
126
|
+
try {
|
|
127
|
+
notes = JSON.parse(task.notes);
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ }
|
|
130
|
+
if (note) {
|
|
131
|
+
notes.push({ text: note, ts: Math.floor(Date.now() / 1000) });
|
|
132
|
+
}
|
|
133
|
+
const notesJson = JSON.stringify(notes);
|
|
134
|
+
stmts.updateTaskStatus.run(status, notesJson, task_id);
|
|
135
|
+
const lines = [`✅ Task #${task_id} → ${status}`];
|
|
136
|
+
if (status === "done") {
|
|
137
|
+
const remaining = stmts.countRemainingTasks.get(task.plan_id);
|
|
138
|
+
if (remaining && remaining.count === 0) {
|
|
139
|
+
stmts.updatePlanStatus.run("completed", task.plan_id);
|
|
140
|
+
const plan = stmts.getPlanById.get(task.plan_id);
|
|
141
|
+
const taskCount = stmts.getTasksByPlanId.all(task.plan_id).length;
|
|
142
|
+
lines.push(`🎉 Plan #${task.plan_id} completat! Toate ${taskCount} task-uri done.`);
|
|
143
|
+
if (plan)
|
|
144
|
+
lines.push(` "${plan.title}"`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AccessibilityAuditSchema: z.ZodObject<{
|
|
3
|
+
code: z.ZodString;
|
|
4
|
+
wcag_level: z.ZodEnum<["A", "AA", "AAA"]>;
|
|
5
|
+
framework: z.ZodEnum<["html", "jsx", "vue"]>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
code: string;
|
|
8
|
+
framework: "vue" | "jsx" | "html";
|
|
9
|
+
wcag_level: "A" | "AA" | "AAA";
|
|
10
|
+
}, {
|
|
11
|
+
code: string;
|
|
12
|
+
framework: "vue" | "jsx" | "html";
|
|
13
|
+
wcag_level: "A" | "AA" | "AAA";
|
|
14
|
+
}>;
|
|
15
|
+
export type A11ySeverity = "critical" | "warning" | "info";
|
|
16
|
+
export interface A11yIssue {
|
|
17
|
+
line: number;
|
|
18
|
+
severity: A11ySeverity;
|
|
19
|
+
criterion: string;
|
|
20
|
+
message: string;
|
|
21
|
+
fix: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function handleAccessibilityAudit(args: z.infer<typeof AccessibilityAuditSchema>): string;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const AccessibilityAuditSchema = z.object({
|
|
6
|
+
code: z.string().describe("HTML, JSX, or Vue template snippet to audit"),
|
|
7
|
+
wcag_level: z.enum(["A", "AA", "AAA"]).describe("WCAG conformance level to check against"),
|
|
8
|
+
framework: z.enum(["html", "jsx", "vue"]).describe("Code framework/format"),
|
|
9
|
+
});
|
|
10
|
+
const RULES = [
|
|
11
|
+
{
|
|
12
|
+
id: "img-alt",
|
|
13
|
+
criterion: "WCAG 1.1.1 (Non-text Content)",
|
|
14
|
+
level: "A",
|
|
15
|
+
severity: "critical",
|
|
16
|
+
// Matches <img> or JSX <img ... /> without an alt attribute
|
|
17
|
+
pattern: /<img(?![^>]*\balt=)[^>]*>/gi,
|
|
18
|
+
message: "<img> is missing an `alt` attribute",
|
|
19
|
+
fix: (m) => m.replace(/<img/, '<img alt=""'),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "anchor-empty",
|
|
23
|
+
criterion: "WCAG 2.4.4 (Link Purpose)",
|
|
24
|
+
level: "A",
|
|
25
|
+
severity: "critical",
|
|
26
|
+
pattern: /<a\b[^>]*>\s*<\/a>/gi,
|
|
27
|
+
message: "Anchor element has no text content",
|
|
28
|
+
fix: (m) => m.replace(/<\/a>/, "<!-- TODO: add descriptive link text --></a>"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "button-empty",
|
|
32
|
+
criterion: "WCAG 4.1.2 (Name, Role, Value)",
|
|
33
|
+
level: "A",
|
|
34
|
+
severity: "critical",
|
|
35
|
+
pattern: /<button\b[^>]*>\s*<\/button>/gi,
|
|
36
|
+
message: "Button element has no accessible text",
|
|
37
|
+
fix: (m) => m.replace(/<\/button>/, "<!-- TODO: add button label --></button>"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "input-no-label",
|
|
41
|
+
criterion: "WCAG 1.3.1 (Info and Relationships)",
|
|
42
|
+
level: "A",
|
|
43
|
+
severity: "critical",
|
|
44
|
+
// Input without aria-label, aria-labelledby, or id (which a <label for=...> would target)
|
|
45
|
+
pattern: /<input\b(?![^>]*(?:aria-label|aria-labelledby|id\s*=))[^>]*>/gi,
|
|
46
|
+
message: "<input> has no associated label (missing aria-label or id for <label for=>)",
|
|
47
|
+
fix: (m) => {
|
|
48
|
+
const withId = m.includes("id=") ? m : m.replace(/<input/, '<input id="field-id"');
|
|
49
|
+
return withId;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "heading-skip",
|
|
54
|
+
criterion: "WCAG 1.3.1 (Info and Relationships)",
|
|
55
|
+
level: "A",
|
|
56
|
+
severity: "warning",
|
|
57
|
+
// Detects <h4>+ without preceding h3 in the same snippet (simplified: warns on h4/h5/h6)
|
|
58
|
+
pattern: /<h[456]\b/gi,
|
|
59
|
+
message: "Heading level h4/h5/h6 found — verify heading hierarchy is not skipped",
|
|
60
|
+
fix: () => "<!-- Ensure h1 → h2 → h3 order is maintained before using this heading level -->",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "tabindex-positive",
|
|
64
|
+
criterion: "WCAG 2.4.3 (Focus Order)",
|
|
65
|
+
level: "A",
|
|
66
|
+
severity: "warning",
|
|
67
|
+
pattern: /tabindex\s*=\s*["'][1-9]\d*["']/gi,
|
|
68
|
+
message: "Positive tabindex disrupts natural focus order",
|
|
69
|
+
fix: (m) => m.replace(/tabindex\s*=\s*["'][^"']*["']/, 'tabindex="0"'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "onclick-non-interactive",
|
|
73
|
+
criterion: "WCAG 2.1.1 (Keyboard)",
|
|
74
|
+
level: "A",
|
|
75
|
+
severity: "warning",
|
|
76
|
+
// onClick/onclick on non-interactive elements (div, span, p)
|
|
77
|
+
pattern: /<(?:div|span|p)\b[^>]*\bon[Cc]lick\b[^>]*>/g,
|
|
78
|
+
message: "onClick on a non-interactive element — use <button> or add role + keyboard handlers",
|
|
79
|
+
fix: (m) => m
|
|
80
|
+
.replace(/^<(div|span|p)/, '<button')
|
|
81
|
+
.replace(/>$/, ' type="button">'),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "color-only",
|
|
85
|
+
criterion: "WCAG 1.4.1 (Use of Color)",
|
|
86
|
+
level: "A",
|
|
87
|
+
severity: "info",
|
|
88
|
+
pattern: /color:\s*(?:red|green|#[0-9a-f]{3,6})\b/gi,
|
|
89
|
+
message: "Color appears to be used alone — ensure information is not conveyed by color only",
|
|
90
|
+
fix: () => "/* Add text, icon, or pattern as secondary indicator alongside color */",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "link-new-tab-warning",
|
|
94
|
+
criterion: "WCAG 3.2.2 (On Input)",
|
|
95
|
+
level: "A",
|
|
96
|
+
severity: "info",
|
|
97
|
+
pattern: /target\s*=\s*["']_blank["']/gi,
|
|
98
|
+
message: 'Links opening in new tab should warn users (add aria-label or visible indicator)',
|
|
99
|
+
fix: (m) => m.replace(/target\s*=\s*["']_blank["']/, 'target="_blank" rel="noopener noreferrer" aria-label="Opens in new tab"'),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "form-no-submit",
|
|
103
|
+
criterion: "WCAG 2.1.1 (Keyboard)",
|
|
104
|
+
level: "A",
|
|
105
|
+
severity: "warning",
|
|
106
|
+
// <form> without an onSubmit handler or submit button
|
|
107
|
+
pattern: /<form\b(?![^>]*(?:onSubmit|action))[^>]*>/gi,
|
|
108
|
+
message: "<form> has no onSubmit/action — keyboard users cannot submit",
|
|
109
|
+
fix: (m) => m.replace(/<form/, '<form onSubmit={handleSubmit}'),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "svg-no-title",
|
|
113
|
+
criterion: "WCAG 1.1.1 (Non-text Content)",
|
|
114
|
+
level: "A",
|
|
115
|
+
severity: "warning",
|
|
116
|
+
// <svg> without aria-label, aria-hidden, or <title>
|
|
117
|
+
pattern: /<svg\b(?![^>]*(?:aria-label|aria-hidden))[^>]*>/gi,
|
|
118
|
+
message: "<svg> is missing aria-label or aria-hidden (decorative SVGs need aria-hidden=\"true\")",
|
|
119
|
+
fix: (m) => m.replace(/<svg/, '<svg aria-hidden="true"'),
|
|
120
|
+
},
|
|
121
|
+
// AA-level rules
|
|
122
|
+
{
|
|
123
|
+
id: "contrast-inline-style",
|
|
124
|
+
criterion: "WCAG 1.4.3 (Contrast Minimum)",
|
|
125
|
+
level: "AA",
|
|
126
|
+
severity: "warning",
|
|
127
|
+
pattern: /color:\s*#(?:ccc|ddd|eee|aaa|bbb|999|888|fff|ffffff|eeeeee|dddddd|cccccc)\b/gi,
|
|
128
|
+
message: "Low-contrast color detected in inline style — verify 4.5:1 ratio for normal text",
|
|
129
|
+
fix: () => "/* Use a color with sufficient contrast ratio (≥4.5:1 for normal text) */",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "small-touch-target",
|
|
133
|
+
criterion: "WCAG 2.5.8 (Target Size)",
|
|
134
|
+
level: "AA",
|
|
135
|
+
severity: "info",
|
|
136
|
+
// Very small explicit width/height on interactive elements
|
|
137
|
+
pattern: /(?:width|height)\s*:\s*(?:[0-9]|1[0-9]|2[0-3])px/gi,
|
|
138
|
+
message: "Touch target may be too small — WCAG 2.5.8 recommends at least 24×24px",
|
|
139
|
+
fix: () => "/* Set min-width/min-height: 44px for touch targets (WCAG 2.5.5 enhanced) */",
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Analyzer
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function auditCode(code, wcagLevel, _framework) {
|
|
146
|
+
const lines = code.split("\n");
|
|
147
|
+
const issues = [];
|
|
148
|
+
// Level hierarchy: AAA includes AA includes A
|
|
149
|
+
const includedLevels = wcagLevel === "A" ? ["A"] : wcagLevel === "AA" ? ["A", "AA"] : ["A", "AA", "AAA"];
|
|
150
|
+
for (const rule of RULES) {
|
|
151
|
+
if (!includedLevels.includes(rule.level))
|
|
152
|
+
continue;
|
|
153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
let match;
|
|
156
|
+
const re = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", "") + "g");
|
|
157
|
+
while ((match = re.exec(line)) !== null) {
|
|
158
|
+
issues.push({
|
|
159
|
+
line: i + 1,
|
|
160
|
+
severity: rule.severity,
|
|
161
|
+
criterion: rule.criterion,
|
|
162
|
+
message: rule.message,
|
|
163
|
+
fix: rule.fix(match[0]),
|
|
164
|
+
});
|
|
165
|
+
// Prevent infinite loops on zero-length matches
|
|
166
|
+
if (match.index === re.lastIndex)
|
|
167
|
+
re.lastIndex++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Sort: critical first, then by line
|
|
172
|
+
const order = { critical: 0, warning: 1, info: 2 };
|
|
173
|
+
issues.sort((a, b) => order[a.severity] - order[b.severity] || a.line - b.line);
|
|
174
|
+
return issues;
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Formatter
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
const SEV_ICON = {
|
|
180
|
+
critical: "🔴",
|
|
181
|
+
warning: "🟠",
|
|
182
|
+
info: "🔵",
|
|
183
|
+
};
|
|
184
|
+
function formatIssues(issues, wcagLevel) {
|
|
185
|
+
if (issues.length === 0) {
|
|
186
|
+
return `✅ No accessibility issues found at WCAG ${wcagLevel} level.`;
|
|
187
|
+
}
|
|
188
|
+
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
189
|
+
const warning = issues.filter((i) => i.severity === "warning").length;
|
|
190
|
+
const info = issues.filter((i) => i.severity === "info").length;
|
|
191
|
+
const lines = [
|
|
192
|
+
`🔍 Accessibility Audit — WCAG ${wcagLevel}`,
|
|
193
|
+
`Found ${issues.length} issue(s): 🔴 ${critical} critical 🟠 ${warning} warning 🔵 ${info} info`,
|
|
194
|
+
``,
|
|
195
|
+
];
|
|
196
|
+
for (const issue of issues) {
|
|
197
|
+
lines.push(`${SEV_ICON[issue.severity]} Line ${issue.line} — ${issue.criterion}`, ` ${issue.message}`, ` Fix: ${issue.fix}`, ``);
|
|
198
|
+
}
|
|
199
|
+
lines.push(`💡 Reasoning: Scanned for WCAG ${wcagLevel} violations including missing alt text, ` +
|
|
200
|
+
`unlabeled form controls, empty interactive elements, keyboard accessibility issues, ` +
|
|
201
|
+
`and color contrast concerns. Automated checks cannot replace manual testing with ` +
|
|
202
|
+
`a screen reader (NVDA, JAWS, VoiceOver).`);
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Handler
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Example call:
|
|
209
|
+
// handleAccessibilityAudit({ code: '<img src="hero.jpg">', wcag_level: "AA", framework: "html" })
|
|
210
|
+
export function handleAccessibilityAudit(args) {
|
|
211
|
+
const { code, wcag_level, framework } = args;
|
|
212
|
+
const issues = auditCode(code, wcag_level, framework);
|
|
213
|
+
return formatIssues(issues, wcag_level);
|
|
214
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ApiClientSchema: z.ZodObject<{
|
|
3
|
+
endpoint: z.ZodString;
|
|
4
|
+
method: z.ZodEnum<["GET", "POST", "PUT", "PATCH", "DELETE"]>;
|
|
5
|
+
request_schema: z.ZodOptional<z.ZodString>;
|
|
6
|
+
response_schema: z.ZodOptional<z.ZodString>;
|
|
7
|
+
auth: z.ZodEnum<["bearer", "cookie", "apikey", "none"]>;
|
|
8
|
+
base_url_var: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
|
|
11
|
+
endpoint: string;
|
|
12
|
+
auth: "none" | "bearer" | "cookie" | "apikey";
|
|
13
|
+
request_schema?: string | undefined;
|
|
14
|
+
response_schema?: string | undefined;
|
|
15
|
+
base_url_var?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
|
|
18
|
+
endpoint: string;
|
|
19
|
+
auth: "none" | "bearer" | "cookie" | "apikey";
|
|
20
|
+
request_schema?: string | undefined;
|
|
21
|
+
response_schema?: string | undefined;
|
|
22
|
+
base_url_var?: string | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function handleApiClient(args: z.infer<typeof ApiClientSchema>): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const ApiClientSchema = z.object({
|
|
6
|
+
endpoint: z.string().describe("API endpoint path (e.g. /users/:id or /api/products)"),
|
|
7
|
+
method: z
|
|
8
|
+
.enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
|
|
9
|
+
.describe("HTTP method"),
|
|
10
|
+
request_schema: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("TypeScript type/interface string describing the request body"),
|
|
14
|
+
response_schema: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("TypeScript type/interface string describing the response"),
|
|
18
|
+
auth: z
|
|
19
|
+
.enum(["bearer", "cookie", "apikey", "none"])
|
|
20
|
+
.describe("Authentication strategy"),
|
|
21
|
+
base_url_var: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Environment variable name for the base URL (e.g. NEXT_PUBLIC_API_URL)"),
|
|
25
|
+
});
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function endpointToFunctionName(endpoint, method) {
|
|
30
|
+
const parts = endpoint
|
|
31
|
+
.replace(/^\/+/, "")
|
|
32
|
+
.split("/")
|
|
33
|
+
.map((seg) => {
|
|
34
|
+
if (seg.startsWith(":") || (seg.startsWith("{") && seg.endsWith("}"))) {
|
|
35
|
+
const name = seg.replace(/[:{}]/g, "");
|
|
36
|
+
return "By" + name.charAt(0).toUpperCase() + name.slice(1);
|
|
37
|
+
}
|
|
38
|
+
return seg.charAt(0).toUpperCase() + seg.slice(1).toLowerCase();
|
|
39
|
+
})
|
|
40
|
+
.join("");
|
|
41
|
+
const methodPrefix = {
|
|
42
|
+
GET: "fetch",
|
|
43
|
+
POST: "create",
|
|
44
|
+
PUT: "update",
|
|
45
|
+
PATCH: "patch",
|
|
46
|
+
DELETE: "delete",
|
|
47
|
+
};
|
|
48
|
+
return (methodPrefix[method] ?? "call") + parts;
|
|
49
|
+
}
|
|
50
|
+
function buildAuthHeaders(auth) {
|
|
51
|
+
switch (auth) {
|
|
52
|
+
case "bearer":
|
|
53
|
+
return ` "Authorization": \`Bearer \${token}\`,`;
|
|
54
|
+
case "apikey":
|
|
55
|
+
return ` "X-Api-Key": apiKey,`;
|
|
56
|
+
default:
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function buildFunctionSignature(fnName, method, endpoint, requestSchema, auth) {
|
|
61
|
+
const params = [];
|
|
62
|
+
// Path params
|
|
63
|
+
const pathParams = [...endpoint.matchAll(/:([a-zA-Z_]+)|{([a-zA-Z_]+)}/g)];
|
|
64
|
+
for (const m of pathParams) {
|
|
65
|
+
params.push(`${m[1] ?? m[2]}: string`);
|
|
66
|
+
}
|
|
67
|
+
// Body param for write methods
|
|
68
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
69
|
+
const bodyType = requestSchema ? "RequestBody" : "Record<string, unknown>";
|
|
70
|
+
params.push(`body: ${bodyType}`);
|
|
71
|
+
}
|
|
72
|
+
// Auth params
|
|
73
|
+
if (auth === "bearer")
|
|
74
|
+
params.push("token: string");
|
|
75
|
+
if (auth === "apikey")
|
|
76
|
+
params.push("apiKey: string");
|
|
77
|
+
return `async function ${fnName}(${params.join(", ")})`;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Handler
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Example call:
|
|
83
|
+
// handleApiClient({ endpoint: "/users/:id", method: "GET", response_schema: "{ id: string; name: string; email: string }", auth: "bearer", base_url_var: "NEXT_PUBLIC_API_URL" })
|
|
84
|
+
export function handleApiClient(args) {
|
|
85
|
+
const { endpoint, method, request_schema, response_schema, auth, base_url_var } = args;
|
|
86
|
+
const fnName = endpointToFunctionName(endpoint, method);
|
|
87
|
+
const baseUrlVar = base_url_var ?? "API_BASE_URL";
|
|
88
|
+
const responseType = response_schema ? "ResponseData" : "unknown";
|
|
89
|
+
const authHeaders = buildAuthHeaders(auth);
|
|
90
|
+
const signature = buildFunctionSignature(fnName, method, endpoint, request_schema, auth);
|
|
91
|
+
// Build path interpolation
|
|
92
|
+
const pathWithVars = endpoint.replace(/:([a-zA-Z_]+)/g, "${$1}").replace(/\{([a-zA-Z_]+)\}/g, "${$1}");
|
|
93
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(method);
|
|
94
|
+
const typeBlock = [];
|
|
95
|
+
if (request_schema && hasBody) {
|
|
96
|
+
typeBlock.push(`type RequestBody = ${request_schema};`);
|
|
97
|
+
}
|
|
98
|
+
if (response_schema) {
|
|
99
|
+
typeBlock.push(`type ResponseData = ${response_schema};`);
|
|
100
|
+
}
|
|
101
|
+
const cookieComment = auth === "cookie" ? "\n // Cookies sent automatically by browser" : "";
|
|
102
|
+
const credentialsOption = auth === "cookie" ? '\n credentials: "include",' : "";
|
|
103
|
+
const bodyOption = hasBody
|
|
104
|
+
? `\n body: JSON.stringify(body),`
|
|
105
|
+
: "";
|
|
106
|
+
const code = `${typeBlock.length ? typeBlock.join("\n") + "\n\n" : ""}const BASE_URL = process.env.${baseUrlVar} ?? "";
|
|
107
|
+
|
|
108
|
+
export ${signature}: Promise<${responseType}> {
|
|
109
|
+
const url = \`\${BASE_URL}${pathWithVars}\`;
|
|
110
|
+
|
|
111
|
+
const response = await fetch(url, {
|
|
112
|
+
method: "${method}",
|
|
113
|
+
headers: {${authHeaders ? "\n" + authHeaders : ""}
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"Accept": "application/json",
|
|
116
|
+
},${credentialsOption}${bodyOption}
|
|
117
|
+
});${cookieComment}
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
121
|
+
throw new Error(\`\${method} \${url} failed: \${response.status} \${errorText}\`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (response.status === 204) {
|
|
125
|
+
return undefined as ${responseType};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return response.json() as Promise<${responseType}>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Usage example:
|
|
132
|
+
// const data = await ${fnName}(${buildUsageExample(endpoint, method, request_schema, auth)});`;
|
|
133
|
+
const lines = [
|
|
134
|
+
`✅ API client: ${method} ${endpoint}`,
|
|
135
|
+
`📄 Function: ${fnName}`,
|
|
136
|
+
`🔧 Auth: ${auth} | Base URL: process.env.${baseUrlVar}`,
|
|
137
|
+
``,
|
|
138
|
+
"```typescript",
|
|
139
|
+
code,
|
|
140
|
+
"```",
|
|
141
|
+
``,
|
|
142
|
+
`💡 Reasoning: Generated a typed fetch wrapper for ${method} ${endpoint}. ` +
|
|
143
|
+
`Throws on non-2xx responses with status + body in the error message. ` +
|
|
144
|
+
`204 No Content returns \`undefined\`. ` +
|
|
145
|
+
(auth === "cookie"
|
|
146
|
+
? "Uses credentials: include for cookie-based auth. "
|
|
147
|
+
: auth === "bearer"
|
|
148
|
+
? "Pass the JWT/token as the `token` parameter. "
|
|
149
|
+
: auth === "apikey"
|
|
150
|
+
? "Pass the API key as the `apiKey` parameter. "
|
|
151
|
+
: "") +
|
|
152
|
+
`Replace type aliases with your actual interfaces.`,
|
|
153
|
+
];
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function buildUsageExample(endpoint, method, requestSchema, auth) {
|
|
157
|
+
const pathParams = [...endpoint.matchAll(/:([a-zA-Z_]+)|{([a-zA-Z_]+)}/g)].map((m) => `"${m[1] ?? m[2]}-value"`);
|
|
158
|
+
const parts = [...pathParams];
|
|
159
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
160
|
+
parts.push(requestSchema ? "{ /* body */ }" : "{}");
|
|
161
|
+
}
|
|
162
|
+
if (auth === "bearer")
|
|
163
|
+
parts.push('"YOUR_TOKEN"');
|
|
164
|
+
if (auth === "apikey")
|
|
165
|
+
parts.push('"YOUR_API_KEY"');
|
|
166
|
+
return parts.join(", ");
|
|
167
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const DesignTokensSchema: z.ZodObject<{
|
|
3
|
+
brand_name: z.ZodString;
|
|
4
|
+
primary_color: z.ZodString;
|
|
5
|
+
mood: z.ZodEnum<["minimal", "bold", "playful", "corporate"]>;
|
|
6
|
+
output_format: z.ZodEnum<["css-variables", "tailwind-config", "json"]>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
brand_name: string;
|
|
9
|
+
primary_color: string;
|
|
10
|
+
mood: "bold" | "minimal" | "playful" | "corporate";
|
|
11
|
+
output_format: "css-variables" | "tailwind-config" | "json";
|
|
12
|
+
}, {
|
|
13
|
+
brand_name: string;
|
|
14
|
+
primary_color: string;
|
|
15
|
+
mood: "bold" | "minimal" | "playful" | "corporate";
|
|
16
|
+
output_format: "css-variables" | "tailwind-config" | "json";
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleDesignTokens(args: z.infer<typeof DesignTokensSchema>): string;
|