Compare commits

...

2 Commits

Author SHA1 Message Date
sagit 79c28103d5 fix(frontend): prevent font blur in sortable card components (#185)
Add backface-visibility: hidden and font-smoothing properties to SortableItem components in tunnel, forward, and node pages to prevent GPU subpixel rendering blur during drag operations.

- tunnel.tsx: SortableItem wrapper anti-blur fix
- forward.tsx: SortableCard wrapper anti-blur fix
- node.tsx: SortableItem component anti-blur fix

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-22 15:11:47 +08:00
sagit f36bf1437c fix(frontend): prevent font blurriness caused by scale animations (#183)
Remove scale transforms from Framer Motion animations that cause subpixel rendering issues on text elements. Replace with opacity + translateY for smooth animations without blur.

- Remove scale from FadeIn component (animated-page.tsx)
- Remove scale from login page entrance animation (index.tsx)
- Remove scale from search bar button animation (search-bar.tsx)
- Remove whileTap scale from sidebar menu buttons (admin.tsx)
- Remove scale from captcha modal animation (globals.css)
- Add .gpu-accelerated utility class for future use

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-22 13:28:47 +08:00
8 changed files with 29 additions and 12 deletions
@@ -100,6 +100,7 @@ export const StaggerItem = ({
/**
* Simple fade-in animation for standalone elements (chips, badges, counters, etc.)
* Uses opacity + translateY to avoid font blurriness caused by scale transforms.
*/
export const FadeIn = ({
children,
@@ -111,9 +112,9 @@ export const FadeIn = ({
className?: string;
}) => (
<motion.div
animate={{ opacity: 1, scale: 1 }}
animate={{ opacity: 1, y: 0 }}
className={className}
initial={{ opacity: 0, scale: 0.95 }}
initial={{ opacity: 0, y: 8 }}
transition={{ duration: 0.2, delay, ease: "easeOut" }}
>
{children}
+2 -2
View File
@@ -29,8 +29,8 @@ export function SearchBar({
<motion.div
key="search-btn"
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
initial={{ opacity: 0, scale: 0.8 }}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
transition={{ duration: 0.12 }}
>
<Button
-1
View File
@@ -346,7 +346,6 @@ export default function AdminLayout({
}
`}
transition={{ duration: 0.15 }}
whileTap={{ scale: 0.97 }}
onClick={() => handleMenuClick(item.path)}
>
{isActive && (
+4 -1
View File
@@ -1175,11 +1175,14 @@ export default function ForwardPage() {
isDragging,
} = useSortable({ id: forward.id });
const style = {
const style: React.CSSProperties = {
transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1,
willChange: "transform",
backfaceVisibility: "hidden",
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale",
};
return (
+2 -2
View File
@@ -154,9 +154,9 @@ export default function IndexPage() {
<DefaultLayout>
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100dvh-120px)] sm:min-h-[calc(100dvh-200px)]">
<motion.div
animate={{ opacity: 1, y: 0, scale: 1 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md px-4 sm:px-0"
initial={{ opacity: 0, y: 24, scale: 0.97 }}
initial={{ opacity: 0, y: 24 }}
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<Card className="w-full">
+4 -1
View File
@@ -129,11 +129,14 @@ const SortableItem = ({
isDragging,
} = useSortable({ id });
const style = {
const style: React.CSSProperties = {
transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1,
willChange: "transform",
backfaceVisibility: "hidden",
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale",
};
return (
+4 -1
View File
@@ -666,11 +666,14 @@ export default function TunnelPage() {
isDragging,
} = useSortable({ id });
const style = {
const style: React.CSSProperties = {
transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1,
willChange: "transform",
backfaceVisibility: "hidden",
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale",
};
return (
+10 -2
View File
@@ -2,6 +2,14 @@
@import "tw-animate-css";
@import "./tailwind-theme.pcss";
/* GPU acceleration helper - prevents font blurriness during transforms */
.gpu-accelerated {
will-change: transform;
backface-visibility: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root {
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
@@ -181,12 +189,12 @@ body {
@keyframes captchaModalIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
transform: translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
transform: translateY(0);
}
}