@brainfish-ai/devdoc 0.1.21 → 0.1.23
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/package.json +1 -1
- package/renderer/app/api/collections/route.ts +2 -16
- package/renderer/app/api/docs/route.ts +10 -0
- package/renderer/app/api/schema/route.ts +11 -3
- package/renderer/app/globals.css +88 -0
- package/renderer/components/docs/mdx/index.ts +33 -0
- package/renderer/components/docs/mdx/landing.tsx +684 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +81 -14
- package/renderer/components/docs-viewer/index.tsx +203 -59
- package/renderer/components/docs-viewer/sidebar/index.tsx +3 -95
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +2 -16
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +2 -16
- package/renderer/lib/api-docs/factories.ts +45 -26
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +1 -1
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +1 -0
- package/renderer/lib/docs/config/schema.ts +11 -1
- package/renderer/lib/docs/mdx/frontmatter.ts +11 -0
- package/renderer/lib/utils/icons.ts +48 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +0 -21
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import {
|
|
6
|
+
Copy,
|
|
7
|
+
Check,
|
|
8
|
+
ArrowRight,
|
|
9
|
+
GithubLogo,
|
|
10
|
+
Terminal,
|
|
11
|
+
ArrowUpRight,
|
|
12
|
+
RocketLaunch,
|
|
13
|
+
} from '@phosphor-icons/react'
|
|
14
|
+
import { getPhosphorIcon, type PhosphorIconComponent } from '@/lib/utils/icons'
|
|
15
|
+
|
|
16
|
+
// Direct icon map for commonly used icons (fallback if dynamic import fails)
|
|
17
|
+
const iconMap: Record<string, PhosphorIconComponent> = {
|
|
18
|
+
'arrow-right': ArrowRight,
|
|
19
|
+
'github-logo': GithubLogo,
|
|
20
|
+
'terminal': Terminal,
|
|
21
|
+
'arrow-up-right': ArrowUpRight,
|
|
22
|
+
'rocket-launch': RocketLaunch,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Landing Page Components for MDX Documentation
|
|
27
|
+
*
|
|
28
|
+
* These components enable creating custom landing pages similar to skills.sh
|
|
29
|
+
* with hero sections, command boxes, feature grids, and more.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hero Section Component
|
|
34
|
+
*
|
|
35
|
+
* A full-width hero section for landing pages with optional
|
|
36
|
+
* logo/ASCII art, tagline, and description.
|
|
37
|
+
*/
|
|
38
|
+
interface HeroProps {
|
|
39
|
+
children?: React.ReactNode
|
|
40
|
+
className?: string
|
|
41
|
+
/** Background variant */
|
|
42
|
+
variant?: 'default' | 'gradient' | 'dark' | 'light' | 'pattern'
|
|
43
|
+
/** Vertical alignment */
|
|
44
|
+
align?: 'top' | 'center' | 'bottom'
|
|
45
|
+
/** Minimum height */
|
|
46
|
+
minHeight?: 'sm' | 'md' | 'lg' | 'full'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Hero({
|
|
50
|
+
children,
|
|
51
|
+
className,
|
|
52
|
+
variant = 'default',
|
|
53
|
+
align = 'center',
|
|
54
|
+
minHeight = 'md'
|
|
55
|
+
}: HeroProps) {
|
|
56
|
+
const variantStyles = {
|
|
57
|
+
default: 'bg-background text-foreground',
|
|
58
|
+
gradient: 'bg-gradient-to-b from-background via-background to-muted/30 text-foreground',
|
|
59
|
+
dark: 'bg-zinc-950 text-white [&_.landing-tagline]:text-zinc-400 [&_.landing-headline]:text-white [&_.landing-description]:text-zinc-300 [&_.landing-button]:border-zinc-600',
|
|
60
|
+
light: 'bg-white text-zinc-900 [&_.landing-tagline]:text-zinc-500 [&_.landing-headline]:text-zinc-900 [&_.landing-description]:text-zinc-600 [&_.landing-button]:border-zinc-300',
|
|
61
|
+
pattern: 'bg-background bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background text-foreground',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const alignStyles = {
|
|
65
|
+
top: 'items-start pt-12',
|
|
66
|
+
center: 'items-center',
|
|
67
|
+
bottom: 'items-end pb-12',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const heightStyles = {
|
|
71
|
+
sm: 'min-h-[40vh]',
|
|
72
|
+
md: 'min-h-[60vh]',
|
|
73
|
+
lg: 'min-h-[80vh]',
|
|
74
|
+
full: 'min-h-screen',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<section
|
|
79
|
+
className={cn(
|
|
80
|
+
'landing-hero w-full flex flex-col justify-center',
|
|
81
|
+
variantStyles[variant],
|
|
82
|
+
alignStyles[align],
|
|
83
|
+
heightStyles[minHeight],
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div className="landing-hero-content w-full max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pre Component
|
|
96
|
+
*
|
|
97
|
+
* Pre-formatted text component for ASCII art, logos, and other
|
|
98
|
+
* monospace content that needs to preserve whitespace.
|
|
99
|
+
*/
|
|
100
|
+
interface PreProps {
|
|
101
|
+
children: React.ReactNode
|
|
102
|
+
className?: string
|
|
103
|
+
/** Font size preset */
|
|
104
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
105
|
+
/** Text alignment */
|
|
106
|
+
align?: 'left' | 'center' | 'right'
|
|
107
|
+
/** Text color variant */
|
|
108
|
+
color?: 'default' | 'muted' | 'primary' | 'white'
|
|
109
|
+
/** Line height */
|
|
110
|
+
leading?: 'none' | 'tight' | 'normal' | 'relaxed'
|
|
111
|
+
/** Hide from screen readers (decorative content) */
|
|
112
|
+
decorative?: boolean
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function Pre({
|
|
116
|
+
children,
|
|
117
|
+
className,
|
|
118
|
+
size = 'md',
|
|
119
|
+
align = 'center',
|
|
120
|
+
color = 'default',
|
|
121
|
+
leading = 'tight',
|
|
122
|
+
decorative = false,
|
|
123
|
+
}: PreProps) {
|
|
124
|
+
const sizeStyles = {
|
|
125
|
+
xs: 'text-[0.5rem] sm:text-xs',
|
|
126
|
+
sm: 'text-xs sm:text-sm',
|
|
127
|
+
md: 'text-sm sm:text-base md:text-lg',
|
|
128
|
+
lg: 'text-base sm:text-lg md:text-xl lg:text-2xl',
|
|
129
|
+
xl: 'text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const alignStyles = {
|
|
133
|
+
left: 'text-left',
|
|
134
|
+
center: 'text-center mx-auto',
|
|
135
|
+
right: 'text-right ml-auto',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const colorStyles = {
|
|
139
|
+
default: 'text-foreground',
|
|
140
|
+
muted: 'text-muted-foreground',
|
|
141
|
+
primary: 'text-primary',
|
|
142
|
+
white: 'text-white',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const leadingStyles = {
|
|
146
|
+
none: 'leading-none',
|
|
147
|
+
tight: 'leading-tight',
|
|
148
|
+
normal: 'leading-normal',
|
|
149
|
+
relaxed: 'leading-relaxed',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<pre
|
|
154
|
+
className={cn(
|
|
155
|
+
'landing-pre font-mono whitespace-pre select-none overflow-x-auto',
|
|
156
|
+
sizeStyles[size],
|
|
157
|
+
alignStyles[align],
|
|
158
|
+
colorStyles[color],
|
|
159
|
+
leadingStyles[leading],
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
aria-hidden={decorative}
|
|
163
|
+
>
|
|
164
|
+
{children}
|
|
165
|
+
</pre>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Tagline Component
|
|
171
|
+
*
|
|
172
|
+
* Displays a prominent tagline or subtitle.
|
|
173
|
+
*/
|
|
174
|
+
interface TaglineProps {
|
|
175
|
+
children: React.ReactNode
|
|
176
|
+
className?: string
|
|
177
|
+
/** Visual style */
|
|
178
|
+
variant?: 'default' | 'muted' | 'accent'
|
|
179
|
+
/** Letter spacing */
|
|
180
|
+
tracking?: 'normal' | 'wide' | 'wider' | 'widest'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function Tagline({
|
|
184
|
+
children,
|
|
185
|
+
className,
|
|
186
|
+
variant = 'default',
|
|
187
|
+
tracking = 'wider'
|
|
188
|
+
}: TaglineProps) {
|
|
189
|
+
const variantStyles = {
|
|
190
|
+
default: '', // Inherit from parent
|
|
191
|
+
muted: 'opacity-60',
|
|
192
|
+
accent: 'text-primary',
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const trackingStyles = {
|
|
196
|
+
normal: 'tracking-normal',
|
|
197
|
+
wide: 'tracking-wide',
|
|
198
|
+
wider: 'tracking-wider',
|
|
199
|
+
widest: 'tracking-widest',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Use div instead of p to avoid hydration errors when MDX wraps content in <p>
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'landing-tagline text-sm sm:text-base font-medium uppercase',
|
|
207
|
+
variantStyles[variant],
|
|
208
|
+
trackingStyles[tracking],
|
|
209
|
+
className
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Headline Component
|
|
219
|
+
*
|
|
220
|
+
* Large display text for hero headlines.
|
|
221
|
+
*/
|
|
222
|
+
interface HeadlineProps {
|
|
223
|
+
children: React.ReactNode
|
|
224
|
+
className?: string
|
|
225
|
+
/** Size preset */
|
|
226
|
+
size?: 'md' | 'lg' | 'xl' | '2xl'
|
|
227
|
+
/** Font weight */
|
|
228
|
+
weight?: 'normal' | 'medium' | 'semibold' | 'bold'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function Headline({
|
|
232
|
+
children,
|
|
233
|
+
className,
|
|
234
|
+
size = 'xl',
|
|
235
|
+
weight = 'normal'
|
|
236
|
+
}: HeadlineProps) {
|
|
237
|
+
const sizeStyles = {
|
|
238
|
+
md: 'text-xl sm:text-2xl md:text-3xl',
|
|
239
|
+
lg: 'text-2xl sm:text-3xl md:text-4xl',
|
|
240
|
+
xl: 'text-3xl sm:text-4xl md:text-5xl',
|
|
241
|
+
'2xl': 'text-4xl sm:text-5xl md:text-6xl',
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const weightStyles = {
|
|
245
|
+
normal: 'font-normal',
|
|
246
|
+
medium: 'font-medium',
|
|
247
|
+
semibold: 'font-semibold',
|
|
248
|
+
bold: 'font-bold',
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<h1
|
|
253
|
+
className={cn(
|
|
254
|
+
'landing-headline leading-tight',
|
|
255
|
+
sizeStyles[size],
|
|
256
|
+
weightStyles[weight],
|
|
257
|
+
className
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
{children}
|
|
261
|
+
</h1>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Description Component
|
|
267
|
+
*
|
|
268
|
+
* Body text for hero descriptions.
|
|
269
|
+
*/
|
|
270
|
+
interface DescriptionProps {
|
|
271
|
+
children: React.ReactNode
|
|
272
|
+
className?: string
|
|
273
|
+
/** Size preset */
|
|
274
|
+
size?: 'sm' | 'md' | 'lg'
|
|
275
|
+
/** Max width */
|
|
276
|
+
maxWidth?: 'sm' | 'md' | 'lg' | 'full'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function Description({
|
|
280
|
+
children,
|
|
281
|
+
className,
|
|
282
|
+
size = 'lg',
|
|
283
|
+
maxWidth = 'lg'
|
|
284
|
+
}: DescriptionProps) {
|
|
285
|
+
const sizeStyles = {
|
|
286
|
+
sm: 'text-sm sm:text-base',
|
|
287
|
+
md: 'text-base sm:text-lg',
|
|
288
|
+
lg: 'text-lg sm:text-xl',
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const maxWidthStyles = {
|
|
292
|
+
sm: 'max-w-md',
|
|
293
|
+
md: 'max-w-xl',
|
|
294
|
+
lg: 'max-w-2xl',
|
|
295
|
+
full: 'max-w-full',
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Use div instead of p to avoid hydration errors when MDX wraps content in <p>
|
|
299
|
+
return (
|
|
300
|
+
<div
|
|
301
|
+
className={cn(
|
|
302
|
+
'landing-description opacity-80 mx-auto',
|
|
303
|
+
sizeStyles[size],
|
|
304
|
+
maxWidthStyles[maxWidth],
|
|
305
|
+
className
|
|
306
|
+
)}
|
|
307
|
+
>
|
|
308
|
+
{children}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Command Box Component
|
|
315
|
+
*
|
|
316
|
+
* A copyable command display with a dark background,
|
|
317
|
+
* similar to the skills.sh installation command box.
|
|
318
|
+
*/
|
|
319
|
+
interface CommandBoxProps {
|
|
320
|
+
/** The command to display */
|
|
321
|
+
command: string
|
|
322
|
+
/** Optional prefix like $ or > */
|
|
323
|
+
prefix?: string
|
|
324
|
+
className?: string
|
|
325
|
+
/** Visual variant - 'default' is dark (terminal style), 'light' adapts to theme */
|
|
326
|
+
variant?: 'default' | 'minimal' | 'bordered' | 'light'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function CommandBox({
|
|
330
|
+
command,
|
|
331
|
+
prefix = '$',
|
|
332
|
+
className,
|
|
333
|
+
variant = 'default'
|
|
334
|
+
}: CommandBoxProps) {
|
|
335
|
+
const [copied, setCopied] = useState(false)
|
|
336
|
+
|
|
337
|
+
const handleCopy = async () => {
|
|
338
|
+
try {
|
|
339
|
+
await navigator.clipboard.writeText(command)
|
|
340
|
+
setCopied(true)
|
|
341
|
+
setTimeout(() => setCopied(false), 2000)
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error('Failed to copy:', err)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const variantStyles = {
|
|
348
|
+
default: 'bg-zinc-950 border border-zinc-800 shadow-lg',
|
|
349
|
+
minimal: 'bg-zinc-900',
|
|
350
|
+
bordered: 'bg-zinc-950 border-2 border-zinc-700',
|
|
351
|
+
light: 'bg-muted border border-border shadow-sm',
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const textStyles = {
|
|
355
|
+
default: 'text-white',
|
|
356
|
+
minimal: 'text-white',
|
|
357
|
+
bordered: 'text-white',
|
|
358
|
+
light: 'text-foreground',
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const prefixStyles = {
|
|
362
|
+
default: 'text-zinc-400',
|
|
363
|
+
minimal: 'text-zinc-400',
|
|
364
|
+
bordered: 'text-zinc-400',
|
|
365
|
+
light: 'text-muted-foreground',
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const buttonStyles = {
|
|
369
|
+
default: 'text-zinc-300 hover:text-white hover:bg-zinc-700',
|
|
370
|
+
minimal: 'text-zinc-300 hover:text-white hover:bg-zinc-700',
|
|
371
|
+
bordered: 'text-zinc-300 hover:text-white hover:bg-zinc-700',
|
|
372
|
+
light: 'text-muted-foreground hover:text-foreground hover:bg-muted-foreground/10',
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<div
|
|
377
|
+
className={cn(
|
|
378
|
+
'landing-command-box inline-flex items-center gap-3 rounded-lg px-4 py-3',
|
|
379
|
+
variantStyles[variant],
|
|
380
|
+
className
|
|
381
|
+
)}
|
|
382
|
+
>
|
|
383
|
+
<code className={cn('flex items-center gap-2 font-mono text-sm sm:text-base', textStyles[variant])}>
|
|
384
|
+
{prefix && (
|
|
385
|
+
<span className={cn('select-none', prefixStyles[variant])}>{prefix}</span>
|
|
386
|
+
)}
|
|
387
|
+
<span>{command}</span>
|
|
388
|
+
</code>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
onClick={handleCopy}
|
|
392
|
+
className={cn(
|
|
393
|
+
'p-1.5 rounded transition-colors',
|
|
394
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
395
|
+
buttonStyles[variant]
|
|
396
|
+
)}
|
|
397
|
+
title="Copy command"
|
|
398
|
+
>
|
|
399
|
+
{copied ? (
|
|
400
|
+
<Check className="h-4 w-4 text-emerald-500" weight="bold" />
|
|
401
|
+
) : (
|
|
402
|
+
<Copy className="h-4 w-4" />
|
|
403
|
+
)}
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Section Component
|
|
411
|
+
*
|
|
412
|
+
* A full-width section wrapper for landing page content.
|
|
413
|
+
*/
|
|
414
|
+
interface SectionProps {
|
|
415
|
+
children: React.ReactNode
|
|
416
|
+
className?: string
|
|
417
|
+
/** Section ID for anchor links */
|
|
418
|
+
id?: string
|
|
419
|
+
/** Background variant */
|
|
420
|
+
variant?: 'default' | 'muted' | 'dark' | 'light' | 'accent'
|
|
421
|
+
/** Padding preset */
|
|
422
|
+
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function Section({
|
|
426
|
+
children,
|
|
427
|
+
className,
|
|
428
|
+
id,
|
|
429
|
+
variant = 'default',
|
|
430
|
+
padding = 'lg'
|
|
431
|
+
}: SectionProps) {
|
|
432
|
+
const variantStyles = {
|
|
433
|
+
default: 'bg-background text-foreground',
|
|
434
|
+
muted: 'bg-muted/30 text-foreground',
|
|
435
|
+
dark: 'bg-zinc-950 text-white [&_.landing-tagline]:text-zinc-400 [&_.landing-headline]:text-white [&_.landing-description]:text-zinc-300 [&_.landing-feature-title]:text-white [&_.landing-feature-description]:text-zinc-400 [&_.landing-button]:border-zinc-600',
|
|
436
|
+
light: 'bg-white text-zinc-900 [&_.landing-tagline]:text-zinc-500 [&_.landing-headline]:text-zinc-900 [&_.landing-description]:text-zinc-600 [&_.landing-feature-title]:text-zinc-900 [&_.landing-feature-description]:text-zinc-600 [&_.landing-button]:border-zinc-300',
|
|
437
|
+
accent: 'bg-primary/5 text-foreground',
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const paddingStyles = {
|
|
441
|
+
none: '',
|
|
442
|
+
sm: 'py-8 sm:py-12',
|
|
443
|
+
md: 'py-12 sm:py-16',
|
|
444
|
+
lg: 'py-16 sm:py-24',
|
|
445
|
+
xl: 'py-24 sm:py-32',
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<section
|
|
450
|
+
id={id}
|
|
451
|
+
className={cn(
|
|
452
|
+
'landing-section w-full',
|
|
453
|
+
variantStyles[variant],
|
|
454
|
+
paddingStyles[padding],
|
|
455
|
+
className
|
|
456
|
+
)}
|
|
457
|
+
>
|
|
458
|
+
<div className="landing-section-content max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
459
|
+
{children}
|
|
460
|
+
</div>
|
|
461
|
+
</section>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Center Component
|
|
467
|
+
*
|
|
468
|
+
* Centers content horizontally and optionally vertically.
|
|
469
|
+
*/
|
|
470
|
+
interface CenterProps {
|
|
471
|
+
children: React.ReactNode
|
|
472
|
+
className?: string
|
|
473
|
+
/** Stack children with gap */
|
|
474
|
+
gap?: 'none' | 'sm' | 'md' | 'lg'
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function Center({ children, className, gap = 'md' }: CenterProps) {
|
|
478
|
+
const gapStyles = {
|
|
479
|
+
none: '',
|
|
480
|
+
sm: 'space-y-2',
|
|
481
|
+
md: 'space-y-4',
|
|
482
|
+
lg: 'space-y-8',
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div
|
|
487
|
+
className={cn(
|
|
488
|
+
'landing-center flex flex-col items-center text-center',
|
|
489
|
+
gapStyles[gap],
|
|
490
|
+
className
|
|
491
|
+
)}
|
|
492
|
+
>
|
|
493
|
+
{children}
|
|
494
|
+
</div>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Feature Grid Component
|
|
500
|
+
*
|
|
501
|
+
* A responsive grid for displaying feature cards.
|
|
502
|
+
*/
|
|
503
|
+
interface FeatureGridProps {
|
|
504
|
+
children: React.ReactNode
|
|
505
|
+
className?: string
|
|
506
|
+
/** Number of columns */
|
|
507
|
+
cols?: 2 | 3 | 4
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function FeatureGrid({ children, className, cols = 3 }: FeatureGridProps) {
|
|
511
|
+
const colStyles = {
|
|
512
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
|
513
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
514
|
+
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<div
|
|
519
|
+
className={cn(
|
|
520
|
+
'landing-feature-grid grid gap-6',
|
|
521
|
+
colStyles[cols],
|
|
522
|
+
className
|
|
523
|
+
)}
|
|
524
|
+
>
|
|
525
|
+
{children}
|
|
526
|
+
</div>
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Feature Item Component
|
|
532
|
+
*
|
|
533
|
+
* Individual feature card with icon, title, and description.
|
|
534
|
+
*/
|
|
535
|
+
interface FeatureItemProps {
|
|
536
|
+
children?: React.ReactNode
|
|
537
|
+
title?: string
|
|
538
|
+
icon?: React.ReactNode
|
|
539
|
+
className?: string
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function FeatureItem({ children, title, icon, className }: FeatureItemProps) {
|
|
543
|
+
return (
|
|
544
|
+
<div
|
|
545
|
+
className={cn(
|
|
546
|
+
'landing-feature-item p-6 rounded-xl border border-border/50 bg-card/50',
|
|
547
|
+
'hover:border-primary/30 hover:bg-card/80 transition-colors',
|
|
548
|
+
className
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
551
|
+
{icon && (
|
|
552
|
+
<div className="landing-feature-icon mb-4 text-primary text-2xl">
|
|
553
|
+
{icon}
|
|
554
|
+
</div>
|
|
555
|
+
)}
|
|
556
|
+
{title && (
|
|
557
|
+
<h3 className="landing-feature-title text-lg font-semibold mb-2">
|
|
558
|
+
{title}
|
|
559
|
+
</h3>
|
|
560
|
+
)}
|
|
561
|
+
{children && (
|
|
562
|
+
<div className="landing-feature-description text-sm opacity-80">
|
|
563
|
+
{children}
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
566
|
+
</div>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Button Link Component
|
|
572
|
+
*
|
|
573
|
+
* Styled button links for CTAs.
|
|
574
|
+
*/
|
|
575
|
+
interface ButtonLinkProps {
|
|
576
|
+
href: string
|
|
577
|
+
children: React.ReactNode
|
|
578
|
+
className?: string
|
|
579
|
+
/** Visual variant */
|
|
580
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
|
|
581
|
+
/** Size preset */
|
|
582
|
+
size?: 'sm' | 'md' | 'lg'
|
|
583
|
+
/** Phosphor icon name (e.g., "arrow-right", "github-logo") */
|
|
584
|
+
icon?: string
|
|
585
|
+
/** Icon position */
|
|
586
|
+
iconPosition?: 'left' | 'right'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
export function ButtonLink({
|
|
591
|
+
href,
|
|
592
|
+
children,
|
|
593
|
+
className,
|
|
594
|
+
variant = 'primary',
|
|
595
|
+
size = 'md',
|
|
596
|
+
icon,
|
|
597
|
+
iconPosition = 'right'
|
|
598
|
+
}: ButtonLinkProps) {
|
|
599
|
+
const variantStyles = {
|
|
600
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
601
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
602
|
+
outline: 'border border-current/30 bg-transparent text-current hover:bg-current/10',
|
|
603
|
+
ghost: 'bg-transparent text-current hover:bg-current/10',
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const sizeStyles = {
|
|
607
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
608
|
+
md: 'px-4 py-2 text-base',
|
|
609
|
+
lg: 'px-6 py-3 text-lg',
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const iconSizes = {
|
|
613
|
+
sm: 'h-4 w-4',
|
|
614
|
+
md: 'h-5 w-5',
|
|
615
|
+
lg: 'h-5 w-5',
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Try direct map first, then dynamic lookup
|
|
619
|
+
const IconComponent = icon ? (iconMap[icon] || getPhosphorIcon(icon)) : null
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
<a
|
|
623
|
+
href={href}
|
|
624
|
+
className={cn(
|
|
625
|
+
'landing-button inline-flex items-center justify-center gap-2 rounded-lg font-medium',
|
|
626
|
+
'transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
627
|
+
variantStyles[variant],
|
|
628
|
+
sizeStyles[size],
|
|
629
|
+
className
|
|
630
|
+
)}
|
|
631
|
+
>
|
|
632
|
+
{IconComponent && iconPosition === 'left' && (
|
|
633
|
+
<IconComponent className={iconSizes[size]} weight="bold" />
|
|
634
|
+
)}
|
|
635
|
+
{children}
|
|
636
|
+
{IconComponent && iconPosition === 'right' && (
|
|
637
|
+
<IconComponent className={iconSizes[size]} weight="bold" />
|
|
638
|
+
)}
|
|
639
|
+
</a>
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Spacer Component
|
|
645
|
+
*
|
|
646
|
+
* Adds vertical spacing.
|
|
647
|
+
*/
|
|
648
|
+
interface SpacerProps {
|
|
649
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
650
|
+
className?: string
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function Spacer({ size = 'md', className }: SpacerProps) {
|
|
654
|
+
const sizeStyles = {
|
|
655
|
+
xs: 'h-2',
|
|
656
|
+
sm: 'h-4',
|
|
657
|
+
md: 'h-8',
|
|
658
|
+
lg: 'h-12',
|
|
659
|
+
xl: 'h-16',
|
|
660
|
+
'2xl': 'h-24',
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return <div className={cn('landing-spacer', sizeStyles[size], className)} />
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Divider Component
|
|
668
|
+
*
|
|
669
|
+
* Horizontal divider line.
|
|
670
|
+
*/
|
|
671
|
+
interface DividerProps {
|
|
672
|
+
className?: string
|
|
673
|
+
variant?: 'solid' | 'dashed' | 'gradient'
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function Divider({ className, variant = 'solid' }: DividerProps) {
|
|
677
|
+
const variantStyles = {
|
|
678
|
+
solid: 'border-t border-border',
|
|
679
|
+
dashed: 'border-t border-dashed border-border',
|
|
680
|
+
gradient: 'h-px bg-gradient-to-r from-transparent via-border to-transparent border-none',
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return <hr className={cn('landing-divider w-full my-8', variantStyles[variant], className)} />
|
|
684
|
+
}
|