create-manifest 1.3.4 → 2.0.1

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.
Files changed (57) hide show
  1. package/README.md +40 -21
  2. package/index.js +51 -0
  3. package/package.json +11 -89
  4. package/starter/.claude/settings.local.json +23 -0
  5. package/starter/.env.example +1 -0
  6. package/starter/README.md +26 -0
  7. package/starter/components.json +24 -0
  8. package/starter/package.json +42 -0
  9. package/starter/src/flows/list-pokemons.flow.ts +129 -0
  10. package/starter/src/server.ts +169 -0
  11. package/starter/src/web/PokemonList.tsx +126 -0
  12. package/starter/src/web/components/blog-post-card.tsx +288 -0
  13. package/starter/src/web/components/blog-post-list.tsx +291 -0
  14. package/starter/src/web/components/payment-methods.tsx +201 -0
  15. package/starter/src/web/components/table.tsx +478 -0
  16. package/starter/src/web/components/ui/.gitkeep +0 -0
  17. package/starter/src/web/components/ui/button.tsx +62 -0
  18. package/starter/src/web/components/ui/checkbox.tsx +30 -0
  19. package/starter/src/web/globals.css +98 -0
  20. package/starter/src/web/hooks/.gitkeep +0 -0
  21. package/starter/src/web/lib/utils.ts +6 -0
  22. package/starter/src/web/root.tsx +36 -0
  23. package/starter/src/web/tsconfig.json +3 -0
  24. package/starter/tsconfig.json +25 -0
  25. package/starter/tsconfig.web.json +24 -0
  26. package/starter/vite.config.ts +37 -0
  27. package/assets/monorepo/README.md +0 -52
  28. package/assets/monorepo/api-package.json +0 -9
  29. package/assets/monorepo/api-readme.md +0 -50
  30. package/assets/monorepo/manifest.yml +0 -34
  31. package/assets/monorepo/root-package.json +0 -15
  32. package/assets/monorepo/web-package.json +0 -10
  33. package/assets/monorepo/web-readme.md +0 -9
  34. package/assets/standalone/README.md +0 -50
  35. package/assets/standalone/api-package.json +0 -9
  36. package/assets/standalone/manifest.yml +0 -34
  37. package/bin/dev.cmd +0 -3
  38. package/bin/dev.js +0 -5
  39. package/bin/run.cmd +0 -3
  40. package/bin/run.js +0 -5
  41. package/dist/commands/index.d.ts +0 -65
  42. package/dist/commands/index.js +0 -480
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +0 -1
  45. package/dist/utils/GetBackendFileContent.d.ts +0 -1
  46. package/dist/utils/GetBackendFileContent.js +0 -21
  47. package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
  48. package/dist/utils/GetLatestPackageVersion.js +0 -5
  49. package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
  50. package/dist/utils/UpdateExtensionJsonFile.js +0 -8
  51. package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
  52. package/dist/utils/UpdatePackageJsonFile.js +0 -21
  53. package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
  54. package/dist/utils/UpdateSettingsJsonFile.js +0 -6
  55. package/dist/utils/helpers.d.ts +0 -1
  56. package/dist/utils/helpers.js +0 -11
  57. package/oclif.manifest.json +0 -47
@@ -0,0 +1,126 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { BlogPostList } from '@/components/blog-post-list'
3
+ import type { BlogPost } from '@/components/blog-post-card'
4
+ import { PaymentMethods } from './components/payment-methods'
5
+
6
+ interface Pokemon {
7
+ id: number
8
+ name: string
9
+ image: string
10
+ types: string[]
11
+ height: number
12
+ weight: number
13
+ }
14
+
15
+ interface StructuredContent {
16
+ pokemons: Pokemon[]
17
+ }
18
+
19
+ function pokemonToBlogPost(pokemon: Pokemon): BlogPost {
20
+ const capitalizedName =
21
+ pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)
22
+
23
+ return {
24
+ id: String(pokemon.id),
25
+ title: capitalizedName,
26
+ excerpt: `Height: ${pokemon.height / 10}m | Weight: ${pokemon.weight / 10}kg`,
27
+ coverImage: pokemon.image,
28
+ author: {
29
+ name: `#${String(pokemon.id).padStart(3, '0')}`,
30
+ avatar: pokemon.image
31
+ },
32
+ publishedAt: new Date().toISOString(),
33
+ readTime: pokemon.types.join(', '),
34
+ tags: pokemon.types.map((t) => t.charAt(0).toUpperCase() + t.slice(1)),
35
+ category: pokemon.types[0]
36
+ ? pokemon.types[0].charAt(0).toUpperCase() + pokemon.types[0].slice(1)
37
+ : 'Normal'
38
+ }
39
+ }
40
+
41
+ export default function PokemonList() {
42
+ const [posts, setPosts] = useState<BlogPost[]>([])
43
+ const [loading, setLoading] = useState(true)
44
+
45
+ useEffect(() => {
46
+ console.log('window.openai:', window.openai)
47
+ console.log('structuredContent:', window.openai?.content?.structuredContent)
48
+
49
+ // Get structured content from OpenAI
50
+ if (window.openai?.content?.structuredContent) {
51
+ const content = window.openai.content
52
+ .structuredContent as StructuredContent
53
+ console.log(
54
+ 'Using structuredContent, pokemons count:',
55
+ content.pokemons?.length
56
+ )
57
+ if (content.pokemons) {
58
+ setPosts(content.pokemons.map(pokemonToBlogPost))
59
+ }
60
+ setLoading(false)
61
+ } else {
62
+ // Fallback: fetch directly if not in OpenAI context
63
+ console.log('No structuredContent, falling back to direct fetch')
64
+ fetchPokemons()
65
+ }
66
+ }, [])
67
+
68
+ async function fetchPokemons() {
69
+ try {
70
+ const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=12')
71
+ const data = await response.json()
72
+
73
+ const pokemons = await Promise.all(
74
+ data.results.map(async (pokemon: { name: string; url: string }) => {
75
+ const detailResponse = await fetch(pokemon.url)
76
+ const detail = await detailResponse.json()
77
+
78
+ return {
79
+ id: detail.id,
80
+ name: detail.name,
81
+ image:
82
+ detail.sprites.other['official-artwork'].front_default ||
83
+ detail.sprites.front_default,
84
+ types: detail.types.map(
85
+ (t: { type: { name: string } }) => t.type.name
86
+ ),
87
+ height: detail.height,
88
+ weight: detail.weight
89
+ }
90
+ })
91
+ )
92
+
93
+ setPosts(pokemons.map(pokemonToBlogPost))
94
+ } catch (error) {
95
+ console.error('Failed to fetch Pokemon:', error)
96
+ } finally {
97
+ setLoading(false)
98
+ }
99
+ }
100
+
101
+ const handleReadMore = (post: BlogPost) => {
102
+ const pokemonName = post.title.toLowerCase()
103
+ window.openai?.sendFollowUpMessage?.(`Tell me more about ${pokemonName}`)
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <div className="flex items-center justify-center p-8">
109
+ <div className="text-muted-foreground">Loading Pokemon...</div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <div className="p-4">
116
+ <PaymentMethods />
117
+ <BlogPostList
118
+ posts={posts}
119
+ variant="carousel"
120
+ showAuthor={true}
121
+ showCategory={true}
122
+ onReadMore={handleReadMore}
123
+ />
124
+ </div>
125
+ )
126
+ }
@@ -0,0 +1,288 @@
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">
113
+ {formatDate(post.publishedAt)}
114
+ </p>
115
+ </div>
116
+ </div>
117
+ )}
118
+ <Button
119
+ size="sm"
120
+ variant="secondary"
121
+ className="w-full sm:w-auto"
122
+ onClick={() => onReadMore?.(post)}
123
+ >
124
+ Read article
125
+ </Button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ )
131
+ }
132
+
133
+ if (variant === 'horizontal') {
134
+ return (
135
+ <div className="test flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3 transition-colors hover:bg-muted/50">
136
+ {showImage && post.coverImage && (
137
+ <div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
138
+ <img
139
+ src={post.coverImage}
140
+ alt={post.title}
141
+ className="h-full w-full object-cover"
142
+ />
143
+ </div>
144
+ )}
145
+ <div className="flex flex-1 flex-col justify-between">
146
+ <div>
147
+ {showCategory && post.category && (
148
+ <span className="mb-1 inline-block rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
149
+ {post.category}
150
+ </span>
151
+ )}
152
+ <h3 className="line-clamp-2 text-sm font-medium leading-tight">
153
+ {post.title}
154
+ </h3>
155
+ <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
156
+ {post.excerpt}
157
+ </p>
158
+ </div>
159
+ <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
160
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
161
+ {showAuthor && post.author.avatar && (
162
+ <img
163
+ src={post.author.avatar}
164
+ alt={post.author.name}
165
+ className="h-4 w-4 rounded-full"
166
+ />
167
+ )}
168
+ <span>{formatDate(post.publishedAt)}</span>
169
+ {post.readTime && (
170
+ <>
171
+ <span>·</span>
172
+ <span>{post.readTime}</span>
173
+ </>
174
+ )}
175
+ </div>
176
+ <Button
177
+ size="sm"
178
+ className="w-full sm:w-auto"
179
+ onClick={() => onReadMore?.(post)}
180
+ >
181
+ Read
182
+ <ArrowRight className="ml-1 h-3 w-3" />
183
+ </Button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ )
188
+ }
189
+
190
+ if (variant === 'compact') {
191
+ return (
192
+ <div className="flex h-full flex-col justify-between rounded-lg border bg-card p-3 transition-colors hover:bg-muted/50">
193
+ <div>
194
+ {showCategory && post.category && (
195
+ <span className="mb-2 inline-block rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
196
+ {post.category}
197
+ </span>
198
+ )}
199
+ <h3 className="line-clamp-2 text-sm font-medium">{post.title}</h3>
200
+ <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
201
+ {post.excerpt}
202
+ </p>
203
+ </div>
204
+ <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
205
+ <div className="flex items-center gap-2">
206
+ {showAuthor && post.author.avatar && (
207
+ <img
208
+ src={post.author.avatar}
209
+ alt={post.author.name}
210
+ className="h-5 w-5 rounded-full"
211
+ />
212
+ )}
213
+ <span className="text-xs text-muted-foreground">
214
+ {formatDate(post.publishedAt)}
215
+ </span>
216
+ </div>
217
+ <Button size="sm" onClick={() => onReadMore?.(post)}>
218
+ Read more
219
+ <ArrowRight className="ml-1 h-3 w-3" />
220
+ </Button>
221
+ </div>
222
+ </div>
223
+ )
224
+ }
225
+
226
+ // Default variant
227
+ return (
228
+ <div className="flex h-full flex-col overflow-hidden rounded-lg border bg-card transition-colors hover:bg-muted/50">
229
+ {showImage && post.coverImage && (
230
+ <div className="aspect-video overflow-hidden">
231
+ <img
232
+ src={post.coverImage}
233
+ alt={post.title}
234
+ className="h-full w-full object-cover transition-transform hover:scale-105"
235
+ />
236
+ </div>
237
+ )}
238
+ <div className="flex flex-1 flex-col justify-between p-4">
239
+ <div>
240
+ <div className="mb-2 flex flex-wrap items-center gap-2">
241
+ {showCategory && post.category && (
242
+ <span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
243
+ {post.category}
244
+ </span>
245
+ )}
246
+ {post.tags &&
247
+ post.tags.length > 0 &&
248
+ post.tags.slice(0, 2).map((tag) => (
249
+ <span
250
+ key={tag}
251
+ className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
252
+ >
253
+ {tag}
254
+ </span>
255
+ ))}
256
+ </div>
257
+ <h3 className="line-clamp-2 font-medium">{post.title}</h3>
258
+ <p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
259
+ {post.excerpt}
260
+ </p>
261
+ </div>
262
+ <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
263
+ {showAuthor && (
264
+ <div className="flex items-center gap-2">
265
+ {post.author.avatar && (
266
+ <img
267
+ src={post.author.avatar}
268
+ alt={post.author.name}
269
+ className="h-6 w-6 rounded-full"
270
+ />
271
+ )}
272
+ <div className="text-xs">
273
+ <p className="font-medium">{post.author.name}</p>
274
+ <p className="text-muted-foreground">
275
+ {formatDate(post.publishedAt)}
276
+ </p>
277
+ </div>
278
+ </div>
279
+ )}
280
+ <Button size="sm" onClick={() => onReadMore?.(post)}>
281
+ Read
282
+ <ArrowRight className="ml-1 h-4 w-4" />
283
+ </Button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ )
288
+ }
@@ -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
+ }