Skip to content

Commit f2984a2

Browse files
committed
fixed layout shift when filtering on changelog
1 parent f7a1c02 commit f2984a2

9 files changed

Lines changed: 259 additions & 96 deletions

File tree

app/components/AppShell.tsx

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { spectrum } from '../spectrum';
1111
import { getChangeBySlug } from '../upgrades/data/changes';
1212
import { getUpgradeById } from '../upgrades/data/upgrades';
1313

14+
import { Breadcrumb } from './ui/Breadcrumb';
15+
import { AnimatedArrowIcon, CloseIcon, VibenetIcon } from './ui/icons';
1416
import { Text } from './ui/Text';
1517

1618
function BaseLogo() {
@@ -194,11 +196,7 @@ function NavGlyph({ name }: NavGlyphProps) {
194196
</svg>
195197
);
196198
case 'vibenet':
197-
return (
198-
<svg {...common} viewBox="5 5 30 30" strokeWidth={2.5} className="nav-vibenet-icon">
199-
<path d="M30.2895 14.8575L20.0038 20.0002M20.0038 20.0002L10.2895 14.2861M20.0038 20.0002L20.0038 30.8571M30.8608 22.8275V17.1724C30.8608 15.3861 29.9078 13.7354 28.3608 12.8423L22.4737 9.44331C20.9267 8.55015 19.0207 8.55015 17.4737 9.44331L11.5865 12.8423C10.0395 13.7354 9.08649 15.3861 9.08649 17.1724V22.8275C9.08649 24.6138 10.0395 26.2644 11.5865 27.1576L17.4737 30.5566C19.0207 31.4497 20.9267 31.4497 22.4737 30.5566L28.3608 27.1576C29.9078 26.2644 30.8608 24.6138 30.8608 22.8275Z" />
200-
</svg>
201-
);
199+
return <VibenetIcon size={common.width} className="nav-vibenet-icon" />;
202200
case 'tips':
203201
return (
204202
<svg {...common}>
@@ -345,10 +343,7 @@ function GlobalBanner() {
345343
<span className="inline-block h-3.5 w-px bg-bds-gray-20"></span>
346344
<Link href="/upgrades/changelog/account-abstraction-by-account-configuration" className="group flex items-center gap-1 no-underline">
347345
<Text as="span" variant="label.medium" className="text-base-blue">Test on Vibenet</Text>
348-
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" className="text-base-blue transition-[transform] duration-200 ease-out group-hover:translate-x-[3px]" aria-hidden="true">
349-
<path d="M7.5 4L13.5 10L7.5 16" stroke="currentColor" strokeWidth="2" />
350-
<path d="M13.5 10H0" stroke="currentColor" strokeWidth="2" strokeDasharray="13.5" strokeDashoffset="13.5" className="transition-[stroke-dashoffset] duration-200 ease-out group-hover:[stroke-dashoffset:0]" />
351-
</svg>
346+
<AnimatedArrowIcon size={14} strokeWidth={2} className="text-base-blue transition-transform duration-200 ease-out group-hover:translate-x-[3px]" />
352347
</Link>
353348
</span>
354349
<button
@@ -357,10 +352,7 @@ function GlobalBanner() {
357352
className="absolute right-4 flex h-5 w-5 items-center justify-center text-bds-gray-40 transition-colors hover:text-black"
358353
aria-label="Dismiss banner"
359354
>
360-
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
361-
<line x1="1" y1="1" x2="9" y2="9" />
362-
<line x1="9" y1="1" x2="1" y2="9" />
363-
</svg>
355+
<CloseIcon size={10} />
364356
</button>
365357
</div>
366358
);
@@ -437,26 +429,22 @@ export function AppShell({ children }: PropsWithChildren) {
437429
if (slugMatch) {
438430
const change = getChangeBySlug(slugMatch[1]);
439431
return (
440-
<span className="flex items-center gap-2">
441-
<Link href="/upgrades/changelog" className="no-underline">
442-
<Text as="span" variant="headline" className="text-bds-gray-40">Changelog</Text>
443-
</Link>
444-
<Text as="span" variant="headline" className="text-bds-gray-30">/</Text>
445-
<Text as="span" variant="headline">{change?.title ?? slugMatch[1]}</Text>
446-
</span>
432+
<Breadcrumb
433+
parentLabel="Changelog"
434+
parentHref="/upgrades/changelog"
435+
childLabel={change?.title ?? slugMatch[1]}
436+
/>
447437
);
448438
}
449439
const upgradeMatch = pathname.match(/^\/upgrades\/upgrade\/(.+)$/);
450440
if (upgradeMatch) {
451441
const upgrade = getUpgradeById(upgradeMatch[1]);
452442
return (
453-
<span className="flex items-center gap-2">
454-
<Link href="/upgrades" className="no-underline">
455-
<Text as="span" variant="headline" className="text-bds-gray-40">Upgrades</Text>
456-
</Link>
457-
<Text as="span" variant="headline" className="text-bds-gray-30">/</Text>
458-
<Text as="span" variant="headline">{upgrade?.name ?? upgradeMatch[1]}</Text>
459-
</span>
443+
<Breadcrumb
444+
parentLabel="Upgrades"
445+
parentHref="/upgrades"
446+
childLabel={upgrade?.name ?? upgradeMatch[1]}
447+
/>
460448
);
461449
}
462450
return <Text as="span" variant="headline">{title}</Text>;

app/components/ui/Breadcrumb.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Link from 'next/link';
2+
3+
import { Text } from './Text';
4+
5+
type BreadcrumbProps = {
6+
parentLabel: string;
7+
parentHref: string;
8+
childLabel: string;
9+
};
10+
11+
export function Breadcrumb({ parentLabel, parentHref, childLabel }: BreadcrumbProps) {
12+
return (
13+
<span className="flex items-center gap-2">
14+
<Link href={parentHref} className="no-underline">
15+
<Text as="span" variant="headline" className="text-bds-gray-40">
16+
{parentLabel}
17+
</Text>
18+
</Link>
19+
<Text as="span" variant="headline" className="text-bds-gray-30">
20+
/
21+
</Text>
22+
<Text as="span" variant="headline">
23+
{childLabel}
24+
</Text>
25+
</span>
26+
);
27+
}

app/components/ui/Button.tsx

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ComponentPropsWithoutRef } from 'react';
22
import Link from 'next/link';
33

44
import { cn } from './cn';
5+
import { AnimatedArrowIcon } from './icons';
56
import { textVariantClasses } from './Text';
67

78
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
@@ -21,27 +22,6 @@ const variantClasses = {
2122
'text-foreground bg-transparent border border-bds-gray-10 hover:bg-bds-gray-5 dark:border-white/[.12] dark:hover:bg-white/[.06]',
2223
} as const;
2324

24-
const arrowSvg = (
25-
<svg
26-
width="16"
27-
height="16"
28-
viewBox="0 0 20 20"
29-
fill="none"
30-
aria-hidden="true"
31-
className="transition-transform duration-300 ease-out group-hover:translate-x-[3px]"
32-
>
33-
<path d="M7.5 4L13.5 10L7.5 16" stroke="currentColor" strokeWidth="1.5" />
34-
<path
35-
d="M13.5 10H0"
36-
stroke="currentColor"
37-
strokeWidth="1.5"
38-
strokeDasharray="13.5"
39-
strokeDashoffset="13.5"
40-
className="transition-all duration-300 ease-out group-hover:[stroke-dashoffset:0]"
41-
/>
42-
</svg>
43-
);
44-
4525
export function Button({
4626
className = '',
4727
type = 'button',
@@ -76,15 +56,15 @@ export function Button({
7656
return (
7757
<Link href={href} target={target} rel={rel} className={classes}>
7858
{children}
79-
{arrow && arrowSvg}
59+
{arrow && <AnimatedArrowIcon className="transition-transform duration-200 ease-out group-hover:translate-x-[3px]" />}
8060
</Link>
8161
);
8262
}
8363

8464
return (
8565
<button type={type === 'submit' ? 'submit' : 'button'} className={classes} {...props}>
8666
{children}
87-
{arrow && arrowSvg}
67+
{arrow && <AnimatedArrowIcon className="transition-transform duration-200 ease-out group-hover:translate-x-[3px]" />}
8868
</button>
8969
);
9070
}

app/components/ui/FadeInUp.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import type { PropsWithChildren } from 'react';
4+
import { motion } from 'motion/react';
5+
6+
type FadeInUpProps = PropsWithChildren<{
7+
index?: number;
8+
offset?: number;
9+
duration?: number;
10+
className?: string;
11+
}>;
12+
13+
const EASE = [0.23, 1, 0.32, 1];
14+
15+
export function FadeInUp({
16+
index = 0,
17+
offset = 8,
18+
duration = 0.35,
19+
className,
20+
children,
21+
}: FadeInUpProps) {
22+
return (
23+
<motion.div
24+
initial={{ opacity: 0, transform: `translateY(${offset}px)` }}
25+
animate={{ opacity: 1, transform: 'translateY(0px)' }}
26+
transition={{ duration, ease: EASE, delay: index * 0.04 }}
27+
className={className}
28+
>
29+
{children}
30+
</motion.div>
31+
);
32+
}

app/components/ui/LabeledCard.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { PropsWithChildren } from 'react';
2+
3+
import { cn } from './cn';
4+
import { Text } from './Text';
5+
6+
type LabeledCardProps = PropsWithChildren<{
7+
label: string;
8+
labelSpacing?: string;
9+
className?: string;
10+
}>;
11+
12+
export function LabeledCard({
13+
label,
14+
labelSpacing = 'mb-4',
15+
className,
16+
children,
17+
}: LabeledCardProps) {
18+
return (
19+
<div className={cn('rounded-xl border border-bds-gray-10 p-5', className)}>
20+
<Text variant="label.medium" tone="muted" className={labelSpacing}>
21+
{label}
22+
</Text>
23+
{children}
24+
</div>
25+
);
26+
}

app/components/ui/icons.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { SVGProps } from 'react';
2+
3+
const VIBENET_PATH =
4+
'M30.2895 14.8575L20.0038 20.0002M20.0038 20.0002L10.2895 14.2861M20.0038 20.0002L20.0038 30.8571M30.8608 22.8275V17.1724C30.8608 15.3861 29.9078 13.7354 28.3608 12.8423L22.4737 9.44331C20.9267 8.55015 19.0207 8.55015 17.4737 9.44331L11.5865 12.8423C10.0395 13.7354 9.08649 15.3861 9.08649 17.1724V22.8275C9.08649 24.6138 10.0395 26.2644 11.5865 27.1576L17.4737 30.5566C19.0207 31.4497 20.9267 31.4497 22.4737 30.5566L28.3608 27.1576C29.9078 26.2644 30.8608 24.6138 30.8608 22.8275Z';
5+
6+
export function ExternalLinkIcon({ className }: { className?: string }) {
7+
return (
8+
<svg
9+
aria-hidden="true"
10+
width="14"
11+
height="14"
12+
viewBox="0 0 16 16"
13+
fill="none"
14+
stroke="currentColor"
15+
strokeWidth="1.5"
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
className={className}
19+
>
20+
<path d="M5 11L11 5M11 5H6M11 5V10" />
21+
</svg>
22+
);
23+
}
24+
25+
export function AnimatedArrowIcon({
26+
size = 16,
27+
strokeWidth = 1.5,
28+
className,
29+
}: {
30+
size?: number;
31+
strokeWidth?: number;
32+
className?: string;
33+
}) {
34+
return (
35+
<svg
36+
width={size}
37+
height={size}
38+
viewBox="0 0 20 20"
39+
fill="none"
40+
aria-hidden="true"
41+
className={className}
42+
>
43+
<path d="M7.5 4L13.5 10L7.5 16" stroke="currentColor" strokeWidth={strokeWidth} />
44+
<path
45+
d="M13.5 10H0"
46+
stroke="currentColor"
47+
strokeWidth={strokeWidth}
48+
strokeDasharray="13.5"
49+
strokeDashoffset="13.5"
50+
className="transition-[stroke-dashoffset] duration-200 ease-out group-hover:[stroke-dashoffset:0]"
51+
/>
52+
</svg>
53+
);
54+
}
55+
56+
export function CloseIcon({ size = 12, className }: { size?: number; className?: string }) {
57+
return (
58+
<svg
59+
aria-hidden="true"
60+
width={size}
61+
height={size}
62+
viewBox="0 0 12 12"
63+
fill="none"
64+
stroke="currentColor"
65+
strokeWidth="1.5"
66+
strokeLinecap="round"
67+
className={className}
68+
>
69+
<line x1="2" y1="2" x2="10" y2="10" />
70+
<line x1="10" y1="2" x2="2" y2="10" />
71+
</svg>
72+
);
73+
}
74+
75+
export function VibenetIcon(props: SVGProps<SVGSVGElement> & { size?: number }) {
76+
const { size = 18, className, ...rest } = props;
77+
return (
78+
<svg
79+
width={size}
80+
height={size}
81+
viewBox="5 5 30 30"
82+
fill="none"
83+
stroke="currentColor"
84+
strokeWidth="2.5"
85+
strokeLinecap="round"
86+
strokeLinejoin="round"
87+
aria-hidden="true"
88+
className={className}
89+
{...rest}
90+
>
91+
<path d={VIBENET_PATH} />
92+
</svg>
93+
);
94+
}

0 commit comments

Comments
 (0)