fix timeline responsiveness (#997)

This commit is contained in:
Ishaan Gupta 2026-05-24 14:20:27 +05:30 committed by GitHub
parent a8d36df38e
commit d30e53fc1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 184 additions and 82 deletions

View file

@ -641,27 +641,29 @@ export function MemoriesGrid({
id="selection-toolbar"
className={cn(
dmSansClassName(),
"flex items-center justify-between gap-3 mb-3 mr-2 px-3 py-2 rounded-full border border-[#2261CA33] bg-[#00173C]/40",
"flex items-center justify-between gap-1.5 mb-3 mr-2 px-2.5 py-2 rounded-full border border-[#2261CA33] bg-[#00173C]/40 sm:gap-3 sm:px-3",
)}
>
<div className="flex items-center gap-2 min-w-0">
<div className="flex min-w-0 shrink items-center gap-1.5 sm:gap-2">
<span className="flex items-center gap-1.5 text-xs text-[#FAFAFA] font-medium shrink-0">
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full bg-[#369BFD] text-[#0B0F14] text-[11px] font-semibold">
{selectedDocumentIds.size}
</span>
{selectedDocumentIds.size === 1 ? "selected" : "selected"}
<span className="hidden sm:inline">
{selectedDocumentIds.size === 1 ? "selected" : "selected"}
</span>
</span>
{selectedDocumentIds.size === 0 && (
<span className="text-xs text-[#737373] truncate">
<span className="hidden truncate text-xs text-[#737373] sm:inline">
Tap documents to select
</span>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
<div className="flex min-w-0 shrink-0 items-center gap-0.5 sm:gap-1">
<button
type="button"
className={cn(
"text-xs px-2.5 h-7 rounded-full transition-colors cursor-pointer",
"h-7 rounded-full px-2 text-xs transition-colors cursor-pointer sm:px-2.5",
allVisibleSelected
? "text-[#737373] hover:text-white"
: "text-[#FAFAFA] hover:bg-white/5",
@ -675,7 +677,7 @@ export function MemoriesGrid({
<button
type="button"
className={cn(
"flex items-center gap-1 text-xs px-3 h-7 rounded-full transition-colors cursor-pointer",
"flex h-7 items-center gap-1 rounded-full px-2 text-xs transition-colors cursor-pointer sm:px-3",
selectedDocumentIds.size === 0 || isBulkDeleting
? "text-[#737373]/60 cursor-not-allowed"
: "text-red-400 hover:text-red-300 hover:bg-red-500/10",
@ -695,7 +697,7 @@ export function MemoriesGrid({
<button
type="button"
aria-label="Exit selection mode"
className="flex items-center gap-1 text-xs px-3 h-7 rounded-full text-[#737373] hover:text-white hover:bg-white/5 transition-colors cursor-pointer"
className="flex h-7 items-center gap-1 rounded-full px-2 text-xs text-[#737373] transition-colors hover:text-white hover:bg-white/5 cursor-pointer sm:px-3"
onClick={onClearSelection}
>
<XIcon className="size-3" />

View file

@ -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,11 +224,13 @@ 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]",
isSelectionMode && canSelect && "pl-10",
? "bg-[#121820] hover:bg-[#17202A]"
: "rounded-xl border border-[#252B35] bg-[#1B1F24] hover:bg-[#21262D] sm:rounded-2xl",
indent && !isLast && "border-b border-[#252B35]/70",
indent && isLast && "rounded-b-xl sm:rounded-b-2xl",
isSelectionMode && canSelect && "pl-14 sm:pl-16",
isSelectionMode && isSelected && "border-[#369BFD]/70 bg-[#00173C]/45",
dmSansClassName(),
)}
@ -233,7 +238,7 @@ function TimelineCard({
aria-pressed={isSelectionMode ? isSelected : undefined}
>
{isSelectionMode && canSelect && (
<span className="absolute left-4 top-4">
<span className="absolute left-5 top-4 z-10">
<SelectionBox isSelected={isSelected} />
</span>
)}
@ -346,20 +351,27 @@ function GroupCard({
}
return (
<div>
<div
className={cn(
"overflow-hidden rounded-xl border border-[#252B35] bg-[#1B1F24] sm:rounded-2xl",
isSelectionMode &&
(isGroupSelected || isGroupPartial) &&
"border-[#369BFD]/70 bg-[#00173C]/45",
)}
>
<div
className={cn(
"flex w-full items-stretch rounded-2xl border border-[#252B35] bg-[#1B1F24] transition-colors hover:bg-[#21262D]",
isExpanded && "rounded-b-none border-b-transparent",
"flex w-full items-stretch transition-colors hover:bg-[#21262D]",
isExpanded && "border-b border-[#252B35]",
isSelectionMode &&
(isGroupSelected || isGroupPartial) &&
"border-[#369BFD]/70 bg-[#00173C]/45",
"bg-[#00173C]/45",
)}
>
{isSelectionMode && selectableDocs.length > 0 && (
<button
type="button"
className="flex shrink-0 items-start px-4 py-4 cursor-pointer"
className="flex w-14 shrink-0 cursor-pointer items-start justify-center py-4 sm:w-16"
onClick={handleGroupSelect}
aria-label={isGroupSelected ? "Deselect group" : "Select group"}
aria-pressed={isGroupSelected}
@ -374,14 +386,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-1"
: "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 +406,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 +428,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 bg-[#121820] 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 +542,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>