deepspace 0.3.9 → 0.3.11

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 (120) hide show
  1. package/dist/cli.js +96 -18
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +283 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +172 -2
  7. package/dist/server.js +581 -1
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +252 -2
  10. package/dist/worker.js +588 -1
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. package/scripts/add-feature.cjs +654 -0
@@ -0,0 +1,627 @@
1
+ /**
2
+ * Landing Page Shell
3
+ *
4
+ * Composable landing page with an optional fixed background and scrolling
5
+ * glassmorphic content overlay. The shell renders Hero, CTA, and Footer.
6
+ * Additional sections are imported and placed in the JSX stack.
7
+ */
8
+
9
+ import { useEffect, useState, useRef, useCallback } from 'react'
10
+ import { useNavigate } from 'react-router-dom'
11
+ import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion'
12
+ import {
13
+ ArrowRight, Menu, X, ChevronDown, Play,
14
+ Github, Twitter, Linkedin, Mail, Sparkles, ChevronRight, Plus, Minus,
15
+ } from 'lucide-react'
16
+ import { useUser } from 'deepspace'
17
+ import { Button, cn } from 'deepspace'
18
+ import {
19
+ Typewriter,
20
+ ScrollReveal,
21
+ GlassCard,
22
+ BrowserMockup,
23
+ } from '../components/landing/primitives'
24
+
25
+ // ============================================================================
26
+ // CSS injection
27
+ // ============================================================================
28
+
29
+ const LANDING_CSS = `@layer base {
30
+ .fixed-bg-layer {
31
+ position: fixed;
32
+ inset: 0;
33
+ z-index: 0;
34
+ overflow: hidden;
35
+ }
36
+ .scrollable-content-layer {
37
+ position: relative;
38
+ z-index: 1;
39
+ height: 100dvh;
40
+ height: 100vh;
41
+ overflow-y: auto;
42
+ overflow-x: hidden;
43
+ scroll-behavior: smooth;
44
+ }
45
+ @supports (height: 100dvh) {
46
+ .scrollable-content-layer { height: 100dvh; }
47
+ }
48
+ }
49
+ @keyframes gradient-shimmer {
50
+ 0% { background-position: 0% 50%; }
51
+ 50% { background-position: 100% 50%; }
52
+ 100% { background-position: 0% 50%; }
53
+ }
54
+ @keyframes border-rotate {
55
+ 0% { --border-angle: 0deg; }
56
+ 100% { --border-angle: 360deg; }
57
+ }
58
+ @layer base {
59
+ .landing-shimmer-text {
60
+ background-size: 200% 200%;
61
+ animation: gradient-shimmer 6s ease-in-out infinite;
62
+ }
63
+ .landing-gradient-border { position: relative; }
64
+ .landing-gradient-border::before {
65
+ content: '';
66
+ position: absolute;
67
+ inset: -1px;
68
+ border-radius: inherit;
69
+ padding: 1px;
70
+ background: conic-gradient(
71
+ from var(--border-angle, 0deg),
72
+ transparent 25%,
73
+ rgba(139, 92, 246, 0.5) 50%,
74
+ transparent 75%
75
+ );
76
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
77
+ mask-composite: exclude;
78
+ animation: border-rotate 4s linear infinite;
79
+ }
80
+ .landing-noise-overlay {
81
+ position: fixed;
82
+ inset: 0;
83
+ z-index: 2;
84
+ pointer-events: none;
85
+ opacity: 0.035;
86
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
87
+ background-repeat: repeat;
88
+ background-size: 128px 128px;
89
+ }
90
+ }
91
+ @property --border-angle {
92
+ syntax: "<angle>";
93
+ initial-value: 0deg;
94
+ inherits: false;
95
+ }`
96
+
97
+ let landingCssInjected = false
98
+
99
+ function useLandingCSS() {
100
+ useEffect(() => {
101
+ if (landingCssInjected) return
102
+ landingCssInjected = true
103
+ const style = document.createElement('style')
104
+ style.textContent = LANDING_CSS
105
+ document.head.appendChild(style)
106
+ return () => {
107
+ document.head.removeChild(style)
108
+ landingCssInjected = false
109
+ }
110
+ }, [])
111
+ }
112
+
113
+ // ============================================================================
114
+ // Landing page "seen" flag
115
+ // ============================================================================
116
+
117
+ const LANDING_SEEN_KEY = 'app-landing-seen'
118
+
119
+ export function hasSeenLanding(): boolean {
120
+ try { return localStorage.getItem(LANDING_SEEN_KEY) === 'true' } catch { return false }
121
+ }
122
+
123
+ export function markLandingSeen(): void {
124
+ try { localStorage.setItem(LANDING_SEEN_KEY, 'true') } catch {}
125
+ }
126
+
127
+ // ============================================================================
128
+ // Configuration
129
+ // ============================================================================
130
+
131
+ const LANDING_BG_URL = ''
132
+ const APP_NAME = 'My App'
133
+ const HERO_HEADLINE = 'Welcome to My App'
134
+ const HERO_SUBHEADLINE = 'A simple, powerful way to get things done. Explore what\'s possible and start building today.'
135
+ const HERO_IMAGE = ''
136
+
137
+ const NAV_SECTIONS = [
138
+ { id: 'features', label: 'Features' },
139
+ { id: 'faq', label: 'FAQ' },
140
+ ] as const
141
+
142
+ const NAV_LINKS = NAV_SECTIONS.map(s => ({ label: s.label, href: `#${s.id}` }))
143
+
144
+ const SHOWCASE_ITEMS = [
145
+ {
146
+ label: 'Overview',
147
+ title: 'See everything at a glance',
148
+ description: 'A clear, organized view of what matters most. Stay on top of your work without the clutter.',
149
+ image: '',
150
+ },
151
+ {
152
+ label: 'Workflow',
153
+ title: 'Work the way you want',
154
+ description: 'Flexible tools that adapt to your process. Set things up once and let the app handle the rest.',
155
+ image: '',
156
+ },
157
+ ]
158
+
159
+ const FAQ_ITEMS = [
160
+ { question: 'How do I get started?', answer: 'Just launch the app and follow the guided setup. You\'ll be up and running in under a minute -- no complicated configuration needed.' },
161
+ { question: 'Is it free to use?', answer: 'Yes, the core features are completely free. Premium features are available for teams and power users who need more.' },
162
+ { question: 'Can I customize it?', answer: 'Absolutely. The app is designed to be flexible -- adjust settings, layouts, and workflows to match exactly how you like to work.' },
163
+ { question: 'How does collaboration work?', answer: 'Invite your team and work together in real time. Everyone sees changes instantly, and permissions keep things organized.' },
164
+ { question: 'Where can I get help?', answer: 'Check out the built-in help section, browse the documentation, or reach out to the community. Support is always available.' },
165
+ ]
166
+
167
+ const FOOTER_LINKS = {
168
+ Product: ['Features', 'Pricing', 'Changelog', 'Documentation'],
169
+ Company: ['About', 'Blog', 'Careers'],
170
+ Resources: ['Community', 'Help Center', 'API Reference'],
171
+ Legal: ['Privacy', 'Terms'],
172
+ }
173
+
174
+ // ============================================================================
175
+ // Navbar
176
+ // ============================================================================
177
+
178
+ function useActiveSection(
179
+ sectionIds: readonly string[],
180
+ scrollRoot: React.RefObject<HTMLElement | null>,
181
+ ): string | null {
182
+ const [active, setActive] = useState<string | null>(null)
183
+
184
+ useEffect(() => {
185
+ const root = scrollRoot.current
186
+ if (!root) return
187
+
188
+ const calculate = () => {
189
+ const rootRect = root.getBoundingClientRect()
190
+ const triggerY = rootRect.top + rootRect.height * 0.3
191
+
192
+ const entries = sectionIds
193
+ .map(id => ({ id, el: document.getElementById(id) }))
194
+ .filter((e): e is { id: string; el: HTMLElement } => e.el !== null)
195
+ .sort((a, b) => a.el.getBoundingClientRect().top - b.el.getBoundingClientRect().top)
196
+
197
+ let current: string | null = null
198
+ for (const { id, el } of entries) {
199
+ if (el.getBoundingClientRect().top <= triggerY) {
200
+ current = id
201
+ }
202
+ }
203
+ setActive(current)
204
+ }
205
+
206
+ calculate()
207
+ root.addEventListener('scroll', calculate, { passive: true })
208
+ return () => root.removeEventListener('scroll', calculate)
209
+ }, [sectionIds, scrollRoot])
210
+
211
+ return active
212
+ }
213
+
214
+ function LandingNav({ isScrolled, scrollRoot }: {
215
+ isScrolled: boolean
216
+ scrollRoot: React.RefObject<HTMLElement | null>
217
+ }) {
218
+ const [mobileOpen, setMobileOpen] = useState(false)
219
+ const navigate = useNavigate()
220
+ const sectionIds = NAV_SECTIONS.map(s => s.id)
221
+ const activeSection = useActiveSection(sectionIds, scrollRoot)
222
+
223
+ const scrollTo = (href: string) => {
224
+ setMobileOpen(false)
225
+ const target = document.querySelector(href) as HTMLElement | null
226
+ const container = scrollRoot.current
227
+ if (target && container) {
228
+ const targetTop = target.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop
229
+ container.scrollTo({ top: targetTop, behavior: 'smooth' })
230
+ }
231
+ }
232
+
233
+ const mobileDropdown = (
234
+ <AnimatePresence>
235
+ {mobileOpen && (
236
+ <motion.div
237
+ initial={{ opacity: 0, y: -8, scale: 0.95 }}
238
+ animate={{ opacity: 1, y: 0, scale: 1 }}
239
+ exit={{ opacity: 0, y: -8, scale: 0.95 }}
240
+ transition={{ duration: 0.2, ease: [0.25, 0.4, 0.25, 1] }}
241
+ className={cn(
242
+ 'md:hidden mt-2 rounded-2xl overflow-hidden',
243
+ 'bg-foreground/[0.06] backdrop-blur-2xl',
244
+ 'border border-foreground/[0.1]',
245
+ 'shadow-[0_8px_32px_rgba(0,0,0,0.4)]',
246
+ )}
247
+ >
248
+ <div className="p-2 flex flex-col gap-0.5">
249
+ {NAV_LINKS.map(link => {
250
+ const isActive = activeSection === link.href.replace('#', '')
251
+ return (
252
+ <button
253
+ key={link.href}
254
+ onClick={() => scrollTo(link.href)}
255
+ className={cn(
256
+ 'px-4 py-2.5 rounded-xl text-sm font-medium text-left transition-colors',
257
+ isActive
258
+ ? 'text-foreground bg-foreground/[0.1]'
259
+ : 'text-muted-foreground hover:text-foreground hover:bg-foreground/[0.08]',
260
+ )}
261
+ >
262
+ {link.label}
263
+ </button>
264
+ )
265
+ })}
266
+ <div className="h-px bg-foreground/[0.08] my-1" />
267
+ <button
268
+ onClick={() => { markLandingSeen(); navigate('/home') }}
269
+ className="px-4 py-2.5 rounded-xl text-sm font-medium text-left text-primary hover:bg-foreground/[0.08] transition-colors"
270
+ >
271
+ Get Started
272
+ </button>
273
+ </div>
274
+ </motion.div>
275
+ )}
276
+ </AnimatePresence>
277
+ )
278
+
279
+ return (
280
+ <>
281
+ <motion.div
282
+ className="absolute top-0 left-0 right-0 z-50"
283
+ initial={{ opacity: 0 }}
284
+ animate={{ opacity: isScrolled ? 0 : 1 }}
285
+ transition={{ duration: 0.3 }}
286
+ style={{ pointerEvents: isScrolled ? 'none' : 'auto' }}
287
+ >
288
+ <div className="max-w-6xl mx-auto px-6 py-5 flex items-center justify-between">
289
+ <span className="font-semibold text-lg tracking-tight text-foreground">{APP_NAME}</span>
290
+ <div className="flex items-center gap-4">
291
+ <div className="hidden md:flex items-center gap-8">
292
+ {NAV_LINKS.map(link => {
293
+ const isActive = activeSection === link.href.replace('#', '')
294
+ return (
295
+ <button key={link.href} onClick={() => scrollTo(link.href)} className={cn('text-sm font-medium transition-colors duration-200', isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>
296
+ {link.label}
297
+ </button>
298
+ )
299
+ })}
300
+ </div>
301
+ <Button size="sm" onClick={() => { markLandingSeen(); navigate('/home') }} className="hidden md:inline-flex">
302
+ Get Started
303
+ </Button>
304
+ <button className="md:hidden transition-colors text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(prev => !prev)} aria-label="Toggle menu">
305
+ {mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
306
+ </button>
307
+ </div>
308
+ </div>
309
+ <div className="max-w-6xl mx-auto px-6">{mobileDropdown}</div>
310
+ </motion.div>
311
+
312
+ <AnimatePresence>
313
+ {isScrolled && (
314
+ <motion.nav
315
+ className="fixed top-4 inset-x-0 z-50 flex justify-center pointer-events-none"
316
+ initial={{ y: -80, opacity: 0 }}
317
+ animate={{ y: 0, opacity: 1 }}
318
+ exit={{ y: -80, opacity: 0 }}
319
+ transition={{ duration: 0.4, ease: [0.25, 0.4, 0.25, 1] }}
320
+ >
321
+ <div className={cn('pointer-events-auto flex items-center gap-1 px-2 py-1.5 rounded-full', 'bg-background/80 backdrop-blur-2xl', 'border border-foreground/[0.1]', 'shadow-[0_8px_32px_rgba(0,0,0,0.4),0_0_0_1px_rgba(255,255,255,0.03)]')}>
322
+ <span className="text-foreground font-semibold text-sm px-3 whitespace-nowrap">{APP_NAME}</span>
323
+ <div className="w-px h-4 bg-foreground/10 mx-1 hidden md:block" />
324
+ <div className="hidden md:flex items-center gap-0.5">
325
+ {NAV_LINKS.map(link => {
326
+ const isActive = activeSection === link.href.replace('#', '')
327
+ return (
328
+ <button key={link.href} onClick={() => scrollTo(link.href)} className={cn('relative px-3.5 py-1.5 rounded-full text-sm font-medium transition-all duration-200', isActive ? 'text-foreground bg-foreground/[0.12]' : 'text-muted-foreground hover:text-foreground hover:bg-foreground/[0.08]')}>
329
+ {link.label}
330
+ </button>
331
+ )
332
+ })}
333
+ </div>
334
+ <button className="md:hidden p-2 rounded-full text-muted-foreground hover:text-foreground hover:bg-foreground/[0.08] transition-colors" onClick={() => setMobileOpen(prev => !prev)} aria-label="Toggle menu">
335
+ {mobileOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
336
+ </button>
337
+ </div>
338
+ <div className="pointer-events-auto">{mobileDropdown}</div>
339
+ </motion.nav>
340
+ )}
341
+ </AnimatePresence>
342
+ </>
343
+ )
344
+ }
345
+
346
+ // ============================================================================
347
+ // Hero
348
+ // ============================================================================
349
+
350
+ function HeroSection() {
351
+ const { user } = useUser()
352
+ const navigate = useNavigate()
353
+ const [typewriterDone, setTypewriterDone] = useState(false)
354
+ const headline = user?.name ? `Welcome, ${user.name}` : HERO_HEADLINE
355
+
356
+ const heroRef = useRef<HTMLDivElement>(null)
357
+ const { scrollYProgress } = useScroll({ target: heroRef, offset: ['start start', 'end start'] })
358
+ const mockupY = useTransform(scrollYProgress, [0, 1], [0, 80])
359
+
360
+ return (
361
+ <section ref={heroRef} className="relative">
362
+ <div className="min-h-screen flex flex-col items-center justify-center px-6">
363
+ <div className="relative z-10 max-w-4xl mx-auto text-center">
364
+ <motion.div initial={{ opacity: 0, y: 20, scale: 0.9 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: 0.6, delay: 0.1, ease: [0.25, 0.4, 0.25, 1] }} className="mb-8">
365
+ <span className={cn('inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-medium tracking-wide uppercase', 'bg-foreground/[0.06] backdrop-blur-sm border border-foreground/[0.1]', 'text-muted-foreground')}>
366
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
367
+ Now Available
368
+ </span>
369
+ </motion.div>
370
+
371
+ <h1 className={cn('text-4xl sm:text-5xl md:text-7xl font-bold leading-[1.08] tracking-[-0.02em]', typewriterDone ? 'bg-gradient-to-r from-foreground via-foreground/80 via-50% to-foreground bg-clip-text text-transparent landing-shimmer-text' : 'text-foreground')}>
372
+ <Typewriter text={headline} cursorClassName="bg-foreground" baseSpeed={70} variance={0.35} startDelay={400} onComplete={() => setTypewriterDone(true)} />
373
+ </h1>
374
+
375
+ <motion.p initial={{ opacity: 0, y: 20 }} animate={typewriterDone ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} transition={{ duration: 0.8, ease: [0.25, 0.4, 0.25, 1] }} className="mt-6 text-lg md:text-xl max-w-2xl mx-auto leading-relaxed text-muted-foreground">
376
+ {HERO_SUBHEADLINE}
377
+ </motion.p>
378
+
379
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={typewriterDone ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} transition={{ duration: 0.7, delay: 0.15, ease: [0.25, 0.4, 0.25, 1] }} className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
380
+ <Button size="lg" onClick={() => { markLandingSeen(); navigate('/home') }} className="min-w-[180px] group">
381
+ Get Started
382
+ <ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
383
+ </Button>
384
+ <button
385
+ onClick={() => {
386
+ const target = document.getElementById('features')
387
+ const container = document.querySelector('.scrollable-content-layer') as HTMLElement | null
388
+ if (target && container) {
389
+ const targetTop = target.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop
390
+ container.scrollTo({ top: targetTop, behavior: 'smooth' })
391
+ }
392
+ }}
393
+ className={cn('inline-flex items-center gap-2 min-w-[180px] justify-center', 'px-6 py-3 rounded-md text-sm font-medium', 'border border-foreground/20 text-foreground bg-foreground/[0.06] hover:bg-foreground/[0.12]', 'transition-all duration-200')}
394
+ >
395
+ <Play className="w-4 h-4" />
396
+ Learn More
397
+ </button>
398
+ </motion.div>
399
+ </div>
400
+
401
+ <motion.div initial={{ opacity: 0 }} animate={typewriterDone ? { opacity: 1 } : { opacity: 0 }} transition={{ delay: 1.2, duration: 0.8 }} className="absolute bottom-8 left-1/2 -translate-x-1/2">
402
+ <motion.div animate={{ y: [0, 8, 0] }} transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}>
403
+ <ChevronDown className="w-5 h-5 text-muted-foreground/50" />
404
+ </motion.div>
405
+ </motion.div>
406
+ </div>
407
+
408
+ <motion.div initial={{ opacity: 0, y: 60 }} animate={typewriterDone ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }} transition={{ duration: 1, delay: 0.3, ease: [0.25, 0.4, 0.25, 1] }} style={{ y: mockupY }} className="relative z-10 w-full max-w-4xl mx-auto px-6 pb-32">
409
+ <BrowserMockup label="App Screenshot" src={HERO_IMAGE || undefined} glowBorder />
410
+ <div className="absolute -bottom-8 left-1/2 -translate-x-1/2 w-3/4 h-24 bg-primary/20 blur-3xl rounded-full" />
411
+ </motion.div>
412
+ </section>
413
+ )
414
+ }
415
+
416
+ // ============================================================================
417
+ // Showcase
418
+ // ============================================================================
419
+
420
+ function ShowcaseSection() {
421
+ return (
422
+ <section id="features" className="relative py-28 md:py-36">
423
+ <div className="max-w-6xl mx-auto px-6 space-y-24 md:space-y-32">
424
+ {SHOWCASE_ITEMS.map((item, idx) => {
425
+ const isReversed = idx % 2 !== 0
426
+ return (
427
+ <div key={item.title} className={cn('flex flex-col gap-10 md:gap-16 items-center', isReversed ? 'md:flex-row-reverse' : 'md:flex-row')}>
428
+ <ScrollReveal className="flex-1 w-full" direction={isReversed ? 'right' : 'left'} delay={0.1}>
429
+ <BrowserMockup label={item.label} src={item.image || undefined} />
430
+ </ScrollReveal>
431
+ <ScrollReveal className="flex-1 w-full md:max-w-md" direction={isReversed ? 'left' : 'right'} delay={0.2}>
432
+ <span className="inline-block text-xs font-semibold tracking-[0.2em] uppercase text-primary/80 mb-3">{item.label}</span>
433
+ <h3 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground leading-tight tracking-[-0.01em]">{item.title}</h3>
434
+ <p className="mt-4 text-muted-foreground text-base md:text-lg leading-relaxed">{item.description}</p>
435
+ <button className="mt-6 inline-flex items-center gap-2 text-sm font-medium text-primary hover:text-primary/80 transition-colors group">
436
+ Learn more
437
+ <ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
438
+ </button>
439
+ </ScrollReveal>
440
+ </div>
441
+ )
442
+ })}
443
+ </div>
444
+ </section>
445
+ )
446
+ }
447
+
448
+ // ============================================================================
449
+ // FAQ
450
+ // ============================================================================
451
+
452
+ function FAQItem({ item, isOpen, onToggle }: { item: typeof FAQ_ITEMS[number]; isOpen: boolean; onToggle: () => void }) {
453
+ return (
454
+ <div className="border-b border-foreground/[0.06] last:border-b-0">
455
+ <button onClick={onToggle} className="w-full flex items-center justify-between gap-4 py-5 text-left group">
456
+ <span className={cn('text-base font-medium transition-colors duration-200', isOpen ? 'text-foreground' : 'text-foreground/70 group-hover:text-foreground')}>{item.question}</span>
457
+ <div className={cn('shrink-0 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300', isOpen ? 'bg-primary/20 text-primary rotate-0' : 'bg-foreground/[0.06] text-muted-foreground group-hover:bg-foreground/[0.1]')}>
458
+ {isOpen ? <Minus className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
459
+ </div>
460
+ </button>
461
+ <AnimatePresence initial={false}>
462
+ {isOpen && (
463
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.3, ease: [0.25, 0.4, 0.25, 1] }} className="overflow-hidden">
464
+ <p className="pb-5 text-sm text-muted-foreground leading-relaxed max-w-2xl">{item.answer}</p>
465
+ </motion.div>
466
+ )}
467
+ </AnimatePresence>
468
+ </div>
469
+ )
470
+ }
471
+
472
+ function FAQSection() {
473
+ const [openIndex, setOpenIndex] = useState<number | null>(null)
474
+ const handleToggle = useCallback((index: number) => { setOpenIndex(prev => prev === index ? null : index) }, [])
475
+
476
+ return (
477
+ <section id="faq" className="relative py-28 md:py-36">
478
+ <div className="max-w-3xl mx-auto px-6">
479
+ <ScrollReveal className="text-center mb-16">
480
+ <span className="inline-block text-xs font-semibold tracking-[0.2em] uppercase text-primary/80 mb-4">FAQ</span>
481
+ <h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground leading-tight tracking-[-0.02em]">Common questions</h2>
482
+ <p className="mt-5 text-muted-foreground max-w-lg mx-auto text-lg leading-relaxed">Everything you need to know to get started.</p>
483
+ </ScrollReveal>
484
+
485
+ <ScrollReveal>
486
+ <GlassCard className="px-7 md:px-10 py-2" hoverEffect={false}>
487
+ {FAQ_ITEMS.map((item, idx) => (
488
+ <FAQItem key={item.question} item={item} isOpen={openIndex === idx} onToggle={() => handleToggle(idx)} />
489
+ ))}
490
+ </GlassCard>
491
+ </ScrollReveal>
492
+ </div>
493
+ </section>
494
+ )
495
+ }
496
+
497
+ // ============================================================================
498
+ // CTA
499
+ // ============================================================================
500
+
501
+ function CTASection() {
502
+ const navigate = useNavigate()
503
+ return (
504
+ <section className="relative py-28 md:py-36">
505
+ <div className="max-w-3xl mx-auto px-6">
506
+ <ScrollReveal>
507
+ <GlassCard className="p-10 md:p-16 text-center" hoverEffect={false}>
508
+ <div className="relative z-10">
509
+ <h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground leading-tight tracking-[-0.02em]">Ready to get started?</h2>
510
+ <p className="mt-5 text-muted-foreground text-lg max-w-md mx-auto leading-relaxed">Jump in and start exploring. No setup required.</p>
511
+ <div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
512
+ <Button size="lg" onClick={() => { markLandingSeen(); navigate('/home') }} className="min-w-[200px] group">
513
+ Launch App
514
+ <ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
515
+ </Button>
516
+ </div>
517
+ </div>
518
+ </GlassCard>
519
+ </ScrollReveal>
520
+ </div>
521
+ </section>
522
+ )
523
+ }
524
+
525
+ // ============================================================================
526
+ // Footer
527
+ // ============================================================================
528
+
529
+ function Footer() {
530
+ return (
531
+ <footer className="relative border-t border-foreground/[0.06]">
532
+ <div className="max-w-6xl mx-auto px-6 py-16">
533
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-10 md:gap-8">
534
+ <div className="col-span-2">
535
+ <span className="text-lg font-semibold text-foreground tracking-tight">{APP_NAME}</span>
536
+ <p className="mt-3 text-sm text-muted-foreground/60 leading-relaxed max-w-xs">{HERO_SUBHEADLINE}</p>
537
+ <div className="flex items-center gap-3 mt-6">
538
+ {[
539
+ { icon: Twitter, label: 'Twitter' },
540
+ { icon: Github, label: 'GitHub' },
541
+ { icon: Linkedin, label: 'LinkedIn' },
542
+ { icon: Mail, label: 'Email' },
543
+ ].map(social => (
544
+ <a key={social.label} href="#" aria-label={social.label} className={cn('w-9 h-9 rounded-lg flex items-center justify-center', 'bg-foreground/[0.04] border border-foreground/[0.08]', 'text-muted-foreground/50 hover:text-muted-foreground hover:bg-foreground/[0.08]', 'transition-all duration-200')}>
545
+ <social.icon className="w-4 h-4" />
546
+ </a>
547
+ ))}
548
+ </div>
549
+ </div>
550
+
551
+ {Object.entries(FOOTER_LINKS).map(([heading, links]) => (
552
+ <div key={heading}>
553
+ <h4 className="text-xs font-semibold tracking-[0.15em] uppercase text-muted-foreground/60 mb-4">{heading}</h4>
554
+ <ul className="space-y-2.5">
555
+ {links.map(link => (
556
+ <li key={link}><a href="#" className="text-sm text-muted-foreground/50 hover:text-muted-foreground transition-colors duration-200">{link}</a></li>
557
+ ))}
558
+ </ul>
559
+ </div>
560
+ ))}
561
+ </div>
562
+ </div>
563
+
564
+ <div className="border-t border-foreground/[0.06]">
565
+ <div className="max-w-6xl mx-auto px-6 py-5 flex flex-col sm:flex-row items-center justify-between gap-3">
566
+ <span className="text-xs text-muted-foreground/50">&copy; {new Date().getFullYear()} {APP_NAME}. All rights reserved.</span>
567
+ <div className="flex items-center gap-6">
568
+ <a href="#" className="text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors">Privacy Policy</a>
569
+ <a href="#" className="text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors">Terms of Service</a>
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ <div className="border-t border-foreground/[0.04]">
575
+ <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-center">
576
+ <a href="https://deep.space" target="_blank" rel="noopener noreferrer" className={cn('inline-flex items-center gap-2 px-3 py-1.5 rounded-full', 'bg-foreground/[0.03] border border-foreground/[0.06]', 'text-[11px] text-muted-foreground/50 hover:text-muted-foreground hover:bg-foreground/[0.06]', 'transition-all duration-200')}>
577
+ <Sparkles className="w-3 h-3" />
578
+ Built with DeepSpace
579
+ <ChevronRight className="w-3 h-3" />
580
+ </a>
581
+ </div>
582
+ </div>
583
+ </footer>
584
+ )
585
+ }
586
+
587
+ // ============================================================================
588
+ // Page Shell
589
+ // ============================================================================
590
+
591
+ export default function LandingPage() {
592
+ useLandingCSS()
593
+
594
+ const containerRef = useRef<HTMLDivElement>(null)
595
+ const [isScrolled, setIsScrolled] = useState(false)
596
+
597
+ useEffect(() => {
598
+ const el = containerRef.current
599
+ if (!el) return
600
+ const handleScroll = () => setIsScrolled(el.scrollTop > 50)
601
+ el.addEventListener('scroll', handleScroll, { passive: true })
602
+ return () => el.removeEventListener('scroll', handleScroll)
603
+ }, [])
604
+
605
+ return (
606
+ <div className="relative h-screen overflow-hidden bg-background">
607
+ {LANDING_BG_URL && (
608
+ <div className="fixed-bg-layer">
609
+ <div className="absolute inset-0 bg-cover bg-center bg-no-repeat" style={{ backgroundImage: `url(${LANDING_BG_URL})` }} />
610
+ <div className="absolute inset-0 bg-background/70" />
611
+ <div className="absolute inset-0 bg-gradient-to-b from-background/60 via-transparent to-background/90" />
612
+ </div>
613
+ )}
614
+
615
+ <div className="landing-noise-overlay" />
616
+
617
+ <div ref={containerRef} className="scrollable-content-layer">
618
+ <LandingNav isScrolled={isScrolled} scrollRoot={containerRef} />
619
+ <HeroSection />
620
+ <ShowcaseSection />
621
+ <FAQSection />
622
+ <CTASection />
623
+ <Footer />
624
+ </div>
625
+ </div>
626
+ )
627
+ }