@sansavision/create-pulse 0.4.1 → 0.4.3

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 (41) hide show
  1. package/README.md +27 -3
  2. package/dist/index.js +3 -1
  3. package/package.json +1 -1
  4. package/templates/nextjs-auth-demo/.env.example +6 -0
  5. package/templates/nextjs-auth-demo/README.md +125 -0
  6. package/templates/nextjs-auth-demo/_gitignore +33 -0
  7. package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
  8. package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
  9. package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
  10. package/templates/nextjs-auth-demo/next.config.ts +7 -0
  11. package/templates/nextjs-auth-demo/package.json +36 -0
  12. package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
  13. package/templates/nextjs-auth-demo/public/file.svg +1 -0
  14. package/templates/nextjs-auth-demo/public/globe.svg +1 -0
  15. package/templates/nextjs-auth-demo/public/next.svg +1 -0
  16. package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
  17. package/templates/nextjs-auth-demo/public/window.svg +1 -0
  18. package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
  19. package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
  20. package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
  21. package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
  22. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
  23. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
  24. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
  25. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
  26. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
  27. package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
  28. package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
  29. package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
  30. package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
  31. package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
  32. package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
  33. package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
  34. package/templates/nextjs-auth-demo/src/lib/auth.ts +14 -0
  35. package/templates/nextjs-auth-demo/src/lib/db.ts +6 -0
  36. package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
  37. package/templates/nextjs-auth-demo/src/lib/schema.ts +107 -0
  38. package/templates/nextjs-auth-demo/tsconfig.json +34 -0
  39. package/templates/react-queue-demo/README.md +6 -7
  40. package/templates/react-queue-demo/src/App.tsx +34 -13
  41. package/src/index.ts +0 -115
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { useSession } from "@/lib/auth-client";
5
+ import { connectWithAuth } from "@/lib/pulse";
6
+ import {
7
+ Video,
8
+ Play,
9
+ Pause,
10
+ SkipForward,
11
+ Users,
12
+ Shield,
13
+ Wifi,
14
+ WifiOff,
15
+ Loader2,
16
+ } from "lucide-react";
17
+ import type { PulseConnection } from "@sansavision/pulse-sdk";
18
+
19
+ export default function WatchTogetherPage() {
20
+ const { data: session } = useSession();
21
+ const [connected, setConnected] = useState(false);
22
+ const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
23
+ const [connecting, setConnecting] = useState(true);
24
+ const [playing, setPlaying] = useState(false);
25
+ const [currentTime, setCurrentTime] = useState(0);
26
+ const [viewers, setViewers] = useState<string[]>([]);
27
+ const [syncEvents, setSyncEvents] = useState<string[]>([]);
28
+ const connRef = useRef<PulseConnection | null>(null);
29
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
30
+
31
+ useEffect(() => {
32
+ if (!session) return;
33
+ let cancelled = false;
34
+
35
+ async function init() {
36
+ try {
37
+ const connection = await connectWithAuth();
38
+ if (cancelled) return;
39
+
40
+ connRef.current = connection;
41
+ setConnected(true);
42
+ setConnecting(false);
43
+
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ const user = (connection as any).user;
46
+ if (user) setAuthUser(user);
47
+
48
+ const stream = connection.stream("watch-room");
49
+
50
+ stream.send(
51
+ JSON.stringify({ type: "viewer-join", name: session!.user.name })
52
+ );
53
+
54
+ stream.on("data", (data: Uint8Array) => {
55
+ try {
56
+ const msg = JSON.parse(new TextDecoder().decode(data));
57
+ if (msg.type === "play") {
58
+ setPlaying(true);
59
+ setSyncEvents((prev) => [
60
+ `${msg.user} pressed play at ${msg.time?.toFixed(1)}s`,
61
+ ...prev.slice(0, 9),
62
+ ]);
63
+ } else if (msg.type === "pause") {
64
+ setPlaying(false);
65
+ setSyncEvents((prev) => [
66
+ `${msg.user} paused at ${msg.time?.toFixed(1)}s`,
67
+ ...prev.slice(0, 9),
68
+ ]);
69
+ } else if (msg.type === "seek") {
70
+ setCurrentTime(msg.time || 0);
71
+ setSyncEvents((prev) => [
72
+ `${msg.user} seeked to ${msg.time?.toFixed(1)}s`,
73
+ ...prev.slice(0, 9),
74
+ ]);
75
+ } else if (msg.type === "viewer-join") {
76
+ setViewers((prev) =>
77
+ prev.includes(msg.name) ? prev : [...prev, msg.name]
78
+ );
79
+ setSyncEvents((prev) => [
80
+ `${msg.name} joined the watch party`,
81
+ ...prev.slice(0, 9),
82
+ ]);
83
+ }
84
+ } catch {
85
+ /* ignore */
86
+ }
87
+ });
88
+
89
+ connection.on("disconnect", () => setConnected(false));
90
+ connection.on("reconnected", () => setConnected(true));
91
+ } catch (err) {
92
+ console.error("Failed to connect:", err);
93
+ setConnecting(false);
94
+ }
95
+ }
96
+
97
+ init();
98
+ return () => {
99
+ cancelled = true;
100
+ connRef.current?.disconnect();
101
+ if (intervalRef.current) clearInterval(intervalRef.current);
102
+ };
103
+ }, [session]);
104
+
105
+ useEffect(() => {
106
+ if (playing) {
107
+ intervalRef.current = setInterval(
108
+ () => setCurrentTime((t) => t + 0.1),
109
+ 100
110
+ );
111
+ } else {
112
+ if (intervalRef.current) clearInterval(intervalRef.current);
113
+ }
114
+ return () => {
115
+ if (intervalRef.current) clearInterval(intervalRef.current);
116
+ };
117
+ }, [playing]);
118
+
119
+ function sendAction(type: string) {
120
+ if (!connRef.current) return;
121
+ const stream = connRef.current.stream("watch-room");
122
+ stream.send(
123
+ JSON.stringify({
124
+ type,
125
+ user: session?.user?.name,
126
+ time: currentTime,
127
+ })
128
+ );
129
+ }
130
+
131
+ function handlePlayPause() {
132
+ const action = playing ? "pause" : "play";
133
+ sendAction(action);
134
+ setPlaying(!playing);
135
+ }
136
+
137
+ function handleSeek(seconds: number) {
138
+ setCurrentTime((prev) => Math.max(0, prev + seconds));
139
+ sendAction("seek");
140
+ }
141
+
142
+ const formatTime = (t: number) => {
143
+ const mins = Math.floor(t / 60);
144
+ const secs = Math.floor(t % 60);
145
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
146
+ };
147
+
148
+ return (
149
+ <div className="p-8">
150
+ <div className="flex items-center justify-between mb-8">
151
+ <div className="flex items-center gap-3">
152
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-pink-500 to-rose-600 flex items-center justify-center">
153
+ <Video className="w-6 h-6 text-white" />
154
+ </div>
155
+ <div>
156
+ <h1 className="text-2xl font-bold">Watch Together</h1>
157
+ <p className="text-sm text-slate-500">
158
+ Synchronized video playback across users
159
+ </p>
160
+ </div>
161
+ </div>
162
+ <div className="flex items-center gap-3">
163
+ {authUser && (
164
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/10 border border-green-500/20">
165
+ <Shield className="w-3.5 h-3.5 text-green-400" />
166
+ <span className="text-xs text-green-400">
167
+ {authUser.claims.name || authUser.id}
168
+ </span>
169
+ </div>
170
+ )}
171
+ {connected ? (
172
+ <div className="flex items-center gap-2">
173
+ <Wifi className="w-4 h-4 text-green-400" />
174
+ <span className="text-xs text-green-400">Connected</span>
175
+ </div>
176
+ ) : (
177
+ <div className="flex items-center gap-2">
178
+ <WifiOff className="w-4 h-4 text-red-400" />
179
+ <span className="text-xs text-red-400">Disconnected</span>
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+
185
+ {connecting ? (
186
+ <div className="flex items-center justify-center py-20">
187
+ <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" />
188
+ <span className="text-slate-400">Connecting...</span>
189
+ </div>
190
+ ) : (
191
+ <div className="grid lg:grid-cols-3 gap-8">
192
+ <div className="lg:col-span-2">
193
+ <div className="glass rounded-2xl overflow-hidden">
194
+ <div className="aspect-video bg-gradient-to-br from-slate-900 to-slate-800 flex items-center justify-center relative">
195
+ <div className="text-center">
196
+ <Video className="w-16 h-16 text-slate-600 mx-auto mb-4" />
197
+ <p className="text-slate-500 text-sm">Simulated Video Player</p>
198
+ <p className="text-3xl font-mono text-white mt-2">{formatTime(currentTime)}</p>
199
+ </div>
200
+ <div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-700">
201
+ <div
202
+ className="h-full bg-gradient-to-r from-pink-500 to-rose-500 transition-all"
203
+ style={{ width: `${Math.min((currentTime / 300) * 100, 100)}%` }}
204
+ />
205
+ </div>
206
+ </div>
207
+
208
+ <div className="p-4 flex items-center gap-4">
209
+ <button onClick={handlePlayPause} disabled={!connected}
210
+ className="w-10 h-10 rounded-full bg-white text-black flex items-center justify-center hover:scale-105 transition-transform disabled:opacity-50">
211
+ {playing ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5 ml-0.5" />}
212
+ </button>
213
+ <button onClick={() => handleSeek(10)} disabled={!connected}
214
+ className="p-2 rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50">
215
+ <SkipForward className="w-4 h-4" />
216
+ </button>
217
+ <span className="text-sm text-slate-400 font-mono">{formatTime(currentTime)} / 5:00</span>
218
+ <div className="flex-1" />
219
+ <div className="flex items-center gap-2">
220
+ <Users className="w-4 h-4 text-slate-400" />
221
+ <span className="text-sm text-slate-400">{Math.max(viewers.length, 1)} watching</span>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <div className="space-y-6">
228
+ <div className="glass rounded-2xl p-6">
229
+ <h3 className="text-sm font-semibold mb-3 text-pink-400">Viewers</h3>
230
+ <div className="space-y-2">
231
+ <div className="flex items-center gap-2">
232
+ <div className="w-6 h-6 rounded-full bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center text-[10px] font-bold">
233
+ {session?.user?.name?.charAt(0) || "?"}
234
+ </div>
235
+ <span className="text-sm">{session?.user?.name} (you)</span>
236
+ </div>
237
+ {viewers.filter((v) => v !== session?.user?.name).map((v) => (
238
+ <div key={v} className="flex items-center gap-2">
239
+ <div className="w-6 h-6 rounded-full bg-gradient-to-br from-pink-500 to-rose-500 flex items-center justify-center text-[10px] font-bold">{v.charAt(0)}</div>
240
+ <span className="text-sm">{v}</span>
241
+ </div>
242
+ ))}
243
+ </div>
244
+ </div>
245
+
246
+ <div className="glass rounded-2xl p-6">
247
+ <h3 className="text-sm font-semibold mb-3 text-cyan-400">Sync Events</h3>
248
+ <div className="space-y-2 max-h-60 overflow-y-auto">
249
+ {syncEvents.length === 0 && <p className="text-xs text-slate-500">No sync events yet. Press play!</p>}
250
+ {syncEvents.map((event, i) => <div key={i} className="text-xs text-slate-400">{event}</div>)}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ )}
256
+ </div>
257
+ );
258
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import Link from "next/link";
5
+ import { useSession, signOut } from "@/lib/auth-client";
6
+ import {
7
+ Zap,
8
+ LayoutDashboard,
9
+ MessageSquare,
10
+ Video,
11
+ Database,
12
+ Gamepad2,
13
+ Lock,
14
+ LogOut,
15
+ Loader2,
16
+ ChevronRight,
17
+ } from "lucide-react";
18
+ import { useEffect } from "react";
19
+
20
+ const navItems = [
21
+ { href: "/dashboard", label: "Overview", icon: LayoutDashboard },
22
+ { href: "/dashboard/demos/chat", label: "Real-time Chat", icon: MessageSquare },
23
+ { href: "/dashboard/demos/watch-together", label: "Watch Together", icon: Video },
24
+ { href: "/dashboard/demos/queues", label: "Durable Queues", icon: Database },
25
+ { href: "/dashboard/demos/game-sync", label: "Game Sync", icon: Gamepad2 },
26
+ { href: "/dashboard/demos/encrypted-chat", label: "E2E Encrypted", icon: Lock },
27
+ ];
28
+
29
+ export default function DashboardLayout({
30
+ children,
31
+ }: {
32
+ children: React.ReactNode;
33
+ }) {
34
+ const { data: session, isPending } = useSession();
35
+ const router = useRouter();
36
+
37
+ useEffect(() => {
38
+ if (!isPending && !session) {
39
+ router.push("/auth/sign-in");
40
+ }
41
+ }, [session, isPending, router]);
42
+
43
+ if (isPending) {
44
+ return (
45
+ <div className="min-h-screen flex items-center justify-center">
46
+ <Loader2 className="w-8 h-8 animate-spin text-purple-500" />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ if (!session) return null;
52
+
53
+ return (
54
+ <div className="min-h-screen flex">
55
+ {/* Sidebar */}
56
+ <aside className="w-64 border-r border-slate-800 flex flex-col">
57
+ <div className="p-5 border-b border-slate-800">
58
+ <Link href="/" className="flex items-center gap-2">
59
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
60
+ <Zap className="w-5 h-5 text-white" />
61
+ </div>
62
+ <span className="text-lg font-bold">Pulse</span>
63
+ </Link>
64
+ </div>
65
+
66
+ <nav className="flex-1 p-3 space-y-1">
67
+ {navItems.map((item) => (
68
+ <Link
69
+ key={item.href}
70
+ href={item.href}
71
+ className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-slate-400 hover:text-white hover:bg-slate-800/50 transition-all group"
72
+ >
73
+ <item.icon className="w-4 h-4" />
74
+ <span className="flex-1">{item.label}</span>
75
+ <ChevronRight className="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity" />
76
+ </Link>
77
+ ))}
78
+ </nav>
79
+
80
+ {/* User info */}
81
+ <div className="p-4 border-t border-slate-800">
82
+ <div className="flex items-center gap-3 mb-3">
83
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center text-xs font-bold">
84
+ {session.user.name?.charAt(0)?.toUpperCase() || "?"}
85
+ </div>
86
+ <div className="flex-1 min-w-0">
87
+ <div className="text-sm font-medium truncate">
88
+ {session.user.name}
89
+ </div>
90
+ <div className="text-xs text-slate-500 truncate">
91
+ {session.user.email}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <button
96
+ onClick={() => signOut().then(() => router.push("/"))}
97
+ className="flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm text-slate-400 hover:text-red-400 hover:bg-red-500/10 transition-all"
98
+ >
99
+ <LogOut className="w-4 h-4" />
100
+ Sign out
101
+ </button>
102
+ </div>
103
+ </aside>
104
+
105
+ {/* Main content */}
106
+ <main className="flex-1 overflow-y-auto">{children}</main>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useSession } from "@/lib/auth-client";
5
+ import {
6
+ MessageSquare,
7
+ Video,
8
+ Database,
9
+ Gamepad2,
10
+ Lock,
11
+ ArrowRight,
12
+ Shield,
13
+ Wifi,
14
+ Activity,
15
+ } from "lucide-react";
16
+
17
+ const demos = [
18
+ {
19
+ href: "/dashboard/demos/chat",
20
+ icon: MessageSquare,
21
+ title: "Real-time Chat",
22
+ desc: "Multi-user rooms with presence, typing indicators, and auth identity. Open a second tab with a different user to see live messaging.",
23
+ color: "from-blue-500 to-blue-600",
24
+ badge: "Pub/Sub",
25
+ },
26
+ {
27
+ href: "/dashboard/demos/watch-together",
28
+ icon: Video,
29
+ title: "Watch Together",
30
+ desc: "Synchronized video playback. Play, pause, and seek in perfect sync across all connected users.",
31
+ color: "from-pink-500 to-rose-600",
32
+ badge: "Broadcast",
33
+ },
34
+ {
35
+ href: "/dashboard/demos/queues",
36
+ icon: Database,
37
+ title: "Durable Queues",
38
+ desc: "Publish messages, simulate going offline, come back online — messages persist and are delivered. Full ACK/NACK flow.",
39
+ color: "from-amber-500 to-orange-600",
40
+ badge: "Queue",
41
+ },
42
+ {
43
+ href: "/dashboard/demos/game-sync",
44
+ icon: Gamepad2,
45
+ title: "Game State Sync",
46
+ desc: "Shared game board with real-time state sync. Multiple users can interact with the same game state simultaneously.",
47
+ color: "from-green-500 to-emerald-600",
48
+ badge: "State",
49
+ },
50
+ {
51
+ href: "/dashboard/demos/encrypted-chat",
52
+ icon: Lock,
53
+ title: "E2E Encrypted Chat",
54
+ desc: "End-to-end encrypted messaging where the relay cannot read content. Key exchange visualization included.",
55
+ color: "from-purple-500 to-violet-600",
56
+ badge: "P2P",
57
+ },
58
+ ];
59
+
60
+ export default function DashboardPage() {
61
+ const { data: session } = useSession();
62
+
63
+ return (
64
+ <div className="p-8">
65
+ {/* Header */}
66
+ <div className="mb-8">
67
+ <h1 className="text-3xl font-bold mb-2">
68
+ Welcome, {session?.user?.name?.split(" ")[0] || "there"} 👋
69
+ </h1>
70
+ <p className="text-slate-400 text-lg">
71
+ Choose a demo to explore Pulse&apos;s capabilities with authenticated
72
+ connections.
73
+ </p>
74
+ </div>
75
+
76
+ {/* Connection status */}
77
+ <div className="glass rounded-xl p-5 mb-8 flex items-center gap-6">
78
+ <div className="flex items-center gap-2">
79
+ <Shield className="w-4 h-4 text-green-400" />
80
+ <span className="text-sm text-green-400 font-medium">
81
+ Authenticated
82
+ </span>
83
+ </div>
84
+ <div className="flex items-center gap-2">
85
+ <Wifi className="w-4 h-4 text-cyan-400" />
86
+ <span className="text-sm text-slate-400">
87
+ Relay:{" "}
88
+ <code className="text-cyan-400 bg-cyan-500/10 px-1.5 py-0.5 rounded text-xs">
89
+ {process.env.NEXT_PUBLIC_PULSE_URL || "ws://localhost:4001"}
90
+ </code>
91
+ </span>
92
+ </div>
93
+ <div className="flex items-center gap-2">
94
+ <Activity className="w-4 h-4 text-purple-400" />
95
+ <span className="text-sm text-slate-400">
96
+ Auth Mode:{" "}
97
+ <code className="text-purple-400 bg-purple-500/10 px-1.5 py-0.5 rounded text-xs">
98
+ webhook
99
+ </code>
100
+ </span>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Tip */}
105
+ <div className="glass rounded-xl p-5 mb-8 border-l-4 border-purple-500">
106
+ <h3 className="text-sm font-semibold text-purple-400 mb-1">
107
+ 💡 Multi-User Tip
108
+ </h3>
109
+ <p className="text-sm text-slate-400">
110
+ Open a second browser tab (or incognito window), create a different
111
+ account, and interact simultaneously. Watch messages, video sync, and
112
+ game state flow in real-time between users.
113
+ </p>
114
+ </div>
115
+
116
+ {/* Demo cards */}
117
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
118
+ {demos.map((demo) => (
119
+ <Link key={demo.href} href={demo.href}>
120
+ <div className="demo-card glass rounded-2xl p-6 h-full cursor-pointer group">
121
+ <div className="flex items-start justify-between mb-4">
122
+ <div
123
+ className={`w-12 h-12 rounded-xl bg-gradient-to-br ${demo.color} flex items-center justify-center`}
124
+ >
125
+ <demo.icon className="w-6 h-6 text-white" />
126
+ </div>
127
+ <span className="text-xs font-mono px-2 py-1 rounded-md bg-slate-800 text-slate-400 border border-slate-700">
128
+ {demo.badge}
129
+ </span>
130
+ </div>
131
+ <h3 className="text-lg font-semibold mb-2 group-hover:text-purple-400 transition-colors">
132
+ {demo.title}
133
+ </h3>
134
+ <p className="text-sm text-slate-400 leading-relaxed mb-4">
135
+ {demo.desc}
136
+ </p>
137
+ <div className="flex items-center gap-1 text-sm text-purple-400 font-medium">
138
+ Open Demo
139
+ <ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
140
+ </div>
141
+ </div>
142
+ </Link>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,96 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --color-primary: #7c3aed;
5
+ --color-primary-light: #a78bfa;
6
+ --color-accent: #06b6d4;
7
+ --color-surface: #0f172a;
8
+ --color-surface-elevated: #1e293b;
9
+ --color-surface-hover: #334155;
10
+ --color-text: #f1f5f9;
11
+ --color-text-muted: #94a3b8;
12
+ --color-border: #334155;
13
+ --color-danger: #ef4444;
14
+ --color-success: #10b981;
15
+ --color-warning: #f59e0b;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: "Inter", "SF Pro Display", -apple-system, system-ui, sans-serif;
24
+ background: var(--color-surface);
25
+ color: var(--color-text);
26
+ -webkit-font-smoothing: antialiased;
27
+ -moz-osx-font-smoothing: grayscale;
28
+ }
29
+
30
+ /* Smooth scrollbar */
31
+ ::-webkit-scrollbar {
32
+ width: 6px;
33
+ }
34
+ ::-webkit-scrollbar-track {
35
+ background: transparent;
36
+ }
37
+ ::-webkit-scrollbar-thumb {
38
+ background: var(--color-border);
39
+ border-radius: 3px;
40
+ }
41
+
42
+ /* Glassmorphism utility */
43
+ .glass {
44
+ background: rgba(30, 41, 59, 0.7);
45
+ backdrop-filter: blur(16px);
46
+ -webkit-backdrop-filter: blur(16px);
47
+ border: 1px solid rgba(148, 163, 184, 0.1);
48
+ }
49
+
50
+ /* Gradient text */
51
+ .gradient-text {
52
+ background: linear-gradient(135deg, #7c3aed, #06b6d4, #10b981);
53
+ -webkit-background-clip: text;
54
+ -webkit-text-fill-color: transparent;
55
+ background-clip: text;
56
+ }
57
+
58
+ /* Pulse animation */
59
+ @keyframes pulse-glow {
60
+ 0%, 100% { box-shadow: 0 0 20px rgba(124, 58, 237, 0.3); }
61
+ 50% { box-shadow: 0 0 40px rgba(124, 58, 237, 0.6); }
62
+ }
63
+
64
+ .pulse-glow {
65
+ animation: pulse-glow 2s ease-in-out infinite;
66
+ }
67
+
68
+ /* Status indicator */
69
+ @keyframes status-blink {
70
+ 0%, 100% { opacity: 1; }
71
+ 50% { opacity: 0.5; }
72
+ }
73
+
74
+ .status-online {
75
+ width: 8px;
76
+ height: 8px;
77
+ border-radius: 50%;
78
+ background: var(--color-success);
79
+ animation: status-blink 2s ease-in-out infinite;
80
+ }
81
+
82
+ .status-offline {
83
+ width: 8px;
84
+ height: 8px;
85
+ border-radius: 50%;
86
+ background: var(--color-danger);
87
+ }
88
+
89
+ /* Card hover effect */
90
+ .demo-card {
91
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
92
+ }
93
+ .demo-card:hover {
94
+ transform: translateY(-4px);
95
+ box-shadow: 0 20px 60px rgba(124, 58, 237, 0.15);
96
+ }
@@ -0,0 +1,27 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Pulse — Real-Time Protocol Demo",
6
+ description:
7
+ "Investor-ready demo showcasing PLP (Pulse Line Protocol) with auth, real-time chat, synchronized video, durable queues, and encrypted messaging.",
8
+ keywords: ["pulse", "real-time", "websocket", "protocol", "auth"],
9
+ };
10
+
11
+ export default function RootLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <html lang="en" className="dark">
18
+ <head>
19
+ <link
20
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
21
+ rel="stylesheet"
22
+ />
23
+ </head>
24
+ <body className="min-h-screen antialiased">{children}</body>
25
+ </html>
26
+ );
27
+ }