@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,476 +0,0 @@
|
|
|
1
|
-
# Dropdown Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for dropdown menus and select components.
|
|
4
|
-
|
|
5
|
-
## Basic Dropdown Menu
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// components/ui/DropdownMenu.tsx
|
|
9
|
-
'use client'
|
|
10
|
-
|
|
11
|
-
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
|
12
|
-
import { forwardRef } from 'react'
|
|
13
|
-
import { cn } from '@/lib/utils'
|
|
14
|
-
|
|
15
|
-
export const DropdownMenu = DropdownMenuPrimitive.Root
|
|
16
|
-
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
17
|
-
|
|
18
|
-
export const DropdownMenuContent = forwardRef<
|
|
19
|
-
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
20
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
21
|
-
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
22
|
-
<DropdownMenuPrimitive.Portal>
|
|
23
|
-
<DropdownMenuPrimitive.Content
|
|
24
|
-
ref={ref}
|
|
25
|
-
sideOffset={sideOffset}
|
|
26
|
-
className={cn(
|
|
27
|
-
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-md',
|
|
28
|
-
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
29
|
-
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
30
|
-
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
31
|
-
className
|
|
32
|
-
)}
|
|
33
|
-
{...props}
|
|
34
|
-
/>
|
|
35
|
-
</DropdownMenuPrimitive.Portal>
|
|
36
|
-
))
|
|
37
|
-
DropdownMenuContent.displayName = 'DropdownMenuContent'
|
|
38
|
-
|
|
39
|
-
export const DropdownMenuItem = forwardRef<
|
|
40
|
-
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
41
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
42
|
-
inset?: boolean
|
|
43
|
-
}
|
|
44
|
-
>(({ className, inset, ...props }, ref) => (
|
|
45
|
-
<DropdownMenuPrimitive.Item
|
|
46
|
-
ref={ref}
|
|
47
|
-
className={cn(
|
|
48
|
-
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
|
49
|
-
'focus:bg-gray-100 focus:text-gray-900',
|
|
50
|
-
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
51
|
-
inset && 'pl-8',
|
|
52
|
-
className
|
|
53
|
-
)}
|
|
54
|
-
{...props}
|
|
55
|
-
/>
|
|
56
|
-
))
|
|
57
|
-
DropdownMenuItem.displayName = 'DropdownMenuItem'
|
|
58
|
-
|
|
59
|
-
export const DropdownMenuSeparator = forwardRef<
|
|
60
|
-
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
61
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
62
|
-
>(({ className, ...props }, ref) => (
|
|
63
|
-
<DropdownMenuPrimitive.Separator
|
|
64
|
-
ref={ref}
|
|
65
|
-
className={cn('-mx-1 my-1 h-px bg-gray-200', className)}
|
|
66
|
-
{...props}
|
|
67
|
-
/>
|
|
68
|
-
))
|
|
69
|
-
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator'
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## User Menu Dropdown
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
// components/UserMenu.tsx
|
|
76
|
-
'use client'
|
|
77
|
-
|
|
78
|
-
import {
|
|
79
|
-
DropdownMenu,
|
|
80
|
-
DropdownMenuContent,
|
|
81
|
-
DropdownMenuItem,
|
|
82
|
-
DropdownMenuSeparator,
|
|
83
|
-
DropdownMenuTrigger
|
|
84
|
-
} from '@/components/ui/DropdownMenu'
|
|
85
|
-
import { signOut, useSession } from 'next-auth/react'
|
|
86
|
-
import { User, Settings, LogOut, CreditCard } from 'lucide-react'
|
|
87
|
-
import Link from 'next/link'
|
|
88
|
-
import Image from 'next/image'
|
|
89
|
-
|
|
90
|
-
export function UserMenu() {
|
|
91
|
-
const { data: session } = useSession()
|
|
92
|
-
|
|
93
|
-
if (!session?.user) return null
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<DropdownMenu>
|
|
97
|
-
<DropdownMenuTrigger asChild>
|
|
98
|
-
<button className="flex items-center gap-2 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
99
|
-
{session.user.image ? (
|
|
100
|
-
<Image
|
|
101
|
-
src={session.user.image}
|
|
102
|
-
alt={session.user.name ?? ''}
|
|
103
|
-
width={32}
|
|
104
|
-
height={32}
|
|
105
|
-
className="rounded-full"
|
|
106
|
-
/>
|
|
107
|
-
) : (
|
|
108
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
|
109
|
-
<User className="h-4 w-4" />
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
</button>
|
|
113
|
-
</DropdownMenuTrigger>
|
|
114
|
-
|
|
115
|
-
<DropdownMenuContent align="end" className="w-56">
|
|
116
|
-
<div className="px-2 py-1.5">
|
|
117
|
-
<p className="text-sm font-medium">{session.user.name}</p>
|
|
118
|
-
<p className="text-xs text-gray-500">{session.user.email}</p>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<DropdownMenuSeparator />
|
|
122
|
-
|
|
123
|
-
<DropdownMenuItem asChild>
|
|
124
|
-
<Link href="/settings/profile" className="flex items-center gap-2">
|
|
125
|
-
<User className="h-4 w-4" />
|
|
126
|
-
Profile
|
|
127
|
-
</Link>
|
|
128
|
-
</DropdownMenuItem>
|
|
129
|
-
|
|
130
|
-
<DropdownMenuItem asChild>
|
|
131
|
-
<Link href="/settings" className="flex items-center gap-2">
|
|
132
|
-
<Settings className="h-4 w-4" />
|
|
133
|
-
Settings
|
|
134
|
-
</Link>
|
|
135
|
-
</DropdownMenuItem>
|
|
136
|
-
|
|
137
|
-
<DropdownMenuItem asChild>
|
|
138
|
-
<Link href="/billing" className="flex items-center gap-2">
|
|
139
|
-
<CreditCard className="h-4 w-4" />
|
|
140
|
-
Billing
|
|
141
|
-
</Link>
|
|
142
|
-
</DropdownMenuItem>
|
|
143
|
-
|
|
144
|
-
<DropdownMenuSeparator />
|
|
145
|
-
|
|
146
|
-
<DropdownMenuItem
|
|
147
|
-
onClick={() => signOut()}
|
|
148
|
-
className="text-red-600 focus:text-red-600"
|
|
149
|
-
>
|
|
150
|
-
<LogOut className="mr-2 h-4 w-4" />
|
|
151
|
-
Sign out
|
|
152
|
-
</DropdownMenuItem>
|
|
153
|
-
</DropdownMenuContent>
|
|
154
|
-
</DropdownMenu>
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Actions Dropdown
|
|
160
|
-
|
|
161
|
-
```tsx
|
|
162
|
-
// components/ActionsDropdown.tsx
|
|
163
|
-
'use client'
|
|
164
|
-
|
|
165
|
-
import {
|
|
166
|
-
DropdownMenu,
|
|
167
|
-
DropdownMenuContent,
|
|
168
|
-
DropdownMenuItem,
|
|
169
|
-
DropdownMenuSeparator,
|
|
170
|
-
DropdownMenuTrigger
|
|
171
|
-
} from '@/components/ui/DropdownMenu'
|
|
172
|
-
import { MoreHorizontal, Edit, Copy, Archive, Trash2 } from 'lucide-react'
|
|
173
|
-
|
|
174
|
-
interface ActionsDropdownProps {
|
|
175
|
-
onEdit?: () => void
|
|
176
|
-
onDuplicate?: () => void
|
|
177
|
-
onArchive?: () => void
|
|
178
|
-
onDelete?: () => void
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function ActionsDropdown({
|
|
182
|
-
onEdit,
|
|
183
|
-
onDuplicate,
|
|
184
|
-
onArchive,
|
|
185
|
-
onDelete
|
|
186
|
-
}: ActionsDropdownProps) {
|
|
187
|
-
return (
|
|
188
|
-
<DropdownMenu>
|
|
189
|
-
<DropdownMenuTrigger asChild>
|
|
190
|
-
<button className="rounded p-1 hover:bg-gray-100 focus:outline-none focus:ring-2">
|
|
191
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
192
|
-
<span className="sr-only">Actions</span>
|
|
193
|
-
</button>
|
|
194
|
-
</DropdownMenuTrigger>
|
|
195
|
-
|
|
196
|
-
<DropdownMenuContent align="end">
|
|
197
|
-
{onEdit && (
|
|
198
|
-
<DropdownMenuItem onClick={onEdit}>
|
|
199
|
-
<Edit className="mr-2 h-4 w-4" />
|
|
200
|
-
Edit
|
|
201
|
-
</DropdownMenuItem>
|
|
202
|
-
)}
|
|
203
|
-
|
|
204
|
-
{onDuplicate && (
|
|
205
|
-
<DropdownMenuItem onClick={onDuplicate}>
|
|
206
|
-
<Copy className="mr-2 h-4 w-4" />
|
|
207
|
-
Duplicate
|
|
208
|
-
</DropdownMenuItem>
|
|
209
|
-
)}
|
|
210
|
-
|
|
211
|
-
{onArchive && (
|
|
212
|
-
<DropdownMenuItem onClick={onArchive}>
|
|
213
|
-
<Archive className="mr-2 h-4 w-4" />
|
|
214
|
-
Archive
|
|
215
|
-
</DropdownMenuItem>
|
|
216
|
-
)}
|
|
217
|
-
|
|
218
|
-
{onDelete && (
|
|
219
|
-
<>
|
|
220
|
-
<DropdownMenuSeparator />
|
|
221
|
-
<DropdownMenuItem
|
|
222
|
-
onClick={onDelete}
|
|
223
|
-
className="text-red-600 focus:text-red-600"
|
|
224
|
-
>
|
|
225
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
226
|
-
Delete
|
|
227
|
-
</DropdownMenuItem>
|
|
228
|
-
</>
|
|
229
|
-
)}
|
|
230
|
-
</DropdownMenuContent>
|
|
231
|
-
</DropdownMenu>
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Combobox / Searchable Select
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
// components/ui/Combobox.tsx
|
|
240
|
-
'use client'
|
|
241
|
-
|
|
242
|
-
import { useState } from 'react'
|
|
243
|
-
import { Check, ChevronsUpDown } from 'lucide-react'
|
|
244
|
-
import * as Popover from '@radix-ui/react-popover'
|
|
245
|
-
import { cn } from '@/lib/utils'
|
|
246
|
-
|
|
247
|
-
interface Option {
|
|
248
|
-
value: string
|
|
249
|
-
label: string
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
interface ComboboxProps {
|
|
253
|
-
options: Option[]
|
|
254
|
-
value?: string
|
|
255
|
-
onChange: (value: string) => void
|
|
256
|
-
placeholder?: string
|
|
257
|
-
searchPlaceholder?: string
|
|
258
|
-
emptyText?: string
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function Combobox({
|
|
262
|
-
options,
|
|
263
|
-
value,
|
|
264
|
-
onChange,
|
|
265
|
-
placeholder = 'Select option...',
|
|
266
|
-
searchPlaceholder = 'Search...',
|
|
267
|
-
emptyText = 'No results found.'
|
|
268
|
-
}: ComboboxProps) {
|
|
269
|
-
const [open, setOpen] = useState(false)
|
|
270
|
-
const [search, setSearch] = useState('')
|
|
271
|
-
|
|
272
|
-
const filteredOptions = options.filter(option =>
|
|
273
|
-
option.label.toLowerCase().includes(search.toLowerCase())
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
const selectedOption = options.find(opt => opt.value === value)
|
|
277
|
-
|
|
278
|
-
return (
|
|
279
|
-
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
280
|
-
<Popover.Trigger asChild>
|
|
281
|
-
<button
|
|
282
|
-
role="combobox"
|
|
283
|
-
aria-expanded={open}
|
|
284
|
-
className={cn(
|
|
285
|
-
'flex w-full items-center justify-between rounded-md border px-3 py-2 text-sm',
|
|
286
|
-
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
|
287
|
-
!value && 'text-gray-500'
|
|
288
|
-
)}
|
|
289
|
-
>
|
|
290
|
-
{selectedOption?.label ?? placeholder}
|
|
291
|
-
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
292
|
-
</button>
|
|
293
|
-
</Popover.Trigger>
|
|
294
|
-
|
|
295
|
-
<Popover.Portal>
|
|
296
|
-
<Popover.Content
|
|
297
|
-
className="z-50 w-[--radix-popover-trigger-width] rounded-md border bg-white shadow-md"
|
|
298
|
-
sideOffset={4}
|
|
299
|
-
>
|
|
300
|
-
<div className="p-2">
|
|
301
|
-
<input
|
|
302
|
-
type="text"
|
|
303
|
-
placeholder={searchPlaceholder}
|
|
304
|
-
value={search}
|
|
305
|
-
onChange={e => setSearch(e.target.value)}
|
|
306
|
-
className="w-full rounded border px-2 py-1.5 text-sm focus:outline-none focus:ring-1"
|
|
307
|
-
/>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
<div className="max-h-60 overflow-auto p-1">
|
|
311
|
-
{filteredOptions.length === 0 ? (
|
|
312
|
-
<p className="p-2 text-center text-sm text-gray-500">{emptyText}</p>
|
|
313
|
-
) : (
|
|
314
|
-
filteredOptions.map(option => (
|
|
315
|
-
<button
|
|
316
|
-
key={option.value}
|
|
317
|
-
onClick={() => {
|
|
318
|
-
onChange(option.value)
|
|
319
|
-
setOpen(false)
|
|
320
|
-
setSearch('')
|
|
321
|
-
}}
|
|
322
|
-
className={cn(
|
|
323
|
-
'flex w-full items-center rounded-sm px-2 py-1.5 text-sm',
|
|
324
|
-
'hover:bg-gray-100 focus:bg-gray-100 focus:outline-none',
|
|
325
|
-
value === option.value && 'bg-gray-100'
|
|
326
|
-
)}
|
|
327
|
-
>
|
|
328
|
-
<Check
|
|
329
|
-
className={cn(
|
|
330
|
-
'mr-2 h-4 w-4',
|
|
331
|
-
value === option.value ? 'opacity-100' : 'opacity-0'
|
|
332
|
-
)}
|
|
333
|
-
/>
|
|
334
|
-
{option.label}
|
|
335
|
-
</button>
|
|
336
|
-
))
|
|
337
|
-
)}
|
|
338
|
-
</div>
|
|
339
|
-
</Popover.Content>
|
|
340
|
-
</Popover.Portal>
|
|
341
|
-
</Popover.Root>
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
## Multi-Select Dropdown
|
|
347
|
-
|
|
348
|
-
```tsx
|
|
349
|
-
// components/ui/MultiSelect.tsx
|
|
350
|
-
'use client'
|
|
351
|
-
|
|
352
|
-
import { useState } from 'react'
|
|
353
|
-
import { X, Check, ChevronsUpDown } from 'lucide-react'
|
|
354
|
-
import * as Popover from '@radix-ui/react-popover'
|
|
355
|
-
import { cn } from '@/lib/utils'
|
|
356
|
-
|
|
357
|
-
interface Option {
|
|
358
|
-
value: string
|
|
359
|
-
label: string
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
interface MultiSelectProps {
|
|
363
|
-
options: Option[]
|
|
364
|
-
value: string[]
|
|
365
|
-
onChange: (value: string[]) => void
|
|
366
|
-
placeholder?: string
|
|
367
|
-
maxDisplay?: number
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export function MultiSelect({
|
|
371
|
-
options,
|
|
372
|
-
value,
|
|
373
|
-
onChange,
|
|
374
|
-
placeholder = 'Select options...',
|
|
375
|
-
maxDisplay = 3
|
|
376
|
-
}: MultiSelectProps) {
|
|
377
|
-
const [open, setOpen] = useState(false)
|
|
378
|
-
|
|
379
|
-
const selectedOptions = options.filter(opt => value.includes(opt.value))
|
|
380
|
-
|
|
381
|
-
const toggleOption = (optionValue: string) => {
|
|
382
|
-
if (value.includes(optionValue)) {
|
|
383
|
-
onChange(value.filter(v => v !== optionValue))
|
|
384
|
-
} else {
|
|
385
|
-
onChange([...value, optionValue])
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const removeOption = (optionValue: string) => {
|
|
390
|
-
onChange(value.filter(v => v !== optionValue))
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return (
|
|
394
|
-
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
395
|
-
<Popover.Trigger asChild>
|
|
396
|
-
<button
|
|
397
|
-
className={cn(
|
|
398
|
-
'flex min-h-[38px] w-full flex-wrap items-center gap-1 rounded-md border px-2 py-1',
|
|
399
|
-
'focus:outline-none focus:ring-2 focus:ring-blue-500'
|
|
400
|
-
)}
|
|
401
|
-
>
|
|
402
|
-
{selectedOptions.length === 0 ? (
|
|
403
|
-
<span className="text-sm text-gray-500">{placeholder}</span>
|
|
404
|
-
) : (
|
|
405
|
-
<>
|
|
406
|
-
{selectedOptions.slice(0, maxDisplay).map(option => (
|
|
407
|
-
<span
|
|
408
|
-
key={option.value}
|
|
409
|
-
className="flex items-center gap-1 rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-800"
|
|
410
|
-
>
|
|
411
|
-
{option.label}
|
|
412
|
-
<button
|
|
413
|
-
onClick={e => {
|
|
414
|
-
e.stopPropagation()
|
|
415
|
-
removeOption(option.value)
|
|
416
|
-
}}
|
|
417
|
-
className="hover:text-blue-600"
|
|
418
|
-
>
|
|
419
|
-
<X className="h-3 w-3" />
|
|
420
|
-
</button>
|
|
421
|
-
</span>
|
|
422
|
-
))}
|
|
423
|
-
{selectedOptions.length > maxDisplay && (
|
|
424
|
-
<span className="text-xs text-gray-500">
|
|
425
|
-
+{selectedOptions.length - maxDisplay} more
|
|
426
|
-
</span>
|
|
427
|
-
)}
|
|
428
|
-
</>
|
|
429
|
-
)}
|
|
430
|
-
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
|
431
|
-
</button>
|
|
432
|
-
</Popover.Trigger>
|
|
433
|
-
|
|
434
|
-
<Popover.Portal>
|
|
435
|
-
<Popover.Content
|
|
436
|
-
className="z-50 w-[--radix-popover-trigger-width] rounded-md border bg-white p-1 shadow-md"
|
|
437
|
-
sideOffset={4}
|
|
438
|
-
>
|
|
439
|
-
<div className="max-h-60 overflow-auto">
|
|
440
|
-
{options.map(option => (
|
|
441
|
-
<button
|
|
442
|
-
key={option.value}
|
|
443
|
-
onClick={() => toggleOption(option.value)}
|
|
444
|
-
className={cn(
|
|
445
|
-
'flex w-full items-center rounded-sm px-2 py-1.5 text-sm',
|
|
446
|
-
'hover:bg-gray-100 focus:bg-gray-100 focus:outline-none'
|
|
447
|
-
)}
|
|
448
|
-
>
|
|
449
|
-
<div
|
|
450
|
-
className={cn(
|
|
451
|
-
'mr-2 flex h-4 w-4 items-center justify-center rounded border',
|
|
452
|
-
value.includes(option.value)
|
|
453
|
-
? 'border-blue-500 bg-blue-500 text-white'
|
|
454
|
-
: 'border-gray-300'
|
|
455
|
-
)}
|
|
456
|
-
>
|
|
457
|
-
{value.includes(option.value) && <Check className="h-3 w-3" />}
|
|
458
|
-
</div>
|
|
459
|
-
{option.label}
|
|
460
|
-
</button>
|
|
461
|
-
))}
|
|
462
|
-
</div>
|
|
463
|
-
</Popover.Content>
|
|
464
|
-
</Popover.Portal>
|
|
465
|
-
</Popover.Root>
|
|
466
|
-
)
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## When to Use
|
|
471
|
-
|
|
472
|
-
- Navigation menus
|
|
473
|
-
- User account menus
|
|
474
|
-
- Action menus
|
|
475
|
-
- Searchable selects
|
|
476
|
-
- Multi-select inputs
|