@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
# Loading State Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for loading states and skeletons.
|
|
4
|
-
|
|
5
|
-
## Skeleton Components
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// components/ui/Skeleton.tsx
|
|
9
|
-
interface Props {
|
|
10
|
-
className?: string
|
|
11
|
-
variant?: 'text' | 'circular' | 'rectangular'
|
|
12
|
-
width?: string | number
|
|
13
|
-
height?: string | number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Skeleton({
|
|
17
|
-
className = '',
|
|
18
|
-
variant = 'text',
|
|
19
|
-
width,
|
|
20
|
-
height
|
|
21
|
-
}: Props) {
|
|
22
|
-
const baseClasses = 'animate-pulse bg-gray-200'
|
|
23
|
-
|
|
24
|
-
const variantClasses = {
|
|
25
|
-
text: 'rounded',
|
|
26
|
-
circular: 'rounded-full',
|
|
27
|
-
rectangular: 'rounded-lg'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const style = {
|
|
31
|
-
width: typeof width === 'number' ? `${width}px` : width,
|
|
32
|
-
height: typeof height === 'number' ? `${height}px` : height
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div
|
|
37
|
-
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
|
|
38
|
-
style={style}
|
|
39
|
-
/>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Usage
|
|
44
|
-
<Skeleton variant="circular" width={40} height={40} />
|
|
45
|
-
<Skeleton variant="text" className="h-4 w-3/4" />
|
|
46
|
-
<Skeleton variant="rectangular" className="h-48 w-full" />
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Card Skeleton
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
// components/skeletons/CardSkeleton.tsx
|
|
53
|
-
import { Skeleton } from '@/components/ui/Skeleton'
|
|
54
|
-
|
|
55
|
-
export function CardSkeleton() {
|
|
56
|
-
return (
|
|
57
|
-
<div className="rounded-lg border bg-white p-6">
|
|
58
|
-
<div className="flex items-center gap-4">
|
|
59
|
-
<Skeleton variant="circular" width={48} height={48} />
|
|
60
|
-
<div className="flex-1 space-y-2">
|
|
61
|
-
<Skeleton className="h-4 w-1/2" />
|
|
62
|
-
<Skeleton className="h-3 w-1/3" />
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
<div className="mt-4 space-y-2">
|
|
66
|
-
<Skeleton className="h-4 w-full" />
|
|
67
|
-
<Skeleton className="h-4 w-full" />
|
|
68
|
-
<Skeleton className="h-4 w-2/3" />
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function CardGridSkeleton({ count = 6 }: { count?: number }) {
|
|
75
|
-
return (
|
|
76
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
77
|
-
{Array.from({ length: count }).map((_, i) => (
|
|
78
|
-
<CardSkeleton key={i} />
|
|
79
|
-
))}
|
|
80
|
-
</div>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Table Skeleton
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
// components/skeletons/TableSkeleton.tsx
|
|
89
|
-
import { Skeleton } from '@/components/ui/Skeleton'
|
|
90
|
-
|
|
91
|
-
export function TableSkeleton({ rows = 5, cols = 4 }: { rows?: number; cols?: number }) {
|
|
92
|
-
return (
|
|
93
|
-
<div className="rounded-lg border">
|
|
94
|
-
{/* Header */}
|
|
95
|
-
<div className="flex gap-4 border-b bg-gray-50 p-4">
|
|
96
|
-
{Array.from({ length: cols }).map((_, i) => (
|
|
97
|
-
<Skeleton key={i} className="h-4 flex-1" />
|
|
98
|
-
))}
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{/* Rows */}
|
|
102
|
-
{Array.from({ length: rows }).map((_, rowIndex) => (
|
|
103
|
-
<div key={rowIndex} className="flex gap-4 border-b p-4 last:border-0">
|
|
104
|
-
{Array.from({ length: cols }).map((_, colIndex) => (
|
|
105
|
-
<Skeleton
|
|
106
|
-
key={colIndex}
|
|
107
|
-
className={`h-4 flex-1 ${colIndex === 0 ? 'w-1/4' : ''}`}
|
|
108
|
-
/>
|
|
109
|
-
))}
|
|
110
|
-
</div>
|
|
111
|
-
))}
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Loading Spinner
|
|
118
|
-
|
|
119
|
-
```tsx
|
|
120
|
-
// components/ui/Spinner.tsx
|
|
121
|
-
interface Props {
|
|
122
|
-
size?: 'sm' | 'md' | 'lg'
|
|
123
|
-
className?: string
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function Spinner({ size = 'md', className = '' }: Props) {
|
|
127
|
-
const sizeClasses = {
|
|
128
|
-
sm: 'h-4 w-4',
|
|
129
|
-
md: 'h-6 w-6',
|
|
130
|
-
lg: 'h-8 w-8'
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<svg
|
|
135
|
-
className={`animate-spin ${sizeClasses[size]} ${className}`}
|
|
136
|
-
viewBox="0 0 24 24"
|
|
137
|
-
fill="none"
|
|
138
|
-
>
|
|
139
|
-
<circle
|
|
140
|
-
className="opacity-25"
|
|
141
|
-
cx="12"
|
|
142
|
-
cy="12"
|
|
143
|
-
r="10"
|
|
144
|
-
stroke="currentColor"
|
|
145
|
-
strokeWidth="4"
|
|
146
|
-
/>
|
|
147
|
-
<path
|
|
148
|
-
className="opacity-75"
|
|
149
|
-
fill="currentColor"
|
|
150
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
151
|
-
/>
|
|
152
|
-
</svg>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Full page loading
|
|
157
|
-
export function PageLoader() {
|
|
158
|
-
return (
|
|
159
|
-
<div className="flex min-h-screen items-center justify-center">
|
|
160
|
-
<Spinner size="lg" />
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Loading Button
|
|
167
|
-
|
|
168
|
-
```tsx
|
|
169
|
-
// components/ui/LoadingButton.tsx
|
|
170
|
-
import { Spinner } from './Spinner'
|
|
171
|
-
|
|
172
|
-
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
173
|
-
loading?: boolean
|
|
174
|
-
children: React.ReactNode
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function LoadingButton({ loading, children, disabled, ...props }: Props) {
|
|
178
|
-
return (
|
|
179
|
-
<button
|
|
180
|
-
{...props}
|
|
181
|
-
disabled={disabled || loading}
|
|
182
|
-
className={`relative inline-flex items-center justify-center ${props.className}`}
|
|
183
|
-
>
|
|
184
|
-
{loading && (
|
|
185
|
-
<Spinner size="sm" className="absolute" />
|
|
186
|
-
)}
|
|
187
|
-
<span className={loading ? 'invisible' : ''}>{children}</span>
|
|
188
|
-
</button>
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Next.js Loading UI
|
|
194
|
-
|
|
195
|
-
```tsx
|
|
196
|
-
// app/dashboard/loading.tsx
|
|
197
|
-
import { CardGridSkeleton } from '@/components/skeletons/CardSkeleton'
|
|
198
|
-
|
|
199
|
-
export default function DashboardLoading() {
|
|
200
|
-
return (
|
|
201
|
-
<div className="space-y-6">
|
|
202
|
-
{/* Stats skeleton */}
|
|
203
|
-
<div className="grid gap-4 md:grid-cols-4">
|
|
204
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
205
|
-
<div key={i} className="rounded-lg border p-6">
|
|
206
|
-
<Skeleton className="mb-2 h-4 w-1/2" />
|
|
207
|
-
<Skeleton className="h-8 w-1/3" />
|
|
208
|
-
</div>
|
|
209
|
-
))}
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
{/* Cards skeleton */}
|
|
213
|
-
<CardGridSkeleton count={6} />
|
|
214
|
-
</div>
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Suspense Boundaries
|
|
220
|
-
|
|
221
|
-
```tsx
|
|
222
|
-
// app/dashboard/page.tsx
|
|
223
|
-
import { Suspense } from 'react'
|
|
224
|
-
import { CardGridSkeleton } from '@/components/skeletons/CardSkeleton'
|
|
225
|
-
|
|
226
|
-
export default function DashboardPage() {
|
|
227
|
-
return (
|
|
228
|
-
<div className="space-y-6">
|
|
229
|
-
{/* Immediately rendered */}
|
|
230
|
-
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
231
|
-
|
|
232
|
-
{/* Stream in */}
|
|
233
|
-
<Suspense fallback={<CardGridSkeleton />}>
|
|
234
|
-
<DataCards />
|
|
235
|
-
</Suspense>
|
|
236
|
-
|
|
237
|
-
<Suspense fallback={<TableSkeleton />}>
|
|
238
|
-
<RecentActivity />
|
|
239
|
-
</Suspense>
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function DataCards() {
|
|
245
|
-
const data = await fetchData()
|
|
246
|
-
return <CardGrid data={data} />
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## Progress Indicator
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
// components/ui/Progress.tsx
|
|
254
|
-
interface Props {
|
|
255
|
-
value: number
|
|
256
|
-
max?: number
|
|
257
|
-
showLabel?: boolean
|
|
258
|
-
size?: 'sm' | 'md' | 'lg'
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function Progress({ value, max = 100, showLabel = false, size = 'md' }: Props) {
|
|
262
|
-
const percentage = Math.min(100, (value / max) * 100)
|
|
263
|
-
|
|
264
|
-
const sizeClasses = {
|
|
265
|
-
sm: 'h-1',
|
|
266
|
-
md: 'h-2',
|
|
267
|
-
lg: 'h-4'
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<div className="w-full">
|
|
272
|
-
<div className={`w-full overflow-hidden rounded-full bg-gray-200 ${sizeClasses[size]}`}>
|
|
273
|
-
<div
|
|
274
|
-
className="h-full rounded-full bg-blue-600 transition-all duration-300"
|
|
275
|
-
style={{ width: `${percentage}%` }}
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
{showLabel && (
|
|
279
|
-
<p className="mt-1 text-sm text-gray-500">{Math.round(percentage)}%</p>
|
|
280
|
-
)}
|
|
281
|
-
</div>
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## When to Use
|
|
287
|
-
|
|
288
|
-
- Data fetching
|
|
289
|
-
- Page transitions
|
|
290
|
-
- Async operations
|
|
291
|
-
- Upload progress
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
# Modal Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for building accessible modals and dialogs.
|
|
4
|
-
|
|
5
|
-
## Basic Modal
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// components/ui/Modal.tsx
|
|
9
|
-
'use client'
|
|
10
|
-
|
|
11
|
-
import { useEffect, useRef } from 'react'
|
|
12
|
-
import { createPortal } from 'react-dom'
|
|
13
|
-
import { X } from 'lucide-react'
|
|
14
|
-
|
|
15
|
-
interface Props {
|
|
16
|
-
isOpen: boolean
|
|
17
|
-
onClose: () => void
|
|
18
|
-
title: string
|
|
19
|
-
children: React.ReactNode
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Modal({ isOpen, onClose, title, children }: Props) {
|
|
23
|
-
const overlayRef = useRef<HTMLDivElement>(null)
|
|
24
|
-
|
|
25
|
-
// Close on escape
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
function handleEscape(e: KeyboardEvent) {
|
|
28
|
-
if (e.key === 'Escape') onClose()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (isOpen) {
|
|
32
|
-
document.addEventListener('keydown', handleEscape)
|
|
33
|
-
document.body.style.overflow = 'hidden'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
document.removeEventListener('keydown', handleEscape)
|
|
38
|
-
document.body.style.overflow = ''
|
|
39
|
-
}
|
|
40
|
-
}, [isOpen, onClose])
|
|
41
|
-
|
|
42
|
-
// Close on overlay click
|
|
43
|
-
function handleOverlayClick(e: React.MouseEvent) {
|
|
44
|
-
if (e.target === overlayRef.current) onClose()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!isOpen) return null
|
|
48
|
-
|
|
49
|
-
return createPortal(
|
|
50
|
-
<div
|
|
51
|
-
ref={overlayRef}
|
|
52
|
-
onClick={handleOverlayClick}
|
|
53
|
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
54
|
-
role="dialog"
|
|
55
|
-
aria-modal="true"
|
|
56
|
-
aria-labelledby="modal-title"
|
|
57
|
-
>
|
|
58
|
-
<div className="relative w-full max-w-lg rounded-lg bg-white p-6 shadow-xl">
|
|
59
|
-
<div className="mb-4 flex items-center justify-between">
|
|
60
|
-
<h2 id="modal-title" className="text-xl font-semibold">
|
|
61
|
-
{title}
|
|
62
|
-
</h2>
|
|
63
|
-
<button
|
|
64
|
-
onClick={onClose}
|
|
65
|
-
className="rounded p-1 hover:bg-gray-100"
|
|
66
|
-
aria-label="Close modal"
|
|
67
|
-
>
|
|
68
|
-
<X className="h-5 w-5" />
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
{children}
|
|
72
|
-
</div>
|
|
73
|
-
</div>,
|
|
74
|
-
document.body
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Radix Dialog
|
|
80
|
-
|
|
81
|
-
```tsx
|
|
82
|
-
// components/ui/Dialog.tsx
|
|
83
|
-
'use client'
|
|
84
|
-
|
|
85
|
-
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
86
|
-
import { X } from 'lucide-react'
|
|
87
|
-
|
|
88
|
-
export function Dialog({ children, ...props }: DialogPrimitive.DialogProps) {
|
|
89
|
-
return <DialogPrimitive.Root {...props}>{children}</DialogPrimitive.Root>
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export const DialogTrigger = DialogPrimitive.Trigger
|
|
93
|
-
|
|
94
|
-
export function DialogContent({
|
|
95
|
-
children,
|
|
96
|
-
title,
|
|
97
|
-
description
|
|
98
|
-
}: {
|
|
99
|
-
children: React.ReactNode
|
|
100
|
-
title: string
|
|
101
|
-
description?: string
|
|
102
|
-
}) {
|
|
103
|
-
return (
|
|
104
|
-
<DialogPrimitive.Portal>
|
|
105
|
-
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-fadeIn" />
|
|
106
|
-
<DialogPrimitive.Content className="fixed left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-xl data-[state=open]:animate-scaleIn">
|
|
107
|
-
<DialogPrimitive.Title className="text-xl font-semibold">
|
|
108
|
-
{title}
|
|
109
|
-
</DialogPrimitive.Title>
|
|
110
|
-
|
|
111
|
-
{description && (
|
|
112
|
-
<DialogPrimitive.Description className="mt-2 text-gray-600">
|
|
113
|
-
{description}
|
|
114
|
-
</DialogPrimitive.Description>
|
|
115
|
-
)}
|
|
116
|
-
|
|
117
|
-
<div className="mt-4">{children}</div>
|
|
118
|
-
|
|
119
|
-
<DialogPrimitive.Close className="absolute right-4 top-4 rounded p-1 hover:bg-gray-100">
|
|
120
|
-
<X className="h-5 w-5" />
|
|
121
|
-
</DialogPrimitive.Close>
|
|
122
|
-
</DialogPrimitive.Content>
|
|
123
|
-
</DialogPrimitive.Portal>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Usage
|
|
128
|
-
<Dialog>
|
|
129
|
-
<DialogTrigger asChild>
|
|
130
|
-
<button>Open Dialog</button>
|
|
131
|
-
</DialogTrigger>
|
|
132
|
-
<DialogContent title="Edit Profile">
|
|
133
|
-
<form>...</form>
|
|
134
|
-
</DialogContent>
|
|
135
|
-
</Dialog>
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Confirmation Dialog
|
|
139
|
-
|
|
140
|
-
```tsx
|
|
141
|
-
// components/ui/ConfirmDialog.tsx
|
|
142
|
-
'use client'
|
|
143
|
-
|
|
144
|
-
import { useState } from 'react'
|
|
145
|
-
import { Dialog, DialogContent } from './Dialog'
|
|
146
|
-
|
|
147
|
-
interface Props {
|
|
148
|
-
title: string
|
|
149
|
-
message: string
|
|
150
|
-
confirmText?: string
|
|
151
|
-
cancelText?: string
|
|
152
|
-
variant?: 'danger' | 'warning' | 'default'
|
|
153
|
-
onConfirm: () => void | Promise<void>
|
|
154
|
-
trigger: React.ReactNode
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function ConfirmDialog({
|
|
158
|
-
title,
|
|
159
|
-
message,
|
|
160
|
-
confirmText = 'Confirm',
|
|
161
|
-
cancelText = 'Cancel',
|
|
162
|
-
variant = 'default',
|
|
163
|
-
onConfirm,
|
|
164
|
-
trigger
|
|
165
|
-
}: Props) {
|
|
166
|
-
const [open, setOpen] = useState(false)
|
|
167
|
-
const [loading, setLoading] = useState(false)
|
|
168
|
-
|
|
169
|
-
async function handleConfirm() {
|
|
170
|
-
setLoading(true)
|
|
171
|
-
try {
|
|
172
|
-
await onConfirm()
|
|
173
|
-
setOpen(false)
|
|
174
|
-
} finally {
|
|
175
|
-
setLoading(false)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const confirmClass = {
|
|
180
|
-
danger: 'bg-red-600 hover:bg-red-700',
|
|
181
|
-
warning: 'bg-amber-600 hover:bg-amber-700',
|
|
182
|
-
default: 'bg-black hover:bg-gray-800'
|
|
183
|
-
}[variant]
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
187
|
-
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
188
|
-
<DialogContent title={title} description={message}>
|
|
189
|
-
<div className="mt-6 flex justify-end gap-3">
|
|
190
|
-
<button
|
|
191
|
-
onClick={() => setOpen(false)}
|
|
192
|
-
className="rounded px-4 py-2 hover:bg-gray-100"
|
|
193
|
-
>
|
|
194
|
-
{cancelText}
|
|
195
|
-
</button>
|
|
196
|
-
<button
|
|
197
|
-
onClick={handleConfirm}
|
|
198
|
-
disabled={loading}
|
|
199
|
-
className={`rounded px-4 py-2 text-white ${confirmClass}`}
|
|
200
|
-
>
|
|
201
|
-
{loading ? 'Loading...' : confirmText}
|
|
202
|
-
</button>
|
|
203
|
-
</div>
|
|
204
|
-
</DialogContent>
|
|
205
|
-
</Dialog>
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Usage
|
|
210
|
-
<ConfirmDialog
|
|
211
|
-
title="Delete Item"
|
|
212
|
-
message="Are you sure you want to delete this item? This action cannot be undone."
|
|
213
|
-
confirmText="Delete"
|
|
214
|
-
variant="danger"
|
|
215
|
-
onConfirm={() => deleteItem(id)}
|
|
216
|
-
trigger={<button>Delete</button>}
|
|
217
|
-
/>
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Sheet / Drawer
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
// components/ui/Sheet.tsx
|
|
224
|
-
'use client'
|
|
225
|
-
|
|
226
|
-
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
|
227
|
-
import { X } from 'lucide-react'
|
|
228
|
-
|
|
229
|
-
type Side = 'left' | 'right' | 'top' | 'bottom'
|
|
230
|
-
|
|
231
|
-
interface Props {
|
|
232
|
-
children: React.ReactNode
|
|
233
|
-
side?: Side
|
|
234
|
-
title: string
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const sideClasses: Record<Side, string> = {
|
|
238
|
-
left: 'left-0 top-0 h-full w-80 data-[state=open]:animate-slideInLeft',
|
|
239
|
-
right: 'right-0 top-0 h-full w-80 data-[state=open]:animate-slideInRight',
|
|
240
|
-
top: 'top-0 left-0 w-full h-80 data-[state=open]:animate-slideInTop',
|
|
241
|
-
bottom: 'bottom-0 left-0 w-full h-80 data-[state=open]:animate-slideInBottom'
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function SheetContent({ children, side = 'right', title }: Props) {
|
|
245
|
-
return (
|
|
246
|
-
<SheetPrimitive.Portal>
|
|
247
|
-
<SheetPrimitive.Overlay className="fixed inset-0 bg-black/50" />
|
|
248
|
-
<SheetPrimitive.Content
|
|
249
|
-
className={`fixed bg-white p-6 shadow-xl ${sideClasses[side]}`}
|
|
250
|
-
>
|
|
251
|
-
<div className="mb-4 flex items-center justify-between">
|
|
252
|
-
<SheetPrimitive.Title className="text-lg font-semibold">
|
|
253
|
-
{title}
|
|
254
|
-
</SheetPrimitive.Title>
|
|
255
|
-
<SheetPrimitive.Close className="rounded p-1 hover:bg-gray-100">
|
|
256
|
-
<X className="h-5 w-5" />
|
|
257
|
-
</SheetPrimitive.Close>
|
|
258
|
-
</div>
|
|
259
|
-
{children}
|
|
260
|
-
</SheetPrimitive.Content>
|
|
261
|
-
</SheetPrimitive.Portal>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Usage
|
|
266
|
-
<Sheet>
|
|
267
|
-
<SheetTrigger asChild>
|
|
268
|
-
<button>Open Menu</button>
|
|
269
|
-
</SheetTrigger>
|
|
270
|
-
<SheetContent side="left" title="Navigation">
|
|
271
|
-
<nav>...</nav>
|
|
272
|
-
</SheetContent>
|
|
273
|
-
</Sheet>
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Modal with Form
|
|
277
|
-
|
|
278
|
-
```tsx
|
|
279
|
-
// components/modals/EditUserModal.tsx
|
|
280
|
-
'use client'
|
|
281
|
-
|
|
282
|
-
import { useState } from 'react'
|
|
283
|
-
import { useForm } from 'react-hook-form'
|
|
284
|
-
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/Dialog'
|
|
285
|
-
|
|
286
|
-
interface Props {
|
|
287
|
-
user: User
|
|
288
|
-
onUpdate: (data: UserData) => Promise<void>
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function EditUserModal({ user, onUpdate }: Props) {
|
|
292
|
-
const [open, setOpen] = useState(false)
|
|
293
|
-
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm({
|
|
294
|
-
defaultValues: { name: user.name, email: user.email }
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
async function onSubmit(data: UserData) {
|
|
298
|
-
await onUpdate(data)
|
|
299
|
-
setOpen(false)
|
|
300
|
-
reset(data)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return (
|
|
304
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
305
|
-
<DialogTrigger asChild>
|
|
306
|
-
<button>Edit</button>
|
|
307
|
-
</DialogTrigger>
|
|
308
|
-
<DialogContent title="Edit User">
|
|
309
|
-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
310
|
-
<div>
|
|
311
|
-
<label>Name</label>
|
|
312
|
-
<input {...register('name')} />
|
|
313
|
-
</div>
|
|
314
|
-
<div>
|
|
315
|
-
<label>Email</label>
|
|
316
|
-
<input {...register('email')} type="email" />
|
|
317
|
-
</div>
|
|
318
|
-
<div className="flex justify-end gap-2">
|
|
319
|
-
<button type="button" onClick={() => setOpen(false)}>
|
|
320
|
-
Cancel
|
|
321
|
-
</button>
|
|
322
|
-
<button type="submit" disabled={isSubmitting}>
|
|
323
|
-
{isSubmitting ? 'Saving...' : 'Save'}
|
|
324
|
-
</button>
|
|
325
|
-
</div>
|
|
326
|
-
</form>
|
|
327
|
-
</DialogContent>
|
|
328
|
-
</Dialog>
|
|
329
|
-
)
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## When to Use
|
|
334
|
-
|
|
335
|
-
- Confirmations
|
|
336
|
-
- Quick forms
|
|
337
|
-
- Notifications
|
|
338
|
-
- Side panels
|