real-prototypes-skill 2.0.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/.claude/skills/agent-browser-skill/SKILL.md +252 -0
- package/.claude/skills/real-prototypes-skill/.gitignore +188 -0
- package/.claude/skills/real-prototypes-skill/ACCESSIBILITY.md +668 -0
- package/.claude/skills/real-prototypes-skill/INSTALL.md +259 -0
- package/.claude/skills/real-prototypes-skill/LICENSE +21 -0
- package/.claude/skills/real-prototypes-skill/PUBLISH.md +310 -0
- package/.claude/skills/real-prototypes-skill/QUICKSTART.md +240 -0
- package/.claude/skills/real-prototypes-skill/README.md +442 -0
- package/.claude/skills/real-prototypes-skill/SKILL.md +329 -0
- package/.claude/skills/real-prototypes-skill/capture/capture-engine.js +1153 -0
- package/.claude/skills/real-prototypes-skill/capture/config.schema.json +170 -0
- package/.claude/skills/real-prototypes-skill/cli.js +596 -0
- package/.claude/skills/real-prototypes-skill/docs/TROUBLESHOOTING.md +278 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/capture-config.md +167 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/design-tokens.md +183 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/manifest.md +169 -0
- package/.claude/skills/real-prototypes-skill/examples/CLAUDE.md.example +73 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/CLAUDE.md +136 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/FEATURES.md +222 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/README.md +82 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/design-tokens.json +87 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/homepage-viewport.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-chatbot-final.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-fullpage-v2.png +0 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-fixes.md +298 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-report.json +253 -0
- package/.claude/skills/real-prototypes-skill/scripts/CAPTURE-ENHANCEMENTS.md +344 -0
- package/.claude/skills/real-prototypes-skill/scripts/IMPLEMENTATION-SUMMARY.md +517 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICK-START.md +229 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICKSTART-layout-analysis.md +148 -0
- package/.claude/skills/real-prototypes-skill/scripts/README-analyze-layout.md +407 -0
- package/.claude/skills/real-prototypes-skill/scripts/analyze-layout.js +880 -0
- package/.claude/skills/real-prototypes-skill/scripts/capture-platform.js +203 -0
- package/.claude/skills/real-prototypes-skill/scripts/comprehensive-capture.js +597 -0
- package/.claude/skills/real-prototypes-skill/scripts/create-manifest.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/enterprise-pipeline.js +428 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-tokens.js +468 -0
- package/.claude/skills/real-prototypes-skill/scripts/full-site-capture.js +738 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-tailwind-config.js +296 -0
- package/.claude/skills/real-prototypes-skill/scripts/integrate-accessibility.sh +161 -0
- package/.claude/skills/real-prototypes-skill/scripts/manifest-schema.json +302 -0
- package/.claude/skills/real-prototypes-skill/scripts/setup-prototype.sh +167 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-analyze-layout.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-validation.js +307 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-accessibility.js +598 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-manifest.js +499 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-output.js +361 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-prerequisites.js +319 -0
- package/.claude/skills/real-prototypes-skill/scripts/verify-layout-analysis.sh +77 -0
- package/.claude/skills/real-prototypes-skill/templates/dashboard-widget.tsx.template +91 -0
- package/.claude/skills/real-prototypes-skill/templates/data-table.tsx.template +193 -0
- package/.claude/skills/real-prototypes-skill/templates/form-section.tsx.template +250 -0
- package/.claude/skills/real-prototypes-skill/templates/modal-dialog.tsx.template +239 -0
- package/.claude/skills/real-prototypes-skill/templates/nav-item.tsx.template +265 -0
- package/.claude/skills/real-prototypes-skill/validation/validation-engine.js +559 -0
- package/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/bin/cli.js +319 -0
- package/package.json +59 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Verification script for layout analysis output
|
|
4
|
+
# Checks that all expected files are present and valid
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "🔍 Verifying Layout Analysis Output"
|
|
9
|
+
echo "===================================="
|
|
10
|
+
echo ""
|
|
11
|
+
|
|
12
|
+
# Get the references directory (passed as argument or use default)
|
|
13
|
+
REFERENCES_DIR="${1:-../../../references}"
|
|
14
|
+
|
|
15
|
+
# Check if references directory exists
|
|
16
|
+
if [ ! -d "$REFERENCES_DIR" ]; then
|
|
17
|
+
echo "❌ Error: References directory not found at $REFERENCES_DIR"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "✓ References directory exists: $REFERENCES_DIR"
|
|
22
|
+
|
|
23
|
+
# Check for structure-manifest.json
|
|
24
|
+
if [ ! -f "$REFERENCES_DIR/structure-manifest.json" ]; then
|
|
25
|
+
echo "❌ Error: structure-manifest.json not found"
|
|
26
|
+
echo " Run: node analyze-layout.js"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "✓ structure-manifest.json exists"
|
|
31
|
+
|
|
32
|
+
# Validate JSON structure (use absolute path)
|
|
33
|
+
MANIFEST_PATH="$(cd "$REFERENCES_DIR" && pwd)/structure-manifest.json"
|
|
34
|
+
if ! node -e "require('$MANIFEST_PATH')" 2>/dev/null; then
|
|
35
|
+
echo "❌ Error: structure-manifest.json is not valid JSON"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo "✓ structure-manifest.json is valid JSON"
|
|
40
|
+
|
|
41
|
+
# Check for component maps
|
|
42
|
+
COMPONENT_MAPS=$(ls "$REFERENCES_DIR"/component-map-*.json 2>/dev/null | wc -l)
|
|
43
|
+
if [ "$COMPONENT_MAPS" -eq 0 ]; then
|
|
44
|
+
echo "❌ Error: No component-map-*.json files found"
|
|
45
|
+
echo " Run: node analyze-layout.js"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "✓ Found $COMPONENT_MAPS component map files"
|
|
50
|
+
|
|
51
|
+
# Extract key metrics from structure-manifest.json
|
|
52
|
+
echo ""
|
|
53
|
+
echo "📊 Analysis Metrics:"
|
|
54
|
+
echo "-------------------"
|
|
55
|
+
|
|
56
|
+
PAGES_ANALYZED=$(node -e "console.log(require('$MANIFEST_PATH').pagesAnalyzed)")
|
|
57
|
+
echo "Pages analyzed: $PAGES_ANALYZED"
|
|
58
|
+
|
|
59
|
+
UI_LIBRARY=$(node -e "console.log(require('$MANIFEST_PATH').library)")
|
|
60
|
+
echo "UI library: $UI_LIBRARY"
|
|
61
|
+
|
|
62
|
+
LAYOUT_TYPE=$(node -e "console.log(require('$MANIFEST_PATH').layout.type)")
|
|
63
|
+
echo "Layout type: $LAYOUT_TYPE"
|
|
64
|
+
|
|
65
|
+
COMPONENT_TYPES=$(node -e "console.log(Object.keys(require('$MANIFEST_PATH').components).join(', '))")
|
|
66
|
+
echo "Component types: $COMPONENT_TYPES"
|
|
67
|
+
|
|
68
|
+
TOTAL_COMPONENTS=$(node -e "const m=require('$MANIFEST_PATH'); console.log(Object.values(m.components).reduce((sum,c)=>sum+c.totalCount,0))")
|
|
69
|
+
echo "Total components: $TOTAL_COMPONENTS"
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
echo "✅ All verification checks passed!"
|
|
73
|
+
echo ""
|
|
74
|
+
echo "💡 Next steps:"
|
|
75
|
+
echo " 1. Review structure-manifest.json for layout patterns"
|
|
76
|
+
echo " 2. Review component-map-*.json for page-specific details"
|
|
77
|
+
echo " 3. Run: node test-analyze-layout.js $REFERENCES_DIR"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
interface DashboardWidgetProps {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
actions?: React.ReactNode;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DashboardWidget({
|
|
14
|
+
title = "{/* FEATURE_TITLE */}",
|
|
15
|
+
description,
|
|
16
|
+
actions,
|
|
17
|
+
children,
|
|
18
|
+
className = "",
|
|
19
|
+
}: DashboardWidgetProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Card
|
|
22
|
+
className={`bg-platform-background border-platform-secondary/20 shadow-sm hover:shadow-md transition-shadow ${className}`}
|
|
23
|
+
role="region"
|
|
24
|
+
aria-labelledby="widget-title"
|
|
25
|
+
>
|
|
26
|
+
<CardHeader className="pb-3">
|
|
27
|
+
<div className="flex items-center justify-between">
|
|
28
|
+
<div>
|
|
29
|
+
<CardTitle
|
|
30
|
+
id="widget-title"
|
|
31
|
+
className="text-lg font-semibold text-platform-foreground"
|
|
32
|
+
>
|
|
33
|
+
{title}
|
|
34
|
+
</CardTitle>
|
|
35
|
+
{description && (
|
|
36
|
+
<CardDescription className="text-platform-foreground/60 mt-1">
|
|
37
|
+
{description}
|
|
38
|
+
</CardDescription>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
{actions && (
|
|
42
|
+
<div className="flex items-center gap-2" role="toolbar" aria-label="Widget actions">
|
|
43
|
+
{actions}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</CardHeader>
|
|
48
|
+
|
|
49
|
+
<CardContent className="text-platform-foreground">
|
|
50
|
+
{/* FEATURE_CONTENT */}
|
|
51
|
+
{children}
|
|
52
|
+
</CardContent>
|
|
53
|
+
|
|
54
|
+
{/* Optional footer for additional actions */}
|
|
55
|
+
{/*
|
|
56
|
+
<CardFooter className="pt-3 border-t border-platform-secondary/10">
|
|
57
|
+
<div className="flex items-center justify-end gap-2 w-full">
|
|
58
|
+
<Button
|
|
59
|
+
variant="outline"
|
|
60
|
+
size="sm"
|
|
61
|
+
className="border-platform-secondary text-platform-foreground hover:bg-platform-secondary/10"
|
|
62
|
+
>
|
|
63
|
+
View Details
|
|
64
|
+
</Button>
|
|
65
|
+
<Button
|
|
66
|
+
size="sm"
|
|
67
|
+
className="bg-platform-primary text-white hover:bg-platform-primary/90"
|
|
68
|
+
>
|
|
69
|
+
Take Action
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
</CardFooter>
|
|
73
|
+
*/}
|
|
74
|
+
</Card>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Example usage:
|
|
79
|
+
// <DashboardWidget
|
|
80
|
+
// title="Sales Overview"
|
|
81
|
+
// description="Monthly sales performance"
|
|
82
|
+
// actions={
|
|
83
|
+
// <Button variant="ghost" size="sm">
|
|
84
|
+
// <RefreshIcon className="h-4 w-4" />
|
|
85
|
+
// </Button>
|
|
86
|
+
// }
|
|
87
|
+
// >
|
|
88
|
+
// <div className="space-y-4">
|
|
89
|
+
// <p>Your content here</p>
|
|
90
|
+
// </div>
|
|
91
|
+
// </DashboardWidget>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCell,
|
|
6
|
+
TableHead,
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow,
|
|
9
|
+
} from "@/components/ui/table";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
|
|
12
|
+
|
|
13
|
+
type SortDirection = "asc" | "desc" | null;
|
|
14
|
+
|
|
15
|
+
interface Column<T> {
|
|
16
|
+
key: keyof T;
|
|
17
|
+
header: string;
|
|
18
|
+
sortable?: boolean;
|
|
19
|
+
render?: (value: T[keyof T], row: T) => React.ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DataTableProps<T extends Record<string, unknown>> {
|
|
24
|
+
data: T[];
|
|
25
|
+
columns: Column<T>[];
|
|
26
|
+
caption?: string;
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
onRowClick?: (row: T) => void;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DataTable<T extends Record<string, unknown>>({
|
|
33
|
+
data,
|
|
34
|
+
columns,
|
|
35
|
+
caption = "{/* FEATURE_TITLE */}",
|
|
36
|
+
emptyMessage = "No data available",
|
|
37
|
+
onRowClick,
|
|
38
|
+
className = "",
|
|
39
|
+
}: DataTableProps<T>) {
|
|
40
|
+
const [sortKey, setSortKey] = useState<keyof T | null>(null);
|
|
41
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
42
|
+
|
|
43
|
+
const handleSort = (key: keyof T) => {
|
|
44
|
+
if (sortKey === key) {
|
|
45
|
+
if (sortDirection === "asc") {
|
|
46
|
+
setSortDirection("desc");
|
|
47
|
+
} else if (sortDirection === "desc") {
|
|
48
|
+
setSortDirection(null);
|
|
49
|
+
setSortKey(null);
|
|
50
|
+
} else {
|
|
51
|
+
setSortDirection("asc");
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
setSortKey(key);
|
|
55
|
+
setSortDirection("asc");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const sortedData = useMemo(() => {
|
|
60
|
+
if (!sortKey || !sortDirection) return data;
|
|
61
|
+
|
|
62
|
+
return [...data].sort((a, b) => {
|
|
63
|
+
const aVal = a[sortKey];
|
|
64
|
+
const bVal = b[sortKey];
|
|
65
|
+
|
|
66
|
+
if (aVal === bVal) return 0;
|
|
67
|
+
if (aVal === null || aVal === undefined) return 1;
|
|
68
|
+
if (bVal === null || bVal === undefined) return -1;
|
|
69
|
+
|
|
70
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
71
|
+
return sortDirection === "asc" ? comparison : -comparison;
|
|
72
|
+
});
|
|
73
|
+
}, [data, sortKey, sortDirection]);
|
|
74
|
+
|
|
75
|
+
const getSortIcon = (key: keyof T) => {
|
|
76
|
+
if (sortKey !== key) {
|
|
77
|
+
return <ChevronsUpDown className="ml-2 h-4 w-4 text-platform-foreground/40" />;
|
|
78
|
+
}
|
|
79
|
+
if (sortDirection === "asc") {
|
|
80
|
+
return <ChevronUp className="ml-2 h-4 w-4 text-platform-primary" />;
|
|
81
|
+
}
|
|
82
|
+
return <ChevronDown className="ml-2 h-4 w-4 text-platform-primary" />;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={`w-full overflow-auto rounded-lg border border-platform-secondary/20 ${className}`}>
|
|
87
|
+
<Table>
|
|
88
|
+
{caption && (
|
|
89
|
+
<caption className="sr-only">{caption}</caption>
|
|
90
|
+
)}
|
|
91
|
+
<TableHeader>
|
|
92
|
+
<TableRow className="bg-platform-secondary/5 hover:bg-platform-secondary/10">
|
|
93
|
+
{columns.map((column) => (
|
|
94
|
+
<TableHead
|
|
95
|
+
key={String(column.key)}
|
|
96
|
+
className={`text-platform-foreground font-semibold ${column.className || ""}`}
|
|
97
|
+
>
|
|
98
|
+
{column.sortable ? (
|
|
99
|
+
<Button
|
|
100
|
+
variant="ghost"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-8 px-2 -ml-2 hover:bg-platform-secondary/10 text-platform-foreground"
|
|
103
|
+
onClick={() => handleSort(column.key)}
|
|
104
|
+
aria-label={`Sort by ${column.header}`}
|
|
105
|
+
>
|
|
106
|
+
{column.header}
|
|
107
|
+
{getSortIcon(column.key)}
|
|
108
|
+
</Button>
|
|
109
|
+
) : (
|
|
110
|
+
column.header
|
|
111
|
+
)}
|
|
112
|
+
</TableHead>
|
|
113
|
+
))}
|
|
114
|
+
</TableRow>
|
|
115
|
+
</TableHeader>
|
|
116
|
+
<TableBody>
|
|
117
|
+
{/* FEATURE_CONTENT */}
|
|
118
|
+
{sortedData.length === 0 ? (
|
|
119
|
+
<TableRow>
|
|
120
|
+
<TableCell
|
|
121
|
+
colSpan={columns.length}
|
|
122
|
+
className="h-24 text-center text-platform-foreground/60"
|
|
123
|
+
>
|
|
124
|
+
{emptyMessage}
|
|
125
|
+
</TableCell>
|
|
126
|
+
</TableRow>
|
|
127
|
+
) : (
|
|
128
|
+
sortedData.map((row, index) => (
|
|
129
|
+
<TableRow
|
|
130
|
+
key={index}
|
|
131
|
+
className={`
|
|
132
|
+
bg-platform-background
|
|
133
|
+
hover:bg-platform-secondary/5
|
|
134
|
+
transition-colors
|
|
135
|
+
${onRowClick ? "cursor-pointer" : ""}
|
|
136
|
+
`}
|
|
137
|
+
onClick={() => onRowClick?.(row)}
|
|
138
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
139
|
+
onKeyDown={(e) => {
|
|
140
|
+
if (onRowClick && (e.key === "Enter" || e.key === " ")) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
onRowClick(row);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
role={onRowClick ? "button" : undefined}
|
|
146
|
+
>
|
|
147
|
+
{columns.map((column) => (
|
|
148
|
+
<TableCell
|
|
149
|
+
key={String(column.key)}
|
|
150
|
+
className={`text-platform-foreground ${column.className || ""}`}
|
|
151
|
+
>
|
|
152
|
+
{column.render
|
|
153
|
+
? column.render(row[column.key], row)
|
|
154
|
+
: String(row[column.key] ?? "")}
|
|
155
|
+
</TableCell>
|
|
156
|
+
))}
|
|
157
|
+
</TableRow>
|
|
158
|
+
))
|
|
159
|
+
)}
|
|
160
|
+
</TableBody>
|
|
161
|
+
</Table>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Example usage:
|
|
167
|
+
// interface User {
|
|
168
|
+
// id: number;
|
|
169
|
+
// name: string;
|
|
170
|
+
// email: string;
|
|
171
|
+
// status: string;
|
|
172
|
+
// }
|
|
173
|
+
//
|
|
174
|
+
// const columns: Column<User>[] = [
|
|
175
|
+
// { key: "name", header: "Name", sortable: true },
|
|
176
|
+
// { key: "email", header: "Email", sortable: true },
|
|
177
|
+
// {
|
|
178
|
+
// key: "status",
|
|
179
|
+
// header: "Status",
|
|
180
|
+
// render: (value) => (
|
|
181
|
+
// <Badge variant={value === "active" ? "default" : "secondary"}>
|
|
182
|
+
// {value}
|
|
183
|
+
// </Badge>
|
|
184
|
+
// ),
|
|
185
|
+
// },
|
|
186
|
+
// ];
|
|
187
|
+
//
|
|
188
|
+
// <DataTable
|
|
189
|
+
// data={users}
|
|
190
|
+
// columns={columns}
|
|
191
|
+
// caption="User list"
|
|
192
|
+
// onRowClick={(user) => console.log(user)}
|
|
193
|
+
// />
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "@/components/ui/select";
|
|
14
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
15
|
+
|
|
16
|
+
interface FormData {
|
|
17
|
+
textField: string;
|
|
18
|
+
emailField: string;
|
|
19
|
+
selectField: string;
|
|
20
|
+
textareaField: string;
|
|
21
|
+
checkboxField: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FormSectionProps {
|
|
25
|
+
title?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
onSubmit?: (data: FormData) => void;
|
|
28
|
+
onCancel?: () => void;
|
|
29
|
+
isLoading?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FormSection({
|
|
34
|
+
title = "{/* FEATURE_TITLE */}",
|
|
35
|
+
description,
|
|
36
|
+
onSubmit,
|
|
37
|
+
onCancel,
|
|
38
|
+
isLoading = false,
|
|
39
|
+
className = "",
|
|
40
|
+
}: FormSectionProps) {
|
|
41
|
+
const {
|
|
42
|
+
register,
|
|
43
|
+
handleSubmit,
|
|
44
|
+
setValue,
|
|
45
|
+
watch,
|
|
46
|
+
formState: { errors },
|
|
47
|
+
} = useForm<FormData>({
|
|
48
|
+
defaultValues: {
|
|
49
|
+
textField: "",
|
|
50
|
+
emailField: "",
|
|
51
|
+
selectField: "",
|
|
52
|
+
textareaField: "",
|
|
53
|
+
checkboxField: false,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const checkboxValue = watch("checkboxField");
|
|
58
|
+
|
|
59
|
+
const handleFormSubmit = (data: FormData) => {
|
|
60
|
+
onSubmit?.(data);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={`bg-platform-background p-6 rounded-lg border border-platform-secondary/20 ${className}`}>
|
|
65
|
+
{(title || description) && (
|
|
66
|
+
<div className="mb-6">
|
|
67
|
+
{title && (
|
|
68
|
+
<h2 className="text-xl font-semibold text-platform-foreground">
|
|
69
|
+
{title}
|
|
70
|
+
</h2>
|
|
71
|
+
)}
|
|
72
|
+
{description && (
|
|
73
|
+
<p className="mt-1 text-sm text-platform-foreground/60">
|
|
74
|
+
{description}
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
|
81
|
+
{/* FEATURE_CONTENT */}
|
|
82
|
+
|
|
83
|
+
{/* Text Input */}
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<Label
|
|
86
|
+
htmlFor="textField"
|
|
87
|
+
className="text-platform-foreground font-medium"
|
|
88
|
+
>
|
|
89
|
+
Text Field
|
|
90
|
+
<span className="text-red-500 ml-1" aria-hidden="true">*</span>
|
|
91
|
+
</Label>
|
|
92
|
+
<Input
|
|
93
|
+
id="textField"
|
|
94
|
+
type="text"
|
|
95
|
+
placeholder="Enter text..."
|
|
96
|
+
className="bg-platform-background border-platform-secondary/30 text-platform-foreground placeholder:text-platform-foreground/40 focus:border-platform-primary focus:ring-platform-primary"
|
|
97
|
+
aria-required="true"
|
|
98
|
+
aria-invalid={!!errors.textField}
|
|
99
|
+
aria-describedby={errors.textField ? "textField-error" : undefined}
|
|
100
|
+
{...register("textField", { required: "This field is required" })}
|
|
101
|
+
/>
|
|
102
|
+
{errors.textField && (
|
|
103
|
+
<p id="textField-error" className="text-sm text-red-500" role="alert">
|
|
104
|
+
{errors.textField.message}
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Email Input */}
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<Label
|
|
112
|
+
htmlFor="emailField"
|
|
113
|
+
className="text-platform-foreground font-medium"
|
|
114
|
+
>
|
|
115
|
+
Email Field
|
|
116
|
+
</Label>
|
|
117
|
+
<Input
|
|
118
|
+
id="emailField"
|
|
119
|
+
type="email"
|
|
120
|
+
placeholder="email@example.com"
|
|
121
|
+
className="bg-platform-background border-platform-secondary/30 text-platform-foreground placeholder:text-platform-foreground/40 focus:border-platform-primary focus:ring-platform-primary"
|
|
122
|
+
aria-invalid={!!errors.emailField}
|
|
123
|
+
aria-describedby={errors.emailField ? "emailField-error" : undefined}
|
|
124
|
+
{...register("emailField", {
|
|
125
|
+
pattern: {
|
|
126
|
+
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
127
|
+
message: "Invalid email address",
|
|
128
|
+
},
|
|
129
|
+
})}
|
|
130
|
+
/>
|
|
131
|
+
{errors.emailField && (
|
|
132
|
+
<p id="emailField-error" className="text-sm text-red-500" role="alert">
|
|
133
|
+
{errors.emailField.message}
|
|
134
|
+
</p>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Select Input */}
|
|
139
|
+
<div className="space-y-2">
|
|
140
|
+
<Label
|
|
141
|
+
htmlFor="selectField"
|
|
142
|
+
className="text-platform-foreground font-medium"
|
|
143
|
+
>
|
|
144
|
+
Select Field
|
|
145
|
+
</Label>
|
|
146
|
+
<Select
|
|
147
|
+
onValueChange={(value) => setValue("selectField", value)}
|
|
148
|
+
>
|
|
149
|
+
<SelectTrigger
|
|
150
|
+
id="selectField"
|
|
151
|
+
className="bg-platform-background border-platform-secondary/30 text-platform-foreground focus:border-platform-primary focus:ring-platform-primary"
|
|
152
|
+
aria-label="Select an option"
|
|
153
|
+
>
|
|
154
|
+
<SelectValue placeholder="Select an option..." />
|
|
155
|
+
</SelectTrigger>
|
|
156
|
+
<SelectContent className="bg-platform-background border-platform-secondary/30">
|
|
157
|
+
<SelectItem
|
|
158
|
+
value="option1"
|
|
159
|
+
className="text-platform-foreground focus:bg-platform-secondary/10"
|
|
160
|
+
>
|
|
161
|
+
Option 1
|
|
162
|
+
</SelectItem>
|
|
163
|
+
<SelectItem
|
|
164
|
+
value="option2"
|
|
165
|
+
className="text-platform-foreground focus:bg-platform-secondary/10"
|
|
166
|
+
>
|
|
167
|
+
Option 2
|
|
168
|
+
</SelectItem>
|
|
169
|
+
<SelectItem
|
|
170
|
+
value="option3"
|
|
171
|
+
className="text-platform-foreground focus:bg-platform-secondary/10"
|
|
172
|
+
>
|
|
173
|
+
Option 3
|
|
174
|
+
</SelectItem>
|
|
175
|
+
</SelectContent>
|
|
176
|
+
</Select>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Textarea */}
|
|
180
|
+
<div className="space-y-2">
|
|
181
|
+
<Label
|
|
182
|
+
htmlFor="textareaField"
|
|
183
|
+
className="text-platform-foreground font-medium"
|
|
184
|
+
>
|
|
185
|
+
Description
|
|
186
|
+
</Label>
|
|
187
|
+
<Textarea
|
|
188
|
+
id="textareaField"
|
|
189
|
+
placeholder="Enter a description..."
|
|
190
|
+
rows={4}
|
|
191
|
+
className="bg-platform-background border-platform-secondary/30 text-platform-foreground placeholder:text-platform-foreground/40 focus:border-platform-primary focus:ring-platform-primary resize-none"
|
|
192
|
+
{...register("textareaField")}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Checkbox */}
|
|
197
|
+
<div className="flex items-start space-x-3">
|
|
198
|
+
<Checkbox
|
|
199
|
+
id="checkboxField"
|
|
200
|
+
checked={checkboxValue}
|
|
201
|
+
onCheckedChange={(checked) => setValue("checkboxField", !!checked)}
|
|
202
|
+
className="border-platform-secondary/50 data-[state=checked]:bg-platform-primary data-[state=checked]:border-platform-primary mt-0.5"
|
|
203
|
+
/>
|
|
204
|
+
<div className="space-y-1">
|
|
205
|
+
<Label
|
|
206
|
+
htmlFor="checkboxField"
|
|
207
|
+
className="text-platform-foreground font-medium cursor-pointer"
|
|
208
|
+
>
|
|
209
|
+
I agree to the terms
|
|
210
|
+
</Label>
|
|
211
|
+
<p className="text-sm text-platform-foreground/60">
|
|
212
|
+
By checking this box, you agree to our Terms of Service and Privacy Policy.
|
|
213
|
+
</p>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Form Actions */}
|
|
218
|
+
<div className="flex items-center justify-end gap-3 pt-4 border-t border-platform-secondary/10">
|
|
219
|
+
{onCancel && (
|
|
220
|
+
<Button
|
|
221
|
+
type="button"
|
|
222
|
+
variant="outline"
|
|
223
|
+
onClick={onCancel}
|
|
224
|
+
disabled={isLoading}
|
|
225
|
+
className="border-platform-secondary text-platform-foreground hover:bg-platform-secondary/10"
|
|
226
|
+
>
|
|
227
|
+
Cancel
|
|
228
|
+
</Button>
|
|
229
|
+
)}
|
|
230
|
+
<Button
|
|
231
|
+
type="submit"
|
|
232
|
+
disabled={isLoading}
|
|
233
|
+
className="bg-platform-primary text-white hover:bg-platform-primary/90 disabled:opacity-50"
|
|
234
|
+
>
|
|
235
|
+
{isLoading ? "Submitting..." : "Submit"}
|
|
236
|
+
</Button>
|
|
237
|
+
</div>
|
|
238
|
+
</form>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Example usage:
|
|
244
|
+
// <FormSection
|
|
245
|
+
// title="Contact Information"
|
|
246
|
+
// description="Please fill out the form below"
|
|
247
|
+
// onSubmit={(data) => console.log(data)}
|
|
248
|
+
// onCancel={() => console.log("Cancelled")}
|
|
249
|
+
// isLoading={false}
|
|
250
|
+
// />
|