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.
- package/dist/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- 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,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">© {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
|
+
}
|