mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-09 22:31:10 +00:00
Make desktop login responsive
This commit is contained in:
parent
41c0a9333b
commit
8fd2b59014
2 changed files with 824 additions and 75 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="grid h-screen min-h-screen overflow-hidden bg-[#030912] text-foreground lg:grid-cols-[65%_35%]">
|
||||
<main className="desktop-login-page bg-[#030912] text-foreground">
|
||||
<DesktopLoginToolsPanel />
|
||||
|
||||
<section className="relative z-10 flex min-h-0 min-w-0 flex-col">
|
||||
<section className="desktop-login-auth-pane relative z-10 min-w-0">
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
className="flex h-[86px] shrink-0 items-center justify-between px-6"
|
||||
className="desktop-login-auth-header flex shrink-0 items-center justify-between"
|
||||
>
|
||||
<div className="flex min-w-0 items-center">
|
||||
<Logo className="h-7 shrink-0" />
|
||||
|
|
@ -123,7 +129,7 @@ export default function LoginPage() {
|
|||
<Button
|
||||
asChild
|
||||
variant="newDefault"
|
||||
className="h-11 rounded-2xl px-4 text-base text-white"
|
||||
className="desktop-login-memory-api-button rounded-2xl text-white"
|
||||
>
|
||||
<a
|
||||
href="https://console.supermemory.ai"
|
||||
|
|
@ -135,19 +141,19 @@ export default function LoginPage() {
|
|||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center px-6 pb-10">
|
||||
<div className="w-full max-w-[390px]">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-balance font-medium text-[28px] leading-tight text-[#f7f9fc]">
|
||||
<div className="desktop-login-auth-content">
|
||||
<div className="desktop-login-auth-stack">
|
||||
<div className="desktop-login-headline text-center">
|
||||
<h1 className="text-balance font-medium leading-tight text-[#f7f9fc]">
|
||||
Never forget anything, anywhere
|
||||
<span className="block text-[#4BA0FA]">with supermemory</span>
|
||||
</h1>
|
||||
<p className="mt-4 text-muted-foreground/50 text-sm">
|
||||
<p className="desktop-login-subtitle text-muted-foreground/50">
|
||||
Save from Chrome, Notion, X - search it all in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative rounded-[22px] bg-linear-to-b from-[#06101F] to-[#030912] px-8 py-8 shadow-[1.5px_1.5px_20px_0_rgba(0,0,0,0.65),1px_1.5px_2px_0_rgba(128,189,255,0.07)_inset,-0.5px_-1.5px_4px_0_rgba(0,35,73,0.40)_inset]">
|
||||
<div className="desktop-login-card relative bg-linear-to-b from-[#06101F] to-[#030912] shadow-[1.5px_1.5px_20px_0_rgba(0,0,0,0.65),1px_1.5px_2px_0_rgba(128,189,255,0.07)_inset,-0.5px_-1.5px_4px_0_rgba(0,35,73,0.40)_inset]">
|
||||
<div className={isBrowserAuthPending ? "invisible" : undefined}>
|
||||
<div className="mb-2 flex justify-end">
|
||||
<Badge className="h-5 rounded-md px-2 text-[10px]">
|
||||
|
|
@ -155,7 +161,7 @@ export default function LoginPage() {
|
|||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="desktop-login-form flex flex-col">
|
||||
<ExternalAuthButton
|
||||
authIcon={<GoogleIcon />}
|
||||
authProvider="Google"
|
||||
|
|
@ -177,7 +183,7 @@ export default function LoginPage() {
|
|||
<TextSeparator text="OR" />
|
||||
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
className="desktop-login-email-form flex flex-col"
|
||||
onSubmit={onBrowserAuthSubmit}
|
||||
>
|
||||
<Input
|
||||
|
|
@ -186,12 +192,12 @@ export default function LoginPage() {
|
|||
placeholder="your@email.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className="h-14 rounded-xl border-[#17202e] bg-[#040a14]/70 px-6 text-base text-foreground placeholder:text-muted-foreground/45"
|
||||
className="desktop-login-email-input rounded-xl border-[#17202e] bg-[#040a14]/70 px-6 text-base text-foreground placeholder:text-muted-foreground/45"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-14 rounded-xl bg-linear-to-r from-[#2935ff] to-[#2f78ff] text-lg text-white hover:from-[#3440ff] hover:to-[#3b83ff]"
|
||||
className="desktop-login-primary-button rounded-xl bg-linear-to-r from-[#2935ff] to-[#2f78ff] text-lg text-white hover:from-[#3440ff] hover:to-[#3b83ff]"
|
||||
disabled={isAuthBusy}
|
||||
>
|
||||
<Logo className="size-5" />
|
||||
|
|
@ -199,7 +205,7 @@ export default function LoginPage() {
|
|||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-muted-foreground/50 text-sm">
|
||||
<p className="desktop-login-terms text-center text-muted-foreground/50">
|
||||
By continuing, you agree to our{" "}
|
||||
<a
|
||||
className="underline underline-offset-4 hover:text-muted-foreground"
|
||||
|
|
@ -276,84 +282,321 @@ export default function LoginPage() {
|
|||
)
|
||||
}
|
||||
|
||||
type ToolNode = {
|
||||
id: string
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
icon: (props: { className?: string }) => 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<ContextConnection | null>(null)
|
||||
const [pulseId, setPulseId] = useState(0)
|
||||
const [phase, setPhase] = useState<ContextPhase>("idle")
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let pulseTimeout: ReturnType<typeof setTimeout>
|
||||
|
||||
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 (
|
||||
<aside className="relative hidden min-h-0 overflow-hidden border-white/[0.06] border-r lg:block">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse 130% 100% at 50% 45%, rgba(70,155,255,0.26) 0%, rgba(35,100,210,0.15) 38%, rgba(8,22,45,0.58) 68%, #030912 100%), radial-gradient(ellipse 90% 70% at 50% 100%, rgba(50,130,240,0.18) 0%, transparent 65%)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_52%,rgba(85,150,255,0.22),transparent_24%),radial-gradient(circle_at_48%_52%,rgba(255,255,255,0.18),transparent_8%)] opacity-80" />
|
||||
<div className="absolute inset-0 bg-linear-to-b from-[#071024]/20 via-transparent to-[#030912]/35" />
|
||||
<aside className="desktop-login-tools-panel" aria-hidden>
|
||||
<div className="desktop-login-panel-orb" />
|
||||
<div className="desktop-login-panel-glow" />
|
||||
<div className="desktop-login-panel-vignette" />
|
||||
|
||||
<div className="relative z-10 h-full">
|
||||
<FloatingToolNode x={14} y={22} label="Chrome">
|
||||
<ChromeLogo className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={84} y={18} label="Notion">
|
||||
<Notion className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={11} y={53} label="Google Drive">
|
||||
<GoogleDrive className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={88} y={45} label="Claude">
|
||||
<ClaudeDesktopIcon className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={22} y={84} label="MCP">
|
||||
<MCPIcon className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={68} y={70} label="Raycast">
|
||||
<RaycastMark className="size-5" />
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={20} y={38} label="Code">
|
||||
<span className="font-semibold text-[#ff9a63] text-lg">▦</span>
|
||||
</FloatingToolNode>
|
||||
<FloatingToolNode x={58} y={14} label="OpenCode">
|
||||
<span className="font-semibold text-[#b8c4d8] text-sm">□</span>
|
||||
</FloatingToolNode>
|
||||
<div className="desktop-login-tools-network">
|
||||
{connection && phase !== "idle" ? (
|
||||
<AnimatedContextFlow
|
||||
key={pulseId}
|
||||
connection={connection}
|
||||
phase={phase}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2">
|
||||
<div className="relative flex size-36 items-center justify-center rounded-full bg-[radial-gradient(circle_at_68%_34%,rgba(145,230,255,0.9),rgba(48,81,255,0.92)_44%,rgba(14,29,90,0.84)_72%,transparent_73%)] shadow-[0_0_54px_rgba(65,135,255,0.6),inset_12px_16px_24px_rgba(255,255,255,0.13)] blur-[1px]">
|
||||
<div className="absolute inset-4 rounded-full bg-[#244cff]/50 blur-xl" />
|
||||
<Logo className="relative z-10 size-8 opacity-85" />
|
||||
{TOOL_NODES.map((node) => (
|
||||
<FloatingToolNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
role={sourceRole(node.id) ?? destinationRole(node.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="desktop-login-orb-wrap">
|
||||
<div
|
||||
className={
|
||||
phase === "hold"
|
||||
? "desktop-login-nova-orb desktop-login-nova-orb-active"
|
||||
: "desktop-login-nova-orb"
|
||||
}
|
||||
>
|
||||
<div className="desktop-login-nova-gradient" />
|
||||
<Logo className="relative z-10 size-8 opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="-translate-x-1/2 absolute bottom-8 left-1/2 whitespace-nowrap text-center text-sm text-white/45">
|
||||
One memory layer - context from any tool, everywhere you need it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="desktop-login-panel-caption">
|
||||
One memory layer - context from any tool, everywhere you need it.
|
||||
</p>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function FloatingToolNode({
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
children,
|
||||
function AnimatedContextFlow({
|
||||
connection,
|
||||
phase,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
connection: ContextConnection
|
||||
phase: ContextPhase
|
||||
}) {
|
||||
const { from, to } = connection
|
||||
const dIn = `M ${from.x} ${from.y} L ${CENTER.x} ${CENTER.y}`
|
||||
const dOut = `M ${CENTER.x} ${CENTER.y} L ${to.x} ${to.y}`
|
||||
const chipPhase =
|
||||
phase === "recall"
|
||||
? "desktop-login-chip-recall"
|
||||
: phase === "hold"
|
||||
? "desktop-login-chip-hold"
|
||||
: "desktop-login-chip-capture"
|
||||
|
||||
return (
|
||||
<div
|
||||
className="-translate-x-1/2 -translate-y-1/2 absolute z-10"
|
||||
style={{ left: `${x}%`, top: `${y}%` }}
|
||||
title={label}
|
||||
>
|
||||
<div className="flex size-12 items-center justify-center rounded-xl border border-white/12 bg-[#0c1628]/85 shadow-[inset_0_1px_0_rgba(255,255,255,0.1),0_6px_20px_rgba(0,0,0,0.22)] backdrop-blur-md">
|
||||
{children}
|
||||
<div className="pointer-events-none absolute inset-0 z-[1]">
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d={dIn}
|
||||
fill="none"
|
||||
stroke="rgb(75 160 250 / 0.12)"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
<path
|
||||
d={dOut}
|
||||
fill="none"
|
||||
stroke="rgb(75 160 250 / 0.12)"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
{phase === "capture" || phase === "hold" ? (
|
||||
<path
|
||||
d={dIn}
|
||||
pathLength={1}
|
||||
className="desktop-login-active-path"
|
||||
fill="none"
|
||||
stroke="rgb(140 205 255 / 0.75)"
|
||||
strokeWidth="1.75"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
) : null}
|
||||
{phase === "recall" ? (
|
||||
<path
|
||||
d={dOut}
|
||||
pathLength={1}
|
||||
className="desktop-login-active-path"
|
||||
fill="none"
|
||||
stroke="rgb(160 220 255 / 0.9)"
|
||||
strokeWidth="1.75"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
) : null}
|
||||
</svg>
|
||||
|
||||
<div
|
||||
className={`desktop-login-memory-chip-motion ${chipPhase}`}
|
||||
style={
|
||||
{
|
||||
"--from-x": `${from.x}%`,
|
||||
"--from-y": `${from.y}%`,
|
||||
"--to-x": `${to.x}%`,
|
||||
"--to-y": `${to.y}%`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<MemoryChip />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FloatingToolNode({
|
||||
node,
|
||||
role,
|
||||
}: {
|
||||
node: ToolNode
|
||||
role?: "source" | "destination"
|
||||
}) {
|
||||
const Icon = node.icon
|
||||
const roleClass =
|
||||
role === "source"
|
||||
? "desktop-login-tool-node-source"
|
||||
: role === "destination"
|
||||
? "desktop-login-tool-node-destination"
|
||||
: ""
|
||||
|
||||
return (
|
||||
<div
|
||||
className="desktop-login-tool-node-wrap"
|
||||
style={{ left: `${node.x}%`, top: `${node.y}%` }}
|
||||
title={node.name}
|
||||
>
|
||||
<div className={`desktop-login-tool-node ${roleClass}`}>
|
||||
<Icon className="desktop-login-tool-node-icon" />
|
||||
</div>
|
||||
<span
|
||||
className={
|
||||
role
|
||||
? "desktop-login-tool-node-label desktop-login-tool-node-label-visible"
|
||||
: "desktop-login-tool-node-label"
|
||||
}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MemoryChip() {
|
||||
return (
|
||||
<div className="desktop-login-memory-chip">
|
||||
<div className="desktop-login-memory-chip-lines" aria-hidden>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CodeMark({ className }: { className?: string }) {
|
||||
return <span className={`${className ?? ""} text-[#ff9a63]`}>CC</span>
|
||||
}
|
||||
|
||||
function CodexMark({ className }: { className?: string }) {
|
||||
return <span className={`${className ?? ""} text-[#d7e6ff]`}>CX</span>
|
||||
}
|
||||
|
||||
function OpenCodeMark({ className }: { className?: string }) {
|
||||
return <span className={`${className ?? ""} text-[#b8c4d8]`}>OC</span>
|
||||
}
|
||||
|
||||
function HermesMark({ className }: { className?: string }) {
|
||||
return <span className={`${className ?? ""} text-[#8ccfff]`}>HM</span>
|
||||
}
|
||||
|
||||
function OpenClawMark({ className }: { className?: string }) {
|
||||
return <span className={`${className ?? ""} text-[#8af0d8]`}>OP</span>
|
||||
}
|
||||
|
||||
function GoogleIcon() {
|
||||
return (
|
||||
<svg
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue