real-prototypes-skill 0.1.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 +375 -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,239 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogTrigger,
|
|
10
|
+
DialogClose,
|
|
11
|
+
} from "@/components/ui/dialog";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { X } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
interface ModalDialogProps {
|
|
16
|
+
open?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
trigger?: React.ReactNode;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
footer?: React.ReactNode;
|
|
23
|
+
showCloseButton?: boolean;
|
|
24
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sizeClasses = {
|
|
29
|
+
sm: "max-w-sm",
|
|
30
|
+
md: "max-w-md",
|
|
31
|
+
lg: "max-w-lg",
|
|
32
|
+
xl: "max-w-xl",
|
|
33
|
+
full: "max-w-4xl",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function ModalDialog({
|
|
37
|
+
open,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
trigger,
|
|
40
|
+
title = "{/* FEATURE_TITLE */}",
|
|
41
|
+
description,
|
|
42
|
+
children,
|
|
43
|
+
footer,
|
|
44
|
+
showCloseButton = true,
|
|
45
|
+
size = "md",
|
|
46
|
+
className = "",
|
|
47
|
+
}: ModalDialogProps) {
|
|
48
|
+
return (
|
|
49
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
50
|
+
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
|
51
|
+
|
|
52
|
+
<DialogContent
|
|
53
|
+
className={`
|
|
54
|
+
bg-platform-background
|
|
55
|
+
border-platform-secondary/20
|
|
56
|
+
text-platform-foreground
|
|
57
|
+
${sizeClasses[size]}
|
|
58
|
+
${className}
|
|
59
|
+
`}
|
|
60
|
+
onOpenAutoFocus={(e) => {
|
|
61
|
+
// Prevent auto-focus on first focusable element if needed
|
|
62
|
+
// e.preventDefault();
|
|
63
|
+
}}
|
|
64
|
+
onEscapeKeyDown={(e) => {
|
|
65
|
+
// Handle escape key if needed
|
|
66
|
+
// e.preventDefault();
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{showCloseButton && (
|
|
70
|
+
<DialogClose
|
|
71
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-platform-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-platform-primary focus:ring-offset-2 disabled:pointer-events-none"
|
|
72
|
+
aria-label="Close dialog"
|
|
73
|
+
>
|
|
74
|
+
<X className="h-4 w-4 text-platform-foreground" />
|
|
75
|
+
</DialogClose>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle className="text-lg font-semibold text-platform-foreground">
|
|
80
|
+
{title}
|
|
81
|
+
</DialogTitle>
|
|
82
|
+
{description && (
|
|
83
|
+
<DialogDescription className="text-platform-foreground/60">
|
|
84
|
+
{description}
|
|
85
|
+
</DialogDescription>
|
|
86
|
+
)}
|
|
87
|
+
</DialogHeader>
|
|
88
|
+
|
|
89
|
+
<div className="py-4">
|
|
90
|
+
{/* FEATURE_CONTENT */}
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{footer !== undefined ? (
|
|
95
|
+
footer
|
|
96
|
+
) : (
|
|
97
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
98
|
+
<DialogClose asChild>
|
|
99
|
+
<Button
|
|
100
|
+
type="button"
|
|
101
|
+
variant="outline"
|
|
102
|
+
className="border-platform-secondary text-platform-foreground hover:bg-platform-secondary/10"
|
|
103
|
+
>
|
|
104
|
+
Cancel
|
|
105
|
+
</Button>
|
|
106
|
+
</DialogClose>
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
className="bg-platform-primary text-white hover:bg-platform-primary/90"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
// Handle primary action
|
|
112
|
+
onOpenChange?.(false);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Confirm
|
|
116
|
+
</Button>
|
|
117
|
+
</DialogFooter>
|
|
118
|
+
)}
|
|
119
|
+
</DialogContent>
|
|
120
|
+
</Dialog>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Confirmation dialog variant
|
|
125
|
+
interface ConfirmDialogProps {
|
|
126
|
+
open?: boolean;
|
|
127
|
+
onOpenChange?: (open: boolean) => void;
|
|
128
|
+
title?: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
confirmText?: string;
|
|
131
|
+
cancelText?: string;
|
|
132
|
+
onConfirm?: () => void;
|
|
133
|
+
onCancel?: () => void;
|
|
134
|
+
variant?: "default" | "destructive";
|
|
135
|
+
isLoading?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ConfirmDialog({
|
|
139
|
+
open,
|
|
140
|
+
onOpenChange,
|
|
141
|
+
title = "Are you sure?",
|
|
142
|
+
description = "This action cannot be undone.",
|
|
143
|
+
confirmText = "Confirm",
|
|
144
|
+
cancelText = "Cancel",
|
|
145
|
+
onConfirm,
|
|
146
|
+
onCancel,
|
|
147
|
+
variant = "default",
|
|
148
|
+
isLoading = false,
|
|
149
|
+
}: ConfirmDialogProps) {
|
|
150
|
+
const handleConfirm = () => {
|
|
151
|
+
onConfirm?.();
|
|
152
|
+
if (!isLoading) {
|
|
153
|
+
onOpenChange?.(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleCancel = () => {
|
|
158
|
+
onCancel?.();
|
|
159
|
+
onOpenChange?.(false);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
164
|
+
<DialogContent
|
|
165
|
+
className="bg-platform-background border-platform-secondary/20 text-platform-foreground max-w-md"
|
|
166
|
+
aria-describedby="confirm-dialog-description"
|
|
167
|
+
>
|
|
168
|
+
<DialogHeader>
|
|
169
|
+
<DialogTitle className="text-lg font-semibold text-platform-foreground">
|
|
170
|
+
{title}
|
|
171
|
+
</DialogTitle>
|
|
172
|
+
<DialogDescription
|
|
173
|
+
id="confirm-dialog-description"
|
|
174
|
+
className="text-platform-foreground/60"
|
|
175
|
+
>
|
|
176
|
+
{description}
|
|
177
|
+
</DialogDescription>
|
|
178
|
+
</DialogHeader>
|
|
179
|
+
|
|
180
|
+
<DialogFooter className="gap-2 sm:gap-0 mt-4">
|
|
181
|
+
<Button
|
|
182
|
+
type="button"
|
|
183
|
+
variant="outline"
|
|
184
|
+
onClick={handleCancel}
|
|
185
|
+
disabled={isLoading}
|
|
186
|
+
className="border-platform-secondary text-platform-foreground hover:bg-platform-secondary/10"
|
|
187
|
+
>
|
|
188
|
+
{cancelText}
|
|
189
|
+
</Button>
|
|
190
|
+
<Button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={handleConfirm}
|
|
193
|
+
disabled={isLoading}
|
|
194
|
+
className={
|
|
195
|
+
variant === "destructive"
|
|
196
|
+
? "bg-red-600 text-white hover:bg-red-700"
|
|
197
|
+
: "bg-platform-primary text-white hover:bg-platform-primary/90"
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
{isLoading ? "Loading..." : confirmText}
|
|
201
|
+
</Button>
|
|
202
|
+
</DialogFooter>
|
|
203
|
+
</DialogContent>
|
|
204
|
+
</Dialog>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Example usage:
|
|
209
|
+
// Basic modal:
|
|
210
|
+
// <ModalDialog
|
|
211
|
+
// trigger={<Button>Open Modal</Button>}
|
|
212
|
+
// title="Edit Profile"
|
|
213
|
+
// description="Make changes to your profile here."
|
|
214
|
+
// >
|
|
215
|
+
// <div className="space-y-4">
|
|
216
|
+
// <p>Modal content goes here</p>
|
|
217
|
+
// </div>
|
|
218
|
+
// </ModalDialog>
|
|
219
|
+
|
|
220
|
+
// Controlled modal:
|
|
221
|
+
// const [open, setOpen] = useState(false);
|
|
222
|
+
// <ModalDialog
|
|
223
|
+
// open={open}
|
|
224
|
+
// onOpenChange={setOpen}
|
|
225
|
+
// title="Settings"
|
|
226
|
+
// >
|
|
227
|
+
// <p>Content here</p>
|
|
228
|
+
// </ModalDialog>
|
|
229
|
+
|
|
230
|
+
// Confirmation dialog:
|
|
231
|
+
// <ConfirmDialog
|
|
232
|
+
// open={showConfirm}
|
|
233
|
+
// onOpenChange={setShowConfirm}
|
|
234
|
+
// title="Delete Item"
|
|
235
|
+
// description="Are you sure you want to delete this item? This action cannot be undone."
|
|
236
|
+
// confirmText="Delete"
|
|
237
|
+
// variant="destructive"
|
|
238
|
+
// onConfirm={() => handleDelete()}
|
|
239
|
+
// />
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "@/components/ui/collapsible";
|
|
9
|
+
import {
|
|
10
|
+
Tooltip,
|
|
11
|
+
TooltipContent,
|
|
12
|
+
TooltipProvider,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "@/components/ui/tooltip";
|
|
15
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
16
|
+
|
|
17
|
+
interface NavItemProps {
|
|
18
|
+
href?: string;
|
|
19
|
+
label: string;
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
isActive?: boolean;
|
|
22
|
+
isCollapsed?: boolean;
|
|
23
|
+
badge?: string | number;
|
|
24
|
+
onClick?: () => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function NavItem({
|
|
29
|
+
href,
|
|
30
|
+
label = "{/* FEATURE_TITLE */}",
|
|
31
|
+
icon,
|
|
32
|
+
isActive = false,
|
|
33
|
+
isCollapsed = false,
|
|
34
|
+
badge,
|
|
35
|
+
onClick,
|
|
36
|
+
className = "",
|
|
37
|
+
}: NavItemProps) {
|
|
38
|
+
const baseClasses = `
|
|
39
|
+
w-full flex items-center gap-3 px-3 py-2 rounded-md
|
|
40
|
+
text-sm font-medium transition-colors
|
|
41
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-platform-primary focus-visible:ring-offset-2
|
|
42
|
+
${
|
|
43
|
+
isActive
|
|
44
|
+
? "bg-platform-primary/10 text-platform-primary"
|
|
45
|
+
: "text-platform-foreground/70 hover:text-platform-foreground hover:bg-platform-secondary/10"
|
|
46
|
+
}
|
|
47
|
+
${className}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const content = (
|
|
51
|
+
<>
|
|
52
|
+
{icon && (
|
|
53
|
+
<span className="flex-shrink-0 w-5 h-5" aria-hidden="true">
|
|
54
|
+
{icon}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
{!isCollapsed && (
|
|
58
|
+
<>
|
|
59
|
+
<span className="flex-1 truncate">{label}</span>
|
|
60
|
+
{badge !== undefined && (
|
|
61
|
+
<span
|
|
62
|
+
className="flex-shrink-0 px-2 py-0.5 text-xs font-medium rounded-full bg-platform-primary/10 text-platform-primary"
|
|
63
|
+
aria-label={`${badge} items`}
|
|
64
|
+
>
|
|
65
|
+
{badge}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Wrap in tooltip when collapsed
|
|
74
|
+
const wrappedContent = isCollapsed ? (
|
|
75
|
+
<TooltipProvider>
|
|
76
|
+
<Tooltip delayDuration={0}>
|
|
77
|
+
<TooltipTrigger asChild>
|
|
78
|
+
{href ? (
|
|
79
|
+
<Link
|
|
80
|
+
href={href}
|
|
81
|
+
className={baseClasses}
|
|
82
|
+
aria-current={isActive ? "page" : undefined}
|
|
83
|
+
>
|
|
84
|
+
{content}
|
|
85
|
+
</Link>
|
|
86
|
+
) : (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={baseClasses}
|
|
90
|
+
onClick={onClick}
|
|
91
|
+
aria-pressed={isActive}
|
|
92
|
+
>
|
|
93
|
+
{content}
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</TooltipTrigger>
|
|
97
|
+
<TooltipContent
|
|
98
|
+
side="right"
|
|
99
|
+
className="bg-platform-foreground text-platform-background"
|
|
100
|
+
>
|
|
101
|
+
<p>{label}</p>
|
|
102
|
+
</TooltipContent>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
</TooltipProvider>
|
|
105
|
+
) : href ? (
|
|
106
|
+
<Link
|
|
107
|
+
href={href}
|
|
108
|
+
className={baseClasses}
|
|
109
|
+
aria-current={isActive ? "page" : undefined}
|
|
110
|
+
>
|
|
111
|
+
{content}
|
|
112
|
+
</Link>
|
|
113
|
+
) : (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
className={baseClasses}
|
|
117
|
+
onClick={onClick}
|
|
118
|
+
aria-pressed={isActive}
|
|
119
|
+
>
|
|
120
|
+
{content}
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return wrappedContent;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Nested navigation group
|
|
128
|
+
interface NavGroupProps {
|
|
129
|
+
label: string;
|
|
130
|
+
icon?: React.ReactNode;
|
|
131
|
+
children: React.ReactNode;
|
|
132
|
+
defaultOpen?: boolean;
|
|
133
|
+
isCollapsed?: boolean;
|
|
134
|
+
className?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function NavGroup({
|
|
138
|
+
label = "{/* FEATURE_TITLE */}",
|
|
139
|
+
icon,
|
|
140
|
+
children,
|
|
141
|
+
defaultOpen = false,
|
|
142
|
+
isCollapsed = false,
|
|
143
|
+
className = "",
|
|
144
|
+
}: NavGroupProps) {
|
|
145
|
+
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
|
146
|
+
|
|
147
|
+
if (isCollapsed) {
|
|
148
|
+
return (
|
|
149
|
+
<TooltipProvider>
|
|
150
|
+
<Tooltip delayDuration={0}>
|
|
151
|
+
<TooltipTrigger asChild>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className={`
|
|
155
|
+
w-full flex items-center justify-center p-2 rounded-md
|
|
156
|
+
text-platform-foreground/70 hover:text-platform-foreground hover:bg-platform-secondary/10
|
|
157
|
+
transition-colors
|
|
158
|
+
${className}
|
|
159
|
+
`}
|
|
160
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
161
|
+
aria-expanded={isOpen}
|
|
162
|
+
>
|
|
163
|
+
{icon && (
|
|
164
|
+
<span className="w-5 h-5" aria-hidden="true">
|
|
165
|
+
{icon}
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
</TooltipTrigger>
|
|
170
|
+
<TooltipContent
|
|
171
|
+
side="right"
|
|
172
|
+
className="bg-platform-foreground text-platform-background"
|
|
173
|
+
>
|
|
174
|
+
<p>{label}</p>
|
|
175
|
+
</TooltipContent>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
</TooltipProvider>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Collapsible open={isOpen} onOpenChange={setIsOpen} className={className}>
|
|
183
|
+
<CollapsibleTrigger asChild>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className={`
|
|
187
|
+
w-full flex items-center gap-3 px-3 py-2 rounded-md
|
|
188
|
+
text-sm font-medium text-platform-foreground/70
|
|
189
|
+
hover:text-platform-foreground hover:bg-platform-secondary/10
|
|
190
|
+
transition-colors
|
|
191
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-platform-primary focus-visible:ring-offset-2
|
|
192
|
+
`}
|
|
193
|
+
aria-expanded={isOpen}
|
|
194
|
+
>
|
|
195
|
+
{icon && (
|
|
196
|
+
<span className="flex-shrink-0 w-5 h-5" aria-hidden="true">
|
|
197
|
+
{icon}
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
<span className="flex-1 text-left truncate">{label}</span>
|
|
201
|
+
{isOpen ? (
|
|
202
|
+
<ChevronDown className="flex-shrink-0 w-4 h-4" aria-hidden="true" />
|
|
203
|
+
) : (
|
|
204
|
+
<ChevronRight className="flex-shrink-0 w-4 h-4" aria-hidden="true" />
|
|
205
|
+
)}
|
|
206
|
+
</button>
|
|
207
|
+
</CollapsibleTrigger>
|
|
208
|
+
<CollapsibleContent className="pl-4 mt-1 space-y-1">
|
|
209
|
+
{/* FEATURE_CONTENT */}
|
|
210
|
+
{children}
|
|
211
|
+
</CollapsibleContent>
|
|
212
|
+
</Collapsible>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Navigation section with label
|
|
217
|
+
interface NavSectionProps {
|
|
218
|
+
label?: string;
|
|
219
|
+
children: React.ReactNode;
|
|
220
|
+
isCollapsed?: boolean;
|
|
221
|
+
className?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function NavSection({
|
|
225
|
+
label,
|
|
226
|
+
children,
|
|
227
|
+
isCollapsed = false,
|
|
228
|
+
className = "",
|
|
229
|
+
}: NavSectionProps) {
|
|
230
|
+
return (
|
|
231
|
+
<nav className={`space-y-1 ${className}`} aria-label={label}>
|
|
232
|
+
{label && !isCollapsed && (
|
|
233
|
+
<h3 className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-platform-foreground/50">
|
|
234
|
+
{label}
|
|
235
|
+
</h3>
|
|
236
|
+
)}
|
|
237
|
+
{isCollapsed && label && (
|
|
238
|
+
<div className="h-px mx-3 my-2 bg-platform-secondary/20" aria-hidden="true" />
|
|
239
|
+
)}
|
|
240
|
+
{children}
|
|
241
|
+
</nav>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Example usage:
|
|
246
|
+
// Basic navigation:
|
|
247
|
+
// <NavSection label="Main">
|
|
248
|
+
// <NavItem href="/" label="Dashboard" icon={<HomeIcon />} isActive />
|
|
249
|
+
// <NavItem href="/analytics" label="Analytics" icon={<ChartIcon />} badge={3} />
|
|
250
|
+
// <NavItem href="/settings" label="Settings" icon={<SettingsIcon />} />
|
|
251
|
+
// </NavSection>
|
|
252
|
+
|
|
253
|
+
// Nested navigation:
|
|
254
|
+
// <NavSection label="Content">
|
|
255
|
+
// <NavGroup label="Products" icon={<BoxIcon />} defaultOpen>
|
|
256
|
+
// <NavItem href="/products" label="All Products" />
|
|
257
|
+
// <NavItem href="/products/new" label="Add New" />
|
|
258
|
+
// <NavItem href="/products/categories" label="Categories" />
|
|
259
|
+
// </NavGroup>
|
|
260
|
+
// </NavSection>
|
|
261
|
+
|
|
262
|
+
// Collapsed sidebar:
|
|
263
|
+
// <NavSection isCollapsed={isSidebarCollapsed}>
|
|
264
|
+
// <NavItem href="/" label="Dashboard" icon={<HomeIcon />} isCollapsed={isSidebarCollapsed} />
|
|
265
|
+
// </NavSection>
|