deepspace 0.3.10 → 0.4.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/dist/cli.js +331 -115
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +199 -16
- package/dist/index.js +354 -166
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +242 -73
- package/dist/server.js +985 -374
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +322 -73
- package/dist/worker.js +961 -343
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Page Primitives
|
|
3
|
+
*
|
|
4
|
+
* Shared animation and UI primitives used by the landing page shell
|
|
5
|
+
* and all landing section components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState, useRef, type ReactNode } from 'react'
|
|
9
|
+
import { motion, AnimatePresence, useInView } from 'framer-motion'
|
|
10
|
+
import { ImageIcon, ChevronDown } from 'lucide-react'
|
|
11
|
+
import { cn } from 'deepspace'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Smooth Typewriter
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
function useTypewriter(
|
|
18
|
+
text: string,
|
|
19
|
+
options: { baseSpeed?: number; variance?: number; startDelay?: number } = {},
|
|
20
|
+
) {
|
|
21
|
+
const { baseSpeed = 65, variance = 0.4, startDelay = 300 } = options
|
|
22
|
+
const [displayedCount, setDisplayedCount] = useState(0)
|
|
23
|
+
const [isComplete, setIsComplete] = useState(false)
|
|
24
|
+
const [hasStarted, setHasStarted] = useState(false)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setDisplayedCount(0)
|
|
28
|
+
setIsComplete(false)
|
|
29
|
+
setHasStarted(false)
|
|
30
|
+
const startTimer = setTimeout(() => setHasStarted(true), startDelay)
|
|
31
|
+
return () => clearTimeout(startTimer)
|
|
32
|
+
}, [text, startDelay])
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!hasStarted || displayedCount >= text.length) {
|
|
36
|
+
if (hasStarted && displayedCount >= text.length) setIsComplete(true)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const char = text[displayedCount]
|
|
40
|
+
const nextChar = text[displayedCount + 1]
|
|
41
|
+
let delay = baseSpeed
|
|
42
|
+
if (char === ' ') delay = baseSpeed * 0.3
|
|
43
|
+
else if ('.!?,;:'.includes(char)) delay = baseSpeed * 2.5
|
|
44
|
+
else if (nextChar === ' ' || displayedCount === text.length - 1) delay = baseSpeed * 1.3
|
|
45
|
+
const varianceFactor = 1 + (Math.random() - 0.5) * 2 * variance
|
|
46
|
+
delay *= varianceFactor
|
|
47
|
+
const timer = setTimeout(() => setDisplayedCount(prev => prev + 1), delay)
|
|
48
|
+
return () => clearTimeout(timer)
|
|
49
|
+
}, [hasStarted, displayedCount, text, baseSpeed, variance])
|
|
50
|
+
|
|
51
|
+
return { displayedText: text.slice(0, displayedCount), isComplete }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Typewriter({
|
|
55
|
+
text,
|
|
56
|
+
className,
|
|
57
|
+
cursorClassName,
|
|
58
|
+
baseSpeed,
|
|
59
|
+
variance,
|
|
60
|
+
startDelay,
|
|
61
|
+
onComplete,
|
|
62
|
+
}: {
|
|
63
|
+
text: string
|
|
64
|
+
className?: string
|
|
65
|
+
cursorClassName?: string
|
|
66
|
+
baseSpeed?: number
|
|
67
|
+
variance?: number
|
|
68
|
+
startDelay?: number
|
|
69
|
+
onComplete?: () => void
|
|
70
|
+
}) {
|
|
71
|
+
const { displayedText, isComplete } = useTypewriter(text, { baseSpeed, variance, startDelay })
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (isComplete && onComplete) onComplete()
|
|
75
|
+
}, [isComplete, onComplete])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<span className={className} aria-label={text}>
|
|
79
|
+
<span>{displayedText}</span>
|
|
80
|
+
<motion.span
|
|
81
|
+
className={cn(
|
|
82
|
+
'inline-block w-[3px] h-[0.9em] ml-[2px] align-middle rounded-full',
|
|
83
|
+
cursorClassName ?? 'bg-foreground',
|
|
84
|
+
)}
|
|
85
|
+
animate={{ opacity: isComplete ? [1, 0] : [1, 1] }}
|
|
86
|
+
transition={
|
|
87
|
+
isComplete
|
|
88
|
+
? { duration: 0.8, repeat: Infinity, repeatType: 'reverse', ease: 'easeInOut' }
|
|
89
|
+
: { duration: 0 }
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
</span>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Scroll-triggered animation primitives
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export function ScrollReveal({
|
|
101
|
+
children,
|
|
102
|
+
className,
|
|
103
|
+
delay = 0,
|
|
104
|
+
direction = 'up',
|
|
105
|
+
}: {
|
|
106
|
+
children: ReactNode
|
|
107
|
+
className?: string
|
|
108
|
+
delay?: number
|
|
109
|
+
direction?: 'up' | 'down' | 'left' | 'right'
|
|
110
|
+
}) {
|
|
111
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
112
|
+
const isInView = useInView(ref, { once: true, margin: '-80px 0px' })
|
|
113
|
+
const directionMap = {
|
|
114
|
+
up: { y: 40, x: 0 },
|
|
115
|
+
down: { y: -40, x: 0 },
|
|
116
|
+
left: { x: 40, y: 0 },
|
|
117
|
+
right: { x: -40, y: 0 },
|
|
118
|
+
}
|
|
119
|
+
const offset = directionMap[direction]
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<motion.div
|
|
123
|
+
ref={ref}
|
|
124
|
+
className={className}
|
|
125
|
+
initial={{ opacity: 0, x: offset.x, y: offset.y }}
|
|
126
|
+
animate={isInView ? { opacity: 1, x: 0, y: 0 } : { opacity: 0, x: offset.x, y: offset.y }}
|
|
127
|
+
transition={{ duration: 0.7, delay, ease: [0.25, 0.4, 0.25, 1] }}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</motion.div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function StaggerContainer({
|
|
135
|
+
children,
|
|
136
|
+
className,
|
|
137
|
+
staggerDelay = 0.1,
|
|
138
|
+
}: {
|
|
139
|
+
children: ReactNode
|
|
140
|
+
className?: string
|
|
141
|
+
staggerDelay?: number
|
|
142
|
+
}) {
|
|
143
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
144
|
+
const isInView = useInView(ref, { once: true, margin: '-60px 0px' })
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<motion.div
|
|
148
|
+
ref={ref}
|
|
149
|
+
className={className}
|
|
150
|
+
initial="hidden"
|
|
151
|
+
animate={isInView ? 'visible' : 'hidden'}
|
|
152
|
+
variants={{
|
|
153
|
+
hidden: {},
|
|
154
|
+
visible: { transition: { staggerChildren: staggerDelay } },
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</motion.div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const staggerChild = {
|
|
163
|
+
hidden: { opacity: 0, y: 30, scale: 0.97 },
|
|
164
|
+
visible: {
|
|
165
|
+
opacity: 1,
|
|
166
|
+
y: 0,
|
|
167
|
+
scale: 1,
|
|
168
|
+
transition: { duration: 0.5, ease: 'easeOut' as const },
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Animated counter
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
function parseStatValue(value: string): { prefix: string; number: number; suffix: string } {
|
|
177
|
+
const match = value.match(/^([<>]?)(\d+(?:\.\d+)?)(.*)$/)
|
|
178
|
+
if (!match) return { prefix: '', number: 0, suffix: value }
|
|
179
|
+
return { prefix: match[1], number: parseFloat(match[2]), suffix: match[3] }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function useCountUp(target: number, duration: number = 1800, shouldStart: boolean = false): number {
|
|
183
|
+
const [current, setCurrent] = useState(0)
|
|
184
|
+
const startTimeRef = useRef<number | null>(null)
|
|
185
|
+
const rafRef = useRef<number>(0)
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!shouldStart) return
|
|
189
|
+
startTimeRef.current = null
|
|
190
|
+
|
|
191
|
+
function tick(timestamp: number) {
|
|
192
|
+
if (startTimeRef.current === null) startTimeRef.current = timestamp
|
|
193
|
+
const elapsed = timestamp - startTimeRef.current
|
|
194
|
+
const progress = Math.min(elapsed / duration, 1)
|
|
195
|
+
const eased = 1 - Math.pow(1 - progress, 3)
|
|
196
|
+
setCurrent(eased * target)
|
|
197
|
+
if (progress < 1) rafRef.current = requestAnimationFrame(tick)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
201
|
+
return () => cancelAnimationFrame(rafRef.current)
|
|
202
|
+
}, [target, duration, shouldStart])
|
|
203
|
+
|
|
204
|
+
return current
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function AnimatedStat({ value, label }: { value: string; label: string }) {
|
|
208
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
209
|
+
const isInView = useInView(ref, { once: true, margin: '-40px 0px' })
|
|
210
|
+
const { prefix, number, suffix } = parseStatValue(value)
|
|
211
|
+
const animated = useCountUp(number, 1800, isInView)
|
|
212
|
+
|
|
213
|
+
const hasDecimal = number % 1 !== 0
|
|
214
|
+
const displayed = hasDecimal ? animated.toFixed(1) : Math.round(animated).toString()
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<motion.div ref={ref} variants={staggerChild} className="text-center">
|
|
218
|
+
<div className="text-3xl md:text-4xl font-bold text-foreground tracking-tight">
|
|
219
|
+
{prefix}{displayed}{suffix}
|
|
220
|
+
</div>
|
|
221
|
+
<div className="text-muted-foreground/60 text-sm font-medium mt-1">{label}</div>
|
|
222
|
+
</motion.div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Glassmorphic Card
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
export function GlassCard({
|
|
231
|
+
children,
|
|
232
|
+
className,
|
|
233
|
+
hoverEffect = true,
|
|
234
|
+
}: {
|
|
235
|
+
children: ReactNode
|
|
236
|
+
className?: string
|
|
237
|
+
hoverEffect?: boolean
|
|
238
|
+
}) {
|
|
239
|
+
return (
|
|
240
|
+
<motion.div
|
|
241
|
+
className={cn(
|
|
242
|
+
'relative rounded-2xl overflow-hidden',
|
|
243
|
+
'bg-background/70',
|
|
244
|
+
'border border-foreground/[0.08]',
|
|
245
|
+
'shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
|
|
246
|
+
hoverEffect && 'group transition-all duration-500',
|
|
247
|
+
className,
|
|
248
|
+
)}
|
|
249
|
+
whileHover={hoverEffect ? { y: -4, transition: { duration: 0.3, ease: 'easeOut' } } : undefined}
|
|
250
|
+
>
|
|
251
|
+
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-foreground/20 to-transparent" />
|
|
252
|
+
{children}
|
|
253
|
+
</motion.div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Placeholder Image
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
export function PlaceholderImage({
|
|
262
|
+
className,
|
|
263
|
+
label,
|
|
264
|
+
src,
|
|
265
|
+
aspectRatio = 'aspect-video',
|
|
266
|
+
}: {
|
|
267
|
+
className?: string
|
|
268
|
+
label?: string
|
|
269
|
+
src?: string
|
|
270
|
+
aspectRatio?: string
|
|
271
|
+
}) {
|
|
272
|
+
if (src) {
|
|
273
|
+
return (
|
|
274
|
+
<div className={cn('relative rounded-xl overflow-hidden', aspectRatio, className)}>
|
|
275
|
+
<img src={src} alt={label ?? ''} className="absolute inset-0 w-full h-full object-cover" />
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
className={cn(
|
|
283
|
+
'relative rounded-xl overflow-hidden',
|
|
284
|
+
'bg-foreground/[0.03] border-2 border-dashed border-foreground/[0.12]',
|
|
285
|
+
aspectRatio,
|
|
286
|
+
className,
|
|
287
|
+
)}
|
|
288
|
+
>
|
|
289
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3">
|
|
290
|
+
<div className="w-12 h-12 rounded-xl bg-foreground/[0.06] border border-foreground/[0.1] flex items-center justify-center">
|
|
291
|
+
<ImageIcon className="w-6 h-6 text-muted-foreground/50" />
|
|
292
|
+
</div>
|
|
293
|
+
{label && (
|
|
294
|
+
<span className="text-[11px] font-medium tracking-wider uppercase text-muted-foreground/50">
|
|
295
|
+
{label}
|
|
296
|
+
</span>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
<svg className="absolute inset-0 w-full h-full opacity-[0.04]" preserveAspectRatio="none">
|
|
300
|
+
<pattern id="diag" width="20" height="20" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
|
301
|
+
<line x1="0" y1="0" x2="0" y2="20" stroke="currentColor" strokeWidth="1" />
|
|
302
|
+
</pattern>
|
|
303
|
+
<rect width="100%" height="100%" fill="url(#diag)" />
|
|
304
|
+
</svg>
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Browser Mockup
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
export function BrowserMockup({
|
|
314
|
+
className,
|
|
315
|
+
label,
|
|
316
|
+
src,
|
|
317
|
+
glowBorder = false,
|
|
318
|
+
}: {
|
|
319
|
+
className?: string
|
|
320
|
+
label?: string
|
|
321
|
+
src?: string
|
|
322
|
+
glowBorder?: boolean
|
|
323
|
+
}) {
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
className={cn(
|
|
327
|
+
'rounded-xl overflow-hidden border border-foreground/[0.1] shadow-2xl',
|
|
328
|
+
glowBorder && 'landing-gradient-border',
|
|
329
|
+
className,
|
|
330
|
+
)}
|
|
331
|
+
>
|
|
332
|
+
<div className="flex items-center gap-2 px-4 py-3 bg-foreground/[0.04] border-b border-foreground/[0.08]">
|
|
333
|
+
<div className="flex gap-1.5">
|
|
334
|
+
<div className="w-3 h-3 rounded-full bg-foreground/[0.1]" />
|
|
335
|
+
<div className="w-3 h-3 rounded-full bg-foreground/[0.1]" />
|
|
336
|
+
<div className="w-3 h-3 rounded-full bg-foreground/[0.1]" />
|
|
337
|
+
</div>
|
|
338
|
+
<div className="flex-1 mx-4">
|
|
339
|
+
<div className="h-6 rounded-md bg-foreground/[0.05] border border-foreground/[0.08] max-w-xs mx-auto flex items-center justify-center">
|
|
340
|
+
<span className="text-[10px] text-muted-foreground/50 font-medium">myapp.com</span>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<PlaceholderImage src={src} label={label} aspectRatio="aspect-[16/10]" className="rounded-none border-0 border-dashed-0" />
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Section Heading
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
export function SectionHeading({
|
|
354
|
+
tag,
|
|
355
|
+
title,
|
|
356
|
+
titleHighlight,
|
|
357
|
+
subtitle,
|
|
358
|
+
}: {
|
|
359
|
+
tag: string
|
|
360
|
+
title: string
|
|
361
|
+
titleHighlight?: string
|
|
362
|
+
subtitle?: string
|
|
363
|
+
}) {
|
|
364
|
+
return (
|
|
365
|
+
<ScrollReveal className="text-center mb-16">
|
|
366
|
+
<span className="inline-block text-xs font-semibold tracking-[0.2em] uppercase text-primary/80 mb-4">
|
|
367
|
+
{tag}
|
|
368
|
+
</span>
|
|
369
|
+
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground leading-tight tracking-[-0.02em]">
|
|
370
|
+
{title}
|
|
371
|
+
{titleHighlight && (
|
|
372
|
+
<>
|
|
373
|
+
<br />
|
|
374
|
+
<span className="bg-gradient-to-r from-primary via-violet-400 to-purple-300 bg-clip-text text-transparent">
|
|
375
|
+
{titleHighlight}
|
|
376
|
+
</span>
|
|
377
|
+
</>
|
|
378
|
+
)}
|
|
379
|
+
</h2>
|
|
380
|
+
{subtitle && (
|
|
381
|
+
<p className="mt-5 text-muted-foreground max-w-xl mx-auto text-lg leading-relaxed">
|
|
382
|
+
{subtitle}
|
|
383
|
+
</p>
|
|
384
|
+
)}
|
|
385
|
+
</ScrollReveal>
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { cn, AnimatePresence, motion, useInView, ChevronDown }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Section: FAQ
|
|
3
|
+
*
|
|
4
|
+
* Accordion FAQ inside a glassmorphic card.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from 'react'
|
|
8
|
+
import { Plus, Minus } from 'lucide-react'
|
|
9
|
+
import { ScrollReveal, GlassCard, SectionHeading, cn, motion, AnimatePresence } from '../primitives'
|
|
10
|
+
|
|
11
|
+
const FAQ_ITEMS = [
|
|
12
|
+
{
|
|
13
|
+
question: 'How do I get started?',
|
|
14
|
+
answer: 'Just launch the app and follow the guided setup. You\'ll be up and running in under a minute -- no complicated configuration needed.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: 'Is it free to use?',
|
|
18
|
+
answer: 'Yes, the core features are completely free. Premium features are available for teams and power users who need more.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
question: 'Can I customize it?',
|
|
22
|
+
answer: 'Absolutely. The app is designed to be flexible -- adjust settings, layouts, and workflows to match exactly how you like to work.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
question: 'How does collaboration work?',
|
|
26
|
+
answer: 'Invite your team and work together in real time. Everyone sees changes instantly, and permissions keep things organized.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
question: 'Where can I get help?',
|
|
30
|
+
answer: 'Check out the built-in help section, browse the documentation, or reach out to the community. Support is always available.',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function FAQItem({ item, isOpen, onToggle }: {
|
|
35
|
+
item: typeof FAQ_ITEMS[number]
|
|
36
|
+
isOpen: boolean
|
|
37
|
+
onToggle: () => void
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="border-b border-foreground/[0.06] last:border-b-0">
|
|
41
|
+
<button
|
|
42
|
+
onClick={onToggle}
|
|
43
|
+
className="w-full flex items-center justify-between gap-4 py-5 text-left group"
|
|
44
|
+
>
|
|
45
|
+
<span className={cn(
|
|
46
|
+
'text-base font-medium transition-colors duration-200',
|
|
47
|
+
isOpen ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground',
|
|
48
|
+
)}>
|
|
49
|
+
{item.question}
|
|
50
|
+
</span>
|
|
51
|
+
<div className={cn(
|
|
52
|
+
'shrink-0 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300',
|
|
53
|
+
isOpen
|
|
54
|
+
? 'bg-primary/20 text-primary rotate-0'
|
|
55
|
+
: 'bg-foreground/[0.06] text-muted-foreground group-hover:bg-foreground/[0.1]',
|
|
56
|
+
)}>
|
|
57
|
+
{isOpen ? <Minus className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
<AnimatePresence initial={false}>
|
|
61
|
+
{isOpen && (
|
|
62
|
+
<motion.div
|
|
63
|
+
initial={{ height: 0, opacity: 0 }}
|
|
64
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
65
|
+
exit={{ height: 0, opacity: 0 }}
|
|
66
|
+
transition={{ duration: 0.3, ease: [0.25, 0.4, 0.25, 1] }}
|
|
67
|
+
className="overflow-hidden"
|
|
68
|
+
>
|
|
69
|
+
<p className="pb-5 text-sm text-muted-foreground leading-relaxed max-w-2xl">
|
|
70
|
+
{item.answer}
|
|
71
|
+
</p>
|
|
72
|
+
</motion.div>
|
|
73
|
+
)}
|
|
74
|
+
</AnimatePresence>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function FAQSection() {
|
|
80
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
|
81
|
+
|
|
82
|
+
const handleToggle = useCallback((index: number) => {
|
|
83
|
+
setOpenIndex(prev => prev === index ? null : index)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<section id="faq" className="relative py-28 md:py-36">
|
|
88
|
+
<div className="max-w-3xl mx-auto px-6">
|
|
89
|
+
<SectionHeading
|
|
90
|
+
tag="FAQ"
|
|
91
|
+
title="Common questions"
|
|
92
|
+
subtitle="Everything you need to know to get started."
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<ScrollReveal>
|
|
96
|
+
<GlassCard className="px-7 md:px-10 py-2" hoverEffect={false}>
|
|
97
|
+
{FAQ_ITEMS.map((item, idx) => (
|
|
98
|
+
<FAQItem
|
|
99
|
+
key={item.question}
|
|
100
|
+
item={item}
|
|
101
|
+
isOpen={openIndex === idx}
|
|
102
|
+
onToggle={() => handleToggle(idx)}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</GlassCard>
|
|
106
|
+
</ScrollReveal>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
)
|
|
110
|
+
}
|