diff options
| author | MaheshtheDev <[email protected]> | 2025-10-06 01:06:32 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-10-06 01:06:32 +0000 |
| commit | 0ca737f3643b9ec64b0aa2bae4e62a571667eacf (patch) | |
| tree | e2402649d1500c7f80ba3d2db8ae28491e4c016a /apps | |
| parent | feat: url cards as content preview with rich details (#452) (diff) | |
| download | supermemory-0ca737f3643b9ec64b0aa2bae4e62a571667eacf.tar.xz supermemory-0ca737f3643b9ec64b0aa2bae4e62a571667eacf.zip | |
fix: mobile responsive issues on onboarding (#453)
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/onboarding/bio-form.tsx | 20 | ||||
| -rw-r--r-- | apps/web/app/onboarding/extension-form.tsx | 1581 | ||||
| -rw-r--r-- | apps/web/app/onboarding/intro.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/onboarding/name-form.tsx | 11 | ||||
| -rw-r--r-- | apps/web/app/onboarding/onboarding-background.tsx | 4 | ||||
| -rw-r--r-- | apps/web/app/onboarding/onboarding-form.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/onboarding/welcome.tsx | 10 | ||||
| -rw-r--r-- | apps/web/components/chrome-extension-button.tsx | 6 |
8 files changed, 913 insertions, 723 deletions
diff --git a/apps/web/app/onboarding/bio-form.tsx b/apps/web/app/onboarding/bio-form.tsx index d985a775..63f359ec 100644 --- a/apps/web/app/onboarding/bio-form.tsx +++ b/apps/web/app/onboarding/bio-form.tsx @@ -31,23 +31,23 @@ export function BioForm() { }) } return ( - <div className="relative"> + <div className="relative w-full"> <div className="space-y-4"> <NavMenu> <p className="text-base text-white/60"> Step {getStepNumberFor("bio")} of {totalSteps} </p> </NavMenu> - <h1 className="max-sm:text-4xl text-white font-medium"> - Tell us about yourself + <h1 className="text-2xl md:text-4xl text-white font-medium"> + Tell Supermemory about yourself </h1> - <p className="text-2xl max-sm:text-lg text-white/80"> - What should Supermemory know about you? + <p className="text-lg md:text-xl text-white/80"> + share with Supermemory what you do, who you are, and what you're interested in </p> </div> <Textarea autoFocus - className="font-sans mt-6 text-base! text-white tracking-normal font-medium border bg-white/30 border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)]" + className="font-sans mt-6 text-base! placeholder:text-white/80 text-white tracking-normal font-medium border bg-white/30 border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)] w-full" placeholder="I'm a software engineer from San Francisco..." rows={3} value={bio} @@ -61,12 +61,12 @@ export function BioForm() { animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }} exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }} transition={{ duration: 0.2, ease: "easeOut" }} - className="flex justify-end mt-2 absolute -bottom-12 right-0" + className="flex justify-end mt-4 md:mt-2 md:absolute md:-bottom-12 md:right-0" > <Button variant="link" size="lg" - className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer" + className="text-white/80 font-medium! text-base md:text-lg underline w-fit px-0! cursor-pointer" onClick={handleNext} > Save & Continue @@ -79,12 +79,12 @@ export function BioForm() { animate={{ opacity: 1, filter: "blur(0px)" }} exit={{ opacity: 0, filter: "blur(5px)" }} transition={{ duration: 0.2, ease: "easeOut" }} - className="flex justify-end mt-2 absolute -bottom-12 right-0" + className="flex justify-end mt-4 md:mt-2 md:absolute md:-bottom-12 md:right-0" > <Button variant="link" size="lg" - className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer" + className="text-white/80 font-medium! text-base md:text-lg underline w-fit px-0! cursor-pointer" onClick={handleNext} > Skip For Now diff --git a/apps/web/app/onboarding/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx index 63e7dff1..721acb4f 100644 --- a/apps/web/app/onboarding/extension-form.tsx +++ b/apps/web/app/onboarding/extension-form.tsx @@ -1,720 +1,907 @@ -"use client"; - -import { ArrowUpIcon, MicIcon, PlusIcon, MousePointer2, LoaderIcon, CheckIcon, XIcon, ChevronRightIcon } from "lucide-react"; -import { NavMenu } from "./nav-menu"; -import { useOnboarding } from "./onboarding-context"; -import { motion, AnimatePresence, type ResolvedValues } from "framer-motion"; -import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react"; -import React from "react"; -import { cn } from "@lib/utils"; -import { Button } from "@ui/components/button"; +"use client" + +import { + ArrowUpIcon, + MicIcon, + PlusIcon, + MousePointer2, + LoaderIcon, + CheckIcon, + XIcon, + ChevronRightIcon, +} from "lucide-react" +import { NavMenu } from "./nav-menu" +import { useOnboarding } from "./onboarding-context" +import { motion, AnimatePresence, type ResolvedValues } from "framer-motion" +import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react" +import React from "react" +import { cn } from "@lib/utils" +import { Button } from "@ui/components/button" type CursorAction = - | { type: 'startAt'; target: React.RefObject<HTMLElement | null> } - | { type: 'startAtPercent'; xPercent: number; yPercent: number } - | { type: 'move'; target: React.RefObject<HTMLElement | null>; duration: number } - | { type: 'move'; xPercent: number; yPercent: number; duration: number } - | { type: 'pause'; duration: number } - | { type: 'click' } - | { type: 'call'; fn: () => void } + | { type: "startAt"; target: React.RefObject<HTMLElement | null> } + | { type: "startAtPercent"; xPercent: number; yPercent: number } + | { + type: "move" + target: React.RefObject<HTMLElement | null> + duration: number + } + | { type: "move"; xPercent: number; yPercent: number; duration: number } + | { type: "pause"; duration: number } + | { type: "click" } + | { type: "call"; fn: () => void } interface CursorProps { - actions: CursorAction[]; - className?: string; - onPositionChange?: (clientX: number, clientY: number) => void; + actions: CursorAction[] + className?: string + onPositionChange?: (clientX: number, clientY: number) => void } function useContainerRect(ref: React.RefObject<HTMLDivElement | null>) { - const rectRef = React.useRef<DOMRect | null>(null); - - useLayoutEffect(function setup() { - if (!ref.current) return; - - function measure() { - if (ref.current) { - rectRef.current = ref.current.getBoundingClientRect(); - } - } - - measure(); - - let resizeObserver: ResizeObserver | null = null; - if (typeof ResizeObserver !== "undefined") { - resizeObserver = new ResizeObserver(function onResize() { - measure(); - }); - if (ref.current) { - resizeObserver.observe(ref.current); - } - } - - function onScroll() { - measure(); - } - - window.addEventListener("resize", measure); - window.addEventListener("scroll", onScroll, true); - - return function cleanup() { - if (resizeObserver) { - resizeObserver.disconnect(); - } - window.removeEventListener("resize", measure); - window.removeEventListener("scroll", onScroll, true); - }; - }, [ref]); - - return rectRef; + const rectRef = React.useRef<DOMRect | null>(null) + + useLayoutEffect( + function setup() { + if (!ref.current) return + + function measure() { + if (ref.current) { + rectRef.current = ref.current.getBoundingClientRect() + } + } + + measure() + + let resizeObserver: ResizeObserver | null = null + if (typeof ResizeObserver !== "undefined") { + resizeObserver = new ResizeObserver(function onResize() { + measure() + }) + if (ref.current) { + resizeObserver.observe(ref.current) + } + } + + function onScroll() { + measure() + } + + window.addEventListener("resize", measure) + window.addEventListener("scroll", onScroll, true) + + return function cleanup() { + if (resizeObserver) { + resizeObserver.disconnect() + } + window.removeEventListener("resize", measure) + window.removeEventListener("scroll", onScroll, true) + } + }, + [ref], + ) + + return rectRef } function Cursor({ actions, className, onPositionChange }: CursorProps) { - const [position, setPosition] = useState({ x: '0px', y: '0px' }); - const [scale, setScale] = useState(1); - const [currentMoveDuration, setCurrentMoveDuration] = useState(0.7); // Default move duration - const containerRef = useRef<HTMLDivElement>(null); - const timeoutsRef = useRef<number[]>([]); - const containerRectRef = useContainerRect(containerRef); - const lastUpdateRef = useRef(0); - - function moveToElement(elementRef: React.RefObject<HTMLElement | null>, duration: number) { - if (!containerRef.current || !elementRef.current) return; - - setCurrentMoveDuration(duration / 1000); // Convert to seconds for Framer Motion - - const containerRect = containerRef.current.getBoundingClientRect(); - const elementRect = elementRef.current.getBoundingClientRect(); - // Position the TOP-LEFT of the cursor at the center of the target element - const x = elementRect.left - containerRect.left + elementRect.width / 2; - const y = elementRect.top - containerRect.top + elementRect.height / 2; - - setPosition({ x: `${x}px`, y: `${y}px` }); - } - - function setPositionByPercent(xPercent: number, yPercent: number) { - if (!containerRef.current) return; - const containerRect = containerRef.current.getBoundingClientRect(); - // Percentages indicate where the TOP-LEFT of the cursor should be placed - const x = (containerRect.width * xPercent) / 100; - const y = (containerRect.height * yPercent) / 100; - setCurrentMoveDuration(0); // snap without animating - setPosition({ x: `${x}px`, y: `${y}px` }); - } - - function moveToPercent(xPercent: number, yPercent: number, duration: number) { - if (!containerRef.current) return; - setCurrentMoveDuration(duration / 1000); - const containerRect = containerRef.current.getBoundingClientRect(); - const x = (containerRect.width * xPercent) / 100; - const y = (containerRect.height * yPercent) / 100; - setPosition({ x: `${x}px`, y: `${y}px` }); - } - - useEffect(() => { - // Clear any existing timeouts before scheduling new ones - timeoutsRef.current.forEach((id) => clearTimeout(id)); - timeoutsRef.current = []; - - let timeAccumulator = 0; - - function schedule(callback: () => void, delay: number): number { - const id = window.setTimeout(callback, delay); - timeoutsRef.current.push(id); - return id; - } - - function executeActions(): void { - actions.forEach((action) => { - // startAt should apply immediately at its place in the sequence and not advance time - if (action.type === 'startAt') { - moveToElement(action.target, 0); - return; - } - if (action.type === 'startAtPercent') { - setPositionByPercent(action.xPercent, action.yPercent); - return; - } - - schedule(() => { - switch (action.type) { - case 'move': - if ('target' in action) { - moveToElement(action.target, action.duration); - } else { - moveToPercent(action.xPercent, action.yPercent, action.duration); - } - break; - case 'click': - setScale(0.9); - schedule(function resetClickScale() { setScale(1); }, 100); // Fixed 100ms click duration - break; - case 'call': - try { - action.fn(); - } catch (_) { - // no-op on errors to avoid breaking demo - } - break; - case 'pause': - // Pause doesn't require any action, just time passing - break; - } - }, timeAccumulator); - - // Add this action's duration to the accumulator for the next action - if (action.type === 'click') { - timeAccumulator += 100; - } else if (action.type === 'pause') { - timeAccumulator += action.duration; - } else if (action.type === 'move') { - timeAccumulator += action.duration; - } else { - // 'call' and startAt/startAtPercent don't consume time - } - }); - } - - // make sure refs are ready - schedule(executeActions, 100); - - return function cleanup(): void { - timeoutsRef.current.forEach((id) => clearTimeout(id)); - timeoutsRef.current = []; - }; - }, [actions]); - - return ( - <div ref={containerRef} className={`absolute inset-0 pointer-events-none ${className || ''}`}> - <motion.div - animate={{ - x: position.x, - y: position.y, - scale: scale - }} - transition={{ - x: { duration: currentMoveDuration, ease: "easeInOut" }, - y: { duration: currentMoveDuration, ease: "easeInOut" }, - scale: { duration: 0.1, ease: "easeInOut" } - }} - className="absolute top-0 left-0" - style={{ zIndex: 10 }} - onUpdate={(latest: ResolvedValues) => { - if (!onPositionChange) return; - const containerRect = containerRectRef.current; - if (!containerRect) return; - const now = performance.now(); - if (now - lastUpdateRef.current < 50) return; // ~20fps throttle - lastUpdateRef.current = now; - const latestX = typeof latest.x === 'number' ? latest.x : parseFloat(String(latest.x || 0)); - const latestY = typeof latest.y === 'number' ? latest.y : parseFloat(String(latest.y || 0)); - const clientX = containerRect.left + latestX; - const clientY = containerRect.top + latestY; - onPositionChange(clientX, clientY); - }} - > - <MousePointer2 className="size-6 drop-shadow" strokeWidth={1.5} fill="white" /> - </motion.div> - </div> - ) + const [position, setPosition] = useState({ x: "0px", y: "0px" }) + const [scale, setScale] = useState(1) + const [currentMoveDuration, setCurrentMoveDuration] = useState(0.7) // Default move duration + const containerRef = useRef<HTMLDivElement>(null) + const timeoutsRef = useRef<number[]>([]) + const containerRectRef = useContainerRect(containerRef) + const lastUpdateRef = useRef(0) + + function moveToElement( + elementRef: React.RefObject<HTMLElement | null>, + duration: number, + ) { + if (!containerRef.current || !elementRef.current) return + + setCurrentMoveDuration(duration / 1000) // Convert to seconds for Framer Motion + + const containerRect = containerRef.current.getBoundingClientRect() + const elementRect = elementRef.current.getBoundingClientRect() + // Position the TOP-LEFT of the cursor at the center of the target element + const x = elementRect.left - containerRect.left + elementRect.width / 2 + const y = elementRect.top - containerRect.top + elementRect.height / 2 + + setPosition({ x: `${x}px`, y: `${y}px` }) + } + + function setPositionByPercent(xPercent: number, yPercent: number) { + if (!containerRef.current) return + const containerRect = containerRef.current.getBoundingClientRect() + // Percentages indicate where the TOP-LEFT of the cursor should be placed + const x = (containerRect.width * xPercent) / 100 + const y = (containerRect.height * yPercent) / 100 + setCurrentMoveDuration(0) // snap without animating + setPosition({ x: `${x}px`, y: `${y}px` }) + } + + function moveToPercent(xPercent: number, yPercent: number, duration: number) { + if (!containerRef.current) return + setCurrentMoveDuration(duration / 1000) + const containerRect = containerRef.current.getBoundingClientRect() + const x = (containerRect.width * xPercent) / 100 + const y = (containerRect.height * yPercent) / 100 + setPosition({ x: `${x}px`, y: `${y}px` }) + } + + useEffect(() => { + // Clear any existing timeouts before scheduling new ones + timeoutsRef.current.forEach((id) => { + clearTimeout(id) + }) + timeoutsRef.current = [] + + let timeAccumulator = 0 + + function schedule(callback: () => void, delay: number): number { + const id = window.setTimeout(callback, delay) + timeoutsRef.current.push(id) + return id + } + + function executeActions(): void { + actions.forEach((action) => { + // startAt should apply immediately at its place in the sequence and not advance time + if (action.type === "startAt") { + moveToElement(action.target, 0) + return + } + if (action.type === "startAtPercent") { + setPositionByPercent(action.xPercent, action.yPercent) + return + } + + schedule(() => { + switch (action.type) { + case "move": + if ("target" in action) { + moveToElement(action.target, action.duration) + } else { + moveToPercent(action.xPercent, action.yPercent, action.duration) + } + break + case "click": + setScale(0.9) + schedule(function resetClickScale() { + setScale(1) + }, 100) // Fixed 100ms click duration + break + case "call": + try { + action.fn() + } catch (_) { + // no-op on errors to avoid breaking demo + } + break + case "pause": + // Pause doesn't require any action, just time passing + break + } + }, timeAccumulator) + + // Add this action's duration to the accumulator for the next action + if (action.type === "click") { + timeAccumulator += 100 + } else if (action.type === "pause") { + timeAccumulator += action.duration + } else if (action.type === "move") { + timeAccumulator += action.duration + } else { + // 'call' and startAt/startAtPercent don't consume time + } + }) + } + + // make sure refs are ready + schedule(executeActions, 100) + + return function cleanup(): void { + timeoutsRef.current.forEach((id) => { + clearTimeout(id) + }) + timeoutsRef.current = [] + } + }, [actions]) + + return ( + <div + ref={containerRef} + className={`absolute inset-0 pointer-events-none ${className || ""}`} + > + <motion.div + animate={{ + x: position.x, + y: position.y, + scale: scale, + }} + transition={{ + x: { duration: currentMoveDuration, ease: "easeInOut" }, + y: { duration: currentMoveDuration, ease: "easeInOut" }, + scale: { duration: 0.1, ease: "easeInOut" }, + }} + className="absolute top-0 left-0" + style={{ zIndex: 10 }} + onUpdate={(latest: ResolvedValues) => { + if (!onPositionChange) return + const containerRect = containerRectRef.current + if (!containerRect) return + const now = performance.now() + if (now - lastUpdateRef.current < 50) return // ~20fps throttle + lastUpdateRef.current = now + const latestX = + typeof latest.x === "number" + ? latest.x + : Number.parseFloat(String(latest.x || 0)) + const latestY = + typeof latest.y === "number" + ? latest.y + : Number.parseFloat(String(latest.y || 0)) + const clientX = containerRect.left + latestX + const clientY = containerRect.top + latestY + onPositionChange(clientX, clientY) + }} + > + <MousePointer2 + className="size-6 drop-shadow" + strokeWidth={1.5} + fill="white" + /> + </motion.div> + </div> + ) } function SnippetDemo() { - const snippetRootRef = useRef<HTMLDivElement>(null); - const sentenceRef = useRef<HTMLSpanElement>(null); - const [currentEndIndex, setCurrentEndIndex] = useState<number>(0); - const lastStableIndexRef = useRef<number>(0); - const [cursorActions, setCursorActions] = useState<CursorAction[]>([]); - const [menuOpen, setMenuOpen] = useState<boolean>(false); - const [hoveredMenuIndex, setHoveredMenuIndex] = useState<number | null>(null); - const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]); - const menuItem6Ref = useRef<HTMLElement | null>(null); - const charRectsRef = useRef<DOMRect[]>([]); - - const targetText = "There's an Italian dish called saltimbocca, which means \"leap into the mouth.\""; - - function getIndexFromClientPoint(clientX: number, clientY: number): number { - const rects = charRectsRef.current; - if (!sentenceRef.current || rects.length === 0) return 0; - let bestIdx = 0; - let bestDist = Number.POSITIVE_INFINITY; - for (let i = 0; i < rects.length; i++) { - const r = rects[i]; - if (!r) continue; - const cx = r.left + r.width / 2; - const cy = r.top + r.height / 2; - const dx = clientX - cx; - const dy = clientY - cy; - const d = dx * dx + dy * dy; - if (d < bestDist) { - bestDist = d; - bestIdx = i; - } - } - return bestIdx; - } - - function getMenuItemIndexFromPoint(clientX: number, clientY: number): number | null { - const el = document.elementFromPoint(clientX, clientY) as HTMLElement | null; - if (!el) return null; - - const menuItem = el.closest('[data-menu-idx]') as HTMLElement | null; - if (menuItem) { - const idx = parseInt(menuItem.dataset.menuIdx || '', 10); - return Number.isFinite(idx) ? idx : null; - } - return null; - } - - useLayoutEffect(function setupCharRectsMeasurement() { - function measureCharRects(): void { - if (!sentenceRef.current) { - charRectsRef.current = []; - return; - } - const spans = sentenceRef.current.querySelectorAll('span[data-idx]'); - const rects: DOMRect[] = []; - spans.forEach(function collect(node) { - rects.push((node as HTMLElement).getBoundingClientRect()); - }); - charRectsRef.current = rects; - } - - measureCharRects(); - - let ro1: ResizeObserver | null = null; - let ro2: ResizeObserver | null = null; - if (typeof ResizeObserver !== "undefined") { - ro1 = new ResizeObserver(function onResize() { measureCharRects(); }); - ro2 = new ResizeObserver(function onResize() { measureCharRects(); }); - if (snippetRootRef.current) ro1.observe(snippetRootRef.current); - if (sentenceRef.current) ro2.observe(sentenceRef.current); - } - - function onScroll(): void { measureCharRects(); } - window.addEventListener("resize", measureCharRects); - window.addEventListener("scroll", onScroll, true); - - return function cleanup(): void { - if (ro1) ro1.disconnect(); - if (ro2) ro2.disconnect(); - window.removeEventListener("resize", measureCharRects); - window.removeEventListener("scroll", onScroll, true); - }; - }, [targetText]); - - useEffect(function setupActionsOnce() { - lastStableIndexRef.current = 0; - setCurrentEndIndex(0); - if (!sentenceRef.current) return; - const total = targetText.length; - const firstSpan = sentenceRef.current.querySelector('span[data-idx="0"]') as HTMLSpanElement | null; - const lastSpan = sentenceRef.current.querySelector(`span[data-idx="${Math.max(0, total - 1)}"]`) as HTMLSpanElement | null; - if (!firstSpan || !lastSpan) return; - const firstRef = { current: firstSpan } as React.RefObject<HTMLElement>; - const lastRef = { current: lastSpan } as React.RefObject<HTMLElement>; - setCursorActions([ - { type: 'call', fn: function reset() { lastStableIndexRef.current = 0; setCurrentEndIndex(0); setHoveredMenuIndex(null); } }, - { type: 'startAt', target: firstRef }, - { type: 'pause', duration: 200 }, - { type: 'move', target: lastRef, duration: 1800 }, - { type: 'pause', duration: 1200 }, - { type: "click" }, - { type: 'call', fn: () => { setMenuOpen(true); } }, - { type: 'pause', duration: 1000 }, - { type: 'move', target: menuItem6Ref, duration: 1000 }, - { type: 'pause', duration: 500 }, - { type: 'click' }, - ]); - }, []); - - return ( - <div ref={snippetRootRef} className="size-full select-none text-xs relative overflow-hidden"> - <Cursor - actions={cursorActions} - onPositionChange={function onPositionChange(clientX: number, clientY: number) { - // Handle text highlighting - const textIdx = getIndexFromClientPoint(clientX, clientY); - const next = textIdx < lastStableIndexRef.current ? lastStableIndexRef.current : textIdx; - if (next !== lastStableIndexRef.current) { - lastStableIndexRef.current = next; - setCurrentEndIndex(next); - } - - // Handle menu item hovering - if (menuOpen) { - const menuIdx = getMenuItemIndexFromPoint(clientX, clientY); - if (menuIdx !== hoveredMenuIndex) { - setHoveredMenuIndex(menuIdx); - } - } - }} - /> - <div className="h-[125%] w-full bg-white text-justify p-4 text-black absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> - writing is easier to read, and the easier something is to read, the more deeply readers will engage with it. The less energy they expend on your prose, the more they'll have left for your ideas. And the further they'll read. Most readers' energy tends to flag part way through an article or essay. If the friction of reading is low enough, more keep going till the end. - {" "} - <span ref={sentenceRef}> - {targetText.split("").map(function renderChar(ch, idx) { - const highlighted = idx <= currentEndIndex; - return ( - <span - key={idx} - data-idx={idx} - className={highlighted ? "bg-blue-200/70" : undefined} - > - {ch} - </span> - ); - })} - </span> - <span className="size-0 relative"> - {menuOpen && ( - <div className="flex flex-col w-48 text-left absolute top-0 right-0 text-white bg-zinc-800/70 text-xs border-0.5 backdrop-blur-sm border-zinc-900/80 rounded-md px-1.5 py-1.5"> - {["Back", "Forward", "Reload", "Save As...", "Print...", "Translate to English", "Save to Supermemory", "View Page Source", "Inspect"].map((item, idx) => ( - <React.Fragment key={item}> - <div - ref={el => { menuItemRefs.current[idx] = el; if (idx === 6) { menuItem6Ref.current = el; } }} - data-menu-idx={idx} - className={cn( - "px-2 py-0.5 flex items-center gap-1.5 rounded-sm transition-colors", - (idx === 0 || idx === 1) && "text-white/30", - hoveredMenuIndex === idx && "bg-blue-500" - )} - > - {idx === 6 && <img src="/images/icon-16.png" alt="Supermemory" className="size-3.5" />} - {item} - </div> - {[2, 5, 6].includes(idx) && <div className="h-px rounded-full my-1 mx-2 bg-zinc-300/20" />} - </React.Fragment> - ))} - </div> - )} - </span> - {" "}My goal when writing might be called saltintesta: the ideas leap into your head and you barely notice the words that got them there. It's too much to hope that writing could ever be pure ideas. You might not even want it to be. But for most writers, most of the time, that's the goal to aim for. The gap between most writing and pure ideas is not filled with poetry. Plus it's more considerate to write simply. When you write in a fancy way to impress people, you're making them do extra work just so you can seem cool. It's like trailing a long train behind you that readers have to carry. - </div> - </div> - ) + const snippetRootRef = useRef<HTMLDivElement>(null) + const sentenceRef = useRef<HTMLSpanElement>(null) + const [currentEndIndex, setCurrentEndIndex] = useState<number>(0) + const lastStableIndexRef = useRef<number>(0) + const [cursorActions, setCursorActions] = useState<CursorAction[]>([]) + const [menuOpen, setMenuOpen] = useState<boolean>(false) + const [hoveredMenuIndex, setHoveredMenuIndex] = useState<number | null>(null) + const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]) + const menuItem6Ref = useRef<HTMLElement | null>(null) + const charRectsRef = useRef<DOMRect[]>([]) + + const targetText = + 'There\'s an Italian dish called saltimbocca, which means "leap into the mouth."' + + function getIndexFromClientPoint(clientX: number, clientY: number): number { + const rects = charRectsRef.current + if (!sentenceRef.current || rects.length === 0) return 0 + let bestIdx = 0 + let bestDist = Number.POSITIVE_INFINITY + for (let i = 0; i < rects.length; i++) { + const r = rects[i] + if (!r) continue + const cx = r.left + r.width / 2 + const cy = r.top + r.height / 2 + const dx = clientX - cx + const dy = clientY - cy + const d = dx * dx + dy * dy + if (d < bestDist) { + bestDist = d + bestIdx = i + } + } + return bestIdx + } + + function getMenuItemIndexFromPoint( + clientX: number, + clientY: number, + ): number | null { + const el = document.elementFromPoint(clientX, clientY) as HTMLElement | null + if (!el) return null + + const menuItem = el.closest("[data-menu-idx]") as HTMLElement | null + if (menuItem) { + const idx = Number.parseInt(menuItem.dataset.menuIdx || "", 10) + return Number.isFinite(idx) ? idx : null + } + return null + } + + useLayoutEffect( + function setupCharRectsMeasurement() { + function measureCharRects(): void { + if (!sentenceRef.current) { + charRectsRef.current = [] + return + } + const spans = sentenceRef.current.querySelectorAll("span[data-idx]") + const rects: DOMRect[] = [] + spans.forEach(function collect(node) { + rects.push((node as HTMLElement).getBoundingClientRect()) + }) + charRectsRef.current = rects + } + + measureCharRects() + + let ro1: ResizeObserver | null = null + let ro2: ResizeObserver | null = null + if (typeof ResizeObserver !== "undefined") { + ro1 = new ResizeObserver(function onResize() { + measureCharRects() + }) + ro2 = new ResizeObserver(function onResize() { + measureCharRects() + }) + if (snippetRootRef.current) ro1.observe(snippetRootRef.current) + if (sentenceRef.current) ro2.observe(sentenceRef.current) + } + + function onScroll(): void { + measureCharRects() + } + window.addEventListener("resize", measureCharRects) + window.addEventListener("scroll", onScroll, true) + + return function cleanup(): void { + if (ro1) ro1.disconnect() + if (ro2) ro2.disconnect() + window.removeEventListener("resize", measureCharRects) + window.removeEventListener("scroll", onScroll, true) + } + }, + [targetText], + ) + + useEffect(function setupActionsOnce() { + lastStableIndexRef.current = 0 + setCurrentEndIndex(0) + if (!sentenceRef.current) return + const total = targetText.length + const firstSpan = sentenceRef.current.querySelector( + 'span[data-idx="0"]', + ) as HTMLSpanElement | null + const lastSpan = sentenceRef.current.querySelector( + `span[data-idx="${Math.max(0, total - 1)}"]`, + ) as HTMLSpanElement | null + if (!firstSpan || !lastSpan) return + const firstRef = { current: firstSpan } as React.RefObject<HTMLElement> + const lastRef = { current: lastSpan } as React.RefObject<HTMLElement> + setCursorActions([ + { + type: "call", + fn: function reset() { + lastStableIndexRef.current = 0 + setCurrentEndIndex(0) + setHoveredMenuIndex(null) + }, + }, + { type: "startAt", target: firstRef }, + { type: "pause", duration: 200 }, + { type: "move", target: lastRef, duration: 1800 }, + { type: "pause", duration: 1200 }, + { type: "click" }, + { + type: "call", + fn: () => { + setMenuOpen(true) + }, + }, + { type: "pause", duration: 1000 }, + { type: "move", target: menuItem6Ref, duration: 1000 }, + { type: "pause", duration: 500 }, + { type: "click" }, + ]) + }, []) + + return ( + <div + ref={snippetRootRef} + className="size-full select-none text-xs relative overflow-hidden" + > + <Cursor + actions={cursorActions} + onPositionChange={function onPositionChange( + clientX: number, + clientY: number, + ) { + // Handle text highlighting + const textIdx = getIndexFromClientPoint(clientX, clientY) + const next = + textIdx < lastStableIndexRef.current + ? lastStableIndexRef.current + : textIdx + if (next !== lastStableIndexRef.current) { + lastStableIndexRef.current = next + setCurrentEndIndex(next) + } + + // Handle menu item hovering + if (menuOpen) { + const menuIdx = getMenuItemIndexFromPoint(clientX, clientY) + if (menuIdx !== hoveredMenuIndex) { + setHoveredMenuIndex(menuIdx) + } + } + }} + /> + <div className="h-[125%] w-full bg-white text-justify p-4 text-black absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> + writing is easier to read, and the easier something is to read, the more + deeply readers will engage with it. The less energy they expend on your + prose, the more they'll have left for your ideas. And the further + they'll read. Most readers' energy tends to flag part way through an + article or essay. If the friction of reading is low enough, more keep + going till the end.{" "} + <span ref={sentenceRef}> + {targetText.split("").map(function renderChar(ch, idx) { + const highlighted = idx <= currentEndIndex + return ( + <span + key={`char-${idx}-${ch}`} + data-idx={idx} + className={highlighted ? "bg-blue-200/70" : undefined} + > + {ch} + </span> + ) + })} + </span> + <span className="size-0 relative"> + {menuOpen && ( + <div className="flex flex-col w-48 text-left absolute top-0 right-0 text-white bg-zinc-800/70 text-xs border-0.5 backdrop-blur-sm border-zinc-900/80 rounded-md px-1.5 py-1.5"> + {[ + "Back", + "Forward", + "Reload", + "Save As...", + "Print...", + "Translate to English", + "Save to Supermemory", + "View Page Source", + "Inspect", + ].map((item, idx) => ( + <React.Fragment key={item}> + <div + ref={(el) => { + menuItemRefs.current[idx] = el + if (idx === 6) { + menuItem6Ref.current = el + } + }} + data-menu-idx={idx} + className={cn( + "px-2 py-0.5 flex items-center gap-1.5 rounded-sm transition-colors", + (idx === 0 || idx === 1) && "text-white/30", + hoveredMenuIndex === idx && "bg-blue-500", + )} + > + {idx === 6 && ( + <img + src="/images/icon-16.png" + alt="Supermemory" + className="size-3.5" + /> + )} + {item} + </div> + {[2, 5, 6].includes(idx) && ( + <div className="h-px rounded-full my-1 mx-2 bg-zinc-300/20" /> + )} + </React.Fragment> + ))} + </div> + )} + </span>{" "} + My goal when writing might be called saltintesta: the ideas leap into + your head and you barely notice the words that got them there. It's too + much to hope that writing could ever be pure ideas. You might not even + want it to be. But for most writers, most of the time, that's the goal + to aim for. The gap between most writing and pure ideas is not filled + with poetry. Plus it's more considerate to write simply. When you write + in a fancy way to impress people, you're making them do extra work just + so you can seem cool. It's like trailing a long train behind you that + readers have to carry. + </div> + </div> + ) } function ChatGPTDemo() { - const iconRef = useRef<HTMLImageElement>(null); - const submitButtonRef = useRef<HTMLDivElement>(null); - const [enhancementStatus, setEnhancementStatus] = useState<"notStarted" | "enhancing" | "done">("notStarted"); - const [memoriesExpanded, setMemoriesExpanded] = useState(false); - - const cursorActions: CursorAction[] = useMemo(() => [ - { - type: "call", fn: function resetStates() { - setEnhancementStatus("notStarted"); - setMemoriesExpanded(false); - } - }, - { type: 'startAtPercent', xPercent: 80, yPercent: 80 }, - { type: 'pause', duration: 1000 }, - { type: 'move', target: iconRef, duration: 1000 }, - { type: 'pause', duration: 1000 }, - { type: 'click' }, - { type: 'call', fn: function startEnhancing() { setEnhancementStatus("enhancing"); } }, - { type: 'pause', duration: 1000 }, - { type: 'move', xPercent: 10, yPercent: 80, duration: 1000 }, - { type: 'call', fn: function finishEnhancing() { setEnhancementStatus("done"); } }, - { type: 'pause', duration: 500 }, - { type: 'move', target: iconRef, duration: 1000 }, - { type: 'call', fn: function expandMemories() { setMemoriesExpanded(true); } }, - { type: "pause", duration: 1000 }, - { type: 'move', xPercent: 80, yPercent: 80, duration: 1000 }, - ], []); - - return ( - <div className="size-full relative overflow-hidden select-none pointer-events-none text-white flex flex-col gap-6 items-center justify-center" style={{ - backgroundColor: "#212121", - fontFamily: "ui-sans-serif, -apple-system, system-ui" - }}> - <Cursor actions={cursorActions} /> - <div className="text-xl">What's on your mind today?</div> - <div className="w-[85%] text-sm rounded-3xl p-2 flex flex-col gap-2" style={{ backgroundColor: "#303030" }}> - <div className="p-2"> - what are my card's benefits? - </div> - <div className="flex justify-between items-center p-1 pt-0"> - <PlusIcon className="size-5" strokeWidth={1.5} /> - <div className="flex items-center gap-2"> - <div className="h-4.5 flex items-center"> - <motion.div - ref={iconRef} - layout - initial={false} - animate={{ - backgroundColor: enhancementStatus === "notStarted" ? "transparent" : "#1e1b4b", - borderColor: enhancementStatus === "notStarted" ? "transparent" : "#4338ca", - borderWidth: enhancementStatus === "notStarted" ? 0 : 1, - paddingLeft: enhancementStatus === "notStarted" ? 0 : (enhancementStatus === "enhancing" ? 4 : 8), - paddingRight: enhancementStatus === "notStarted" ? 0 : (enhancementStatus === "enhancing" ? 6 : 8), - paddingTop: enhancementStatus === "notStarted" ? 0 : 4, - paddingBottom: enhancementStatus === "notStarted" ? 0 : 4, - marginTop: enhancementStatus === "notStarted" ? 0 : 4, - marginBottom: enhancementStatus === "notStarted" ? 0 : 4, - }} - transition={{ - duration: 0.2, - ease: "easeInOut", - layout: { duration: 0.2, ease: "easeInOut" } - }} - className="rounded-full text-xs w-fit flex items-center relative" - style={{ border: "solid" }} - > - {enhancementStatus === "notStarted" && ( - <img src="/images/icon-16.png" alt="Enhance with Supermemory" className="size-5" /> - )} - {enhancementStatus === "enhancing" && ( - <> - <LoaderIcon className="size-3 animate-spin" /> - <motion.span - initial={{ opacity: 0, width: 0 }} - animate={{ opacity: 1, width: "auto" }} - transition={{ delay: 0.1, duration: 0.2 }} - className="ml-2 whitespace-nowrap overflow-hidden" - > - Searching... - </motion.span> - </> - )} - {enhancementStatus === "done" && ( - <> - <CheckIcon className="size-3 text-green-400" /> - <motion.span - initial={{ opacity: 0, width: "auto" }} - animate={{ opacity: 1, width: "auto" }} - transition={{ delay: 0.1, duration: 0.2 }} - className="ml-2">Including 1 memory</motion.span> - </> - )} - <AnimatePresence> - {memoriesExpanded && ( - <motion.div - initial={{ - opacity: 0, - scale: 0.95, - y: -8 - }} - animate={{ - opacity: 1, - scale: 1, - y: 0 - }} - exit={{ - opacity: 0, - scale: 0.95, - y: -8 - }} - transition={{ - duration: 0.15, - ease: [0.16, 1, 0.3, 1], - }} - className="absolute left-1/2 -translate-x-1/2 top-8 bg-[#1e1b4b] border border-[#4338ca] w-56 rounded-lg p-2" - style={{ transformOrigin: 'top center' }} - > - <div className="flex items-center gap-2"> - <img src="/images/icon-16.png" alt="Enhance with Supermemory" className="size-5" /> - <span className="text-xs">User possesses an American Express Platinum card</span> - </div> - </motion.div> - )} - </AnimatePresence> - </motion.div> - </div> - - <MicIcon className="size-4" strokeWidth={1.5} /> - <div ref={submitButtonRef} className="rounded-full bg-white size-6 flex items-center justify-center"> - <ArrowUpIcon className="size-4 text-black" /> - </div> - </div> - </div> - </div> - </div> - ) + const iconRef = useRef<HTMLImageElement>(null) + const submitButtonRef = useRef<HTMLDivElement>(null) + const [enhancementStatus, setEnhancementStatus] = useState< + "notStarted" | "enhancing" | "done" + >("notStarted") + const [memoriesExpanded, setMemoriesExpanded] = useState(false) + + const cursorActions: CursorAction[] = useMemo( + () => [ + { + type: "call", + fn: function resetStates() { + setEnhancementStatus("notStarted") + setMemoriesExpanded(false) + }, + }, + { type: "startAtPercent", xPercent: 80, yPercent: 80 }, + { type: "pause", duration: 1000 }, + { type: "move", target: iconRef, duration: 1000 }, + { type: "pause", duration: 1000 }, + { type: "click" }, + { + type: "call", + fn: function startEnhancing() { + setEnhancementStatus("enhancing") + }, + }, + { type: "pause", duration: 1000 }, + { type: "move", xPercent: 10, yPercent: 80, duration: 1000 }, + { + type: "call", + fn: function finishEnhancing() { + setEnhancementStatus("done") + }, + }, + { type: "pause", duration: 500 }, + { type: "move", target: iconRef, duration: 1000 }, + { + type: "call", + fn: function expandMemories() { + setMemoriesExpanded(true) + }, + }, + { type: "pause", duration: 1000 }, + { type: "move", xPercent: 80, yPercent: 80, duration: 1000 }, + ], + [], + ) + + return ( + <div + className="size-full relative overflow-hidden select-none pointer-events-none text-white flex flex-col gap-6 items-center justify-center" + style={{ + backgroundColor: "#212121", + fontFamily: "ui-sans-serif, -apple-system, system-ui", + }} + > + <Cursor actions={cursorActions} /> + <div className="text-xl">What's on your mind today?</div> + <div + className="w-[85%] text-sm rounded-3xl p-2 flex flex-col gap-2" + style={{ backgroundColor: "#303030" }} + > + <div className="p-2">what are my card's benefits?</div> + <div className="flex justify-between items-center p-1 pt-0"> + <PlusIcon className="size-5" strokeWidth={1.5} /> + <div className="flex items-center gap-2"> + <div className="h-4.5 flex items-center"> + <motion.div + ref={iconRef} + layout + initial={false} + animate={{ + backgroundColor: + enhancementStatus === "notStarted" + ? "transparent" + : "#1e1b4b", + borderColor: + enhancementStatus === "notStarted" + ? "transparent" + : "#4338ca", + borderWidth: enhancementStatus === "notStarted" ? 0 : 1, + paddingLeft: + enhancementStatus === "notStarted" + ? 0 + : enhancementStatus === "enhancing" + ? 4 + : 8, + paddingRight: + enhancementStatus === "notStarted" + ? 0 + : enhancementStatus === "enhancing" + ? 6 + : 8, + paddingTop: enhancementStatus === "notStarted" ? 0 : 4, + paddingBottom: enhancementStatus === "notStarted" ? 0 : 4, + marginTop: enhancementStatus === "notStarted" ? 0 : 4, + marginBottom: enhancementStatus === "notStarted" ? 0 : 4, + }} + transition={{ + duration: 0.2, + ease: "easeInOut", + layout: { duration: 0.2, ease: "easeInOut" }, + }} + className="rounded-full text-xs w-fit flex items-center relative" + style={{ border: "solid" }} + > + {enhancementStatus === "notStarted" && ( + <img + src="/images/icon-16.png" + alt="Enhance with Supermemory" + className="size-5" + /> + )} + {enhancementStatus === "enhancing" && ( + <> + <LoaderIcon className="size-3 animate-spin" /> + <motion.span + initial={{ opacity: 0, width: 0 }} + animate={{ opacity: 1, width: "auto" }} + transition={{ delay: 0.1, duration: 0.2 }} + className="ml-2 whitespace-nowrap overflow-hidden" + > + Searching... + </motion.span> + </> + )} + {enhancementStatus === "done" && ( + <> + <CheckIcon className="size-3 text-green-400" /> + <motion.span + initial={{ opacity: 0, width: "auto" }} + animate={{ opacity: 1, width: "auto" }} + transition={{ delay: 0.1, duration: 0.2 }} + className="ml-2" + > + Including 1 memory + </motion.span> + </> + )} + <AnimatePresence> + {memoriesExpanded && ( + <motion.div + initial={{ + opacity: 0, + scale: 0.95, + y: -8, + }} + animate={{ + opacity: 1, + scale: 1, + y: 0, + }} + exit={{ + opacity: 0, + scale: 0.95, + y: -8, + }} + transition={{ + duration: 0.15, + ease: [0.16, 1, 0.3, 1], + }} + className="absolute left-1/2 -translate-x-1/2 top-8 bg-[#1e1b4b] border border-[#4338ca] w-56 rounded-lg p-2" + style={{ transformOrigin: "top center" }} + > + <div className="flex items-center gap-2"> + <img + src="/images/icon-16.png" + alt="Enhance with Supermemory" + className="size-5" + /> + <span className="text-xs"> + User possesses an American Express Platinum card + </span> + </div> + </motion.div> + )} + </AnimatePresence> + </motion.div> + </div> + + <MicIcon className="size-4" strokeWidth={1.5} /> + <div + ref={submitButtonRef} + className="rounded-full bg-white size-6 flex items-center justify-center" + > + <ArrowUpIcon className="size-4 text-black" /> + </div> + </div> + </div> + </div> + </div> + ) } function TwitterDemo() { - const importButtonRef = useRef<HTMLButtonElement>(null); - const [importStatus, setImportStatus] = useState<"notStarted" | "importing" | "done">("notStarted"); - - const cursorActions: CursorAction[] = useMemo(() => [ - { - type: "call", fn: function resetStates() { - setImportStatus("notStarted"); - } - }, - { type: 'startAtPercent', xPercent: 10, yPercent: 80 }, - { type: 'pause', duration: 1300 }, - { type: 'move', target: importButtonRef, duration: 1100 }, - { type: 'pause', duration: 800 }, - { type: 'click' }, - { type: 'call', fn: function startImporting() { setImportStatus("importing"); } }, - { type: 'pause', duration: 700 }, - { type: 'move', xPercent: 80, yPercent: 80, duration: 1200 }, - { type: 'call', fn: function finishImporting() { setImportStatus("done"); } }, - ], []); - - return ( - <div className="size-full relative overflow-hidden select-none flex flex-col items-center justify-center"> - <div className="bg-white text-black px-5 py-3 text-sm rounded-2xl w-9/10"> - <div className="flex justify-between items-center"> - <div className="flex items-center gap-2"> - <span className="text-xl font-bold">𝕏</span> - <span className="text-base font-medium">Import Twitter Bookmarks</span> - </div> - <XIcon className="size-4" /> - </div> - <div className="mt-3"> - <p className="text-sm text-zinc-600"> - This will import all your Twitter bookmarks to Supermemory - </p> - </div> - <div className="mt-3"> - <motion.button - ref={importButtonRef} - animate={{ - backgroundColor: importStatus === "importing" - ? "#f59e0b" - : importStatus === "done" - ? "#10b981" - : "#3b82f6" - }} - transition={{ duration: 0.3, ease: "easeInOut" }} - className="text-white px-4 py-2 rounded-lg flex items-center gap-2 min-w-[180px] justify-center" - > - <AnimatePresence mode="wait"> - {importStatus === "importing" && ( - <motion.div - initial={{ opacity: 0, scale: 0 }} - animate={{ opacity: 1, scale: 1 }} - exit={{ opacity: 0, scale: 0 }} - transition={{ duration: 0.2 }} - > - <LoaderIcon className="size-4 animate-spin" /> - </motion.div> - )} - {importStatus === "done" && ( - <motion.div - initial={{ opacity: 0, scale: 0 }} - animate={{ opacity: 1, scale: 1 }} - exit={{ opacity: 0, scale: 0 }} - transition={{ duration: 0.2 }} - > - <CheckIcon className="size-4" /> - </motion.div> - )} - </AnimatePresence> - <motion.span - key={importStatus} - initial={{ opacity: 0, y: 10 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -10 }} - transition={{ duration: 0.3, ease: "easeInOut" }} - > - {importStatus === "importing" - ? "Importing bookmarks..." - : importStatus === "done" - ? "Import successful" - : "Import All Bookmarks"} - </motion.span> - </motion.button> - </div> - </div> - <Cursor actions={cursorActions} /> - </div> - ) + const importButtonRef = useRef<HTMLButtonElement>(null) + const [importStatus, setImportStatus] = useState< + "notStarted" | "importing" | "done" + >("notStarted") + + const cursorActions: CursorAction[] = useMemo( + () => [ + { + type: "call", + fn: function resetStates() { + setImportStatus("notStarted") + }, + }, + { type: "startAtPercent", xPercent: 10, yPercent: 80 }, + { type: "pause", duration: 1300 }, + { type: "move", target: importButtonRef, duration: 1100 }, + { type: "pause", duration: 800 }, + { type: "click" }, + { + type: "call", + fn: function startImporting() { + setImportStatus("importing") + }, + }, + { type: "pause", duration: 700 }, + { type: "move", xPercent: 80, yPercent: 80, duration: 1200 }, + { + type: "call", + fn: function finishImporting() { + setImportStatus("done") + }, + }, + ], + [], + ) + + return ( + <div className="size-full relative overflow-hidden select-none flex flex-col items-center justify-center"> + <div className="bg-white text-black px-5 py-3 text-sm rounded-2xl w-9/10"> + <div className="flex justify-between items-center"> + <div className="flex items-center gap-2"> + <span className="text-xl font-bold">𝕏</span> + <span className="text-base font-medium"> + Import Twitter Bookmarks + </span> + </div> + <XIcon className="size-4" /> + </div> + <div className="mt-3"> + <p className="text-sm text-zinc-600"> + This will import all your Twitter bookmarks to Supermemory + </p> + </div> + <div className="mt-3"> + <motion.button + ref={importButtonRef} + animate={{ + backgroundColor: + importStatus === "importing" + ? "#f59e0b" + : importStatus === "done" + ? "#10b981" + : "#3b82f6", + }} + transition={{ duration: 0.3, ease: "easeInOut" }} + className="text-white px-4 py-2 rounded-lg flex items-center gap-2 min-w-[180px] justify-center" + > + <AnimatePresence mode="wait"> + {importStatus === "importing" && ( + <motion.div + initial={{ opacity: 0, scale: 0 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0 }} + transition={{ duration: 0.2 }} + > + <LoaderIcon className="size-4 animate-spin" /> + </motion.div> + )} + {importStatus === "done" && ( + <motion.div + initial={{ opacity: 0, scale: 0 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0 }} + transition={{ duration: 0.2 }} + > + <CheckIcon className="size-4" /> + </motion.div> + )} + </AnimatePresence> + <motion.span + key={importStatus} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + transition={{ duration: 0.3, ease: "easeInOut" }} + > + {importStatus === "importing" + ? "Importing bookmarks..." + : importStatus === "done" + ? "Import successful" + : "Import All Bookmarks"} + </motion.span> + </motion.button> + </div> + </div> + <Cursor actions={cursorActions} /> + </div> + ) } export function ExtensionForm() { - const { totalSteps, nextStep, getStepNumberFor } = useOnboarding(); - return ( - <div className="relative flex items-start flex-col gap-6"> - <div className="flex items-center justify-between w-full"> - <div className="flex flex-col items-start text-left gap-4"> - <NavMenu> - <p className="text-base text-white/60"> - Step {getStepNumberFor("extension")} of {totalSteps} - </p> - </NavMenu> - <h1 className="text-white font-medium"> - Install the Chrome extension - </h1> - <p className="text-white/80 text-2xl"> - {/* Install the Supermemory extension to start saving and organizing everything that matters. */} - Bring Supermemory everywhere - </p> - </div> - <div className="flex flex-col items-center text-center gap-4"> - <a - href="https://chromewebstore.google.com/detail/afpgkkipfdpeaflnpoaffkcankadgjfc?utm_source=item-share-cb" - rel="noopener noreferrer" - target="_blank" - className="bg-zinc-50/80 backdrop-blur-lg border-2 hover:bg-zinc-100/80 transition-colors duration-100 border-zinc-200/80 shadow-xs rounded-full mt-6 pl-4.5 pr-6 py-4 text-2xl font-sans tracking-tight font-medium flex items-center gap-4" - > - <img - src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Google_Chrome_icon_%28February_2022%29.svg/2048px-Google_Chrome_icon_%28February_2022%29.svg.png" - alt="Chrome" - className="size-8" - /> - Add to Chrome - </a> - </div> - </div> - - <div className="grid grid-cols-3 w-fit gap-4 text-base font-sans font-medium tracking-normal"> - <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> - <div className="p-4 bg-white"> - <h2 className="text-lg">Remember anything, anywhere</h2> - <p className="text-zinc-600 text-sm"> - Just right-click to save instantly. - </p> - </div> - <div className="aspect-square bg-blue-500"> - <SnippetDemo /> - </div> - </div> - <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> - <div className="p-4 bg-white"> - <h2 className="text-lg">Integrate with your AI</h2> - {/* Supercharge your AI with memory */} - {/* Supercharge any AI with Supermemory. */} - {/* ChatGPT is better with Supermemory. */} - {/* Seamless integration with your workflow */} - <p className="text-zinc-600 text-sm"> - {/* Integrates with ChatGPT and Claude. */} - {/* Integrates with your chat apps */} - Enhance any prompt with Supermemory. - {/* Seamlessly */} - </p> - </div> - <div className="aspect-square bg-blue-500"> - <ChatGPTDemo /> - </div> - </div> - <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> - <div className="p-4 bg-white"> - <h2 className="text-lg">Import Twitter bookmarks</h2> - <p className="text-zinc-600 text-sm"> - Search semantically and effortlessly. - {/* Import instantly and search effortlessly. */} - </p> - </div> - <div className="aspect-square bg-blue-500"> - <TwitterDemo /> - </div> - </div> - </div> - <div className="flex w-full justify-end"> - <Button - variant="link" - size="lg" - className="text-black/40 hover:text-black font-medium! text-lg underline w-fit px-0! cursor-pointer" - onClick={nextStep} - > - Continue - <ChevronRightIcon className="size-4" /> - </Button> - </div> + const { totalSteps, nextStep, getStepNumberFor } = useOnboarding() + return ( + <div className="relative flex items-start flex-col gap-6 w-full"> + <div className="flex flex-col md:flex-row items-start md:items-center justify-between w-full gap-6"> + <div className="flex flex-col items-start text-left gap-4 flex-1"> + <NavMenu> + <p className="text-base text-white/60"> + Step {getStepNumberFor("extension")} of {totalSteps} + </p> + </NavMenu> + <h1 className="text-white font-medium text-2xl md:text-4xl"> + Install the Chrome extension + </h1> + <p className="text-white/80 text-lg md:text-2xl"> + {/* Install the Supermemory extension to start saving and organizing everything that matters. */} + Bring Supermemory everywhere + </p> + </div> + <div className="flex flex-col items-center text-center gap-4 w-full md:w-auto"> + <a + href="https://chromewebstore.google.com/detail/afpgkkipfdpeaflnpoaffkcankadgjfc?utm_source=item-share-cb" + rel="noopener noreferrer" + target="_blank" + className="bg-zinc-50/80 backdrop-blur-lg border-2 hover:bg-zinc-100/80 transition-colors duration-100 border-zinc-200/80 shadow-xs rounded-full pl-4.5 pr-6 py-4 text-lg md:text-2xl font-sans tracking-tight font-medium flex items-center gap-4 w-full md:w-auto justify-center" + > + <img + src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Google_Chrome_icon_%28February_2022%29.svg/2048px-Google_Chrome_icon_%28February_2022%29.svg.png" + alt="Chrome" + className="size-6 md:size-8" + /> + Add to Chrome + </a> + </div> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full max-w-6xl gap-4 text-base font-sans font-medium tracking-normal"> + <div className="flex flex-col w-full max-w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> + <div className="p-4 bg-white"> + <h2 className="text-lg">Remember anything, anywhere</h2> + <p className="text-zinc-600 text-sm"> + Just right-click to save instantly. + </p> </div> - ) -}
\ No newline at end of file + <div className="aspect-square bg-blue-500"> + <SnippetDemo /> + </div> + </div> + <div className="flex flex-col w-full max-w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> + <div className="p-4 bg-white"> + <h2 className="text-lg">Integrate with your AI</h2> + {/* Supercharge your AI with memory */} + {/* Supercharge any AI with Supermemory. */} + {/* ChatGPT is better with Supermemory. */} + {/* Seamless integration with your workflow */} + <p className="text-zinc-600 text-sm"> + {/* Integrates with ChatGPT and Claude. */} + {/* Integrates with your chat apps */} + Enhance any prompt with Supermemory. + {/* Seamlessly */} + </p> + </div> + <div className="aspect-square bg-blue-500"> + <ChatGPTDemo /> + </div> + </div> + <div className="flex flex-col w-full max-w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden"> + <div className="p-4 bg-white"> + <h2 className="text-lg">Import Twitter bookmarks</h2> + <p className="text-zinc-600 text-sm"> + Search semantically and effortlessly. + {/* Import instantly and search effortlessly. */} + </p> + </div> + <div className="aspect-square bg-blue-500"> + <TwitterDemo /> + </div> + </div> + </div> + <div className="flex w-full justify-end"> + <Button + variant="link" + size="lg" + className="text-black/40 hover:text-black font-medium! text-lg underline w-fit px-0! cursor-pointer" + onClick={nextStep} + > + Continue + <ChevronRightIcon className="size-4" /> + </Button> + </div> + </div> + ) +} diff --git a/apps/web/app/onboarding/intro.tsx b/apps/web/app/onboarding/intro.tsx index cc242df3..ebf9b471 100644 --- a/apps/web/app/onboarding/intro.tsx +++ b/apps/web/app/onboarding/intro.tsx @@ -11,7 +11,7 @@ export function Intro() { return ( <motion.div - className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full text-white text-center" + className="flex flex-col gap-4 relative text-2xl md:text-4xl w-full text-white text-center" layout transition={{ layout: { duration: 0.8, ease: "anticipate" }, diff --git a/apps/web/app/onboarding/name-form.tsx b/apps/web/app/onboarding/name-form.tsx index df734601..018508b1 100644 --- a/apps/web/app/onboarding/name-form.tsx +++ b/apps/web/app/onboarding/name-form.tsx @@ -1,11 +1,9 @@ "use client" -import { Button } from "@repo/ui/components/button" import { useOnboarding } from "./onboarding-context" import { useAuth } from "@lib/auth-context" import Link from "next/link" import { useEffect, useState } from "react" -import { Input } from "@ui/components/input" import { CheckIcon } from "lucide-react" import { AnimatePresence, motion } from "motion/react" import { NavMenu } from "./nav-menu" @@ -20,8 +18,7 @@ export function NameForm() { if (!name && user?.name) { setName(user.name) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user?.name]) + }, [name, user?.name]) function handleNext(): void { const trimmed = name.trim() @@ -51,13 +48,15 @@ export function NameForm() { } return ( - <div className="flex flex-col gap-4"> + <div className="flex flex-col gap-4 w-full"> <NavMenu> <p className="text-base text-white/60"> Step {getStepNumberFor("name")} of {totalSteps} </p> </NavMenu> - <p className="text-4xl text-white font-medium">What should we call you?</p> + <p className="text-2xl md:text-4xl text-white font-medium"> + What should we call you? + </p> <form onSubmit={handleSubmit} className="flex flex-col group text-white"> <div className="relative flex flex-col"> diff --git a/apps/web/app/onboarding/onboarding-background.tsx b/apps/web/app/onboarding/onboarding-background.tsx index 366648fa..ea9ba540 100644 --- a/apps/web/app/onboarding/onboarding-background.tsx +++ b/apps/web/app/onboarding/onboarding-background.tsx @@ -17,7 +17,7 @@ export function OnboardingBackground({ children }: OnboardingBackgroundProps) { currentZoomStepIndex >= 0 ? 1.0 + currentZoomStepIndex * 0.1 : 1.0 return ( - <div className="min-h-screen w-full overflow-x-hidden text-zinc-900 flex items-center justify-center relative"> + <div className="min-h-screen w-full overflow-x-hidden text-zinc-900 flex items-center justify-center relative px-4 md:px-0"> <div className="absolute inset-0 transition-transform duration-700 ease-in-out" style={{ @@ -29,7 +29,7 @@ export function OnboardingBackground({ children }: OnboardingBackgroundProps) { transformOrigin: "center bottom", }} /> - <div className="relative z-10">{children}</div> + <div className="relative z-10 w-full max-w-4xl mx-auto">{children}</div> </div> ) } diff --git a/apps/web/app/onboarding/onboarding-form.tsx b/apps/web/app/onboarding/onboarding-form.tsx index 520a2691..b943e5f4 100644 --- a/apps/web/app/onboarding/onboarding-form.tsx +++ b/apps/web/app/onboarding/onboarding-form.tsx @@ -22,7 +22,7 @@ export function OnboardingForm() { return ( <div className={cn( - "text-4xl px-6 py-8 flex flex-col justify-center max-sm:w-full", + "text-2xl md:text-4xl px-4 md:px-6 py-6 md:py-8 flex flex-col justify-center w-full max-w-4xl mx-auto", sans.variable, )} > diff --git a/apps/web/app/onboarding/welcome.tsx b/apps/web/app/onboarding/welcome.tsx index 93313f7a..a236037c 100644 --- a/apps/web/app/onboarding/welcome.tsx +++ b/apps/web/app/onboarding/welcome.tsx @@ -14,16 +14,18 @@ export function Welcome() { } return ( - <div className="flex flex-col gap-4 items-center text-center"> - <h1 className="text-white font-medium">Welcome to Supermemory</h1> - <p className="text-white/80 text-2xl"> + <div className="flex flex-col gap-4 items-center text-center w-full"> + <h1 className="text-white font-medium text-2xl md:text-4xl"> + Welcome to Supermemory + </h1> + <p className="text-white/80 text-lg md:text-2xl"> We're excited to have you on board. </p> <button type="button" onClick={handleGetStarted} - className="tracking-normal w-fit flex items-center justify-center text-2xl underline cursor-pointer font-medium text-white/80 hover:text-white transition-colors" + className="tracking-normal w-fit flex items-center justify-center text-lg md:text-2xl underline cursor-pointer font-medium text-white/80 hover:text-white transition-colors" > Get started <ArrowRightIcon className="size-4 ml-2" /> diff --git a/apps/web/components/chrome-extension-button.tsx b/apps/web/components/chrome-extension-button.tsx index 2b2ca713..8f2b74c8 100644 --- a/apps/web/components/chrome-extension-button.tsx +++ b/apps/web/components/chrome-extension-button.tsx @@ -14,12 +14,14 @@ import { useEffect, useState } from "react" import { motion } from "framer-motion" import Image from "next/image" import { analytics } from "@/lib/analytics" +import { useIsMobile } from "@hooks/use-mobile" export function ChromeExtensionButton() { const [isExtensionInstalled, setIsExtensionInstalled] = useState(false) const [isChecking, setIsChecking] = useState(true) const [isDismissed, setIsDismissed] = useState(false) const [isMinimized, setIsMinimized] = useState(false) + const isMobile = useIsMobile() useEffect(() => { const dismissed = @@ -80,8 +82,8 @@ export function ChromeExtensionButton() { setIsDismissed(true) } - // Don't show if extension is installed, checking, or dismissed - if (isExtensionInstalled || isChecking || isDismissed) { + // Don't show if extension is installed, checking, dismissed, or on mobile + if (isExtensionInstalled || isChecking || isDismissed || isMobile) { return null } |