mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
add responsivness in share graph, bookmark section, navigation controls
This commit is contained in:
parent
68a7781de7
commit
96f8bd99c2
5 changed files with 97 additions and 57 deletions
|
|
@ -238,8 +238,8 @@ function PillButton({
|
|||
disabled={disabled}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"relative flex h-9 min-w-[116px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-5",
|
||||
"text-[14px] font-medium text-[#FAFAFA]",
|
||||
"relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
|
||||
"text-[12px] font-medium text-[#FAFAFA] sm:text-[14px]",
|
||||
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
|
||||
"cursor-pointer transition-opacity hover:opacity-80",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
|
|
@ -253,15 +253,17 @@ function PillButton({
|
|||
function DocsLink({ href }: { href: string }) {
|
||||
return (
|
||||
<a
|
||||
aria-label="Open plugin docs"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex shrink-0 items-center gap-1 text-[12px] text-[#A1A1AA] transition-colors hover:text-white",
|
||||
"flex size-8 shrink-0 items-center justify-center gap-1 rounded-full text-[12px] text-[#A1A1AA] transition-colors hover:text-white sm:h-auto sm:w-auto sm:justify-start sm:rounded-none",
|
||||
)}
|
||||
>
|
||||
<BookOpen className="size-3.5" /> Docs
|
||||
<BookOpen className="size-3.5" />{" "}
|
||||
<span className="hidden sm:inline">Docs</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
@ -304,7 +306,7 @@ function ConnectedPill({
|
|||
type="button"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex h-9 min-w-[116px] shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-[#0D121A] px-4 text-[13px] font-medium text-[#00AC3F]",
|
||||
"flex h-8 min-w-[104px] shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-medium text-[#00AC3F] sm:h-9 sm:min-w-[116px] sm:gap-2 sm:px-4 sm:text-[13px]",
|
||||
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80",
|
||||
)}
|
||||
>
|
||||
|
|
@ -376,21 +378,21 @@ function PluginRow({
|
|||
}) {
|
||||
const isConnected = connectedKeys.length > 0
|
||||
return (
|
||||
<div className="flex items-center gap-3.5 border-b border-white/[0.06] py-4 last:border-b-0">
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-2.5 border-b border-white/[0.06] py-4 last:border-b-0 sm:flex sm:items-center sm:gap-3.5">
|
||||
<PluginIconBox
|
||||
src={plugin.icon}
|
||||
alt={plugin.name}
|
||||
dimmed={needsProUpgrade && !isConnected}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{isConnected && (
|
||||
<span className="size-1.5 shrink-0 rounded-full bg-[#00AC3F]" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"truncate text-[14px] font-medium text-[#FAFAFA]",
|
||||
"min-w-0 truncate text-[14px] font-medium text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{plugin.name}
|
||||
|
|
@ -400,13 +402,13 @@ function PluginRow({
|
|||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"mt-0.5 truncate text-[13px] text-[#A1A1AA]",
|
||||
"mt-0.5 line-clamp-2 text-[12px] leading-snug text-[#A1A1AA] sm:truncate sm:text-[13px]",
|
||||
)}
|
||||
>
|
||||
{plugin.tagline}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-4">
|
||||
<div className="col-start-2 flex min-w-0 shrink-0 items-center gap-2 sm:col-start-auto sm:gap-4">
|
||||
{plugin.docsUrl && <DocsLink href={plugin.docsUrl} />}
|
||||
{isConnected ? (
|
||||
<ConnectedPill connectedKeys={connectedKeys} onRevoke={onRevoke} />
|
||||
|
|
@ -782,7 +784,7 @@ export function PluginsDetail() {
|
|||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-[14px] bg-[#14161A] p-6",
|
||||
"relative overflow-hidden rounded-[14px] bg-[#14161A] p-4 sm:p-6",
|
||||
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full p-8">
|
||||
<div className="flex flex-col flex-1 w-full p-5 sm:p-8">
|
||||
<div className="mx-auto w-full max-w-3xl flex flex-col flex-1">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
|
|
@ -63,13 +63,13 @@ export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
|
|||
need, right when you need it.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mb-6 max-w-5xl w-full">
|
||||
<div className="grid w-full max-w-5xl grid-cols-1 gap-3 mb-6 sm:grid-cols-3 sm:gap-4">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.number}
|
||||
className="flex flex-col items-center text-center bg-[#080B0F] p-3 rounded-[10px]"
|
||||
className="flex items-center gap-3 rounded-[10px] bg-[#080B0F] p-3 sm:flex-col sm:items-center sm:gap-0 sm:text-center"
|
||||
>
|
||||
<div className="rounded-2xl p-6 mb-3 w-full aspect-4/4 flex items-center justify-center relative overflow-hidden">
|
||||
<div className="relative flex size-24 shrink-0 items-center justify-center overflow-hidden rounded-2xl sm:mb-3 sm:h-auto sm:w-full sm:aspect-square">
|
||||
<Image
|
||||
src={step.image}
|
||||
alt={`Step ${step.number}`}
|
||||
|
|
@ -78,15 +78,15 @@ export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
|
|||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="mb-2">
|
||||
<div className="flex min-w-0 flex-col items-start justify-start">
|
||||
<div className="mb-1 sm:mb-2">
|
||||
<span className="text-white text-sm font-medium">
|
||||
Step {step.number}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-[#8B8B8B] text-sm text-start",
|
||||
"text-[#8B8B8B] text-sm text-start leading-snug",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -258,13 +258,14 @@ const SocialButton = ({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="size-8 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
|
||||
className="size-8 shrink-0 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
|
||||
style={{
|
||||
background: "#0D121A",
|
||||
border: "0.875px solid rgba(115, 115, 115, 0.2)",
|
||||
boxShadow: "inset 1.3125px 1.3125px 3.9375px rgba(0, 0, 0, 0.7)",
|
||||
}}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
|
|
@ -387,7 +388,7 @@ export function ShareModal({
|
|||
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"w-[90%]! max-w-[706px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-4 rounded-[22px]",
|
||||
"w-[calc(100vw-20px)]! max-w-[706px]! max-h-[min(92dvh,720px)] overflow-y-auto border-none bg-[#1B1F24] flex flex-col p-3 sm:p-4 gap-3 sm:gap-4 rounded-[20px] sm:rounded-[22px]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -396,13 +397,14 @@ export function ShareModal({
|
|||
}}
|
||||
showCloseButton={false}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 min-w-0">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center px-2">
|
||||
<DialogHeader className="flex-1">
|
||||
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem] items-center gap-2 px-1 sm:px-2">
|
||||
<div aria-hidden />
|
||||
<DialogHeader className="min-w-0">
|
||||
<DialogTitle
|
||||
className={cn(
|
||||
"font-semibold text-[#fafafa] text-base text-center",
|
||||
"font-semibold text-[#fafafa] text-sm sm:text-base text-center leading-tight text-balance",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
|
|
@ -411,7 +413,7 @@ export function ShareModal({
|
|||
</DialogHeader>
|
||||
<DialogPrimitive.Close
|
||||
onClick={handleClose}
|
||||
className="bg-[#0D121A] size-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
|
||||
className="bg-[#0D121A] size-7 justify-self-end flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 0.711px 2.842px 0 rgba(0, 0, 0, 0.25), 0.178px 0.178px 0.178px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
|
|
@ -425,7 +427,7 @@ export function ShareModal({
|
|||
{/* Preview area */}
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="relative w-full aspect-[674/505] rounded-[14px] overflow-hidden"
|
||||
className="relative w-full aspect-[674/505] max-h-[48dvh] rounded-[14px] overflow-hidden"
|
||||
style={{
|
||||
boxShadow: "inset 2.42px 2.42px 4.26316px rgba(11, 15, 21, 0.7)",
|
||||
}}
|
||||
|
|
@ -462,9 +464,9 @@ export function ShareModal({
|
|||
</div>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* Theme selectors */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
||||
<ThemeButton
|
||||
theme="gradient"
|
||||
isSelected={selectedTheme === "gradient"}
|
||||
|
|
@ -483,12 +485,12 @@ export function ShareModal({
|
|||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid w-full grid-cols-[minmax(0,1fr)_2rem_2rem_2rem_2rem] items-center gap-2 sm:flex sm:w-auto">
|
||||
<Button
|
||||
onClick={handleCopySnapshot}
|
||||
disabled={isCopying}
|
||||
className={cn(
|
||||
"h-8 px-3 rounded-full text-sm font-normal gap-1.5",
|
||||
"h-8 min-w-0 justify-center px-3 rounded-full text-sm font-normal gap-1.5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -500,11 +502,11 @@ export function ShareModal({
|
|||
{copied ? (
|
||||
<>
|
||||
<Check className="size-4 text-green-500" />
|
||||
<span className="text-green-500">Copied!</span>
|
||||
<span className="truncate text-green-500">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-white">Copy snapshot</span>
|
||||
<span className="truncate text-white">Copy snapshot</span>
|
||||
<Copy className="size-4 text-[#737373]" />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ export function MemoryGraph({
|
|||
containerSize.height,
|
||||
colors,
|
||||
)
|
||||
const isCompactViewport = containerSize.width > 0 && containerSize.width < 640
|
||||
const graphFitHeight = isCompactViewport
|
||||
? Math.max(containerSize.height - 170, 240)
|
||||
: containerSize.height
|
||||
|
||||
// Rebuild version chain index during render (not in an effect) so that
|
||||
// the chain data is up-to-date when getChain() is called in useMemo below.
|
||||
|
|
@ -127,7 +131,8 @@ export function MemoryGraph({
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Auto-fit when data first loads
|
||||
// Auto-fit when data first loads. Mobile needs a few passes because the
|
||||
// force simulation can move nodes after the first layout frame.
|
||||
const hasAutoFittedRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -136,22 +141,47 @@ export function MemoryGraph({
|
|||
viewportRef.current &&
|
||||
containerSize.width > 0
|
||||
) {
|
||||
const timer = setTimeout(() => {
|
||||
viewportRef.current?.fitToNodes(
|
||||
nodes,
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
)
|
||||
hasAutoFittedRef.current = true
|
||||
}, 100)
|
||||
return () => clearTimeout(timer)
|
||||
const fitDelays = isCompactViewport ? [100, 450, 900] : [100]
|
||||
const timers = fitDelays.map((delay, index) =>
|
||||
setTimeout(() => {
|
||||
viewportRef.current?.fitToNodes(
|
||||
nodes,
|
||||
containerSize.width,
|
||||
graphFitHeight,
|
||||
)
|
||||
if (index === fitDelays.length - 1) {
|
||||
hasAutoFittedRef.current = true
|
||||
}
|
||||
}, delay),
|
||||
)
|
||||
return () => {
|
||||
for (const timer of timers) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [nodes, containerSize.width, containerSize.height])
|
||||
}, [nodes, containerSize.width, graphFitHeight, isCompactViewport])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCompactViewport || nodes.length === 0 || !viewportRef.current) return
|
||||
const timer = setTimeout(() => {
|
||||
viewportRef.current?.fitToNodes(
|
||||
nodes,
|
||||
containerSize.width,
|
||||
graphFitHeight,
|
||||
)
|
||||
}, 120)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isCompactViewport, nodes, containerSize.width, graphFitHeight])
|
||||
|
||||
useEffect(() => {
|
||||
if (nodes.length === 0) hasAutoFittedRef.current = false
|
||||
}, [nodes.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompactViewport) {
|
||||
hasAutoFittedRef.current = false
|
||||
}
|
||||
}, [isCompactViewport])
|
||||
|
||||
// Container resize observer
|
||||
useEffect(() => {
|
||||
const el = containerRef.current
|
||||
|
|
@ -190,7 +220,6 @@ export function MemoryGraph({
|
|||
const loadMoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const loadMoreRef = useRef({ hasMore, isLoadingMore, onLoadMore })
|
||||
loadMoreRef.current = { hasMore, isLoadingMore, onLoadMore }
|
||||
|
||||
const handleViewportChange = useCallback(
|
||||
(zoom: number, popoverVisible: boolean) => {
|
||||
setZoomDisplay(Math.round(zoom * 100))
|
||||
|
|
@ -251,9 +280,9 @@ export function MemoryGraph({
|
|||
viewportRef.current.fitToNodes(
|
||||
nodes,
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
graphFitHeight,
|
||||
)
|
||||
}, [nodes, containerSize.width, containerSize.height])
|
||||
}, [nodes, containerSize.width, graphFitHeight])
|
||||
|
||||
const handleCenter = useCallback(() => {
|
||||
if (nodes.length === 0 || !viewportRef.current) return
|
||||
|
|
@ -267,21 +296,21 @@ export function MemoryGraph({
|
|||
sx / nodes.length,
|
||||
sy / nodes.length,
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
graphFitHeight,
|
||||
)
|
||||
}, [nodes, containerSize.width, containerSize.height])
|
||||
}, [nodes, containerSize.width, graphFitHeight])
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
const vp = viewportRef.current
|
||||
if (!vp) return
|
||||
vp.zoomTo(vp.zoom * 1.3, containerSize.width / 2, containerSize.height / 2)
|
||||
}, [containerSize.width, containerSize.height])
|
||||
vp.zoomTo(vp.zoom * 1.3, containerSize.width / 2, graphFitHeight / 2)
|
||||
}, [containerSize.width, graphFitHeight])
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
const vp = viewportRef.current
|
||||
if (!vp) return
|
||||
vp.zoomTo(vp.zoom / 1.3, containerSize.width / 2, containerSize.height / 2)
|
||||
}, [containerSize.width, containerSize.height])
|
||||
vp.zoomTo(vp.zoom / 1.3, containerSize.width / 2, graphFitHeight / 2)
|
||||
}, [containerSize.width, graphFitHeight])
|
||||
|
||||
// Wrap onOpenDocument to dismiss the popover before opening the modal.
|
||||
// Without this, the popover (z-index: 100) stays mounted on top of the
|
||||
|
|
@ -619,7 +648,7 @@ export function MemoryGraph({
|
|||
|
||||
const navControlsStyle: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
bottom: 72,
|
||||
bottom: isCompactViewport ? 148 : 72,
|
||||
left: 16,
|
||||
zIndex: 15,
|
||||
}
|
||||
|
|
@ -683,6 +712,7 @@ export function MemoryGraph({
|
|||
<div style={navControlsStyle}>
|
||||
<NavigationControls
|
||||
nodes={nodes}
|
||||
compact={isCompactViewport}
|
||||
onAutoFit={handleAutoFit}
|
||||
onCenter={handleCenter}
|
||||
onZoomIn={handleZoomIn}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface NavigationControlsProps {
|
|||
onAutoFit: () => void
|
||||
nodes: GraphNode[]
|
||||
className?: string
|
||||
compact?: boolean
|
||||
zoomLevel: number
|
||||
colors: GraphThemeColors
|
||||
}
|
||||
|
|
@ -87,6 +88,7 @@ export const NavigationControls = memo<NavigationControlsProps>(
|
|||
onAutoFit,
|
||||
nodes,
|
||||
className = "",
|
||||
compact = false,
|
||||
zoomLevel,
|
||||
colors,
|
||||
}) => {
|
||||
|
|
@ -94,8 +96,12 @@ export const NavigationControls = memo<NavigationControlsProps>(
|
|||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
flexDirection: compact ? "row" : "column",
|
||||
alignItems: compact ? "center" : "stretch",
|
||||
gap: compact ? 6 : 4,
|
||||
maxWidth: compact ? "calc(100vw - 32px)" : undefined,
|
||||
overflowX: compact ? "auto" : undefined,
|
||||
overflowY: "hidden",
|
||||
}
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
|
|
@ -147,12 +153,12 @@ export const NavigationControls = memo<NavigationControlsProps>(
|
|||
<div className={className} style={containerStyle}>
|
||||
<NavBtn onClick={onAutoFit} colors={colors}>
|
||||
<span style={labelStyle}>Fit</span>
|
||||
<KeyBadge keys="Z" colors={colors} />
|
||||
{!compact && <KeyBadge keys="Z" colors={colors} />}
|
||||
</NavBtn>
|
||||
|
||||
<NavBtn onClick={onCenter} colors={colors}>
|
||||
<span style={labelStyle}>Center</span>
|
||||
<KeyBadge keys="C" colors={colors} />
|
||||
{!compact && <KeyBadge keys="C" colors={colors} />}
|
||||
</NavBtn>
|
||||
|
||||
<div style={zoomRowStyle}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue