feat: implement glassmorphic depth design system for UI components

- Introduced a comprehensive glassmorphic design system to enhance visual hierarchy in UI elements such as modals, toasts, and floating panels.
- Added detailed design principles, color usage guidelines, and component structure examples to AGENTS.md.
- Updated AISmartSidebar component to utilize layered glass effects and smooth animations.
- Enhanced CSS styles for glassmorphic elements, ensuring performance optimizations and accessibility considerations.

These changes aim to elevate the user experience through refined aesthetics and improved interaction dynamics.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-10-23 21:55:13 +08:00
parent b26ebc0b53
commit b53db73211
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 390 additions and 110 deletions

View File

@ -107,6 +107,114 @@ import { Button, Modal } from "@follow/components"
import { FeedList } from "~/modules/name/components"
```
## Glassmorphic Depth Design System
Follow uses a sophisticated glassmorphic depth design system for elevated UI components (modals, toasts, floating panels, etc.). This design provides visual hierarchy through layered transparency and subtle color accents.
### Design Principles
- **Multi-layer Depth**: Create visual depth through stacked transparent layers
- **Subtle Color Accents**: Use brand colors at very low opacity (5-20%) for borders, glows, and backgrounds
- **Refined Blur**: Heavy backdrop blur (backdrop-blur-2xl) for frosted glass effect
- **Minimal Shadows**: Combine multiple soft shadows with accent colors for depth perception
- **Smooth Animations**: Use Spring presets for all transitions
### Color Usage
- **Primary Accent**: `#FF5C00` (orange) at 5-20% opacity for borders, glows, and highlights
- **Border**: `rgba(255, 92, 0, 0.2)` for main borders
- **Inner Glow**: `rgba(255, 92, 0, 0.05)` for subtle radial/linear gradients inside containers
- **Shadows**: Layered shadows with accent tint:
- `0 8px 32px rgba(255, 92, 0, 0.08)` - large soft glow
- `0 4px 16px rgba(255, 92, 0, 0.06)` - medium shadow
- `0 2px 8px rgba(0, 0, 0, 0.1)` - close depth
### Component Structure
```tsx
<div
className="rounded-2xl backdrop-blur-2xl"
style={{
backgroundImage:
"linear-gradient(to bottom right, rgba(var(--color-background) / 0.98), rgba(var(--color-background) / 0.95))",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "rgba(255, 92, 0, 0.2)",
boxShadow:
"0 8px 32px rgba(255, 92, 0, 0.08), 0 4px 16px rgba(255, 92, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.1)",
}}
>
{/* Inner glow layer */}
<div
className="absolute inset-0 rounded-2xl"
style={{
background:
"linear-gradient(to bottom right, rgba(255, 92, 0, 0.05), transparent, rgba(255, 92, 0, 0.05))",
}}
/>
{/* Content */}
<div className="relative">{/* Your content here */}</div>
</div>
```
### Interactive Elements
For hover states on buttons or interactive areas within glass containers:
```tsx
<button
onMouseEnter={(e) => {
e.currentTarget.style.background =
"linear-gradient(to right, rgba(255, 92, 0, 0.08), rgba(255, 140, 0, 0.05))"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
}}
>
{/* Subtle shine effect */}
<div className="via-gray/5 absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent to-transparent transition-transform duration-700 group-hover:translate-x-full dark:via-white/5" />
</button>
```
### Dividers
Use gradient dividers within glass containers:
```tsx
<div
className="mx-4 h-px"
style={{
background: "linear-gradient(to right, transparent, rgba(255, 92, 0, 0.2), transparent)",
}}
/>
```
### Animation Guidelines
- Entry animations: `initial={{ y: 8, opacity: 0 }}``animate={{ y: 0, opacity: 1 }}`
- Use `Spring.presets.snappy` for quick interactions
- Use `Spring.presets.smooth` for larger movements
- Keep scale animations subtle (1.0 ↔ 1.02)
### When to Use
Apply this design system to:
- Toast notifications
- Modal dialogs
- Floating panels and popovers
- Ambient UI prompts
- Contextual menus
- Elevated cards with actions
### Accessibility
- Ensure sufficient contrast for text over glass backgrounds
- Maintain border visibility in both light and dark modes
- Preserve keyboard focus indicators
- Keep animations respectful of `prefers-reduced-motion`
## Build Outputs
- Desktop: `apps/desktop/out/` for packaged applications

