mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
add proper responsiveness
This commit is contained in:
parent
9a410f68c0
commit
092e80ca55
1 changed files with 160 additions and 68 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import type { z } from "zod"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -194,6 +195,7 @@ function TimelineCard({
|
|||
isSelected = false,
|
||||
onToggleSelection,
|
||||
indent = false,
|
||||
isLast = false,
|
||||
}: {
|
||||
doc: DocumentWithMemories
|
||||
onOpenDocument: (doc: DocumentWithMemories) => void
|
||||
|
|
@ -201,6 +203,7 @@ function TimelineCard({
|
|||
isSelected?: boolean
|
||||
onToggleSelection?: (doc: DocumentWithMemories) => void
|
||||
indent?: boolean
|
||||
isLast?: boolean
|
||||
}) {
|
||||
const preview = getPreviewText(doc)
|
||||
const typeLabel = doc.type
|
||||
|
|
@ -221,10 +224,11 @@ function TimelineCard({
|
|||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"relative w-full text-left px-4 py-3 cursor-pointer transition-colors",
|
||||
"relative w-full text-left px-3 py-3 cursor-pointer transition-colors sm:px-4",
|
||||
indent
|
||||
? "bg-transparent hover:bg-white/[0.04]"
|
||||
: "rounded-2xl border border-[#252B35] bg-[#1B1F24] hover:bg-[#21262D]",
|
||||
? "bg-[#121820]/95 hover:bg-[#17202A]"
|
||||
: "rounded-xl border border-[#252B35] bg-[#1B1F24] hover:bg-[#21262D] sm:rounded-2xl",
|
||||
indent && !isLast && "border-b border-[#252B35]/70",
|
||||
isSelectionMode && canSelect && "pl-10",
|
||||
isSelectionMode && isSelected && "border-[#369BFD]/70 bg-[#00173C]/45",
|
||||
dmSansClassName(),
|
||||
|
|
@ -349,7 +353,7 @@ function GroupCard({
|
|||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-stretch rounded-2xl border border-[#252B35] bg-[#1B1F24] transition-colors hover:bg-[#21262D]",
|
||||
"flex w-full items-stretch rounded-xl border border-[#252B35] bg-[#1B1F24] transition-colors hover:bg-[#21262D] sm:rounded-2xl",
|
||||
isExpanded && "rounded-b-none border-b-transparent",
|
||||
isSelectionMode &&
|
||||
(isGroupSelected || isGroupPartial) &&
|
||||
|
|
@ -374,14 +378,16 @@ function GroupCard({
|
|||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 py-3 pr-4 text-left",
|
||||
isSelectionMode && selectableDocs.length > 0 ? "pl-0" : "pl-4",
|
||||
"flex min-w-0 flex-1 cursor-pointer items-start justify-between gap-2 py-3 pr-3 text-left sm:items-center sm:gap-3 sm:pr-4",
|
||||
isSelectionMode && selectableDocs.length > 0
|
||||
? "pl-0"
|
||||
: "pl-3 sm:pl-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={onToggle}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2.5 gap-y-1">
|
||||
<DocumentIcon
|
||||
type={firstDoc.type}
|
||||
source={firstDoc.source ?? undefined}
|
||||
|
|
@ -392,13 +398,13 @@ function GroupCard({
|
|||
{countLabel}
|
||||
</span>
|
||||
{preview && (
|
||||
<span className="text-[12px] text-white/35 truncate">
|
||||
<span className="basis-full text-[12px] leading-relaxed text-white/35 line-clamp-2 sm:basis-auto sm:truncate sm:leading-normal">
|
||||
· {preview}
|
||||
</span>
|
||||
)}
|
||||
{totalMemories > 0 && (
|
||||
<span
|
||||
className="text-[11px] font-medium shrink-0 ml-auto"
|
||||
className="shrink-0 text-[11px] font-medium sm:ml-auto"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
|
||||
|
|
@ -414,33 +420,55 @@ function GroupCard({
|
|||
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-3.5 text-white/20 shrink-0 transition-transform duration-200",
|
||||
"mt-0.5 size-3.5 text-white/20 shrink-0 transition-transform duration-200 sm:mt-0",
|
||||
isExpanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div
|
||||
id={`group-${expandKey}`}
|
||||
className="border border-t-0 border-[#252B35] rounded-b-2xl overflow-hidden divide-y divide-[#252B35]"
|
||||
>
|
||||
{group.docs.map((doc) => (
|
||||
<TimelineCard
|
||||
key={doc.id}
|
||||
doc={doc}
|
||||
onOpenDocument={onOpenDocument}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={doc.id ? selectedDocumentIds.has(doc.id) : false}
|
||||
onToggleSelection={(doc) => {
|
||||
if (doc.id) onToggleSelection?.(doc.id)
|
||||
}}
|
||||
indent
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
id={`group-${expandKey}`}
|
||||
className="overflow-hidden rounded-b-2xl border border-t-0 border-[#252B35] bg-[#121820]/95 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
initial={{ height: 0, opacity: 0, y: -4 }}
|
||||
animate={{ height: "auto", opacity: 1, y: 0 }}
|
||||
exit={{ height: 0, opacity: 0, y: -3 }}
|
||||
transition={{
|
||||
height: { duration: 0.28, ease: [0.4, 0, 0.2, 1] },
|
||||
opacity: { duration: 0.18 },
|
||||
y: { duration: 0.22, ease: [0.4, 0, 0.2, 1] },
|
||||
}}
|
||||
>
|
||||
{group.docs.map((doc, index) => (
|
||||
<motion.div
|
||||
key={doc.id}
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -3 }}
|
||||
transition={{
|
||||
duration: 0.18,
|
||||
delay: Math.min(index * 0.025, 0.12),
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
>
|
||||
<TimelineCard
|
||||
doc={doc}
|
||||
onOpenDocument={onOpenDocument}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={doc.id ? selectedDocumentIds.has(doc.id) : false}
|
||||
onToggleSelection={(doc) => {
|
||||
if (doc.id) onToggleSelection?.(doc.id)
|
||||
}}
|
||||
indent
|
||||
isLast={index === group.docs.length - 1}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -506,57 +534,121 @@ export function TimelineView({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full max-w-[780px] mx-auto py-4 pb-12 space-y-6",
|
||||
"w-full max-w-[820px] mx-auto py-3 pb-12 space-y-5 sm:py-4 sm:space-y-7",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{periodGroups.map((period) => (
|
||||
<div key={period.label} className="grid grid-cols-[88px_1fr] gap-x-4">
|
||||
<div className="pt-3 text-right shrink-0">
|
||||
<span className="text-[10px] text-white/30 font-medium uppercase tracking-[0.15em] leading-none">
|
||||
{period.label}
|
||||
</span>
|
||||
</div>
|
||||
{periodGroups.map((period, periodIndex) => {
|
||||
const periodHasExpandedGroup = period.typeGroups.some((group) =>
|
||||
expandedGroups.has(`${period.label}::${group.categoryInfo.key}`),
|
||||
)
|
||||
|
||||
<div className="space-y-1.5 min-w-0">
|
||||
{period.typeGroups.map((group) => {
|
||||
const expandKey = `${period.label}::${group.categoryInfo.key}`
|
||||
return (
|
||||
<div
|
||||
key={period.label}
|
||||
className="grid grid-cols-[22px_minmax(0,1fr)] gap-x-2 gap-y-2 sm:grid-cols-[96px_24px_minmax(0,1fr)] sm:gap-x-3 sm:gap-y-0"
|
||||
>
|
||||
<div className="col-start-2 row-start-1 shrink-0 pt-0 sm:col-start-1 sm:row-start-1 sm:pt-3 sm:text-right">
|
||||
<span className="inline-flex rounded-full border border-[#252B35] bg-[#0D121A] px-2.5 py-1 text-[10px] font-medium uppercase leading-none tracking-[0.15em] text-white/45 sm:border-transparent sm:bg-transparent sm:px-0 sm:py-0 sm:text-white/30">
|
||||
{period.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
if (group.docs.length === 1) {
|
||||
const doc = group.docs[0]
|
||||
if (!doc) return null
|
||||
<div className="relative col-start-1 row-start-1 row-span-2 flex justify-center sm:col-start-2 sm:row-start-1 sm:row-span-1">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-4 bottom-[-20px] w-px bg-[#1F2835] sm:top-5 sm:bottom-[-28px]",
|
||||
periodIndex === periodGroups.length - 1 && "bottom-2",
|
||||
)}
|
||||
/>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute top-4 bottom-[-20px] w-px origin-top bg-linear-to-b from-[#369BFD] via-[#369BFD]/70 to-transparent sm:top-5 sm:bottom-[-28px]",
|
||||
periodIndex === periodGroups.length - 1 && "bottom-2",
|
||||
)}
|
||||
initial={false}
|
||||
animate={{
|
||||
scaleY: periodHasExpandedGroup ? 1 : 0,
|
||||
opacity: periodHasExpandedGroup ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.34, ease: [0.4, 0, 0.2, 1] }}
|
||||
/>
|
||||
<motion.div
|
||||
className="relative mt-1.5 flex size-3 items-center justify-center rounded-full border border-[#369BFD]/35 bg-[#0D121A] shadow-[0_0_0_4px_rgba(54,155,253,0.06)] sm:mt-3"
|
||||
initial={false}
|
||||
animate={{
|
||||
borderColor: periodHasExpandedGroup
|
||||
? "rgba(54,155,253,0.8)"
|
||||
: "rgba(54,155,253,0.35)",
|
||||
boxShadow: periodHasExpandedGroup
|
||||
? "0 0 0 7px rgba(54,155,253,0.09)"
|
||||
: "0 0 0 4px rgba(54,155,253,0.06)",
|
||||
}}
|
||||
transition={{ duration: 0.28, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{periodHasExpandedGroup && (
|
||||
<motion.div
|
||||
className="absolute inset-[-6px] rounded-full border border-[#369BFD]/35"
|
||||
initial={{ scale: 0.6, opacity: 0.8 }}
|
||||
animate={{ scale: 1.45, opacity: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.52,
|
||||
ease: [0.2, 0.8, 0.2, 1],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className="size-1.5 rounded-full bg-[#369BFD]"
|
||||
initial={false}
|
||||
animate={{ scale: periodHasExpandedGroup ? 1.15 : 1 }}
|
||||
transition={{ duration: 0.24, ease: [0.4, 0, 0.2, 1] }}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="col-start-2 row-start-2 min-w-0 space-y-1.5 sm:col-start-3 sm:row-start-1">
|
||||
{period.typeGroups.map((group) => {
|
||||
const expandKey = `${period.label}::${group.categoryInfo.key}`
|
||||
|
||||
if (group.docs.length === 1) {
|
||||
const doc = group.docs[0]
|
||||
if (!doc) return null
|
||||
|
||||
return (
|
||||
<TimelineCard
|
||||
key={expandKey}
|
||||
doc={doc}
|
||||
onOpenDocument={onOpenDocument}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={
|
||||
doc.id ? selectedDocumentIds.has(doc.id) : false
|
||||
}
|
||||
onToggleSelection={handleTimelineCardSelection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TimelineCard
|
||||
<GroupCard
|
||||
key={expandKey}
|
||||
doc={doc}
|
||||
group={group}
|
||||
expandKey={expandKey}
|
||||
isExpanded={expandedGroups.has(expandKey)}
|
||||
onToggle={() => toggleGroup(expandKey)}
|
||||
onOpenDocument={onOpenDocument}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={
|
||||
doc.id ? selectedDocumentIds.has(doc.id) : false
|
||||
}
|
||||
onToggleSelection={handleTimelineCardSelection}
|
||||
selectedDocumentIds={selectedDocumentIds}
|
||||
onToggleSelection={onToggleSelection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupCard
|
||||
key={expandKey}
|
||||
group={group}
|
||||
expandKey={expandKey}
|
||||
isExpanded={expandedGroups.has(expandKey)}
|
||||
onToggle={() => toggleGroup(expandKey)}
|
||||
onOpenDocument={onOpenDocument}
|
||||
isSelectionMode={isSelectionMode}
|
||||
selectedDocumentIds={selectedDocumentIds}
|
||||
onToggleSelection={onToggleSelection}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
<div ref={sentinelRef} className="h-1" />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue