diff --git a/apps/desktop/app/globals.css b/apps/desktop/app/globals.css index 79ae690c..c0edc111 100644 --- a/apps/desktop/app/globals.css +++ b/apps/desktop/app/globals.css @@ -84,3 +84,509 @@ body { user-select: none; -webkit-user-select: none; } + +.desktop-login-page { + display: grid; + height: 100dvh; + min-height: 100dvh; + overflow: hidden; + grid-template-columns: 1fr; +} + +@media (min-width: 1024px) { + .desktop-login-page { + grid-template-columns: minmax(0, 65fr) minmax(360px, 35fr); + } +} + +.desktop-login-tools-panel { + position: relative; + display: none; + min-height: 0; + overflow: hidden; + border-right: 1px solid rgb(255 255 255 / 0.06); +} + +@media (min-width: 1024px) { + .desktop-login-tools-panel { + display: block; + } +} + +.desktop-login-panel-orb { + position: absolute; + inset: 0; + background: + radial-gradient( + ellipse 130% 100% at 50% 45%, + rgba(70, 155, 255, 0.24) 0%, + rgba(35, 100, 210, 0.14) 38%, + rgba(8, 22, 45, 0.55) 68%, + #030912 100% + ), + radial-gradient( + ellipse 90% 70% at 50% 100%, + rgba(50, 130, 240, 0.16) 0%, + transparent 65% + ); +} + +.desktop-login-panel-glow { + position: absolute; + inset: -10%; + background: + radial-gradient( + circle at 50% 52%, + rgba(85, 150, 255, 0.24), + transparent 24% + ), + radial-gradient( + circle at 48% 52%, + rgba(255, 255, 255, 0.18), + transparent 8% + ), + radial-gradient( + ellipse 60% 38% at 52% 42%, + rgba(40, 96, 255, 0.22), + transparent 74% + ); + opacity: 0.85; +} + +.desktop-login-panel-vignette { + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgb(7 16 36 / 0.2), + transparent 42%, + rgb(3 9 18 / 0.36) + ); +} + +.desktop-login-tools-network { + position: relative; + z-index: 1; + height: 100%; + min-height: 360px; +} + +.desktop-login-panel-caption { + position: absolute; + left: 50%; + bottom: 2rem; + z-index: 2; + margin: 0; + white-space: nowrap; + transform: translateX(-50%); + color: rgb(255 255 255 / 0.45); + font-size: 0.875rem; + text-align: center; +} + +.desktop-login-tool-node-wrap { + position: absolute; + z-index: 2; + transform: translate(-50%, -50%); +} + +.desktop-login-tool-node { + display: flex; + width: 2.875rem; + height: 2.875rem; + align-items: center; + justify-content: center; + border: 1px solid rgb(255 255 255 / 0.12); + border-radius: 0.8125rem; + background: rgb(12 22 40 / 0.82); + box-shadow: + 0 1px 0 rgb(255 255 255 / 0.1) inset, + 0 6px 20px rgb(0 0 0 / 0.22); + backdrop-filter: blur(12px); + transition: + border-color 0.35s ease, + background 0.35s ease, + box-shadow 0.35s ease, + transform 0.35s cubic-bezier(0.34, 1.2, 0.64, 1); +} + +.desktop-login-tool-node-icon { + width: 1.375rem; + height: 1.375rem; + flex-shrink: 0; + font-weight: 700; + font-size: 0.625rem; + line-height: 1.375rem; + text-align: center; + opacity: 0.88; + transition: opacity 0.35s ease; +} + +.desktop-login-tool-node-source { + border-color: rgb(100 180 255 / 0.55); + background: rgb(14 28 50 / 0.92); + box-shadow: + 0 0 0 1px rgb(100 180 255 / 0.25), + 0 0 24px rgb(75 160 250 / 0.28), + 0 1px 0 rgb(255 255 255 / 0.1) inset; + transform: scale(1.05); +} + +.desktop-login-tool-node-destination { + border-color: rgb(130 210 255 / 0.65); + background: rgb(16 32 58 / 0.95); + box-shadow: + 0 0 0 1px rgb(130 210 255 / 0.3), + 0 0 32px rgb(100 180 255 / 0.35), + 0 1px 0 rgb(255 255 255 / 0.12) inset; + transform: scale(1.08); +} + +.desktop-login-tool-node-source .desktop-login-tool-node-icon, +.desktop-login-tool-node-destination .desktop-login-tool-node-icon { + opacity: 1; +} + +.desktop-login-tool-node-label { + position: absolute; + top: calc(100% + 0.4rem); + left: 50%; + max-width: 5.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transform: translateX(-50%); + color: rgb(200 225 255 / 0.95); + font-weight: 500; + font-size: 0.6875rem; + text-shadow: 0 1px 8px rgb(0 0 0 / 0.65); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; +} + +.desktop-login-tool-node-label-visible { + opacity: 1; +} + +.desktop-login-active-path { + stroke-dasharray: 1; + stroke-dashoffset: 1; + animation: desktop-login-draw-path 1.1s cubic-bezier(0.33, 0, 0.2, 1) forwards; +} + +.desktop-login-memory-chip-motion { + position: absolute; + z-index: 2; + transform: translate(-50%, -50%); +} + +.desktop-login-chip-capture { + animation: desktop-login-chip-capture 1.1s cubic-bezier(0.35, 0, 0.15, 1) + forwards; +} + +.desktop-login-chip-hold { + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(1.05); +} + +.desktop-login-chip-recall { + animation: desktop-login-chip-recall 1.1s cubic-bezier(0.35, 0, 0.15, 1) + forwards; +} + +.desktop-login-memory-chip { + display: flex; + width: 2.75rem; + height: 1.625rem; + align-items: center; + justify-content: center; + border: 1px solid rgb(180 220 255 / 0.35); + border-radius: 0.375rem; + background: rgb(12 24 44 / 0.95); + box-shadow: + 0 0 16px rgb(100 180 255 / 0.4), + 0 1px 0 rgb(255 255 255 / 0.12) inset; +} + +.desktop-login-memory-chip-lines { + display: flex; + width: 1.5rem; + flex-direction: column; + gap: 0.2rem; +} + +.desktop-login-memory-chip-lines span { + display: block; + height: 0.125rem; + border-radius: 9999px; + background: rgb(180 220 255 / 0.55); +} + +.desktop-login-memory-chip-lines span:nth-child(2) { + width: 78%; + opacity: 0.75; +} + +.desktop-login-memory-chip-lines span:nth-child(3) { + width: 52%; + opacity: 0.5; +} + +.desktop-login-orb-wrap { + position: absolute; + top: 50%; + left: 50%; + z-index: 3; + transform: translate(-50%, -50%); +} + +.desktop-login-nova-orb { + position: relative; + display: flex; + width: 9rem; + height: 9rem; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 9999px; + box-shadow: + 0.3rem 0.6rem 1.2rem #0a0e14 inset, + 0 0 54px rgb(65 135 255 / 0.58); + filter: blur(1px); + transition: transform 0.35s ease; +} + +.desktop-login-nova-orb-active { + animation: desktop-login-orb-pulse 0.55s ease-out; +} + +.desktop-login-nova-gradient { + position: absolute; + top: -32%; + left: -58%; + width: 16rem; + height: 16rem; + background: + radial-gradient( + circle at 28% 28%, + rgba(255, 255, 255, 0.95), + transparent 12% + ), + conic-gradient(from 30deg, #7ce7ff, #295fff, #7c5cff, #22d3ee, #7ce7ff); + animation: desktop-login-orb-rotate 12s linear infinite; +} + +.desktop-login-auth-pane { + display: flex; + min-height: 0; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; +} + +.desktop-login-auth-header { + height: 5.375rem; + padding-inline: 1.5rem; +} + +.desktop-login-memory-api-button { + height: 2.75rem; + padding-inline: 1rem; + font-size: 1rem; +} + +.desktop-login-auth-content { + display: flex; + min-height: 0; + flex: 1; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem 2.5rem; +} + +.desktop-login-auth-stack { + width: 100%; + max-width: 24.375rem; +} + +.desktop-login-headline { + margin-bottom: 2rem; +} + +.desktop-login-headline h1 { + font-size: 1.75rem; +} + +.desktop-login-subtitle { + margin-top: 1rem; + font-size: 0.875rem; +} + +.desktop-login-card { + border-radius: 1.375rem; + padding: 2rem; +} + +.desktop-login-form { + gap: 0.75rem; +} + +.desktop-login-email-form { + gap: 1.5rem; +} + +.desktop-login-email-input, +.desktop-login-primary-button { + height: 3.5rem; +} + +.desktop-login-terms { + font-size: 0.875rem; +} + +@media (max-width: 1023px) { + .desktop-login-page { + height: 100dvh; + overflow-y: auto; + } + + .desktop-login-auth-pane { + min-height: 100dvh; + } + + .desktop-login-auth-content { + align-items: center; + padding-inline: 1rem; + } + + .desktop-login-auth-stack { + max-width: 25rem; + } +} + +@media (max-width: 760px) { + .desktop-login-auth-header { + height: 4.5rem; + padding-inline: 1rem; + } + + .desktop-login-memory-api-button { + height: 2.5rem; + padding-inline: 0.75rem; + font-size: 0.875rem; + } + + .desktop-login-headline h1 { + font-size: 1.5rem; + } + + .desktop-login-card { + padding: 1.5rem; + } +} + +@media (max-height: 620px) { + .desktop-login-auth-header { + height: 3.75rem; + } + + .desktop-login-auth-content { + align-items: flex-start; + padding-block: 0.5rem 1rem; + } + + .desktop-login-headline { + margin-bottom: 0.75rem; + } + + .desktop-login-headline h1 { + font-size: 1.375rem; + } + + .desktop-login-subtitle { + display: none; + } + + .desktop-login-card { + padding: 1.125rem 1.25rem; + border-radius: 1rem; + } + + .desktop-login-form { + gap: 0.625rem; + } + + .desktop-login-email-form { + gap: 0.75rem; + } + + .desktop-login-email-input, + .desktop-login-primary-button { + height: 2.75rem; + } + + .desktop-login-terms { + font-size: 0.6875rem; + } + + .desktop-login-panel-caption { + bottom: 1rem; + } +} + +@media (max-width: 1180px) and (min-width: 1024px) { + .desktop-login-page { + grid-template-columns: minmax(0, 58fr) minmax(360px, 42fr); + } + + .desktop-login-panel-caption { + font-size: 0.75rem; + } +} + +@keyframes desktop-login-draw-path { + to { + stroke-dashoffset: 0; + } +} + +@keyframes desktop-login-chip-capture { + from { + left: var(--from-x); + top: var(--from-y); + } + to { + left: 50%; + top: 50%; + } +} + +@keyframes desktop-login-chip-recall { + from { + left: 50%; + top: 50%; + } + to { + left: var(--to-x); + top: var(--to-y); + } +} + +@keyframes desktop-login-orb-rotate { + to { + transform: rotate(360deg); + } +} + +@keyframes desktop-login-orb-pulse { + 0%, + 100% { + transform: scale(1); + } + 45% { + transform: scale(1.08); + } +} diff --git a/apps/desktop/app/login/page.tsx b/apps/desktop/app/login/page.tsx index e9643eb1..b8175760 100644 --- a/apps/desktop/app/login/page.tsx +++ b/apps/desktop/app/login/page.tsx @@ -15,7 +15,13 @@ import { Label } from "@ui/components/label" import { TextSeparator } from "@ui/components/text-separator" import { Loader2 } from "lucide-react" import { useRouter } from "next/navigation" -import { type FormEvent, useEffect, useState } from "react" +import { + type CSSProperties, + type FormEvent, + type ReactNode, + useEffect, + useState, +} from "react" import { beginBrowserAuth, desktopDevAuthEnabled, @@ -106,13 +112,13 @@ export default function LoginPage() { const isAuthBusy = isBrowserAuthPending || isSubmitting return ( -
+
-
+
@@ -123,7 +129,7 @@ export default function LoginPage() {
-
-
-
-

+
+
+
+

Never forget anything, anywhere with supermemory

-

+

Save from Chrome, Notion, X - search it all in one place.

-
+
@@ -155,7 +161,7 @@ export default function LoginPage() {
-
+
} authProvider="Google" @@ -177,7 +183,7 @@ export default function LoginPage() {
-

+

By continuing, you agree to our{" "} ReactNode +} + +type ContextConnection = { + from: ToolNode + to: ToolNode +} + +type ContextPhase = "idle" | "capture" | "hold" | "recall" + +const CENTER = { x: 50, y: 50 } +const IN_MS = 1100 +const HOLD_MS = 700 +const OUT_MS = 1100 +const TOTAL_MS = IN_MS + HOLD_MS + OUT_MS + +const TOOL_NODES: ToolNode[] = [ + { id: "chrome", name: "Chrome", x: 14, y: 20, icon: ChromeLogo }, + { id: "notion", name: "Notion", x: 84, y: 16, icon: Notion }, + { id: "drive", name: "Google Drive", x: 10, y: 52, icon: GoogleDrive }, + { id: "claude", name: "Claude", x: 90, y: 44, icon: ClaudeDesktopIcon }, + { id: "raycast", name: "Raycast", x: 76, y: 76, icon: RaycastMark }, + { id: "mcp", name: "MCP", x: 22, y: 84, icon: MCPIcon }, + { + id: "claude-code", + name: "Claude Code", + x: 20, + y: 36, + icon: CodeMark, + }, + { id: "codex", name: "Codex", x: 92, y: 28, icon: CodexMark }, + { id: "opencode", name: "OpenCode", x: 58, y: 10, icon: OpenCodeMark }, + { id: "hermes", name: "Hermes", x: 36, y: 90, icon: HermesMark }, + { id: "openclaw", name: "OpenClaw", x: 68, y: 68, icon: OpenClawMark }, +] + +const CONTEXT_FLOWS: [string, string][] = [ + ["chrome", "claude"], + ["notion", "raycast"], + ["drive", "claude-code"], + ["opencode", "codex"], + ["claude-code", "mcp"], + ["hermes", "openclaw"], + ["claude", "mcp"], + ["notion", "claude"], +] + +function nodeById(id: string) { + return TOOL_NODES.find((node) => node.id === id) +} + +function pickContextFlow(): ContextConnection | null { + const flow = CONTEXT_FLOWS[Math.floor(Math.random() * CONTEXT_FLOWS.length)] + if (!flow) return null + const [fromId, toId] = flow + const from = nodeById(fromId) + const to = nodeById(toId) + if (!from || !to) return null + return { from, to } +} + function DesktopLoginToolsPanel() { + const [connection, setConnection] = useState(null) + const [pulseId, setPulseId] = useState(0) + const [phase, setPhase] = useState("idle") + + useEffect(() => { + let cancelled = false + let pulseTimeout: ReturnType + + function runPulse() { + if (cancelled) return + const next = pickContextFlow() + if (!next) return + setConnection(next) + setPulseId((id) => id + 1) + pulseTimeout = setTimeout(runPulse, TOTAL_MS + 900 + Math.random() * 500) + } + + runPulse() + return () => { + cancelled = true + clearTimeout(pulseTimeout) + } + }, []) + + useEffect(() => { + if (!connection) return + + setPhase("capture") + const holdTimer = setTimeout(() => setPhase("hold"), IN_MS) + const recallTimer = setTimeout(() => setPhase("recall"), IN_MS + HOLD_MS) + const idleTimer = setTimeout(() => setPhase("idle"), TOTAL_MS) + + return () => { + clearTimeout(holdTimer) + clearTimeout(recallTimer) + clearTimeout(idleTimer) + } + }, [connection]) + + const sourceRole = (nodeId: string) => + connection && + connection.from.id === nodeId && + (phase === "capture" || phase === "hold") + ? ("source" as const) + : undefined + + const destinationRole = (nodeId: string) => + connection && connection.to.id === nodeId && phase === "recall" + ? ("destination" as const) + : undefined + return ( -