View File

@ -1,60 +1,54 @@
/* CSS animations for AISmartSidebar (replacing motion/react) */
/* Glassmorphic Depth Design - Apple-inspired elegant AI sidebar */
.ai-ambient-bar {
/* Multi-layer glass edge bars with depth perception */
.ai-glass-layer-1,
.ai-glass-layer-2,
.ai-glass-layer-3 {
transform-origin: right center;
transition:
width 0.3s ease-out,
opacity 0.3s ease-out,
transform 0.3s ease-out;
width 0.5s cubic-bezier(0.23, 1, 0.32, 1),
opacity 0.5s cubic-bezier(0.23, 1, 0.32, 1),
transform 0.5s cubic-bezier(0.23, 1, 0.32, 1),
box-shadow 0.5s cubic-bezier(0.23, 1, 0.32, 1);
will-change: width, opacity, transform, box-shadow;
}
@keyframes ai-bar-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
/* Layered glass card with depth */
.ai-glass-card {
will-change: transform;
}
.ai-ambient-pulse {
animation: ai-bar-pulse 2s ease-in-out infinite;
/* Premium glass button */
.ai-glass-button {
will-change: transform, box-shadow;
position: relative;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ai-glow-blob {
transition:
opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
will-change: transform, opacity;
border-radius: 9999px;
/* Subtle shimmer on hover */
.ai-glass-button:hover {
box-shadow:
0 4px 16px rgba(255, 92, 0, 0.15),
0 8px 32px rgba(255, 92, 0, 0.08),
inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
.ai-prompt-container {
opacity: 0;
transform: translateX(20px);
transition:
opacity 0.25s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
/* Depth shadow for glass layers */
.ai-glass-card > div {
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.ai-prompt-visible {
opacity: 1;
transform: translateX(0);
}
@keyframes ai-dot-pulse-kf {
0%,
100% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.2);
opacity: 1;
}
}
.ai-dot-pulse {
animation: ai-dot-pulse-kf 2s ease-in-out infinite;
/* Performance optimization with hardware acceleration */
.ai-glass-layer-1,
.ai-glass-layer-2,
.ai-glass-layer-3,
.ai-glass-card,
.ai-glass-button {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
perspective: 1000px;
-webkit-perspective: 1000px;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}

View File

@ -1,4 +1,4 @@
// Replaced motion/react animations with CSS animations
// Glassmorphic Depth Design - Apple-inspired elegant AI sidebar
import "./AISmartSidebar.css"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
@ -55,70 +55,177 @@ const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
const toggleAIChatShortcut = useCommandShortcut(COMMAND_ID.global.toggleAIChat)
if (!canShowPrompt) return null
return (
<>
<div
className={`ai-ambient-bar pointer-events-none fixed right-0 top-0 z-40 h-full w-2 ${
intensity > 0.3 ? "ai-ambient-pulse" : ""
}`}
style={{
opacity: intensity * 0.8,
width: intensity > 0 ? 8 + intensity * 40 : 2,
background: `linear-gradient(to left, rgba(255, 92, 0, ${intensity * 0.4}) 0%, rgba(255, 140, 0, ${intensity * 0.3}) 50%, transparent 100%)`,
}}
/>
{/* Multi-layer glass edge with depth */}
<div className="pointer-events-none fixed right-0 top-0 z-40 h-full">
{/* Background layer - deepest */}
<m.div
className="ai-glass-layer-3 absolute right-0 top-0 h-full"
style={{
width: intensity > 0.1 ? 1 + intensity * 3 : 0,
opacity: intensity * 0.15,
background: "linear-gradient(to left, rgba(255, 92, 0, 0.15), transparent)",
transform: `translateX(${intensity * -8}px)`,
}}
/>
{/* Middle layer */}
<m.div
className="ai-glass-layer-2 absolute right-0 top-0 h-full"
style={{
width: intensity > 0.2 ? 1.5 + intensity * 4 : 0,
opacity: intensity * 0.25,
background: "linear-gradient(to left, rgba(255, 92, 0, 0.2), transparent)",
transform: `translateX(${intensity * -4}px)`,
boxShadow:
intensity > 0.3
? `0 0 ${12 + intensity * 20}px rgba(255, 92, 0, ${intensity * 0.15})`
: "none",
}}
/>
{/* Front layer - most prominent */}
<m.div
className="ai-glass-layer-1 absolute right-0 top-0 h-full"
style={{
width: intensity > 0 ? 2 + intensity * 6 : 1,
opacity: intensity * 0.6,
background: `linear-gradient(to left,
rgba(255, 92, 0, ${intensity * 0.4}),
rgba(255, 140, 0, ${intensity * 0.2}),
transparent)`,
boxShadow:
intensity > 0.5
? `0 0 ${16 + intensity * 24}px rgba(255, 92, 0, ${intensity * 0.25})`
: "none",
}}
/>
{/* Subtle ambient glow */}
{intensity > 0.4 && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute right-0 top-1/2 size-32 -translate-y-1/2"
style={{
opacity: (intensity - 0.4) * 0.3,
background: `radial-gradient(ellipse at center,
rgba(255, 92, 0, ${(intensity - 0.4) * 0.12}) 0%,
transparent 70%)`,
filter: "blur(30px)",
transform: `translateY(-50%) translateX(${intensity * -20}px)`,
}}
/>
)}
</div>
<AnimatePresence>
{showPrompt && (
<>
<m.div
initial={{ opacity: 0, x: 30, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 30, scale: 0.95 }}
transition={Spring.presets.smooth}
className="fixed bottom-12 right-6 z-50 flex flex-col items-end gap-3"
style={{
transform: `translateX(${intensity * -3}px)`,
}}
>
{/* Unified glass card with integrated button */}
<m.div
initial={{ opacity: 0, scale: 0.5, x: 0 }}
animate={{
opacity: intensity * 0.6,
scale: 0.5 + intensity * 1.5,
x: intensity > 0 ? -10 - intensity * 30 : 0,
}}
exit={{ opacity: 0, scale: 0.5, x: 0 }}
transition={Spring.presets.smooth}
className="ai-glow-blob pointer-events-none fixed bottom-12 right-0 z-40"
style={{
opacity: intensity * 0.6,
transform: `translateX(${intensity > 0 ? -10 - intensity * 30 : 0}px) scale(${0.5 + intensity * 1.5})`,
width: 100,
height: 100,
background: `radial-gradient(circle, rgba(255, 92, 0, ${intensity * 0.3}) 0%, rgba(255, 140, 0, ${intensity * 0.2}) 40%, transparent 70%)`,
}}
/>
<m.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="ai-prompt-container ai-prompt-visible fixed bottom-12 right-6 z-50 flex flex-col items-end gap-3"
className="ai-glass-card relative"
initial={{ y: 8, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={Spring.presets.snappy}
>
<div className="rounded-2xl border border-folo/30 bg-background/90 px-4 py-3 backdrop-blur-xl">
<div className="text-right">
{/* Main unified card */}
<div
className="relative overflow-hidden rounded-2xl bg-gradient-to-br to-background/95 backdrop-blur-2xl"
style={{
backgroundImage:
"linear-gradient(to bottom right, rgba(var(--color-background) / 0.98), rgba(var(--color-background) / 0.95))",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "rgba(255, 92, 0, 0.2)",
boxShadow:
"0 8px 32px rgba(255, 92, 0, 0.08), 0 4px 16px rgba(255, 92, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.1)",
}}
>
{/* Inner glow */}
<div
className="absolute inset-0 rounded-2xl"
style={{
background:
"linear-gradient(to bottom right, rgba(255, 92, 0, 0.05), transparent, rgba(255, 92, 0, 0.05))",
}}
/>
{/* Info section */}
<div className="relative px-5 py-3.5 text-right">
<p className="text-sm font-medium text-text">Ask AI anything</p>
<p className="mt-1 text-xs text-text-secondary">
<p className="mt-0.5 text-xs text-text-secondary">
Get insights about this article
</p>
</div>
</div>
<button
type="button"
className="rounded-full border border-folo/40 bg-gradient-to-r from-folo/20 to-red-500/20 px-6 py-2 backdrop-blur-xl transition-all duration-300 hover:from-folo/30 hover:to-red-500/30"
onClick={onExpand}
>
<div className="flex items-center gap-2">
<div className="ai-dot-pulse size-2 rounded-full bg-gradient-to-r from-folo to-red-500" />
<span className="text-sm font-medium text-text">Open AI Chat</span>
<KbdCombined abbr="Open AI Chat" joint className="rounded-full px-2">
{toggleAIChatShortcut}
</KbdCombined>
</div>
</button>
{/* Divider */}
<div
className="mx-4 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(255, 92, 0, 0.2), transparent)",
}}
/>
{/* Button section */}
<button
type="button"
className="group relative w-full px-5 py-3 text-left transition-all duration-300"
onClick={onExpand}
onMouseEnter={(e) => {
e.currentTarget.style.background =
"linear-gradient(to right, rgba(255, 92, 0, 0.08), rgba(255, 140, 0, 0.05))"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
}}
>
{/* Subtle shine effect on hover */}
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-gray/5 to-transparent transition-transform duration-700 group-hover:translate-x-full dark:via-white/5" />
<div className="relative flex items-center justify-between">
<div className="flex items-center gap-2.5">
{/* Minimal indicator dot */}
<m.div
className="size-2 rounded-full"
style={{ backgroundColor: "#FF5C00" }}
animate={{
opacity: [0.6, 1, 0.6],
}}
transition={{
duration: 2,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
/>
<span className="text-sm font-medium text-text">Open AI Chat</span>
</div>
<KbdCombined
abbr="Open AI Chat"
joint
className="rounded-md bg-fill/40 px-2 backdrop-blur-sm"
>
{toggleAIChatShortcut}
</KbdCombined>
</div>
</button>
</div>
</m.div>
</>
</m.div>
)}
</AnimatePresence>
</>

View File

@ -97,8 +97,8 @@ const DateItemInner: FC<DateItemInnerProps> = ({
<div
className={cn(
className,
isSticky && "border-border bg-background",
"border-b border-transparent",
"border-b border-transparent bg-background",
isSticky && "border-border",
)}
onClick={stopPropagation}
onMouseEnter={removeConfirm.cancel}

View File

@ -52,3 +52,64 @@
top: calc(env(safe-area-inset-top, 0px) + 20px);
}
}
[data-sonner-toast] {
background-image: linear-gradient(
to bottom right,
rgba(var(--color-background) / 0.98),
rgba(var(--color-background) / 0.95)
) !important;
}
[data-sonner-toast]::before {
content: "";
position: absolute;
inset: 0;
border-radius: 1rem;
background: linear-gradient(
to bottom right,
rgba(255, 92, 0, 0.05),
transparent,
rgba(255, 92, 0, 0.05)
);
z-index: -1;
}
[data-sonner-toast][data-type="success"]::before {
background: linear-gradient(
to bottom right,
rgba(40, 205, 65, 0.05),
transparent,
rgba(40, 205, 65, 0.05)
);
}
[data-sonner-toast][data-type="error"]::before {
background: linear-gradient(
to bottom right,
rgba(255, 69, 58, 0.05),
transparent,
rgba(255, 69, 58, 0.05)
);
}
[data-sonner-toast][data-type="warning"]::before {
background: linear-gradient(
to bottom right,
rgba(255, 149, 0, 0.05),
transparent,
rgba(255, 149, 0, 0.05)
);
}
[data-sonner-toast][data-type="info"]::before {
background: linear-gradient(
to bottom right,
rgba(0, 122, 255, 0.05),
transparent,
rgba(0, 122, 255, 0.05)
);
}
[data-sonner-toast][data-type="loading"]::before {
background: linear-gradient(
to bottom right,
rgba(142, 142, 147, 0.05),
transparent,
rgba(142, 142, 147, 0.05)
);
}

View File

@ -13,6 +13,7 @@
--fo-background: theme(colors.background);
--background: 0 0% 100%;
--color-background: 255 255 255;
--border: 20 5.9% 90%;
--radius: 0.5rem;
@ -37,7 +38,7 @@
--fo-background: theme(colors.background);
--background: 0 0% 7.1%;
--color-background: 18 18 18;
--border: 0 0% 22.1%;
}
}

View File

@ -12,6 +12,7 @@
[data-theme="light"] {
--background: 0 0% 100%;
--color-background: 255 255 255;
--border: 20 5.9% 90%;
--radius: 0.5rem;
@ -19,6 +20,7 @@
[data-theme="dark"] {
--background: 0 0% 7.1%;
--color-background: 18 18 18;
--border: 0 0% 22.1%;
}

View File

@ -1,15 +1,22 @@
export const toastStyles = {
toast: tw`
group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg
backdrop-blur-background border border-border/50
bg-material-ultra-thick duration-300 ease-out
data-[type=success]:border-green/30 data-[type=success]:bg-green/20
data-[type=error]:border-red/30 data-[type=error]:bg-red/20
data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/20
data-[type=info]:border-blue/30 data-[type=info]:bg-blue/20
data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/20
group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4
backdrop-blur-2xl duration-300 ease-out overflow-hidden
max-w-md min-w-[320px]
font-theme
[&]:border [&]:border-solid
[&]:data-[type=default]:border-[rgba(255,92,0,0.2)]
[&]:data-[type=success]:border-[rgba(40,205,65,0.2)]
[&]:data-[type=error]:border-[rgba(255,69,58,0.2)]
[&]:data-[type=warning]:border-[rgba(255,149,0,0.2)]
[&]:data-[type=info]:border-[rgba(0,122,255,0.2)]
[&]:data-[type=loading]:border-[rgba(142,142,147,0.2)]
[&]:shadow-[0_8px_32px_rgba(255,92,0,0.08),0_4px_16px_rgba(255,92,0,0.06),0_2px_8px_rgba(0,0,0,0.1)]
[&]:data-[type=success]:shadow-[0_8px_32px_rgba(40,205,65,0.08),0_4px_16px_rgba(40,205,65,0.06),0_2px_8px_rgba(0,0,0,0.1)]
[&]:data-[type=error]:shadow-[0_8px_32px_rgba(255,69,58,0.08),0_4px_16px_rgba(255,69,58,0.06),0_2px_8px_rgba(0,0,0,0.1)]
[&]:data-[type=warning]:shadow-[0_8px_32px_rgba(255,149,0,0.08),0_4px_16px_rgba(255,149,0,0.06),0_2px_8px_rgba(0,0,0,0.1)]
[&]:data-[type=info]:shadow-[0_8px_32px_rgba(0,122,255,0.08),0_4px_16px_rgba(0,122,255,0.06),0_2px_8px_rgba(0,0,0,0.1)]
[&]:data-[type=loading]:shadow-[0_8px_32px_rgba(142,142,147,0.08),0_4px_16px_rgba(142,142,147,0.06),0_2px_8px_rgba(0,0,0,0.1)]
`,
title: tw`
text-sm font-medium text-text
@ -25,7 +32,7 @@ export const toastStyles = {
icon: tw`
flex-shrink-0 mt-0.5 size-5
[li[data-type="success"]_&]:text-green
[li[data-type="error"]_&]:text-red
[li[data-type="error"]_&]:text-red
[li[data-type="warning"]_&]:text-orange
[li[data-type="info"]_&]:text-blue
[li[data-type="loading"]_&]:text-gray