add responsivness in share graph, bookmark section, navigation controls

This commit is contained in:
Ishaan Gupta 2026-05-13 23:07:32 +05:30
parent 68a7781de7
commit 96f8bd99c2
5 changed files with 97 additions and 57 deletions

View file

@ -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)]",
)}
>

View file

@ -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(),
)}
>

View file

@ -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]" />
</>
)}

View file

@ -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}

View file

@ -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}>