create-manifest 1.3.3 → 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/README.md +39 -24
- package/index.js +51 -0
- package/package.json +11 -89
- package/starter/.claude/settings.local.json +21 -0
- package/starter/.env.example +1 -0
- package/starter/@/components/table.tsx +478 -0
- package/starter/@/components/ui/button.tsx +62 -0
- package/starter/@/components/ui/checkbox.tsx +30 -0
- package/starter/README-DEV.md +167 -0
- package/starter/components.json +24 -0
- package/starter/package.json +42 -0
- package/starter/src/flows/list-pokemons.flow.ts +131 -0
- package/starter/src/server.ts +165 -0
- package/starter/src/web/PokemonList.tsx +125 -0
- package/starter/src/web/components/blog-post-card.tsx +286 -0
- package/starter/src/web/components/blog-post-list.tsx +291 -0
- package/starter/src/web/components/ui/.gitkeep +0 -0
- package/starter/src/web/components/ui/button.tsx +62 -0
- package/starter/src/web/globals.css +98 -0
- package/starter/src/web/hooks/.gitkeep +0 -0
- package/starter/src/web/lib/utils.ts +6 -0
- package/starter/src/web/root.tsx +36 -0
- package/starter/src/web/tsconfig.json +3 -0
- package/starter/tsconfig.json +21 -0
- package/starter/tsconfig.web.json +24 -0
- package/starter/vite.config.ts +36 -0
- package/assets/monorepo/README.md +0 -51
- package/assets/monorepo/api-package.json +0 -9
- package/assets/monorepo/api-readme.md +0 -49
- package/assets/monorepo/manifest.yml +0 -34
- package/assets/monorepo/root-package.json +0 -15
- package/assets/monorepo/web-package.json +0 -10
- package/assets/monorepo/web-readme.md +0 -9
- package/assets/standalone/README.md +0 -49
- package/assets/standalone/api-package.json +0 -9
- package/assets/standalone/manifest.yml +0 -34
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -5
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -5
- package/dist/commands/index.d.ts +0 -65
- package/dist/commands/index.js +0 -480
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/utils/GetBackendFileContent.d.ts +0 -1
- package/dist/utils/GetBackendFileContent.js +0 -21
- package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
- package/dist/utils/GetLatestPackageVersion.js +0 -5
- package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
- package/dist/utils/UpdateExtensionJsonFile.js +0 -8
- package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
- package/dist/utils/UpdatePackageJsonFile.js +0 -21
- package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
- package/dist/utils/UpdateSettingsJsonFile.js +0 -6
- package/dist/utils/helpers.d.ts +0 -1
- package/dist/utils/helpers.js +0 -11
- package/oclif.manifest.json +0 -47
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { ArrowRight } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export interface BlogPost {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
excerpt: string
|
|
10
|
+
coverImage?: string
|
|
11
|
+
author: {
|
|
12
|
+
name: string
|
|
13
|
+
avatar?: string
|
|
14
|
+
}
|
|
15
|
+
publishedAt: string
|
|
16
|
+
readTime?: string
|
|
17
|
+
tags?: string[]
|
|
18
|
+
category?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultPost: BlogPost = {
|
|
22
|
+
id: '1',
|
|
23
|
+
title: 'Getting Started with Agentic UI Components',
|
|
24
|
+
excerpt:
|
|
25
|
+
'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
|
|
26
|
+
coverImage:
|
|
27
|
+
'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800',
|
|
28
|
+
author: {
|
|
29
|
+
name: 'Sarah Chen',
|
|
30
|
+
avatar: 'https://i.pravatar.cc/150?u=sarah'
|
|
31
|
+
},
|
|
32
|
+
publishedAt: '2024-01-15',
|
|
33
|
+
readTime: '5 min read',
|
|
34
|
+
tags: ['Tutorial', 'Components'],
|
|
35
|
+
category: 'Tutorial'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BlogPostCardProps {
|
|
39
|
+
post?: BlogPost
|
|
40
|
+
variant?: 'default' | 'compact' | 'horizontal' | 'covered'
|
|
41
|
+
showImage?: boolean
|
|
42
|
+
showAuthor?: boolean
|
|
43
|
+
showCategory?: boolean
|
|
44
|
+
onReadMore?: (post: BlogPost) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function BlogPostCard({
|
|
48
|
+
post = defaultPost,
|
|
49
|
+
variant = 'default',
|
|
50
|
+
showImage = true,
|
|
51
|
+
showAuthor = true,
|
|
52
|
+
showCategory = true,
|
|
53
|
+
onReadMore
|
|
54
|
+
}: BlogPostCardProps) {
|
|
55
|
+
const formatDate = (dateStr: string) => {
|
|
56
|
+
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
57
|
+
month: 'short',
|
|
58
|
+
day: 'numeric',
|
|
59
|
+
year: 'numeric'
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (variant === 'covered') {
|
|
64
|
+
return (
|
|
65
|
+
<div className="relative overflow-hidden rounded-lg">
|
|
66
|
+
<div className="min-h-[320px] sm:aspect-[16/9] sm:min-h-0 w-full">
|
|
67
|
+
{post.coverImage ? (
|
|
68
|
+
<img
|
|
69
|
+
src={post.coverImage}
|
|
70
|
+
alt={post.title}
|
|
71
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
72
|
+
/>
|
|
73
|
+
) : (
|
|
74
|
+
<div className="absolute inset-0 h-full w-full bg-muted" />
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-black/20" />
|
|
78
|
+
<div className="absolute inset-0 flex flex-col justify-between p-4 text-white">
|
|
79
|
+
<div className="flex gap-1">
|
|
80
|
+
{showCategory && post.category && (
|
|
81
|
+
<span className="rounded-full bg-white/20 px-2 py-0.5 text-xs backdrop-blur-sm">
|
|
82
|
+
{post.category}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{post.tags &&
|
|
86
|
+
post.tags.slice(0, 1).map((tag) => (
|
|
87
|
+
<span
|
|
88
|
+
key={tag}
|
|
89
|
+
className="rounded-full bg-white/20 px-2 py-0.5 text-xs backdrop-blur-sm"
|
|
90
|
+
>
|
|
91
|
+
{tag}
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<h2 className="text-lg font-bold leading-tight">{post.title}</h2>
|
|
97
|
+
<p className="mt-1 line-clamp-2 text-sm text-white/80">
|
|
98
|
+
{post.excerpt}
|
|
99
|
+
</p>
|
|
100
|
+
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
101
|
+
{showAuthor && (
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
{post.author.avatar && (
|
|
104
|
+
<img
|
|
105
|
+
src={post.author.avatar}
|
|
106
|
+
alt={post.author.name}
|
|
107
|
+
className="h-6 w-6 rounded-full ring-2 ring-white/30"
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
<div className="text-xs">
|
|
111
|
+
<p className="font-medium">{post.author.name}</p>
|
|
112
|
+
<p className="text-white/60">{formatDate(post.publishedAt)}</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<Button
|
|
117
|
+
size="sm"
|
|
118
|
+
variant="secondary"
|
|
119
|
+
className="w-full sm:w-auto"
|
|
120
|
+
onClick={() => onReadMore?.(post)}
|
|
121
|
+
>
|
|
122
|
+
Read article
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (variant === 'horizontal') {
|
|
132
|
+
return (
|
|
133
|
+
<div className="test flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3 transition-colors hover:bg-muted/50">
|
|
134
|
+
{showImage && post.coverImage && (
|
|
135
|
+
<div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
|
|
136
|
+
<img
|
|
137
|
+
src={post.coverImage}
|
|
138
|
+
alt={post.title}
|
|
139
|
+
className="h-full w-full object-cover"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
<div className="flex flex-1 flex-col justify-between">
|
|
144
|
+
<div>
|
|
145
|
+
{showCategory && post.category && (
|
|
146
|
+
<span className="mb-1 inline-block rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
|
147
|
+
{post.category}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
<h3 className="line-clamp-2 text-sm font-medium leading-tight">
|
|
151
|
+
{post.title}
|
|
152
|
+
</h3>
|
|
153
|
+
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
|
154
|
+
{post.excerpt}
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
158
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
159
|
+
{showAuthor && post.author.avatar && (
|
|
160
|
+
<img
|
|
161
|
+
src={post.author.avatar}
|
|
162
|
+
alt={post.author.name}
|
|
163
|
+
className="h-4 w-4 rounded-full"
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
<span>{formatDate(post.publishedAt)}</span>
|
|
167
|
+
{post.readTime && (
|
|
168
|
+
<>
|
|
169
|
+
<span>·</span>
|
|
170
|
+
<span>{post.readTime}</span>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
<Button
|
|
175
|
+
size="sm"
|
|
176
|
+
className="w-full sm:w-auto"
|
|
177
|
+
onClick={() => onReadMore?.(post)}
|
|
178
|
+
>
|
|
179
|
+
Read
|
|
180
|
+
<ArrowRight className="ml-1 h-3 w-3" />
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (variant === 'compact') {
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex h-full flex-col justify-between rounded-lg border bg-card p-3 transition-colors hover:bg-muted/50">
|
|
191
|
+
<div>
|
|
192
|
+
{showCategory && post.category && (
|
|
193
|
+
<span className="mb-2 inline-block rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
|
194
|
+
{post.category}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
<h3 className="line-clamp-2 text-sm font-medium">{post.title}</h3>
|
|
198
|
+
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
|
199
|
+
{post.excerpt}
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
203
|
+
<div className="flex items-center gap-2">
|
|
204
|
+
{showAuthor && post.author.avatar && (
|
|
205
|
+
<img
|
|
206
|
+
src={post.author.avatar}
|
|
207
|
+
alt={post.author.name}
|
|
208
|
+
className="h-5 w-5 rounded-full"
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
<span className="text-xs text-muted-foreground">
|
|
212
|
+
{formatDate(post.publishedAt)}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
<Button size="sm" onClick={() => onReadMore?.(post)}>
|
|
216
|
+
Read more
|
|
217
|
+
<ArrowRight className="ml-1 h-3 w-3" />
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Default variant
|
|
225
|
+
return (
|
|
226
|
+
<div className="flex h-full flex-col overflow-hidden rounded-lg border bg-card transition-colors hover:bg-muted/50">
|
|
227
|
+
{showImage && post.coverImage && (
|
|
228
|
+
<div className="aspect-video overflow-hidden">
|
|
229
|
+
<img
|
|
230
|
+
src={post.coverImage}
|
|
231
|
+
alt={post.title}
|
|
232
|
+
className="h-full w-full object-cover transition-transform hover:scale-105"
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
<div className="flex flex-1 flex-col justify-between p-4">
|
|
237
|
+
<div>
|
|
238
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
239
|
+
{showCategory && post.category && (
|
|
240
|
+
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
|
241
|
+
{post.category}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
{post.tags &&
|
|
245
|
+
post.tags.length > 0 &&
|
|
246
|
+
post.tags.slice(0, 2).map((tag) => (
|
|
247
|
+
<span
|
|
248
|
+
key={tag}
|
|
249
|
+
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
|
250
|
+
>
|
|
251
|
+
{tag}
|
|
252
|
+
</span>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
<h3 className="line-clamp-2 font-medium">{post.title}</h3>
|
|
256
|
+
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
|
|
257
|
+
{post.excerpt}
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
261
|
+
{showAuthor && (
|
|
262
|
+
<div className="flex items-center gap-2">
|
|
263
|
+
{post.author.avatar && (
|
|
264
|
+
<img
|
|
265
|
+
src={post.author.avatar}
|
|
266
|
+
alt={post.author.name}
|
|
267
|
+
className="h-6 w-6 rounded-full"
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
<div className="text-xs">
|
|
271
|
+
<p className="font-medium">{post.author.name}</p>
|
|
272
|
+
<p className="text-muted-foreground">
|
|
273
|
+
{formatDate(post.publishedAt)}
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
<Button size="sm" onClick={() => onReadMore?.(post)}>
|
|
279
|
+
Read
|
|
280
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { BlogPost, BlogPostCard } from './blog-post-card'
|
|
6
|
+
|
|
7
|
+
const defaultPosts: BlogPost[] = [
|
|
8
|
+
{
|
|
9
|
+
id: '1',
|
|
10
|
+
title: 'Getting Started with Agentic UI Components',
|
|
11
|
+
excerpt:
|
|
12
|
+
'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
|
|
13
|
+
coverImage:
|
|
14
|
+
'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800',
|
|
15
|
+
author: {
|
|
16
|
+
name: 'Sarah Chen',
|
|
17
|
+
avatar: 'https://i.pravatar.cc/150?u=sarah'
|
|
18
|
+
},
|
|
19
|
+
publishedAt: '2024-01-15',
|
|
20
|
+
readTime: '5 min read',
|
|
21
|
+
tags: ['Tutorial', 'Components', 'AI'],
|
|
22
|
+
category: 'Tutorial'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: '2',
|
|
26
|
+
title: 'Designing for Conversational Interfaces',
|
|
27
|
+
excerpt:
|
|
28
|
+
'Best practices for creating intuitive UI components that work within chat environments.',
|
|
29
|
+
coverImage:
|
|
30
|
+
'https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800',
|
|
31
|
+
author: {
|
|
32
|
+
name: 'Alex Rivera',
|
|
33
|
+
avatar: 'https://i.pravatar.cc/150?u=alex'
|
|
34
|
+
},
|
|
35
|
+
publishedAt: '2024-01-12',
|
|
36
|
+
readTime: '8 min read',
|
|
37
|
+
tags: ['Design', 'UX'],
|
|
38
|
+
category: 'Design'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: '3',
|
|
42
|
+
title: 'MCP Integration Patterns',
|
|
43
|
+
excerpt:
|
|
44
|
+
'How to leverage Model Context Protocol for seamless backend communication in your agentic applications.',
|
|
45
|
+
coverImage:
|
|
46
|
+
'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800',
|
|
47
|
+
author: {
|
|
48
|
+
name: 'Jordan Kim',
|
|
49
|
+
avatar: 'https://i.pravatar.cc/150?u=jordan'
|
|
50
|
+
},
|
|
51
|
+
publishedAt: '2024-01-10',
|
|
52
|
+
readTime: '12 min read',
|
|
53
|
+
tags: ['MCP', 'Backend', 'Integration'],
|
|
54
|
+
category: 'Development'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: '4',
|
|
58
|
+
title: 'Building Payment Flows in Chat',
|
|
59
|
+
excerpt:
|
|
60
|
+
'A complete guide to implementing secure, user-friendly payment experiences within conversational interfaces.',
|
|
61
|
+
coverImage:
|
|
62
|
+
'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800',
|
|
63
|
+
author: {
|
|
64
|
+
name: 'Morgan Lee',
|
|
65
|
+
avatar: 'https://i.pravatar.cc/150?u=morgan'
|
|
66
|
+
},
|
|
67
|
+
publishedAt: '2024-01-08',
|
|
68
|
+
readTime: '10 min read',
|
|
69
|
+
tags: ['Payments', 'Security'],
|
|
70
|
+
category: 'Tutorial'
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
export interface BlogPostListProps {
|
|
75
|
+
posts?: BlogPost[]
|
|
76
|
+
variant?: 'list' | 'grid' | 'carousel'
|
|
77
|
+
columns?: 2 | 3
|
|
78
|
+
showAuthor?: boolean
|
|
79
|
+
showCategory?: boolean
|
|
80
|
+
onReadMore?: (post: BlogPost) => void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function BlogPostList({
|
|
84
|
+
posts = defaultPosts,
|
|
85
|
+
variant = 'list',
|
|
86
|
+
columns = 2,
|
|
87
|
+
showAuthor = true,
|
|
88
|
+
showCategory = true,
|
|
89
|
+
onReadMore
|
|
90
|
+
}: BlogPostListProps) {
|
|
91
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
92
|
+
|
|
93
|
+
// List variant
|
|
94
|
+
if (variant === 'list') {
|
|
95
|
+
return (
|
|
96
|
+
<div className="space-y-3">
|
|
97
|
+
{posts.slice(0, 3).map((post) => (
|
|
98
|
+
<BlogPostCard
|
|
99
|
+
key={post.id}
|
|
100
|
+
post={post}
|
|
101
|
+
variant="horizontal"
|
|
102
|
+
showAuthor={showAuthor}
|
|
103
|
+
showCategory={showCategory}
|
|
104
|
+
onReadMore={onReadMore}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Grid variant
|
|
112
|
+
if (variant === 'grid') {
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
'grid gap-4 grid-cols-1',
|
|
117
|
+
columns === 2 ? 'sm:grid-cols-2' : 'sm:grid-cols-3'
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{posts.map((post) => (
|
|
121
|
+
<BlogPostCard
|
|
122
|
+
key={post.id}
|
|
123
|
+
post={post}
|
|
124
|
+
variant="compact"
|
|
125
|
+
showImage={false}
|
|
126
|
+
showAuthor={showAuthor}
|
|
127
|
+
showCategory={showCategory}
|
|
128
|
+
onReadMore={onReadMore}
|
|
129
|
+
/>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Carousel variant
|
|
136
|
+
const maxIndexMobile = posts.length - 1
|
|
137
|
+
const maxIndexTablet = Math.max(0, posts.length - 2)
|
|
138
|
+
const maxIndexDesktop = Math.max(0, posts.length - 3)
|
|
139
|
+
|
|
140
|
+
const prev = () => {
|
|
141
|
+
setCurrentIndex((i) => Math.max(0, i - 1))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const next = () => {
|
|
145
|
+
setCurrentIndex((i) => i + 1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isAtStart = currentIndex === 0
|
|
149
|
+
const isAtEndMobile = currentIndex >= maxIndexMobile
|
|
150
|
+
const isAtEndTablet = currentIndex >= maxIndexTablet
|
|
151
|
+
const isAtEndDesktop = currentIndex >= maxIndexDesktop
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="relative">
|
|
155
|
+
<div className="overflow-hidden rounded-lg">
|
|
156
|
+
{/* Mobile: 1 card, slides by 100% */}
|
|
157
|
+
<div
|
|
158
|
+
className="flex transition-transform duration-300 ease-out md:hidden"
|
|
159
|
+
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
|
160
|
+
>
|
|
161
|
+
{posts.map((post) => (
|
|
162
|
+
<div key={post.id} className="w-full shrink-0 px-0.5">
|
|
163
|
+
<BlogPostCard
|
|
164
|
+
post={post}
|
|
165
|
+
variant="compact"
|
|
166
|
+
showAuthor={showAuthor}
|
|
167
|
+
showCategory={showCategory}
|
|
168
|
+
onReadMore={onReadMore}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Tablet: 2 cards visible, slides by 50% */}
|
|
175
|
+
<div
|
|
176
|
+
className="hidden md:flex lg:hidden transition-transform duration-300 ease-out"
|
|
177
|
+
style={{ transform: `translateX(-${currentIndex * 50}%)` }}
|
|
178
|
+
>
|
|
179
|
+
{posts.map((post) => (
|
|
180
|
+
<div key={post.id} className="w-1/2 shrink-0 px-1.5">
|
|
181
|
+
<BlogPostCard
|
|
182
|
+
post={post}
|
|
183
|
+
variant="compact"
|
|
184
|
+
showAuthor={showAuthor}
|
|
185
|
+
showCategory={showCategory}
|
|
186
|
+
onReadMore={onReadMore}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Desktop: 3 cards visible, slides by 33.333% */}
|
|
193
|
+
<div
|
|
194
|
+
className="hidden lg:flex transition-transform duration-300 ease-out"
|
|
195
|
+
style={{ transform: `translateX(-${currentIndex * (100 / 3)}%)` }}
|
|
196
|
+
>
|
|
197
|
+
{posts.map((post) => (
|
|
198
|
+
<div key={post.id} className="w-1/3 shrink-0 px-1.5">
|
|
199
|
+
<BlogPostCard
|
|
200
|
+
post={post}
|
|
201
|
+
variant="compact"
|
|
202
|
+
showAuthor={showAuthor}
|
|
203
|
+
showCategory={showCategory}
|
|
204
|
+
onReadMore={onReadMore}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="mt-3 flex items-center justify-between px-2">
|
|
211
|
+
<div className="flex gap-1">
|
|
212
|
+
{posts.map((_, i) => (
|
|
213
|
+
<button
|
|
214
|
+
key={i}
|
|
215
|
+
onClick={() => setCurrentIndex(i)}
|
|
216
|
+
className={cn(
|
|
217
|
+
'h-1.5 rounded-full transition-all',
|
|
218
|
+
i === currentIndex
|
|
219
|
+
? 'w-4 bg-foreground'
|
|
220
|
+
: 'w-1.5 bg-muted-foreground/30'
|
|
221
|
+
)}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
{/* Mobile navigation */}
|
|
226
|
+
<div className="flex gap-1 md:hidden">
|
|
227
|
+
<Button
|
|
228
|
+
variant="outline"
|
|
229
|
+
size="icon"
|
|
230
|
+
className="h-8 w-8"
|
|
231
|
+
onClick={prev}
|
|
232
|
+
disabled={isAtStart}
|
|
233
|
+
>
|
|
234
|
+
<ChevronLeft className="h-4 w-4" />
|
|
235
|
+
</Button>
|
|
236
|
+
<Button
|
|
237
|
+
variant="outline"
|
|
238
|
+
size="icon"
|
|
239
|
+
className="h-8 w-8"
|
|
240
|
+
onClick={next}
|
|
241
|
+
disabled={isAtEndMobile}
|
|
242
|
+
>
|
|
243
|
+
<ChevronRight className="h-4 w-4" />
|
|
244
|
+
</Button>
|
|
245
|
+
</div>
|
|
246
|
+
{/* Tablet navigation */}
|
|
247
|
+
<div className="hidden md:flex lg:hidden gap-1">
|
|
248
|
+
<Button
|
|
249
|
+
variant="outline"
|
|
250
|
+
size="icon"
|
|
251
|
+
className="h-8 w-8"
|
|
252
|
+
onClick={prev}
|
|
253
|
+
disabled={isAtStart}
|
|
254
|
+
>
|
|
255
|
+
<ChevronLeft className="h-4 w-4" />
|
|
256
|
+
</Button>
|
|
257
|
+
<Button
|
|
258
|
+
variant="outline"
|
|
259
|
+
size="icon"
|
|
260
|
+
className="h-8 w-8"
|
|
261
|
+
onClick={next}
|
|
262
|
+
disabled={isAtEndTablet}
|
|
263
|
+
>
|
|
264
|
+
<ChevronRight className="h-4 w-4" />
|
|
265
|
+
</Button>
|
|
266
|
+
</div>
|
|
267
|
+
{/* Desktop navigation */}
|
|
268
|
+
<div className="hidden lg:flex gap-1">
|
|
269
|
+
<Button
|
|
270
|
+
variant="outline"
|
|
271
|
+
size="icon"
|
|
272
|
+
className="h-8 w-8"
|
|
273
|
+
onClick={prev}
|
|
274
|
+
disabled={isAtStart}
|
|
275
|
+
>
|
|
276
|
+
<ChevronLeft className="h-4 w-4" />
|
|
277
|
+
</Button>
|
|
278
|
+
<Button
|
|
279
|
+
variant="outline"
|
|
280
|
+
size="icon"
|
|
281
|
+
className="h-8 w-8"
|
|
282
|
+
onClick={next}
|
|
283
|
+
disabled={isAtEndDesktop}
|
|
284
|
+
>
|
|
285
|
+
<ChevronRight className="h-4 w-4" />
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
size = "default",
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
data-size={size}
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants }
|