Skip to content

Commit 0d45ee5

Browse files
committed
v2.52 - Experimental Theme
1 parent 995247b commit 0d45ee5

11 files changed

Lines changed: 374 additions & 78 deletions

File tree

frontend/src/app/globals.css

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,99 @@ a:visited {
125125
background: rgba(255, 255, 255, 0.35);
126126
}
127127

128+
/* ═══════════════════════════════════════════════════════════
129+
* Exp1 theme — Ive/Jobs-era Apple design system
130+
* Frosted glass, spring animations, refined depth cues
131+
* ═══════════════════════════════════════════════════════════ */
132+
133+
/* Exp1 theme root overrides — applied via data attribute */
134+
[data-theme="exp1"] {
135+
--exp1-ease: cubic-bezier(0.22, 1, 0.36, 1);
136+
--exp1-ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
137+
--exp1-duration: 0.5s;
138+
--exp1-duration-fast: 0.3s;
139+
}
140+
141+
/* Exp1 fade-in for content blocks */
142+
@keyframes exp1FadeIn {
143+
from {
144+
opacity: 0;
145+
transform: translateY(8px);
146+
}
147+
to {
148+
opacity: 1;
149+
transform: translateY(0);
150+
}
151+
}
152+
153+
/* Exp1 scale-in for modals and floating UI */
154+
@keyframes exp1ScaleIn {
155+
from {
156+
opacity: 0;
157+
transform: scale(0.96);
158+
}
159+
to {
160+
opacity: 1;
161+
transform: scale(1);
162+
}
163+
}
164+
165+
/* Exp1 slide-in for sidebar items */
166+
@keyframes exp1SlideIn {
167+
from {
168+
opacity: 0;
169+
transform: translateX(-6px);
170+
}
171+
to {
172+
opacity: 1;
173+
transform: translateX(0);
174+
}
175+
}
176+
177+
/* Exp1 subtle breathing glow for focus states */
178+
@keyframes exp1FocusGlow {
179+
0%, 100% {
180+
box-shadow: 0 0 0 2px rgba(212, 168, 83, 0.15);
181+
}
182+
50% {
183+
box-shadow: 0 0 0 3px rgba(212, 168, 83, 0.25);
184+
}
185+
}
186+
187+
/* Exp1 shimmer — elegant loading state */
188+
@keyframes exp1Shimmer {
189+
0% {
190+
background-position: 200% 0;
191+
}
192+
100% {
193+
background-position: -200% 0;
194+
}
195+
}
196+
197+
/* Exp1 scrollbar — thinner, more translucent */
198+
[data-theme="exp1"] [data-fp-scrollbar] {
199+
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
200+
}
201+
[data-theme="exp1"] [data-fp-scrollbar]::-webkit-scrollbar {
202+
width: 4px;
203+
}
204+
[data-theme="exp1"] [data-fp-scrollbar]::-webkit-scrollbar-thumb {
205+
border-radius: 2px;
206+
}
207+
[data-theme="exp1"] [data-fp-scrollbar][data-fp-scroll-visible]::-webkit-scrollbar-thumb {
208+
background: rgba(255, 255, 255, 0.18);
209+
}
210+
[data-theme="exp1"] [data-fp-scrollbar]:hover::-webkit-scrollbar-thumb {
211+
background: rgba(255, 255, 255, 0.22);
212+
}
213+
214+
/* Exp1 smooth transitions on all interactive elements */
215+
[data-theme="exp1"] button,
216+
[data-theme="exp1"] a,
217+
[data-theme="exp1"] input {
218+
transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1);
219+
}
220+
128221
/* Theme-aware: use data-fp-scrollbar-light when container has light bg */
129222
[data-fp-scrollbar-light] {
130223
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;

frontend/src/components/app/AppShell.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { usePathname, useRouter } from "next/navigation"
55
import { Sidebar } from "@/components/app/Sidebar"
66
import { ErrorBoundary } from "@/components/app/ErrorBoundary"
77
import { ChatProvider, useChat } from "@/context/ChatContext"
8-
import { ThemeProvider, useThemeColors, type ThemeColors } from "@/context/ThemeContext"
8+
import { ThemeProvider, useTheme, useThemeColors, useExp1, type ThemeColors } from "@/context/ThemeContext"
99
import { useAuth } from "@/context/AuthContext"
1010
import { ShellLayoutProvider } from "@/context/ShellLayoutContext"
1111
import OnboardingModal from "@/components/app/OnboardingModal"
@@ -43,6 +43,8 @@ function AuthGate({ children }: { children: React.ReactNode }) {
4343

4444
function ShellInner({ children }: { children: React.ReactNode }) {
4545
const COLORS = useThemeColors()
46+
const { resolved } = useTheme()
47+
const exp1 = useExp1()
4648
const { state } = useChat()
4749
const { user, loading } = useAuth()
4850
const pathname = usePathname()
@@ -128,12 +130,17 @@ function ShellInner({ children }: { children: React.ReactNode }) {
128130
return (
129131
<AuthGate>
130132
<div
133+
data-theme={resolved}
131134
style={{
132135
position: "fixed",
133136
inset: 0,
134137
zIndex: 50,
135138
display: "flex",
136139
background: COLORS.bg,
140+
...(exp1.active ? {
141+
backgroundImage: exp1.noise,
142+
backgroundSize: "256px 256px",
143+
} : {}),
137144
}}
138145
>
139146
<Suspense fallback={null}>

frontend/src/components/app/ChatInput.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useTypewriter } from "@/hooks/useTypewriter"
77
import { useAutoResize } from "@/hooks/useAutoResize"
88
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
99
import { FONTS, ROW_HEIGHT } from "@/lib/constants"
10-
import { useThemeColors, useIsDark } from "@/context/ThemeContext"
10+
import { useThemeColors, useIsDark, useExp1 } from "@/context/ThemeContext"
1111
import { LengthBadge, ModelBadge } from "@/components/blocks/SessionView"
1212
import { DEFAULT_MODEL_ID, type ModelOptionId } from "@/lib/models"
1313
import { FileAttachmentCard } from "@/components/ui/FileAttachmentCard"
@@ -29,13 +29,14 @@ const ChatInput = memo(function ChatInput() {
2929
const router = useRouter()
3030
const COLORS = useThemeColors()
3131
const isDark = useIsDark()
32+
const exp1 = useExp1()
3233
const { user, loading, openLoginModal } = useAuth()
3334
const { state, setInputValue, setInputFeatures, addConversation, deleteConversation, sendMessage } = useChat()
34-
const bg = isDark ? "#0A0A0A" : COLORS.modalBg
35-
const border = isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.18)"
36-
const borderFocused = isDark ? "rgba(255,255,255,0.35)" : "rgba(0,0,0,0.28)"
37-
const borderHover = isDark ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.24)"
38-
const bgHover = isDark ? "#0E0E0E" : COLORS.sidebarBg
35+
const bg = exp1.active ? "rgba(18,18,24,0.6)" : isDark ? "#0A0A0A" : COLORS.modalBg
36+
const border = exp1.active ? "rgba(255,255,255,0.08)" : isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.18)"
37+
const borderFocused = exp1.active ? "rgba(212,168,83,0.3)" : isDark ? "rgba(255,255,255,0.35)" : "rgba(0,0,0,0.28)"
38+
const borderHover = exp1.active ? "rgba(255,255,255,0.12)" : isDark ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.24)"
39+
const bgHover = exp1.active ? "rgba(22,22,30,0.7)" : isDark ? "#0E0E0E" : COLORS.sidebarBg
3940
const { inputValue, inputFeatures, activeView, isSending } = state
4041

4142
/* Restore model + content types from localStorage on mount */
@@ -225,13 +226,22 @@ const ChatInput = memo(function ChatInput() {
225226
maxWidth: "85vw",
226227
background: inputFocused ? bgHover : bg,
227228
border: `1px solid ${effectiveBorder}`,
228-
borderRadius: "24px",
229+
borderRadius: exp1.active ? "28px" : "24px",
229230
boxSizing: "border-box",
230231
display: "flex",
231232
flexDirection: "column",
232-
padding: "18px 24px",
233+
padding: exp1.active ? "20px 26px" : "18px 24px",
233234
gap: "12px",
234-
transition: "border-color 0.3s, background 0.3s",
235+
transition: exp1.active
236+
? `border-color 0.5s ${exp1.ease}, background 0.5s ${exp1.ease}, box-shadow 0.5s ${exp1.ease}`
237+
: "border-color 0.3s, background 0.3s",
238+
...(exp1.active ? {
239+
backdropFilter: exp1.glass,
240+
WebkitBackdropFilter: exp1.glass,
241+
boxShadow: inputFocused
242+
? "0 0 0 1px rgba(212,168,83,0.12), 0 8px 32px rgba(0,0,0,0.3)"
243+
: exp1.shadow,
244+
} : {}),
235245
}}
236246
onMouseEnter={(e) => {
237247
if (!inputFocused) {

frontend/src/components/app/MessageList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { memo, useRef, useEffect } from "react"
44
import type { Message } from "@/types/chat"
55
import { FONTS } from "@/lib/constants"
6-
import { useThemeColors, useIsDark } from "@/context/ThemeContext"
6+
import { useThemeColors, useIsDark, useExp1 } from "@/context/ThemeContext"
77
import { UserIcon } from "./Icons"
88
import { InlineRich, Markdownish } from "@/components/ui/RichText"
99
import { MediaPlaceholder } from "@/components/blocks/MediaPlaceholder"
@@ -24,6 +24,7 @@ const MessageBubble = memo(
2424
function MessageBubble({ message, sessionId }: { message: Message; sessionId?: string }) {
2525
const COLORS = useThemeColors()
2626
const isDark = useIsDark()
27+
const exp1 = useExp1()
2728
const isUser = message.role === "user"
2829
const isStreaming = message.streamStatus === "streaming"
2930
const hasMeaningfulContent = Boolean((message.content || "").trim())
@@ -45,7 +46,10 @@ const MessageBubble = memo(
4546
display: "flex",
4647
gap: "12px",
4748
opacity: message.streamStatus === "idle" && !message.content ? 0.5 : 1,
48-
transition: "opacity 0.2s",
49+
transition: exp1.active ? `opacity 0.5s ${exp1.ease}` : "opacity 0.2s",
50+
...(exp1.active ? {
51+
animation: "exp1FadeIn 0.6s cubic-bezier(0.22,1,0.36,1) both",
52+
} : {}),
4953
}}
5054
role="article"
5155
aria-label={`${isUser ? "You" : "Assistant"} said`}

frontend/src/components/app/Sidebar.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import Image from "next/image"
1010
import { useRouter, usePathname } from "next/navigation"
1111
import { useChat } from "@/context/ChatContext"
12-
import { useThemeColors, useLogoFilter, useIsDark } from "@/context/ThemeContext"
12+
import { useThemeColors, useLogoFilter, useIsDark, useExp1 } from "@/context/ThemeContext"
1313
import { useAuth } from "@/context/AuthContext"
1414
import { useIsMobile } from "@/hooks/useIsMobile"
1515
import { useScrollbarOnScroll } from "@/hooks/useScrollbarOnScroll"
@@ -381,6 +381,7 @@ const RecentItem = memo(function RecentItem({
381381
export const Sidebar = memo(function Sidebar() {
382382
const COLORS = useThemeColors()
383383
const isDark = useIsDark()
384+
const exp1 = useExp1()
384385
const isMobile = useIsMobile(900)
385386
const logoFilter = useLogoFilter()
386387
const router = useRouter()
@@ -672,26 +673,36 @@ export const Sidebar = memo(function Sidebar() {
672673
: `${effectiveWidth}px`,
673674
maxWidth: isMobile && expanded ? "320px" : undefined,
674675
height: "100dvh",
675-
background: COLORS.sidebarBg,
676+
background: exp1.active ? exp1.sidebarGlass : COLORS.sidebarBg,
676677
borderRight: "none",
677-
boxShadow: "0.5px 0 0 0 rgba(255,255,255,0.3)",
678+
boxShadow: exp1.active
679+
? "1px 0 0 0 rgba(255,255,255,0.06)"
680+
: "0.5px 0 0 0 rgba(255,255,255,0.3)",
678681
display: "flex",
679682
flexDirection: "column",
680-
transition: resizing ? "none" : `width 0.25s ${EASE_OUT}`,
683+
transition: resizing ? "none" : exp1.active
684+
? `width 0.5s ${exp1.ease}`
685+
: `width 0.25s ${EASE_OUT}`,
681686
overflow: "hidden",
682687
flexShrink: 0,
683688
contain: "layout style",
684689
position: isMobile && expanded ? "fixed" : "relative",
685690
top: isMobile && expanded ? 0 : undefined,
686691
left: isMobile && expanded ? 0 : undefined,
687692
zIndex: isMobile && expanded ? 20 : 10,
693+
...(exp1.active ? {
694+
backdropFilter: exp1.glass,
695+
WebkitBackdropFilter: exp1.glass,
696+
} : {}),
688697
}}
689698
>
690699
{/* Header — with drop shadow separation below (50% opacity); spacing before menu */}
691700
<div
692701
style={{
693702
flexShrink: 0,
694-
boxShadow: "0 2px 2px 0 rgba(255,255,255,0.25)",
703+
boxShadow: exp1.active
704+
? "0 1px 0 0 rgba(255,255,255,0.04)"
705+
: "0 2px 2px 0 rgba(255,255,255,0.25)",
695706
marginBottom: "14px",
696707
}}
697708
>

frontend/src/components/app/ThemeToggle.tsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client"
22

33
import React from "react"
4-
import { useTheme, useThemeColors, useIsDark, type ThemePreference } from "@/context/ThemeContext"
4+
import { useTheme, useThemeColors, useIsDark, useExp1, type ThemePreference } from "@/context/ThemeContext"
55
import { FONTS, EASE_OUT } from "@/lib/constants"
66

77
const THEME_OPTIONS: { key: ThemePreference; label: string; icon: React.ReactNode }[] = [
@@ -42,36 +42,52 @@ const THEME_OPTIONS: { key: ThemePreference; label: string; icon: React.ReactNod
4242
</svg>
4343
),
4444
},
45+
{
46+
key: "exp1",
47+
label: "Exp1",
48+
icon: (
49+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
50+
<path d="M12 2L2 7l10 5 10-5-10-5z" />
51+
<path d="M2 17l10 5 10-5" />
52+
<path d="M2 12l10 5 10-5" />
53+
</svg>
54+
),
55+
},
4556
]
4657

47-
/** Theme toggle — Dark / Light / System. Persists to localStorage. */
58+
/** Theme toggle — Dark / Light / System / Exp1. Persists to localStorage. */
4859
export function ThemeToggle() {
4960
const COLORS = useThemeColors()
5061
const isDark = useIsDark()
62+
const exp1 = useExp1()
5163
const { preference, setPreference } = useTheme()
5264
const activeIdx = THEME_OPTIONS.findIndex((o) => o.key === preference)
5365

5466
const TRACK_H = 36
5567
const PILL_PAD = 3
5668
const segmentCount = THEME_OPTIONS.length
57-
/* Pill width keeps left/right padding symmetric: so right segment doesn't overlap right margin */
5869
const pillWidthPct = `calc(${100 / segmentCount}% - ${PILL_PAD * 2}px)`
5970
const pillLeftPct = `calc(${(activeIdx * 100) / segmentCount}% + ${PILL_PAD}px)`
6071

61-
const trackBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)"
62-
const pillBg = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"
72+
const trackBg = exp1.active
73+
? "rgba(255,255,255,0.03)"
74+
: isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)"
75+
const pillBg = exp1.active
76+
? "rgba(255,255,255,0.08)"
77+
: isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"
6378

6479
return (
6580
<div
6681
style={{
6782
position: "relative",
6883
display: "flex",
6984
width: "100%",
70-
maxWidth: "320px",
85+
maxWidth: "420px",
7186
background: trackBg,
72-
borderRadius: "10px",
87+
borderRadius: exp1.active ? "14px" : "10px",
7388
padding: `${PILL_PAD}px`,
7489
border: `1px solid ${COLORS.border}`,
90+
...(exp1.active ? { backdropFilter: exp1.glass } : {}),
7591
}}
7692
role="radiogroup"
7793
aria-label="Theme"
@@ -83,9 +99,9 @@ export function ThemeToggle() {
8399
left: pillLeftPct,
84100
width: pillWidthPct,
85101
height: `${TRACK_H - PILL_PAD * 2}px`,
86-
borderRadius: "7px",
102+
borderRadius: exp1.active ? "11px" : "7px",
87103
background: pillBg,
88-
transition: `left 0.25s ${EASE_OUT}`,
104+
transition: exp1.active ? `left 0.5s ${exp1.ease}` : `left 0.25s ${EASE_OUT}`,
89105
pointerEvents: "none",
90106
}}
91107
/>
@@ -109,14 +125,15 @@ export function ThemeToggle() {
109125
gap: "6px",
110126
background: "transparent",
111127
border: "none",
112-
borderRadius: "7px",
128+
borderRadius: exp1.active ? "11px" : "7px",
113129
color: isActive ? COLORS.textPrimary : COLORS.textTertiary,
114130
cursor: "pointer",
115-
transition: "color 0.2s",
131+
transition: exp1.active ? `color 0.4s ${exp1.ease}` : "color 0.2s",
116132
padding: 0,
117133
fontFamily: FONTS.sans,
118134
fontSize: "13px",
119135
fontWeight: isActive ? 500 : 400,
136+
letterSpacing: exp1.active ? "0.01em" : undefined,
120137
}}
121138
>
122139
<span style={{ display: "flex", flexShrink: 0 }}>{opt.icon}</span>

0 commit comments

Comments
 (0)