mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
Add Dynamic Island
This commit is contained in:
parent
0a282da5b1
commit
004c952dc3
8 changed files with 285 additions and 36 deletions
194
apps/web/app/(dash)/dynamicisland.tsx
Normal file
194
apps/web/app/(dash)/dynamicisland.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client";
|
||||
|
||||
import { AddIcon } from "@repo/ui/icons";
|
||||
import Image from "next/image";
|
||||
|
||||
import { AnimatePresence, useMotionValueEvent, useScroll } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Label } from "@repo/ui/shadcn/label";
|
||||
import { Input } from "@repo/ui/shadcn/input";
|
||||
import { Textarea } from "@repo/ui/shadcn/textarea";
|
||||
|
||||
export function DynamicIsland() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useMotionValueEvent(scrollYProgress, "change", (current) => {
|
||||
// Check if current is not undefined and is a number
|
||||
if (typeof current === "number") {
|
||||
let direction = current! - scrollYProgress.getPrevious()!;
|
||||
|
||||
if (direction < 0 || direction === 1) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fixed z-40 left-1/2 -translate-x-1/2 top-12">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 1,
|
||||
y: -150,
|
||||
}}
|
||||
animate={{
|
||||
y: visible ? 0 : -150,
|
||||
opacity: visible ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<DynamicIslandContent />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicIsland;
|
||||
|
||||
function DynamicIslandContent() {
|
||||
const [show, setshow] = useState(true);
|
||||
function cancelfn(){
|
||||
setshow(true);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{show ? (
|
||||
<div
|
||||
onClick={() => setshow(!show)}
|
||||
className="bg-[#1F2428] p-2 rounded-3xl"
|
||||
>
|
||||
<Image src={AddIcon} alt="Add icon" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<ToolBar cancelfn={cancelfn} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const fakeitems = ["spaces", "page", "note"];
|
||||
|
||||
function ToolBar({cancelfn}: {cancelfn: ()=> void}) {
|
||||
const [index, setIndex] = useState(0);
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-[#1F2428] py-[.35rem] px-[.6rem] rounded-2xl">
|
||||
<HoverEffect
|
||||
items={fakeitems}
|
||||
index={index}
|
||||
indexFn={(i) => setIndex(i)}
|
||||
/>
|
||||
</div>
|
||||
{ index === 0 ?
|
||||
<SpaceForm cancelfn={cancelfn} /> :
|
||||
index === 1 ?
|
||||
<PageForm cancelfn={cancelfn} /> :
|
||||
<NoteForm cancelfn={cancelfn} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const HoverEffect = ({
|
||||
items,
|
||||
index,
|
||||
indexFn,
|
||||
}: {
|
||||
items: string[];
|
||||
index: number;
|
||||
indexFn: (i: number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3"}>
|
||||
{items.map((item, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className="relative block h-full w-full px-2 py-1"
|
||||
onClick={() => indexFn(idx)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{index === idx && (
|
||||
<motion.span
|
||||
className="absolute inset-0 block h-full w-full rounded-xl bg-[#2B3237]"
|
||||
layoutId="hoverBackground"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transition: { duration: 0.15 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.15, delay: 0.2 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<h3 className="text-[#858B92] z-50 relative">{item}</h3>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function SpaceForm({cancelfn}: {cancelfn: ()=> void}) {
|
||||
return (
|
||||
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
|
||||
<div >
|
||||
<Label className="text-[#858B92]" htmlFor="name">Name</Label>
|
||||
<Input className="bg-[#2B3237] outline-none border-none focus:border-none focus:outline-none" id="name"/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<a className="text-blue-500" href="">
|
||||
pull from store
|
||||
</a>
|
||||
<div onClick={cancelfn} className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer">cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PageForm({cancelfn}: {cancelfn: ()=> void}) {
|
||||
return (
|
||||
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
|
||||
<div >
|
||||
<Label className="text-[#858B92]" htmlFor="name">Space</Label>
|
||||
<Input className="bg-[#2B3237] outline-none border-none focus:border-none focus:outline-none" id="name"/>
|
||||
</div>
|
||||
<div >
|
||||
<Label className="text-[#858B92]" htmlFor="name">Page Url</Label>
|
||||
<Input className="bg-[#2B3237] outline-none border-none focus:border-none focus:outline-none" id="name"/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div onClick={cancelfn} className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer">cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteForm({cancelfn}: {cancelfn: ()=> void}) {
|
||||
return (
|
||||
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
|
||||
<div >
|
||||
<Label className="text-[#858B92]" htmlFor="name">Space</Label>
|
||||
<Input className="bg-[#2B3237] outline-none border-none focus:border-none focus:outline-none" id="name"/>
|
||||
</div>
|
||||
<div >
|
||||
<Label className="text-[#858B92]" htmlFor="name">Note</Label>
|
||||
<Textarea cols={4} className="bg-[#2B3237] outline-none border-none resize-none" id="name"/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div onClick={cancelfn} className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer">cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,49 +2,25 @@ import React from "react";
|
|||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Logo from "../../public/logo.svg";
|
||||
import { AddIcon, ChatIcon } from "@repo/ui/icons";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/ui/shadcn/tabs";
|
||||
import { ChatIcon } from "@repo/ui/icons";
|
||||
|
||||
import DynamicIsland from "./dynamicisland";
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
<Link href="/">
|
||||
<div className="absolute left-0 w-full flex items-center justify-between z-10">
|
||||
<Link className="px-5" href="/home">
|
||||
<Image
|
||||
src={Logo}
|
||||
alt="SuperMemory logo"
|
||||
className="hover:brightness-125 duration-200"
|
||||
className="hover:brightness-75 brightness-50 duration-200"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Tabs
|
||||
className="absolute flex flex-col justify-center items-center w-full -z-10 group top-0 transition-transform duration-1000 ease-out"
|
||||
defaultValue="account"
|
||||
>
|
||||
<div className="bg-secondary all-center h-11 rounded-full p-2 min-w-14">
|
||||
<button className="p-2 group-hover:hidden transition duration-500 ease-in-out">
|
||||
<Image src={AddIcon} alt="Add icon" />
|
||||
</button>
|
||||
<DynamicIsland />
|
||||
|
||||
<div className="hidden group-hover:flex inset-0 transition-opacity duration-500 ease-in-out">
|
||||
<TabsList className="p-2">
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-secondary all-center rounded-full p-2 mt-4 min-w-14 hidden group-hover:block">
|
||||
<TabsContent value="account">
|
||||
Make changes to your account here.
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
Change your password here.
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
<button className="flex shrink-0 duration-200 items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-secondary">
|
||||
<button className="flex shrink-0 duration-200 items-center gap-2 px-5 py-1.5 rounded-xl hover:bg-secondary">
|
||||
<Image src={ChatIcon} alt="Chat icon" />
|
||||
Start new chat
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async function Page({
|
|||
const spaces = await getSpaces();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl flex mx-auto w-full flex-col">
|
||||
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
|
||||
{/* all content goes here */}
|
||||
{/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function QueryInput({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-secondary rounded-t-[24px] w-full mt-40">
|
||||
<div className="bg-secondary rounded-t-[24px]">
|
||||
{/* input and action button */}
|
||||
<form action={async () => push(parseQ())} className="flex gap-4 p-3">
|
||||
<textarea
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function Menu() {
|
|||
return (
|
||||
<div className="absolute h-full p-4 flex items-center top-0 left-0">
|
||||
<div className="">
|
||||
<div className="hover:rounded-2x group inline-flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] bg-secondary px-3 py-4 duration-200 hover:w-40">
|
||||
<div className="group inline-flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] bg-secondary px-3 py-4 duration-200 hover:w-40">
|
||||
{menuItems.map((item) => (
|
||||
<div
|
||||
key={item.url}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ChatIcon from "./chat.svg";
|
|||
import HistoryIcon from "./history.svg";
|
||||
import ExploreIcon from "./explore.svg";
|
||||
import MemoriesIcon from "./memories.svg";
|
||||
import ArrowRightIcon from "./arrowRight.svg";
|
||||
import ArrowRightIcon from "./arrowright.svg";
|
||||
import SelectIcon from "./select.svg";
|
||||
|
||||
export {
|
||||
|
|
|
|||
55
packages/ui/shadcn/tabs.tsx
Normal file
55
packages/ui/shadcn/tabs.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@repo/ui/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-xl px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-[#2B3237] data-[state=active]:text-[#858B92] data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
24
packages/ui/shadcn/textarea.tsx
Normal file
24
packages/ui/shadcn/textarea.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@repo/ui/lib/utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Loading…
Add table
Reference in a new issue