From 864ab8862780e6bb3f24dca6de1cb5c015d94b56 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 15 Oct 2026 15:01:20 -0400 Subject: [PATCH] New look: slate-indigo panels, pink accent, Poppins, and a gradient card for your song Inspired by a dashboard design the user shared. - App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue gradient card for your own song (text and controls switch to white on it), flat cards for friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab. - Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel, and a right column with your profile, recent activity and recent plays. Phones keep the top bar and bottom tabs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP --- apps/mobile/app.json | 2 +- apps/mobile/assets/images/ambient-blue.png | Bin 35801 -> 0 bytes apps/mobile/assets/images/ambient-purple.png | Bin 35801 -> 0 bytes apps/mobile/package.json | 2 +- apps/mobile/src/app/(tabs)/_layout.tsx | 7 +- apps/mobile/src/app/(tabs)/index.tsx | 25 +- apps/mobile/src/app/(tabs)/search.tsx | 6 +- apps/mobile/src/app/(tabs)/settings.tsx | 6 +- apps/mobile/src/app/_layout.tsx | 4 +- apps/mobile/src/app/blocked.tsx | 4 +- apps/mobile/src/app/notifications.tsx | 6 +- apps/mobile/src/app/onboarding/friends.tsx | 6 +- apps/mobile/src/app/onboarding/index.tsx | 6 +- apps/mobile/src/app/play/[id].tsx | 10 +- apps/mobile/src/app/qr.tsx | 6 +- apps/mobile/src/app/scan.tsx | 4 +- apps/mobile/src/app/send/[trackId].tsx | 10 +- apps/mobile/src/components/ActivityBell.tsx | 15 +- .../src/components/AmbientBackground.tsx | 39 +-- apps/mobile/src/components/AuthForm.tsx | 4 +- apps/mobile/src/components/Avatar.tsx | 4 +- apps/mobile/src/components/FeaturedPicker.tsx | 8 +- apps/mobile/src/components/FollowList.tsx | 6 +- apps/mobile/src/components/Lyrics.tsx | 24 +- apps/mobile/src/components/NowPlayingCard.tsx | 45 ++- apps/mobile/src/components/ProfileView.tsx | 6 +- apps/mobile/src/components/ProgressBar.tsx | 17 +- apps/mobile/src/components/Reactions.tsx | 23 +- apps/mobile/src/components/RecentPlays.tsx | 8 +- apps/mobile/src/components/TrackCard.tsx | 38 +-- .../src/components/setup/SpotifySetup.tsx | 10 +- apps/mobile/src/components/setup/common.tsx | 4 +- apps/mobile/src/components/ui.tsx | 76 ++--- apps/mobile/src/lib/theme.ts | 72 +++-- apps/web/src/app/globals.css | 282 +++++++++++------- apps/web/src/app/layout.tsx | 30 +- apps/web/src/app/manifest.ts | 4 +- apps/web/src/app/page.tsx | 24 +- apps/web/src/components/Nav.tsx | 10 +- apps/web/src/components/NowPlayingCard.tsx | 17 +- apps/web/src/components/Shell.tsx | 160 ++++++++++ apps/web/src/components/UserSearch.tsx | 4 +- package-lock.json | 8 +- 43 files changed, 648 insertions(+), 394 deletions(-) delete mode 100644 apps/mobile/assets/images/ambient-blue.png delete mode 100644 apps/mobile/assets/images/ambient-purple.png create mode 100644 apps/web/src/components/Shell.tsx diff --git a/apps/mobile/app.json b/apps/mobile/app.json index baef268..ba8e30d 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -31,7 +31,7 @@ [ "expo-splash-screen", { - "backgroundColor": "#0b0820", + "backgroundColor": "#2a2942", "image": "./assets/images/splash-icon.png", "imageWidth": 150 } diff --git a/apps/mobile/assets/images/ambient-blue.png b/apps/mobile/assets/images/ambient-blue.png deleted file mode 100644 index 1524ed126b00470a52f086804e8b5f136e46a4d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35801 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 M07*qoM6N<$f?DAhE&u=k diff --git a/apps/mobile/assets/images/ambient-purple.png b/apps/mobile/assets/images/ambient-purple.png deleted file mode 100644 index c603188a9578bf634259a5bc725c4095ffd007cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35801 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 diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 35a94c0..54477fd 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -95,7 +95,19 @@ export default function FeedScreen() { ListHeaderComponent={ - musicshare + {me ? ( + router.push('/profile')} accessibilityRole="link"> + + + + Hi {me.displayName ?? me.username}, + + Welcome back + + + ) : ( + musicshare + )} @@ -115,7 +127,7 @@ export default function FeedScreen() { )} {mine?.nowPlaying && ( - YOU + Your song )} @@ -143,7 +155,7 @@ export default function FeedScreen() { ListFooterComponent={ quiet.length > 0 ? ( - FOLLOWING · NOTHING PLAYED YET + Following · nothing played yet {quiet.map((u) => ( router.push(`/u/${u.username}`)} style={styles.quietPerson} accessibilityLabel={u.displayName ?? u.username}> @@ -183,8 +195,11 @@ const styles = StyleSheet.create({ header: { gap: space.md, marginBottom: space.xs }, topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, brand: { color: colors.text, fontFamily: fonts.display, fontSize: 22, letterSpacing: -0.4 }, - sectionLabel: { color: colors.muted, fontSize: 12, fontWeight: '700', letterSpacing: 0.8 }, + greeting: { flexDirection: 'row', alignItems: 'center', gap: space.md, flexShrink: 1 }, + hi: { color: colors.text, fontFamily: fonts.display, fontSize: 19 }, + welcome: { color: colors.muted, fontFamily: fonts.body, fontSize: 13 }, + sectionLabel: { color: colors.muted, fontSize: 13, fontFamily: fonts.bodyMedium }, quiet: { gap: space.lg, paddingVertical: space.xs }, quietPerson: { width: 64, alignItems: 'center', gap: 4 }, - quietName: { color: colors.text, fontSize: 12, textAlign: 'center' }, + quietName: { color: colors.text, fontSize: 11.5, fontFamily: fonts.body, textAlign: 'center' }, }); diff --git a/apps/mobile/src/app/(tabs)/search.tsx b/apps/mobile/src/app/(tabs)/search.tsx index 7f7e0a3..d02d015 100644 --- a/apps/mobile/src/app/(tabs)/search.tsx +++ b/apps/mobile/src/app/(tabs)/search.tsx @@ -10,7 +10,7 @@ import { PersonRow, emptyFollowText, useFollowList } from '@/components/FollowLi import { Body, Button, Chip, Field, Row, Title } from '@/components/ui'; import { useSession } from '@/lib/session'; import { useDebounced } from '@/lib/useDebounced'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; /** Friends: who you follow and who follows you, plus search and QR codes to find new people. */ export default function FriendsScreen() { @@ -95,6 +95,6 @@ const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: colors.bg }, list: { padding: space.lg, gap: space.md }, row: { flexDirection: 'row', alignItems: 'center', gap: space.md, paddingVertical: space.sm }, - name: { color: colors.text, fontWeight: '700', fontSize: 15 }, - handle: { color: colors.muted, fontSize: 13 }, + name: { color: colors.text, fontFamily: fonts.display, fontSize: 15 }, + handle: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/app/(tabs)/settings.tsx b/apps/mobile/src/app/(tabs)/settings.tsx index ffbab8a..47a9947 100644 --- a/apps/mobile/src/app/(tabs)/settings.tsx +++ b/apps/mobile/src/app/(tabs)/settings.tsx @@ -33,7 +33,7 @@ import { openReaderSettings, useReader } from '@/lib/nowPlayingReader'; import { NTFY_URL, disablePush, enablePush, pushAvailable, usePushStatus } from '@/lib/push'; import { useSession } from '@/lib/session'; import { checkForUpdate, currentVersion, installUpdate, updaterAvailable, useUpdateState } from '@/lib/updates'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; export default function SettingsScreen() { return ( @@ -670,7 +670,7 @@ function Account() { } const styles = StyleSheet.create({ - small: { fontSize: 13 }, + small: { fontSize: 13, fontFamily: fonts.body }, avatarRow: { flexDirection: 'row', alignItems: 'center', gap: space.lg }, connection: { flexDirection: 'row', alignItems: 'center', gap: space.md }, connectionColumn: { @@ -680,6 +680,6 @@ const styles = StyleSheet.create({ paddingTop: space.md, }, result: { flexDirection: 'row', alignItems: 'center', gap: space.md, paddingVertical: space.xs }, - resultText: { color: colors.text, flex: 1, fontSize: 14 }, + resultText: { color: colors.text, flex: 1, fontSize: 14, fontFamily: fonts.body }, thumb: { width: 40, height: 40, borderRadius: radius.sm }, }); diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index c627895..000f1db 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -1,4 +1,4 @@ -import { SpaceGrotesk_500Medium, SpaceGrotesk_700Bold, useFonts } from '@expo-google-fonts/space-grotesk'; +import { Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold, useFonts } from '@expo-google-fonts/poppins'; import { DarkTheme, Stack, ThemeProvider } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import { StatusBar } from 'expo-status-bar'; @@ -37,7 +37,7 @@ function AppNavigator() { const { status, me, retry } = useSession(); const signedIn = status === 'signedIn'; const onboarded = Boolean(me?.onboardedAt); - const [fontsLoaded, fontError] = useFonts({ SpaceGrotesk_500Medium, SpaceGrotesk_700Bold }); + const [fontsLoaded, fontError] = useFonts({ Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold }); const ready = status !== 'loading' && (fontsLoaded || Boolean(fontError)); useEffect(() => { diff --git a/apps/mobile/src/app/blocked.tsx b/apps/mobile/src/app/blocked.tsx index f1483fb..a1091da 100644 --- a/apps/mobile/src/app/blocked.tsx +++ b/apps/mobile/src/app/blocked.tsx @@ -6,7 +6,7 @@ import { Avatar } from '@/components/Avatar'; import { Body, Button } from '@/components/ui'; import { api } from '@/lib/api'; import { showError } from '@/lib/errors'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; export default function BlockedScreen() { const { data: blocked = [], mutate, isLoading } = useSWR('/me/blocks'); @@ -45,5 +45,5 @@ const styles = StyleSheet.create({ list: { padding: space.lg, gap: space.md }, row: { flexDirection: 'row', alignItems: 'center', gap: space.md }, person: { flexDirection: 'row', alignItems: 'center', gap: space.md }, - name: { color: colors.text, fontSize: 15, fontWeight: '600', flexShrink: 1 }, + name: { color: colors.text, fontSize: 15, fontFamily: fonts.bodyMedium, flexShrink: 1 }, }); diff --git a/apps/mobile/src/app/notifications.tsx b/apps/mobile/src/app/notifications.tsx index 956450e..8deb23e 100644 --- a/apps/mobile/src/app/notifications.tsx +++ b/apps/mobile/src/app/notifications.tsx @@ -8,7 +8,7 @@ import { notificationPath, notificationText, timeAgo, type NotificationDTO, type import { Avatar } from '@/components/Avatar'; import { Body } from '@/components/ui'; import { api } from '@/lib/api'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; /** Reactions, new followers and songs sent to you, newest first. Opening it marks them read. */ export default function NotificationsScreen() { @@ -77,8 +77,8 @@ const styles = StyleSheet.create({ row: { flexDirection: 'row', gap: space.md, alignItems: 'center', padding: space.md, borderRadius: 14 }, unread: { backgroundColor: colors.surface2 }, text: { flex: 1, minWidth: 0, gap: 2 }, - title: { color: colors.text, fontWeight: '700', fontSize: 15 }, - body: { color: colors.text, opacity: 0.85, fontSize: 14 }, + title: { color: colors.text, fontFamily: fonts.display, fontSize: 15 }, + body: { color: colors.text, opacity: 0.85, fontSize: 14, fontFamily: fonts.body }, time: { color: colors.muted, fontSize: 12 }, art: { width: 44, height: 44, borderRadius: 8 }, }); diff --git a/apps/mobile/src/app/onboarding/friends.tsx b/apps/mobile/src/app/onboarding/friends.tsx index c49c7f0..2b99f4a 100644 --- a/apps/mobile/src/app/onboarding/friends.tsx +++ b/apps/mobile/src/app/onboarding/friends.tsx @@ -11,7 +11,7 @@ import { WEB_URL } from '@/lib/config'; import { showError } from '@/lib/errors'; import { successHaptic } from '@/lib/haptics'; import { useSession } from '@/lib/session'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; import { useDebounced } from '@/lib/useDebounced'; /** Step 3: an empty feed is the fastest way to lose someone, so find friends now. */ @@ -90,6 +90,6 @@ export default function FriendsStep() { const styles = StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: space.md }, who: { flexDirection: 'row', alignItems: 'center', gap: space.md, flexShrink: 1 }, - name: { color: colors.text, fontWeight: '700', fontSize: 15 }, - handle: { color: colors.muted, fontSize: 13 }, + name: { color: colors.text, fontFamily: fonts.display, fontSize: 15 }, + handle: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/app/onboarding/index.tsx b/apps/mobile/src/app/onboarding/index.tsx index 24930a9..e29e6e7 100644 --- a/apps/mobile/src/app/onboarding/index.tsx +++ b/apps/mobile/src/app/onboarding/index.tsx @@ -9,7 +9,7 @@ import { showError } from '@/lib/errors'; import { selectionHaptic } from '@/lib/haptics'; import { LISTEN_APPS, finishOnboarding } from '@/lib/onboarding'; import { useSession } from '@/lib/session'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; /** Step 1: where do you listen? Sets where friends' songs open, and picks the right setup next. */ export default function ListenAppStep() { @@ -69,7 +69,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: space.md, }, - optionPicked: { borderColor: colors.accent, backgroundColor: 'rgba(124,77,255,0.18)' }, - optionText: { color: colors.text, fontWeight: '600', fontSize: 15 }, + optionPicked: { borderColor: colors.accent, backgroundColor: colors.accentSoft }, + optionText: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 15 }, optionTextPicked: { color: '#fff' }, }); diff --git a/apps/mobile/src/app/play/[id].tsx b/apps/mobile/src/app/play/[id].tsx index 1307335..b563020 100644 --- a/apps/mobile/src/app/play/[id].tsx +++ b/apps/mobile/src/app/play/[id].tsx @@ -13,7 +13,7 @@ import { api } from '@/lib/api'; import { showError } from '@/lib/errors'; import { tapHaptic } from '@/lib/haptics'; import { useSession } from '@/lib/session'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; /** One play and its comments: where the 💬 chip and comment notifications lead. */ export default function PlayScreen() { @@ -163,9 +163,9 @@ const styles = StyleSheet.create({ header: { gap: space.md, marginBottom: space.sm }, playedBy: { flexDirection: 'row', alignItems: 'center', gap: space.sm }, comment: { flexDirection: 'row', gap: space.md, alignItems: 'flex-start' }, - name: { color: colors.text, fontWeight: '700' }, - muted: { color: colors.muted, fontSize: 13 }, - body: { color: colors.text, fontSize: 15, lineHeight: 21 }, + name: { color: colors.text, fontFamily: fonts.display }, + muted: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, + body: { color: colors.text, fontSize: 15, lineHeight: 21, fontFamily: fonts.body }, more: { color: colors.muted, fontSize: 18, paddingHorizontal: space.xs }, composer: { flexDirection: 'row', @@ -190,6 +190,6 @@ const styles = StyleSheet.create({ borderColor: colors.border, }, send: { paddingHorizontal: space.md, paddingVertical: space.sm + 2, borderRadius: radius.pill, backgroundColor: colors.accent }, - sendText: { color: '#fff', fontWeight: '700' }, + sendText: { color: '#fff', fontFamily: fonts.display }, closed: { margin: space.lg, marginTop: 0 }, }); diff --git a/apps/mobile/src/app/qr.tsx b/apps/mobile/src/app/qr.tsx index 5af1cf3..a4f88fa 100644 --- a/apps/mobile/src/app/qr.tsx +++ b/apps/mobile/src/app/qr.tsx @@ -7,7 +7,7 @@ import { Screen } from '@/components/Screen'; import { Body, Button } from '@/components/ui'; import { WEB_URL } from '@/lib/config'; import { useSession } from '@/lib/session'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; /** "My code": friends scan it (in musicshare or any camera app) to open your profile. */ export default function MyQrScreen() { @@ -40,8 +40,8 @@ export default function MyQrScreen() { const styles = StyleSheet.create({ content: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', gap: space.xl }, who: { alignItems: 'center', gap: space.xs }, - name: { color: colors.text, fontSize: 22, fontWeight: '800' }, - handle: { color: colors.muted, fontSize: 15 }, + name: { color: colors.text, fontSize: 22, fontFamily: fonts.display }, + handle: { color: colors.muted, fontSize: 15, fontFamily: fonts.body }, hint: { textAlign: 'center', maxWidth: 300 }, actions: { alignSelf: 'stretch', gap: space.sm }, }); diff --git a/apps/mobile/src/app/scan.tsx b/apps/mobile/src/app/scan.tsx index fd4bc5a..696989b 100644 --- a/apps/mobile/src/app/scan.tsx +++ b/apps/mobile/src/app/scan.tsx @@ -8,7 +8,7 @@ import { Body, Button, Title } from '@/components/ui'; import { FOSS_BUILD } from '@/lib/build'; import { decodeQrFromJpeg, pickSnapshotSize } from '@/lib/qrSnapshot'; import { useSession } from '@/lib/session'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; /** Scan a friend's musicshare QR code and open their profile (with its Follow button). */ export default function ScanScreen() { @@ -115,7 +115,7 @@ const styles = StyleSheet.create({ prompt: { color: colors.accentText, fontSize: 16, - fontWeight: '600', + fontFamily: fonts.bodyMedium, textAlign: 'center', backgroundColor: 'rgba(0,0,0,0.55)', paddingHorizontal: space.md, diff --git a/apps/mobile/src/app/send/[trackId].tsx b/apps/mobile/src/app/send/[trackId].tsx index 992d56a..aa0c43f 100644 --- a/apps/mobile/src/app/send/[trackId].tsx +++ b/apps/mobile/src/app/send/[trackId].tsx @@ -10,7 +10,7 @@ import { api } from '@/lib/api'; import { showError } from '@/lib/errors'; import { successHaptic, tapHaptic } from '@/lib/haptics'; import { useSession } from '@/lib/session'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; /** Send a song to some of your followers, with an optional note. */ export default function SendSongScreen() { @@ -98,10 +98,10 @@ export default function SendSongScreen() { const styles = StyleSheet.create({ list: { padding: space.lg, gap: space.sm, paddingBottom: space.xl * 2 }, - song: { color: colors.text, fontSize: 17, fontWeight: '700' }, - muted: { color: colors.muted, fontWeight: '400' }, + song: { color: colors.text, fontSize: 17, fontFamily: fonts.display }, + muted: { color: colors.muted, fontFamily: fonts.body }, person: { flexDirection: 'row', alignItems: 'center', gap: space.md, padding: space.sm, borderRadius: radius.md }, on: { backgroundColor: colors.surface2 }, - name: { flex: 1, color: colors.text, fontSize: 15, fontWeight: '600' }, - check: { color: colors.muted, fontSize: 18 }, + name: { flex: 1, color: colors.text, fontSize: 15, fontFamily: fonts.bodyMedium }, + check: { color: colors.muted, fontSize: 18, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/components/ActivityBell.tsx b/apps/mobile/src/components/ActivityBell.tsx index 586ef51..ede1d60 100644 --- a/apps/mobile/src/components/ActivityBell.tsx +++ b/apps/mobile/src/components/ActivityBell.tsx @@ -1,8 +1,9 @@ import { router } from 'expo-router'; import { Pressable, StyleSheet, Text, View } from 'react-native'; +import Svg, { Path } from 'react-native-svg'; import useSWR from 'swr'; import type { NotificationListDTO } from '@musicshare/shared'; -import { colors } from '@/lib/theme'; +import { colors, fonts } from '@/lib/theme'; /** Bell with the unread count; opens your notifications. */ export function ActivityBell() { @@ -15,7 +16,10 @@ export function ActivityBell() { accessibilityRole="button" accessibilityLabel={unread ? `Notifications, ${unread} new` : 'Notifications'} style={styles.bell}> - 🔔 + + + + {unread > 0 && ( {unread > 99 ? '99+' : unread} @@ -26,8 +30,7 @@ export function ActivityBell() { } const styles = StyleSheet.create({ - bell: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.surface2 }, - icon: { fontSize: 18 }, + bell: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.surface2 }, badge: { position: 'absolute', top: -2, @@ -36,9 +39,9 @@ const styles = StyleSheet.create({ height: 18, paddingHorizontal: 4, borderRadius: 9, - backgroundColor: colors.danger, + backgroundColor: colors.accent, alignItems: 'center', justifyContent: 'center', }, - badgeText: { color: '#fff', fontSize: 11, fontWeight: '700' }, + badgeText: { color: '#fff', fontSize: 11, fontFamily: fonts.display }, }); diff --git a/apps/mobile/src/components/AmbientBackground.tsx b/apps/mobile/src/components/AmbientBackground.tsx index 0649371..e53c2de 100644 --- a/apps/mobile/src/components/AmbientBackground.tsx +++ b/apps/mobile/src/components/AmbientBackground.tsx @@ -1,36 +1,15 @@ -import { Image } from 'expo-image'; -import { useEffect } from 'react'; -import { StyleSheet, useWindowDimensions, View } from 'react-native'; -import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; +import { LinearGradient } from 'expo-linear-gradient'; +import { StyleSheet } from 'react-native'; import { colors } from '@/lib/theme'; -const AnimatedImage = Animated.createAnimatedComponent(Image); - -/** Two soft colour blobs drifting slowly behind the content, so screens never feel static. */ +/** The page behind everything: flat slate, a touch lighter at the top. */ export function AmbientBackground() { - const { width, height } = useWindowDimensions(); - const t = useSharedValue(0); - - useEffect(() => { - t.set(withRepeat(withTiming(1, { duration: 18000, easing: Easing.inOut(Easing.sin) }), -1, true)); - }, [t]); - - const size = Math.max(width, height) * 0.9; - const purple = useAnimatedStyle(() => ({ - transform: [{ translateX: -size * 0.35 + t.value * width * 0.35 }, { translateY: -size * 0.3 + t.value * height * 0.12 }], - })); - const blue = useAnimatedStyle(() => ({ - transform: [{ translateX: width - size * 0.6 - t.value * width * 0.3 }, { translateY: height - size * 0.55 - t.value * height * 0.1 }], - })); - return ( - - - - + ); } - -const styles = StyleSheet.create({ - blob: { position: 'absolute', top: 0, left: 0, opacity: 0.45 }, -}); diff --git a/apps/mobile/src/components/AuthForm.tsx b/apps/mobile/src/components/AuthForm.tsx index 8bd7efd..7fbd702 100644 --- a/apps/mobile/src/components/AuthForm.tsx +++ b/apps/mobile/src/components/AuthForm.tsx @@ -5,7 +5,7 @@ import { KeyboardAvoidingView, Linking, Platform, StyleSheet, Text, View } from import { api } from '@/lib/api'; import { WEB_URL } from '@/lib/config'; import { useSession } from '@/lib/session'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; import { Screen } from './Screen'; import { Body, Button, Field, Title } from './ui'; @@ -106,7 +106,7 @@ export function AuthForm({ mode }: { mode: 'login' | 'register' }) { const styles = StyleSheet.create({ content: { flexGrow: 1, justifyContent: 'center' }, link: { color: colors.muted, textAlign: 'center', paddingVertical: space.sm }, - legal: { color: colors.muted, fontSize: 12, textAlign: 'center', lineHeight: 17 }, + legal: { color: colors.muted, fontSize: 12, textAlign: 'center', lineHeight: 17, fontFamily: fonts.body }, legalLink: { color: colors.text, textDecorationLine: 'underline' }, logo: { width: 76, height: 76, borderRadius: 20, marginBottom: space.sm }, }); diff --git a/apps/mobile/src/components/Avatar.tsx b/apps/mobile/src/components/Avatar.tsx index 06a771a..c3ff366 100644 --- a/apps/mobile/src/components/Avatar.tsx +++ b/apps/mobile/src/components/Avatar.tsx @@ -20,7 +20,7 @@ export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?: const face = user.avatarUrl ? ( ) : ( - + {(user.displayName ?? user.username).slice(0, 1).toUpperCase()} ); @@ -31,7 +31,7 @@ export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?: (`/tracks/${track.id}/lyrics`); const [full, setFull] = useState(false); - if (isLoading) return Finding lyrics…; - if (error) return Couldn't load lyrics right now.; - if (!data) return No lyrics found for this song.; - if (data.instrumental && !data.synced && !data.plain) return ♪ Instrumental; + if (isLoading) return Finding lyrics…; + if (error) return Couldn't load lyrics right now.; + if (!data) return No lyrics found for this song.; + if (data.instrumental && !data.synced && !data.plain) return ♪ Instrumental; const synced = data.synced; const live = Boolean(np?.isPlaying && playbackPosition(np, 0)); @@ -48,11 +50,11 @@ export function Lyrics({ track, np }: { track: TrackDTO; np?: NowPlayingDTO }) { {canToggle && ( - setFull((v) => !v)}> + setFull((v) => !v)}> {!full ? 'All lyrics' : synced && live ? 'Follow along' : 'Less'} )} - Linking.openURL(data.source.url)}> + Linking.openURL(data.source.url)}> Lyrics from {data.source.name} @@ -85,13 +87,13 @@ function FollowAlong({ lines, np }: { lines: LyricLine[]; np: NowPlayingDTO }) { const styles = StyleSheet.create({ wrap: { gap: space.sm }, - note: { color: colors.muted, fontSize: 13 }, - line: { color: colors.text, fontSize: 15, lineHeight: 22 }, + note: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, + line: { color: colors.text, fontSize: 14.5, lineHeight: 22, fontFamily: fonts.body }, plain: { opacity: 0.9 }, past: { opacity: 0.4 }, now: { fontFamily: fonts.display, fontSize: 17, lineHeight: 24 }, next: { opacity: 0.65 }, footer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, - toggle: { color: colors.accent2, fontSize: 13, fontWeight: '600' }, - source: { color: colors.muted, fontSize: 11, marginLeft: 'auto' }, + toggle: { color: colors.accent, fontSize: 13, fontFamily: fonts.bodyMedium }, + source: { color: colors.muted, fontSize: 11, fontFamily: fonts.body, marginLeft: 'auto' }, }); diff --git a/apps/mobile/src/components/NowPlayingCard.tsx b/apps/mobile/src/components/NowPlayingCard.tsx index 356e8dd..ffdfdd5 100644 --- a/apps/mobile/src/components/NowPlayingCard.tsx +++ b/apps/mobile/src/components/NowPlayingCard.tsx @@ -1,4 +1,3 @@ -import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import { useEffect, useRef, useState } from 'react'; @@ -13,7 +12,7 @@ import Animated, { } from 'react-native-reanimated'; import { SERVICE_LABELS, timeAgo, type NowPlayingDTO, type StreamingService } from '@musicshare/shared'; import { successHaptic, tapHaptic } from '@/lib/haptics'; -import { colors, radius, space } from '@/lib/theme'; +import { OnHero, colors, fonts, heroGradient, panelShadow, radius, space } from '@/lib/theme'; import { Avatar } from './Avatar'; import { Equalizer } from './Equalizer'; import { Lyrics } from './Lyrics'; @@ -54,15 +53,12 @@ export function NowPlayingCard({ onTogglePin?.(); }; + // Your own song gets the gradient "hero" card; friends' songs sit on plain panels. + const hero = !showUser; return ( - - {/* The card glows with its album art; crossfades when the song changes. */} - {np.track.artworkUrl && ( - - - - )} - + + + {hero && } {showUser && ( @@ -74,11 +70,11 @@ export function NowPlayingCard({ )} - {np.isPlaying && } - + {np.isPlaying && } + {np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`} Linking.openURL(np.sourceUrl!) : undefined}> {' · '} {np.sourceApp ?? SERVICE_LABELS[np.source]} @@ -109,30 +105,33 @@ export function NowPlayingCard({ )} + ); } const styles = StyleSheet.create({ card: { borderRadius: radius.lg, - padding: space.md + 2, + padding: space.md + 4, gap: space.sm + 2, - overflow: 'hidden', backgroundColor: colors.surface, borderWidth: 1, - borderColor: colors.border, + borderColor: 'transparent', + ...panelShadow, }, + heroCard: { overflow: 'hidden', shadowColor: colors.accent, shadowOpacity: 0.4, shadowRadius: 22 }, + heroText: { color: 'rgba(255,255,255,0.85)' }, + heroLive: { color: '#ffffff', fontFamily: fonts.bodyMedium }, details: { gap: space.md }, - pinned: { borderColor: 'rgba(251,191,36,0.55)' }, - backdrop: { position: 'absolute', top: '-40%', left: '-40%', width: '180%', height: '180%', opacity: 0.55 }, + pinned: { borderColor: colors.accentLine }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: space.sm }, user: { flexDirection: 'row', alignItems: 'center', gap: space.sm, flexShrink: 1 }, - name: { color: colors.text, fontWeight: '700', fontSize: 15, flexShrink: 1 }, + name: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 14.5, flexShrink: 1 }, statusRow: { flexDirection: 'row', alignItems: 'center', gap: space.xs + 2, flexShrink: 1 }, - status: { color: colors.muted, fontSize: 13, flexShrink: 1 }, - live: { color: colors.live, fontWeight: '600' }, - via: { color: colors.muted, fontWeight: '400' }, + status: { color: colors.muted, fontSize: 12.5, fontFamily: fonts.body, flexShrink: 1 }, + live: { color: colors.live, fontFamily: fonts.bodyMedium }, + via: { color: colors.muted, fontFamily: fonts.body }, viaLink: { textDecorationLine: 'underline' }, pin: { color: colors.muted, fontSize: 21, marginLeft: space.xs }, - pinOn: { color: colors.pin, textShadowColor: 'rgba(251,191,36,0.6)', textShadowRadius: 10 }, + pinOn: { color: colors.accent, textShadowColor: 'rgba(255,79,139,0.6)', textShadowRadius: 10 }, }); diff --git a/apps/mobile/src/components/ProfileView.tsx b/apps/mobile/src/components/ProfileView.tsx index cfb7089..6668679 100644 --- a/apps/mobile/src/components/ProfileView.tsx +++ b/apps/mobile/src/components/ProfileView.tsx @@ -7,7 +7,7 @@ import { api } from '@/lib/api'; import { showError } from '@/lib/errors'; import { useRealtime } from '@/lib/useRealtime'; import { useSession } from '@/lib/session'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; import { Avatar } from './Avatar'; import { NowPlayingCard } from './NowPlayingCard'; import { RecentPlays } from './RecentPlays'; @@ -162,9 +162,9 @@ export function ProfileView({ username, edges }: { username: string; edges?: ('t const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', gap: space.lg }, - meta: { color: colors.muted, fontSize: 13 }, + meta: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, counts: { flexDirection: 'row', gap: space.lg, marginTop: 2 }, - count: { color: colors.text, fontWeight: '700' }, + count: { color: colors.text, fontFamily: fonts.display }, quote: { color: colors.text, fontStyle: 'italic', diff --git a/apps/mobile/src/components/ProgressBar.tsx b/apps/mobile/src/components/ProgressBar.tsx index bb76d57..c62d9b0 100644 --- a/apps/mobile/src/components/ProgressBar.tsx +++ b/apps/mobile/src/components/ProgressBar.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { formatDuration, playbackPosition, type NowPlayingDTO } from '@musicshare/shared'; -import { colors } from '@/lib/theme'; +import { brandGradient, fonts, useInk } from '@/lib/theme'; /** Where they are in the song. Renders nothing when the source doesn't report a position. */ export function ProgressBar({ np, compact }: { np: NowPlayingDTO; /** Just the bar, no times. */ compact?: boolean }) { @@ -20,6 +20,7 @@ function Bar({ np, compact }: { np: NowPlayingDTO; compact?: boolean }) { return () => clearInterval(id); }, []); + const ink = useInk(); const pos = playbackPosition(np, now - receivedAt); const fraction = pos ? pos.positionMs / pos.durationMs : 0; const width = useSharedValue(fraction); @@ -31,15 +32,15 @@ function Bar({ np, compact }: { np: NowPlayingDTO; compact?: boolean }) { if (!pos) return null; return ( - - - + + + {!ink.hero && } {!compact && ( - {formatDuration(pos.positionMs)} - {formatDuration(pos.durationMs)} + {formatDuration(pos.positionMs)} + {formatDuration(pos.durationMs)} )} @@ -48,8 +49,8 @@ function Bar({ np, compact }: { np: NowPlayingDTO; compact?: boolean }) { const styles = StyleSheet.create({ wrap: { marginTop: 2 }, - track: { height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.14)', overflow: 'hidden' }, + track: { height: 4, borderRadius: 2, overflow: 'hidden' }, fill: { height: '100%', borderRadius: 2, overflow: 'hidden' }, times: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 }, - time: { color: colors.muted, fontSize: 12, fontVariant: ['tabular-nums'] }, + time: { fontSize: 11.5, fontFamily: fonts.body, fontVariant: ['tabular-nums'] }, }); diff --git a/apps/mobile/src/components/Reactions.tsx b/apps/mobile/src/components/Reactions.tsx index c887812..f674cf7 100644 --- a/apps/mobile/src/components/Reactions.tsx +++ b/apps/mobile/src/components/Reactions.tsx @@ -7,7 +7,7 @@ import { api } from '@/lib/api'; import { showError } from '@/lib/errors'; import { tapHaptic } from '@/lib/haptics'; import { useSession } from '@/lib/session'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, panelShadow, radius, space } from '@/lib/theme'; /** * Emoji reactions on one play. Tap an emoji to add yours (or take it back); "☺+" @@ -168,13 +168,13 @@ const styles = StyleSheet.create({ paddingHorizontal: 9, paddingVertical: 3, borderRadius: radius.pill, - backgroundColor: 'rgba(255,255,255,0.07)', - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(255,255,255,0.16)', + backgroundColor: 'rgba(255,255,255,0.1)', + borderWidth: 1, + borderColor: 'transparent', }, - mine: { backgroundColor: 'rgba(123,47,247,0.3)', borderColor: 'rgba(123,47,247,0.75)' }, + mine: { backgroundColor: colors.accentSoft, borderColor: colors.accentLine }, emoji: { fontSize: 16 }, - count: { color: colors.text, fontSize: 12, fontVariant: ['tabular-nums'] }, + count: { color: colors.text, fontSize: 12, fontVariant: ['tabular-nums'], fontFamily: fonts.body }, quick: { flexDirection: 'row', alignSelf: 'flex-start', @@ -182,16 +182,15 @@ const styles = StyleSheet.create({ gap: 2, padding: 4, borderRadius: radius.pill, - backgroundColor: '#1a1535', - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(255,255,255,0.16)', + backgroundColor: colors.surface2, + ...panelShadow, }, quickItem: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' }, quickEmoji: { fontSize: 26 }, - backdrop: { flex: 1, backgroundColor: 'rgba(5,3,15,0.6)' }, + backdrop: { flex: 1, backgroundColor: 'rgba(20,18,40,0.6)' }, sheet: { height: '55%', - backgroundColor: '#15112c', + backgroundColor: colors.surface, borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg, padding: space.md, @@ -199,7 +198,7 @@ const styles = StyleSheet.create({ }, search: { color: colors.text, - backgroundColor: 'rgba(255,255,255,0.07)', + backgroundColor: colors.surface2, borderRadius: radius.md, paddingHorizontal: space.md, paddingVertical: 10, diff --git a/apps/mobile/src/components/RecentPlays.tsx b/apps/mobile/src/components/RecentPlays.tsx index 30bdb02..78a2a27 100644 --- a/apps/mobile/src/components/RecentPlays.tsx +++ b/apps/mobile/src/components/RecentPlays.tsx @@ -2,7 +2,7 @@ import { Image } from 'expo-image'; import { Linking, Pressable, StyleSheet, Text, View } from 'react-native'; import { openUrl, timeAgo, type ProfileDTO, type RecentPlayDTO, type StreamingService } from '@musicshare/shared'; import { showError } from '@/lib/errors'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; import { Card, Heading } from './ui'; import { Reactions } from './Reactions'; @@ -64,9 +64,9 @@ const styles = StyleSheet.create({ divider: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border }, art: { width: 40, height: 40, borderRadius: radius.sm, backgroundColor: colors.surface2 }, artEmpty: { alignItems: 'center', justifyContent: 'center' }, - title: { color: colors.text, fontWeight: '600', fontSize: 14 }, - artist: { color: colors.muted, fontSize: 13 }, + title: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 14 }, + artist: { color: colors.muted, fontSize: 13, fontFamily: fonts.body }, time: { color: colors.muted, fontSize: 12 }, reactions: { marginLeft: 40 + space.md, marginTop: -space.sm, marginBottom: space.xs }, - credit: { color: colors.muted, fontSize: 12, marginTop: space.sm }, + credit: { color: colors.muted, fontSize: 12, marginTop: space.sm, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/components/TrackCard.tsx b/apps/mobile/src/components/TrackCard.tsx index 86690b5..3df70d5 100644 --- a/apps/mobile/src/components/TrackCard.tsx +++ b/apps/mobile/src/components/TrackCard.tsx @@ -5,7 +5,7 @@ import { SERVICE_LABELS, openUrl, shareText, trackUrl, viewerServices, type Stre import { WEB_URL } from '@/lib/config'; import { useSession } from '@/lib/session'; import { showError } from '@/lib/errors'; -import { colors, fonts, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space, useInk } from '@/lib/theme'; import { Button } from './ui'; /** @@ -50,6 +50,8 @@ function PrimaryButton({ track, preferred, label }: { track: TrackDTO; preferred /** Quieter links for your other services, and Share. */ export function ServiceLinks({ track, preferred }: { track: TrackDTO; preferred: StreamingService | null | undefined }) { const { me } = useSession(); + const ink = useInk(); + const chip = [styles.service, ink.hero && styles.serviceHero]; const others = viewerServices(me).filter((s) => s !== preferred); return ( @@ -57,17 +59,17 @@ export function ServiceLinks({ track, preferred }: { track: TrackDTO; preferred: const { url, exact } = openUrl(track, s); return ( open(url)} hitSlop={6} accessibilityRole="link"> - {SERVICE_LABELS[s]} + {SERVICE_LABELS[s]} ); })} {me && ( router.push(`/send/${track.id}`)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Send to friends"> - Send ✉ + Send ✉ )} shareTrack(track, me?.username)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Share this song"> - Share ↗ + Share ↗ ); @@ -88,6 +90,7 @@ export function TrackRow({ expanded: boolean; onToggle: () => void; }) { + const ink = useInk(); return ( @@ -98,7 +101,7 @@ export function TrackRow({ {track.artist} - {expanded && track.album ? · {track.album} : null} + {expanded && track.album ? · {track.album} : null} @@ -109,7 +112,7 @@ export function TrackRow({ accessibilityRole="button" accessibilityLabel={expanded ? 'Show less' : 'Show more'} style={styles.chevron}> - ⌄ + ⌄ ); @@ -143,32 +146,29 @@ const styles = StyleSheet.create({ track: { flexDirection: 'row', gap: space.md + 2, alignItems: 'flex-start' }, row: { flexDirection: 'row', alignItems: 'center', gap: space.sm }, rowMain: { flex: 1, minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: space.md }, - artwork: { - backgroundColor: colors.surface2, - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(255,255,255,0.12)', - }, - artworkEmpty: { alignItems: 'center', justifyContent: 'center', backgroundColor: '#3a1a6e' }, + artwork: { backgroundColor: colors.surface2 }, + artworkEmpty: { alignItems: 'center', justifyContent: 'center', backgroundColor: '#4b3f78' }, note: { color: 'rgba(255,255,255,0.85)' }, body: { flex: 1, minWidth: 0, gap: 2 }, title: { color: colors.text, fontFamily: fonts.display, fontSize: 16, letterSpacing: -0.2 }, titleBig: { fontSize: 18 }, - artist: { color: colors.text, opacity: 0.85, fontSize: 14 }, + artist: { color: colors.text, opacity: 0.85, fontSize: 13.5, fontFamily: fonts.body }, muted: { color: colors.muted }, chevron: { paddingHorizontal: space.xs, alignSelf: 'stretch', justifyContent: 'center' }, chevronText: { color: colors.muted, fontSize: 22, lineHeight: 24 }, - share: { backgroundColor: 'rgba(123,47,247,0.28)', borderColor: 'rgba(123,47,247,0.5)' }, + share: { backgroundColor: colors.accentSoft, color: '#ffd1e1' }, + shareHero: { backgroundColor: 'rgba(255,255,255,0.32)' }, + serviceHero: { backgroundColor: 'rgba(255,255,255,0.2)', color: '#ffffff' }, primary: { alignSelf: 'flex-start', marginTop: space.sm }, services: { flexDirection: 'row', flexWrap: 'wrap', gap: space.xs + 2, marginTop: space.sm + 2 }, service: { color: colors.text, fontSize: 12, - paddingHorizontal: space.sm + 2, - paddingVertical: 4, + fontFamily: fonts.bodyMedium, + paddingHorizontal: space.sm + 4, + paddingVertical: 5, borderRadius: radius.pill, overflow: 'hidden', - backgroundColor: 'rgba(255,255,255,0.08)', - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(255,255,255,0.12)', + backgroundColor: colors.surface2, }, }); diff --git a/apps/mobile/src/components/setup/SpotifySetup.tsx b/apps/mobile/src/components/setup/SpotifySetup.tsx index 16ed46c..fb054c2 100644 --- a/apps/mobile/src/components/setup/SpotifySetup.tsx +++ b/apps/mobile/src/components/setup/SpotifySetup.tsx @@ -5,7 +5,7 @@ import type { ConnectionDTO } from '@musicshare/shared'; import { api } from '@/lib/api'; import { SignInError, openSignupPage, openSpotifyLinkPage, signInWithService } from '@/lib/connect'; import { successHaptic } from '@/lib/haptics'; -import { colors, radius, space } from '@/lib/theme'; +import { colors, fonts, radius, space } from '@/lib/theme'; import { Body, Button, Field } from '../ui'; import { ErrorText, SetupActions, SetupHeader, type SetupProps } from './common'; @@ -153,9 +153,9 @@ function Option({ selected, onPress, title, badge, detail }: { selected: boolean const styles = StyleSheet.create({ option: { borderRadius: radius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface, padding: space.md, gap: space.xs }, - optionSelected: { borderColor: colors.accent, backgroundColor: 'rgba(124,77,255,0.16)' }, + optionSelected: { borderColor: colors.accent, backgroundColor: colors.accentSoft }, optionHead: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, - optionTitle: { color: colors.text, fontWeight: '700', fontSize: 16 }, - badge: { color: colors.accent2, fontSize: 12, fontWeight: '700' }, - small: { fontSize: 13 }, + optionTitle: { color: colors.text, fontFamily: fonts.display, fontSize: 16 }, + badge: { color: colors.accent, fontSize: 12, fontFamily: fonts.display }, + small: { fontSize: 13, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/components/setup/common.tsx b/apps/mobile/src/components/setup/common.tsx index 8ad017d..6800012 100644 --- a/apps/mobile/src/components/setup/common.tsx +++ b/apps/mobile/src/components/setup/common.tsx @@ -1,5 +1,5 @@ import { StyleSheet, View } from 'react-native'; -import { colors, space } from '@/lib/theme'; +import { colors, fonts, space } from '@/lib/theme'; import { Body, Button, Card, Title } from '../ui'; export interface SetupProps { @@ -55,5 +55,5 @@ export const ErrorText = ({ children }: { children: string | null }) => children ? {children} : null; const styles = StyleSheet.create({ - point: { color: colors.text, fontSize: 14 }, + point: { color: colors.text, fontSize: 14, fontFamily: fonts.body }, }); diff --git a/apps/mobile/src/components/ui.tsx b/apps/mobile/src/components/ui.tsx index 27c0bc6..a12951e 100644 --- a/apps/mobile/src/components/ui.tsx +++ b/apps/mobile/src/components/ui.tsx @@ -1,5 +1,5 @@ import { LinearGradient } from 'expo-linear-gradient'; -import { forwardRef, useState } from 'react'; +import { forwardRef, useContext, useState } from 'react'; import { ActivityIndicator, Pressable, @@ -15,7 +15,7 @@ import { } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import { tapHaptic } from '@/lib/haptics'; -import { brandGradient, colors, fonts, radius, space } from '@/lib/theme'; +import { OnHero, brandGradient, colors, fonts, panelShadow, radius, space } from '@/lib/theme'; export function Title({ style, ...props }: TextProps) { return ; @@ -65,17 +65,19 @@ export function Button({ ...props }: PressableProps & { title: string; variant?: Variant; loading?: boolean; small?: boolean; style?: ViewProps['style'] }) { const press = usePressScale(); + const hero = useContext(OnHero); const inactive = disabled || loading; const handlePress = (e: GestureResponderEvent) => { if (variant === 'primary') tapHaptic(); onPress?.(e); }; + // On the pink hero card a pink button would vanish: primary goes white with pink text instead. + const heroPrimary = hero && variant === 'primary'; + const textColor = heroPrimary ? colors.accent : variant === 'secondary' ? colors.text : colors.accentText; const label = loading ? ( - + ) : ( - - {title} - + {title} ); return ( @@ -86,9 +88,15 @@ export function Button({ onPressIn={press.onPressIn} onPressOut={press.onPressOut} onPress={handlePress} - style={[styles.button, small && styles.buttonSmall, variant === 'danger' && styles.buttonDanger, variant === 'primary' && styles.buttonPrimary]} + style={[ + styles.button, + small && styles.buttonSmall, + variant === 'danger' && styles.buttonDanger, + variant === 'primary' && styles.buttonPrimary, + hero && (variant === 'primary' ? styles.buttonHeroPrimary : styles.buttonHero), + ]} {...props}> - {variant === 'primary' && ( + {variant === 'primary' && !hero && ( )} {label} @@ -183,20 +191,18 @@ export function Chip({ label, selected, onPress }: { label: string; selected?: b } const styles = StyleSheet.create({ - title: { color: colors.text, fontSize: 30, fontFamily: fonts.display, letterSpacing: -0.6 }, - heading: { color: colors.text, fontSize: 18, fontFamily: fonts.display, marginBottom: space.xs }, - body: { color: colors.text, fontSize: 15, lineHeight: 21 }, + title: { color: colors.text, fontSize: 26, fontFamily: fonts.display, letterSpacing: -0.4 }, + heading: { color: colors.text, fontSize: 17, fontFamily: fonts.display, marginBottom: space.xs }, + body: { color: colors.text, fontSize: 14.5, lineHeight: 21, fontFamily: fonts.body }, muted: { color: colors.muted }, card: { backgroundColor: colors.surface, - borderColor: colors.border, - borderWidth: StyleSheet.hairlineWidth, borderRadius: radius.lg, - padding: space.lg, + padding: space.lg + 2, gap: space.md, - overflow: 'hidden', + ...panelShadow, }, - cardHighlighted: { borderColor: 'rgba(251,191,36,0.55)', borderWidth: 1 }, + cardHighlighted: { borderColor: colors.accentLine, borderWidth: 1 }, row: { flexDirection: 'row', alignItems: 'center', gap: space.sm, flexWrap: 'wrap' }, button: { minHeight: 46, @@ -205,51 +211,49 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', backgroundColor: colors.surface2, - borderWidth: 1, - borderColor: colors.borderStrong, overflow: 'hidden', }, buttonSmall: { minHeight: 36, paddingHorizontal: space.md + 2 }, buttonPrimary: { - borderColor: 'transparent', - shadowColor: '#5b4cf0', - shadowOpacity: 0.6, + shadowColor: colors.accent, + shadowOpacity: 0.45, shadowRadius: 14, shadowOffset: { width: 0, height: 6 }, elevation: 6, }, - buttonDanger: { backgroundColor: colors.danger, borderColor: colors.danger }, - buttonText: { color: colors.text, fontWeight: '700', fontSize: 15 }, + buttonHero: { backgroundColor: 'rgba(255,255,255,0.22)' }, + buttonHeroPrimary: { backgroundColor: '#ffffff', shadowColor: '#7a1e4a', shadowOpacity: 0.25 }, + buttonDanger: { backgroundColor: colors.danger }, + buttonText: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 14.5 }, buttonTextSmall: { fontSize: 13 }, - label: { color: colors.text, fontWeight: '600', fontSize: 14 }, + label: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 14 }, listRow: { flexDirection: 'row', alignItems: 'center', gap: space.md, paddingVertical: 13, paddingHorizontal: 2, borderRadius: radius.sm }, listDivider: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border }, - listLabel: { color: colors.text, fontSize: 15, fontWeight: '600' }, - listValue: { flex: 1, textAlign: 'right', color: colors.muted, fontSize: 14 }, - listChevron: { color: colors.muted, fontSize: 20, lineHeight: 20 }, + listLabel: { color: colors.text, fontSize: 14.5, fontFamily: fonts.bodyMedium }, + listValue: { flex: 1, textAlign: 'right', color: colors.muted, fontSize: 13.5, fontFamily: fonts.body }, + listChevron: { color: colors.muted, fontSize: 20, lineHeight: 20, fontFamily: fonts.body }, reveal: { position: 'absolute', right: 0, top: 0, bottom: 0, justifyContent: 'center', paddingHorizontal: space.md }, - revealText: { color: colors.accent2, fontWeight: '600', fontSize: 14 }, + revealText: { color: colors.accent, fontFamily: fonts.bodyMedium, fontSize: 13.5 }, input: { minHeight: 48, paddingHorizontal: space.md + 2, paddingVertical: space.sm, borderRadius: radius.md, borderWidth: 1, - borderColor: colors.border, + borderColor: 'transparent', backgroundColor: colors.surface2, color: colors.text, - fontSize: 15, + fontSize: 14.5, + fontFamily: fonts.body, }, - inputFocused: { borderColor: colors.accent, backgroundColor: 'rgba(124,77,255,0.10)' }, + inputFocused: { borderColor: colors.accentLine, backgroundColor: '#463f62' }, chip: { - paddingHorizontal: space.md, + paddingHorizontal: space.md + 2, paddingVertical: space.sm, borderRadius: radius.pill, - borderWidth: 1, - borderColor: colors.border, backgroundColor: colors.surface2, overflow: 'hidden', }, - chipSelected: { borderColor: 'transparent' }, - chipText: { color: colors.text, fontWeight: '600', fontSize: 13 }, + chipSelected: {}, + chipText: { color: colors.text, fontFamily: fonts.bodyMedium, fontSize: 13 }, }); diff --git a/apps/mobile/src/lib/theme.ts b/apps/mobile/src/lib/theme.ts index facb8dd..7c21590 100644 --- a/apps/mobile/src/lib/theme.ts +++ b/apps/mobile/src/lib/theme.ts @@ -1,28 +1,62 @@ -/** Mirrors the web palette (apps/web/src/app/globals.css). Brand colours come from the logo. */ +import { createContext, useContext } from 'react'; + +/** + * Mirrors the web palette (apps/web/src/app/globals.css): slate-indigo layers with a pink accent. + * Panels are a shade lighter than the page instead of outlined. + */ export const colors = { - bg: '#07070b', - surface: 'rgba(255,255,255,0.05)', - surfaceSolid: '#12121a', - surface2: 'rgba(255,255,255,0.09)', - border: 'rgba(255,255,255,0.10)', - borderStrong: 'rgba(255,255,255,0.18)', - text: '#f4f3fa', - muted: '#9b99ae', - accent: '#7c4dff', - accent2: '#2f8cff', + bg: '#2a2942', + surface: '#34334f', + surfaceSolid: '#34334f', + surface2: '#403e5e', + border: 'rgba(255,255,255,0.07)', + borderStrong: 'rgba(255,255,255,0.14)', + text: '#f4f3fb', + muted: '#a19fbd', + accent: '#ff4f8b', + accent2: '#8b7dff', accentText: '#ffffff', - live: '#34d399', - danger: '#fb7185', - pin: '#fbbf24', + /** Pink washes for selected chips, "mine" reactions and focus. */ + accentSoft: 'rgba(255,79,139,0.18)', + accentLine: 'rgba(255,79,139,0.6)', + live: '#3ee0a1', + danger: '#ff6b81', + pin: '#ffc95c', } as const; -/** The logo's purple-to-blue, for buttons and highlights. */ -export const brandGradient = ['#7b2ff7', '#5b4cf0', '#2f8cff'] as const; +/** Buttons and selected chips: pink into violet. */ +export const brandGradient = ['#ff4f8b', '#d14fc4', '#8b5cf6'] as const; +/** The big "your song" card: pink, lavender, sky blue (like a holographic card). */ +export const heroGradient = ['#ff4f8b', '#e27ad8', '#a99cf6', '#8fc8ff'] as const; export const fonts = { - display: 'SpaceGrotesk_700Bold', - displayMedium: 'SpaceGrotesk_500Medium', + display: 'Poppins_600SemiBold', + displayMedium: 'Poppins_500Medium', + body: 'Poppins_400Regular', + bodyMedium: 'Poppins_500Medium', } as const; export const space = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 } as const; -export const radius = { sm: 8, md: 12, lg: 20, pill: 999 } as const; +export const radius = { sm: 10, md: 14, lg: 24, pill: 999 } as const; + +/** Soft drop shadow for panels (the reference separates layers with shadow, not borders). */ +export const panelShadow = { + shadowColor: '#15142a', + shadowOpacity: 0.35, + shadowRadius: 18, + shadowOffset: { width: 0, height: 8 }, + elevation: 4, +} as const; + +/** + * Inside the gradient hero card, text and controls switch to white so they read on pink. + * Components that draw muted text or filled buttons ask useInk() which to use. + */ +export const OnHero = createContext(false); + +export function useInk() { + const hero = useContext(OnHero); + return hero + ? { hero, text: '#ffffff', muted: 'rgba(255,255,255,0.82)', faint: 'rgba(255,255,255,0.28)', live: '#ffffff' } + : { hero, text: colors.text, muted: colors.muted, faint: 'rgba(255,255,255,0.12)', live: colors.live }; +} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index c512382..f823756 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1,23 +1,31 @@ :root { - --bg: #07070b; - --surface: rgba(255, 255, 255, 0.045); - --surface-solid: #12121a; - --surface-2: rgba(255, 255, 255, 0.08); - --border: rgba(255, 255, 255, 0.09); - --border-strong: rgba(255, 255, 255, 0.16); - --text: #f4f3fa; - --muted: #9b99ae; - /* Brand: the logo's purple-to-blue. */ - --accent: #7c4dff; - --accent-2: #2f8cff; - --grad: linear-gradient(135deg, #7b2ff7 0%, #5b4cf0 50%, #2f8cff 100%); + /* Slate-indigo layers with a pink accent. Panels are a shade apart instead of outlined. */ + --bg: #2a2942; + --side: #2a2942; + --panel: #34334f; + --card: #34334f; + --surface: var(--card); + --surface-solid: #34334f; + --surface-2: #403e5e; + --border: rgba(255, 255, 255, 0.07); + --border-strong: rgba(255, 255, 255, 0.13); + --text: #f4f3fb; + --muted: #a19fbd; + --accent: #ff4f8b; + --accent-2: #8b7dff; + --accent-soft: rgba(255, 79, 139, 0.16); + --accent-line: rgba(255, 79, 139, 0.55); + --grad: linear-gradient(135deg, #ff4f8b 0%, #d14fc4 55%, #8b5cf6 100%); + /* Your own song's card: pink, lavender, sky blue, like a holographic card. */ + --hero: linear-gradient(135deg, #ff4f8b 0%, #e27ad8 38%, #a99cf6 70%, #8fc8ff 100%); --accent-text: #fff; - --live: #34d399; - --pin: #fbbf24; - --danger: #fb7185; - --radius: 20px; - --font-display: var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif; - --font-body: var(--font-inter), ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --live: #3ee0a1; + --pin: #ffc95c; + --danger: #ff6b81; + --radius: 24px; + --shadow: 0 18px 40px -22px rgba(14, 12, 32, 0.75); + --font-display: var(--font-poppins), ui-sans-serif, system-ui, sans-serif; + --font-body: var(--font-poppins), ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; --spring: cubic-bezier(0.34, 1.56, 0.64, 1); color-scheme: dark; } @@ -29,67 +37,57 @@ body { min-height: 100vh; background: var(--bg); color: var(--text); - font: 15px/1.55 var(--font-body); + font: 14.5px/1.6 var(--font-body); -webkit-font-smoothing: antialiased; } -/* Slowly drifting colour behind everything, so the app never feels static. */ -.ambient { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; } -.ambient::before, .ambient::after { - content: ''; position: absolute; width: 60vmax; height: 60vmax; border-radius: 50%; - filter: blur(90px); opacity: 0.34; -} -.ambient::before { background: #6d28d9; top: -25vmax; left: -15vmax; animation: drift-a 28s ease-in-out infinite alternate; } -.ambient::after { background: #1d6fe0; bottom: -30vmax; right: -20vmax; animation: drift-b 34s ease-in-out infinite alternate; } -@keyframes drift-a { to { transform: translate(18vmax, 12vmax) scale(1.15); } } -@keyframes drift-b { to { transform: translate(-16vmax, -10vmax) scale(0.9); } } a { color: inherit; } -h1, h2, .brand, .display { font-family: var(--font-display); letter-spacing: -0.02em; } +h1, h2, .brand, .display { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em; } h1 { font-size: 1.85rem; margin: 0; line-height: 1.15; } h2 { font-size: 1.1rem; margin: 0 0 0.5rem; } input, textarea, select { width: 100%; padding: 0.7rem 0.85rem; - border-radius: 12px; - border: 1px solid var(--border); + border-radius: 14px; + border: 1px solid transparent; background: var(--surface-2); color: var(--text); font: inherit; transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; } input:focus, textarea:focus, select:focus { - outline: none; border-color: var(--accent); background: rgba(124, 77, 255, 0.08); - box-shadow: 0 0 0 4px rgba(124, 77, 255, 0.2); + outline: none; border-color: var(--accent-line); background: #463f62; + box-shadow: 0 0 0 4px var(--accent-soft); } .container { max-width: 680px; margin: 0 auto; padding: 1.75rem 1rem 5rem; } .stack { display: flex; flex-direction: column; gap: 1rem; } .row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; } -.service-name { display: flex; align-items: center; gap: 0.6rem; font-family: var(--font-display); font-size: 1.12rem; font-weight: 700; margin-bottom: 0.2rem; } +.service-name { display: flex; align-items: center; gap: 0.6rem; font-family: var(--font-display); font-size: 1.05rem; font-weight: 600; margin-bottom: 0.2rem; } .service-badge { width: 1.7rem; height: 1.7rem; border-radius: 0.5rem; display: grid; place-items: center; color: #fff; font-size: 0.95rem; border: 1px solid rgba(255, 255, 255, 0.25); flex-shrink: 0; } .chip-small { display: inline-block; margin-left: 0.5rem; padding: 0.05rem 0.5rem; border-radius: 999px; background: rgba(255, 255, 255, 0.1); font-size: 0.75rem; color: var(--text); } .stats a { color: inherit; text-decoration: none; } .stats a:hover strong { text-decoration: underline; } .menu { position: relative; } -.menu-list { position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 20; min-width: 12rem; display: flex; flex-direction: column; padding: 0.35rem; border-radius: 0.8rem; background: #1a1535; border: 1px solid rgba(255, 255, 255, 0.12); box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.6); } +.menu-list { position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 20; min-width: 12rem; display: flex; flex-direction: column; padding: 0.35rem; border-radius: 16px; background: var(--surface-2); box-shadow: var(--shadow); } .menu-list button { all: unset; cursor: pointer; padding: 0.55rem 0.75rem; border-radius: 0.55rem; font-size: 0.92rem; } .menu-list button:hover, .menu-list button:focus-visible { background: rgba(255, 255, 255, 0.08); } .menu-list .danger { color: var(--danger); } -.modal { max-width: 26rem; width: calc(100% - 2rem); color: var(--text); background: #15112c; } -.modal::backdrop { background: rgba(5, 3, 15, 0.7); backdrop-filter: blur(4px); } +.modal { max-width: 26rem; width: calc(100% - 2rem); color: var(--text); background: var(--panel); border: 0; } +.modal::backdrop { background: rgba(20, 18, 40, 0.65); backdrop-filter: blur(4px); } .modal p { margin-bottom: 1rem; } .modal .form p { margin: 0; } .pick-list { max-height: 45vh; overflow-y: auto; } .pick-list .checkbox { display: flex; align-items: center; gap: 0.6rem; cursor: pointer; font-weight: 500; } .bell { position: relative; display: grid; place-items: center; width: 38px; height: 38px; border-radius: 999px; margin-left: auto; margin-right: 1rem; - color: var(--muted); background: rgba(255, 255, 255, 0.06); transition: color 0.2s, background 0.2s; + color: var(--text); background: var(--surface-2); transition: color 0.2s, background 0.2s; } -.bell:hover { color: var(--text); background: rgba(255, 255, 255, 0.12); } +.bell:hover { color: var(--accent); background: #4a4868; } .bell-badge { position: absolute; top: -3px; right: -5px; min-width: 18px; height: 18px; padding: 0 4px; border-radius: 9px; - background: var(--danger); color: #fff; font-size: 0.68rem; font-weight: 700; display: grid; place-items: center; + background: var(--accent); color: #fff; font-size: 0.68rem; font-weight: 600; display: grid; place-items: center; } .notifications { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; } .notification { display: flex; align-items: center; gap: 0.8rem; padding: 0.7rem 0.8rem; border-radius: 14px; text-decoration: none; color: var(--text); } @@ -103,12 +101,12 @@ input:focus, textarea:focus, select:focus { .people .user-chip { min-width: 0; } .admin-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 0.75rem; } .admin-stat { text-align: center; padding: 1rem; } -.admin-stat-value { font-family: var(--font-display); font-size: 1.8rem; font-weight: 700; } +.admin-stat-value { font-family: var(--font-display); font-size: 1.8rem; font-weight: 600; } .reports { list-style: none; padding: 0; margin: 0.75rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; } -.report { padding: 0.85rem; border-radius: 0.8rem; background: rgba(255, 255, 255, 0.04); display: flex; flex-direction: column; gap: 0.5rem; } +.report { padding: 0.85rem; border-radius: 16px; background: var(--surface-2); display: flex; flex-direction: column; gap: 0.5rem; } .report p { margin: 0; } .chip-small.danger { background: rgba(251, 113, 133, 0.18); color: var(--danger); } -.banner { max-width: 680px; margin: 0 auto 0.5rem; padding: 0.6rem 1rem; border-radius: 0.8rem; background: rgba(123, 47, 247, 0.16); border: 1px solid rgba(123, 47, 247, 0.35); font-size: 0.9rem; } +.banner { max-width: 680px; margin: 0 auto 0.5rem; padding: 0.6rem 1rem; border-radius: 0.8rem; background: var(--accent-soft); border: 1px solid var(--accent-line); font-size: 0.9rem; } .banner .link-button { font-size: inherit; color: var(--text); text-decoration: underline; } .footer { max-width: 680px; margin: 0 auto; padding: 0 1rem 2.5rem; display: flex; gap: 1.25rem; justify-content: center; font-size: 0.85rem; } .footer a { color: var(--muted); text-decoration: none; } @@ -122,11 +120,11 @@ input:focus, textarea:focus, select:focus { .song-art { width: 100%; max-width: 360px; aspect-ratio: 1; object-fit: cover; border-radius: 1.1rem; align-self: center; box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.8); } .reactions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-top: 0.7rem; position: relative; } .recent .reactions { margin: 0.25rem 0 0.4rem 3.4rem; } -.reaction { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.15rem 0.55rem; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.07); color: var(--text); font: inherit; font-size: 0.9rem; cursor: pointer; transition: transform 0.15s, background 0.2s; } -.reaction:hover:not(:disabled) { transform: translateY(-1px); background: rgba(255, 255, 255, 0.13); } +.reaction { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.15rem 0.6rem; border-radius: 999px; border: 1px solid transparent; background: rgba(255, 255, 255, 0.1); color: var(--text); font: inherit; font-size: 0.9rem; cursor: pointer; transition: transform 0.15s, background 0.2s; } +.reaction:hover:not(:disabled) { transform: translateY(-1px); background: rgba(255, 255, 255, 0.16); } .reaction:active:not(:disabled) { transform: scale(0.9); } .reaction:disabled { cursor: default; } -.reaction.mine { border-color: rgba(123, 47, 247, 0.7); background: rgba(123, 47, 247, 0.28); } +.reaction.mine { border-color: var(--accent-line); background: var(--accent-soft); } .reaction-emoji { font-size: 1.05rem; line-height: 1.3; } .reaction-count { font-size: 0.78rem; font-variant-numeric: tabular-nums; } .reaction-add { color: var(--muted); font-size: 1rem; } @@ -150,10 +148,10 @@ a.reaction { text-decoration: none; } @media (max-width: 520px) { .comment-actions { opacity: 1; } } .reaction-add span { font-size: 0.75rem; margin-left: -0.15rem; } .reaction-add-wrap { position: static; } /* the quick menu anchors to the row, so it never runs off the card */ -.reaction-quick { position: absolute; bottom: calc(100% + 0.4rem); left: 0; z-index: 30; display: flex; align-items: center; gap: 0.15rem; padding: 0.3rem; border-radius: 999px; background: #1a1535; border: 1px solid rgba(255, 255, 255, 0.14); box-shadow: 0 14px 30px -10px rgba(0, 0, 0, 0.7); animation: pop 0.18s ease-out; white-space: nowrap; } +.reaction-quick { position: absolute; bottom: calc(100% + 0.4rem); left: 0; z-index: 30; display: flex; align-items: center; gap: 0.15rem; padding: 0.3rem; border-radius: 999px; background: var(--surface-2); box-shadow: var(--shadow); animation: pop 0.18s ease-out; white-space: nowrap; } .reaction-quick button { all: unset; cursor: pointer; font-size: 1.45rem; padding: 0.2rem 0.3rem; border-radius: 999px; transition: transform 0.15s; } .reaction-quick button:hover { transform: scale(1.25); } -.reaction-quick button.mine { background: rgba(123, 47, 247, 0.35); } +.reaction-quick button.mine { background: var(--accent-soft); } .reaction-quick .reaction-more { font-size: 0.85rem; color: var(--muted); padding: 0.3rem 0.6rem; } @keyframes pop { from { opacity: 0; transform: translateY(6px) scale(0.92); } to { opacity: 1; transform: none; } } .emoji-search { width: 100%; margin-bottom: 0.6rem; } @@ -167,8 +165,8 @@ a.reaction { text-decoration: none; } .list { display: flex; flex-direction: column; margin: 0.25rem -0.4rem 0; } .list-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 0.75rem; padding: 0.8rem 0.4rem; border-top: 1px solid var(--border); cursor: pointer; border-radius: 0.5rem; transition: background 0.15s; } .list-row:first-child { border-top: 0; } -.list-row:hover, .list-row:focus-visible { background: rgba(255, 255, 255, 0.05); } -.list-label { font-weight: 600; flex-shrink: 0; } +.list-row:hover, .list-row:focus-visible { background: rgba(255, 255, 255, 0.04); } +.list-label { font-weight: 500; flex-shrink: 0; } .list-value { flex: 1; min-width: 0; text-align: right; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .list-chevron { color: var(--muted); font-size: 1.2rem; line-height: 1; transition: transform 0.2s; margin-left: auto; } .list-value + .list-chevron { margin-left: 0; } @@ -186,25 +184,22 @@ a.reaction { text-decoration: none; } .grow { flex: 1; min-width: 0; } .muted { color: var(--muted); } .error { color: var(--danger); margin: 0; } -.notice { - background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 0.85rem 1rem; margin: 0; - backdrop-filter: blur(16px); -} -.section-label { margin: 0; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); font-weight: 700; } +.notice { background: var(--card); border-radius: 18px; padding: 0.9rem 1.1rem; margin: 0; box-shadow: var(--shadow); } +.section-label { margin: 0; font-size: 0.85rem; color: var(--muted); font-weight: 500; } .narrow { max-width: 420px; margin: 2.5rem auto; } .nav { display: flex; justify-content: space-between; align-items: center; padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--border); - position: sticky; top: 0; background: rgba(7, 7, 11, 0.65); backdrop-filter: blur(18px) saturate(1.4); z-index: 10; + position: sticky; top: 0; background: rgba(42, 41, 66, 0.85); backdrop-filter: blur(18px) saturate(1.4); z-index: 10; } .brand { font-weight: 700; text-decoration: none; font-size: 1.2rem; display: inline-flex; align-items: center; gap: 0.5rem; } -.brand-logo { width: 30px; height: 30px; border-radius: 8px; box-shadow: 0 4px 14px -4px rgba(47, 140, 255, 0.6); } +.brand-logo { width: 30px; height: 30px; border-radius: 8px; box-shadow: 0 4px 14px -4px rgba(255, 79, 139, 0.6); } .nav-links { display: flex; gap: 1.1rem; align-items: center; } .nav-links a { text-decoration: none; color: var(--muted); transition: color 0.2s; } .nav-links a:hover { color: var(--text); } .nav-links a.btn-primary { color: var(--accent-text); } -.nav-links a.active { color: var(--text); font-weight: 600; } +.nav-links a.active { color: var(--accent); font-weight: 500; } /* Phones: main places live in a bottom tab bar, like a native app. */ .tabbar { display: none; } @media (max-width: 640px) { @@ -215,41 +210,40 @@ a.reaction { text-decoration: none; } .tabbar { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; padding: 0.35rem 0.5rem calc(0.35rem + env(safe-area-inset-bottom)); - background: rgba(7, 7, 11, 0.82); backdrop-filter: blur(18px) saturate(1.4); border-top: 1px solid var(--border); + background: rgba(42, 41, 66, 0.92); backdrop-filter: blur(18px) saturate(1.4); border-top: 1px solid var(--border); } .tabbar a { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding: 0.35rem 0; color: var(--muted); text-decoration: none; font-size: 0.7rem; font-weight: 600; transition: color 0.2s; } - .tabbar a.active { color: var(--text); } - .tabbar a.active svg { color: #8b9dff; } + .tabbar a.active { color: var(--accent); } body:has(.tabbar) .container { padding-bottom: 6.5rem; } body:has(.tabbar) .footer { padding-bottom: 6rem; } } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; - padding: 0.55rem 1.05rem; border-radius: 999px; border: 1px solid var(--border-strong); - background: var(--surface-2); color: var(--text); font: inherit; font-weight: 600; + padding: 0.55rem 1.1rem; border-radius: 999px; border: 1px solid transparent; + background: var(--surface-2); color: var(--text); font: inherit; font-weight: 500; text-decoration: none; cursor: pointer; white-space: nowrap; transition: transform 0.25s var(--spring), border-color 0.2s, background 0.2s, box-shadow 0.25s, filter 0.2s; } -.btn:hover { border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.12); transform: translateY(-1px); } +.btn:hover { background: #4a4868; transform: translateY(-1px); } .btn:active { transform: scale(0.96); } .btn:disabled { opacity: 0.55; cursor: default; transform: none; } .btn-primary { background: var(--grad); border-color: transparent; color: var(--accent-text); - box-shadow: 0 6px 22px -6px rgba(91, 76, 240, 0.6); + box-shadow: 0 8px 22px -8px rgba(255, 79, 139, 0.7); } -.btn-primary:hover { background: var(--grad); filter: brightness(1.08); box-shadow: 0 10px 28px -6px rgba(47, 140, 255, 0.65); } +.btn-primary:hover { background: var(--grad); filter: brightness(1.08); box-shadow: 0 12px 28px -8px rgba(255, 79, 139, 0.75); } .btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; } .btn-small { padding: 0.3rem 0.75rem; font-size: 0.82rem; } .link-button { background: none; border: 0; color: var(--muted); font: inherit; cursor: pointer; padding: 0; transition: color 0.2s; } .link-button:hover { color: var(--text); } .card { - position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); - padding: 1.1rem; backdrop-filter: blur(20px) saturate(1.3); overflow: hidden; + position: relative; background: var(--card); border-radius: var(--radius); + padding: 1.25rem; box-shadow: var(--shadow); } .card-header { position: relative; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-bottom: 0.85rem; } .card-header .user-chip { flex-shrink: 0; } @@ -263,45 +257,46 @@ a.reaction { text-decoration: none; } .services-picker legend { font-weight: 600; font-size: 0.9rem; padding: 0; margin-bottom: 0.35rem; } .form label.checkbox { flex-direction: row; align-items: center; gap: 0.5rem; font-weight: 500; } -/* Now-playing cards take their colour from the album art. */ -.np-card { border-color: rgba(255, 255, 255, 0.11); padding: 0.9rem 1rem; } -.np-card .card-header { margin-bottom: 0.65rem; } -.np-card .reactions { margin-top: 0.55rem; } -.np-card.pinned { border-color: rgba(251, 191, 36, 0.45); box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.15), 0 18px 40px -20px rgba(251, 191, 36, 0.35); } -.np-backdrop { - position: absolute; inset: -40%; z-index: 0; background-size: cover; background-position: center; - filter: blur(40px) saturate(2.2) brightness(1.1); opacity: 0.6; transform: scale(1.1); transition: opacity 0.6s; -} -.np-card.playing .np-backdrop { animation: breathe 9s ease-in-out infinite alternate; } -@keyframes breathe { to { transform: scale(1.3) rotate(8deg); opacity: 0.75; } } -.np-card::after { - content: ''; position: absolute; inset: 0; z-index: 0; - background: linear-gradient(180deg, rgba(7, 7, 11, 0.05), rgba(7, 7, 11, 0.45)); -} -.np-card > *:not(.np-backdrop) { position: relative; z-index: 1; } +.np-card { padding: 1rem 1.15rem; border: 1px solid transparent; } +.np-card .card-header { margin-bottom: 0.7rem; } +.np-card .reactions { margin-top: 0.6rem; } +.np-card.pinned { border-color: var(--accent-line); } + +/* Your own song: the gradient "hero" card, with everything on it in white. */ +.np-hero { background: var(--hero); overflow: hidden; box-shadow: 0 24px 50px -24px rgba(255, 79, 139, 0.75); color: #fff; } +.np-hero .muted, .np-hero .status, .np-hero .status-live, .np-hero .progress-times, .np-hero .lyrics-note, .np-hero .lyrics-source { color: rgba(255, 255, 255, 0.85); } +.np-hero .status-live { font-weight: 600; color: #fff; } +.np-hero .progress-track { background: rgba(255, 255, 255, 0.3); } +.np-hero .progress-fill { background: #fff; } +.np-hero .btn-primary { background: #fff; color: var(--accent); box-shadow: 0 8px 20px -10px rgba(122, 30, 74, 0.5); } +.np-hero .btn-primary:hover { background: #fff; filter: none; } +.np-hero .btn, .np-hero .service-link, .np-hero .chevron, .np-hero .reaction { background: rgba(255, 255, 255, 0.24); color: #fff; } +.np-hero .chevron:hover, .np-hero .service-link:hover, .np-hero .reaction:hover:not(:disabled) { background: rgba(255, 255, 255, 0.36); color: #fff; } +.np-hero .track-artist, .np-hero a { color: #fff; } +.np-hero .artwork { box-shadow: 0 12px 26px -12px rgba(80, 20, 70, 0.6); } .pin { background: none; border: 0; cursor: pointer; font-size: 1.25rem; line-height: 1; color: var(--muted); padding: 0.15rem 0.25rem; transition: color 0.2s; } -.pin:hover, .pin.pinned { color: var(--pin); } -.pin.pinned { text-shadow: 0 0 14px rgba(251, 191, 36, 0.6); } +.pin:hover, .pin.pinned { color: var(--accent); } +.pin.pinned { text-shadow: 0 0 14px rgba(255, 79, 139, 0.6); } -.user-chip { display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; font-weight: 600; } +.user-chip { display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; font-weight: 500; } .user-chip:hover .user-name { text-decoration: underline; } .avatar { border-radius: 50%; object-fit: cover; flex-shrink: 0; display: block; } -.avatar-empty { display: grid; place-items: center; background: linear-gradient(135deg, #4a1278, #1872cf); color: #fff; font-weight: 700; font-family: var(--font-display); } +.avatar-empty { display: grid; place-items: center; background: linear-gradient(135deg, #ff4f8b, #8b5cf6); color: #fff; font-weight: 600; font-family: var(--font-display); } /* A spinning gradient ring while someone is listening. */ .avatar-ring { position: relative; display: inline-grid; place-items: center; border-radius: 50%; padding: 2px; flex-shrink: 0; } .avatar-ring.live::before { content: ''; position: absolute; inset: 0; border-radius: 50%; - background: conic-gradient(from 0deg, #7b2ff7, #2f8cff, #34d399, #7b2ff7); animation: spin 3.5s linear infinite; + background: conic-gradient(from 0deg, #ff4f8b, #8b7dff, #3ee0a1, #ff4f8b); animation: spin 3.5s linear infinite; } -.avatar-ring > * { position: relative; border: 2px solid var(--bg); border-radius: 50%; } +.avatar-ring > * { position: relative; border: 2px solid var(--card); border-radius: 50%; } @keyframes spin { to { transform: rotate(360deg); } } .status { font-size: 0.85rem; color: var(--muted); display: inline-flex; align-items: center; gap: 0.45rem; } -.status-live { color: var(--live); font-weight: 600; } +.status-live { color: var(--live); font-weight: 500; } /* Equalizer bars: dancing while playing, resting when paused. */ .eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; } @@ -314,15 +309,15 @@ a.reaction { text-decoration: none; } .track { display: flex; gap: 1rem; align-items: flex-start; } .artwork { - width: 112px; height: 112px; border-radius: 14px; object-fit: cover; flex-shrink: 0; background: var(--surface-2); - box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.75), 0 0 0 1px rgba(255, 255, 255, 0.08); + width: 112px; height: 112px; border-radius: 16px; object-fit: cover; flex-shrink: 0; background: var(--surface-2); + box-shadow: 0 14px 30px -14px rgba(10, 8, 30, 0.8); transition: transform 0.4s var(--spring); } .track:hover .artwork { transform: scale(1.04) rotate(-1deg); } -.artwork-empty { display: grid; place-items: center; font-size: 2rem; color: rgba(255, 255, 255, 0.85); background: linear-gradient(135deg, #4a1278, #1872cf); } +.artwork-empty { display: grid; place-items: center; font-size: 2rem; color: rgba(255, 255, 255, 0.85); background: linear-gradient(135deg, #4b3f78, #6a4c9c); } .artwork-sm { width: 44px; height: 44px; border-radius: 9px; object-fit: cover; flex-shrink: 0; box-shadow: 0 6px 14px -8px rgba(0, 0, 0, 0.8); } .track-body { min-width: 0; flex: 1; } -.track-title { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.track-title { font-family: var(--font-display); font-weight: 600; font-size: 1.15rem; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .track-artist { color: var(--text); opacity: 0.82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .progress { margin-top: 0.85rem; } .track-row { display: flex; align-items: center; gap: 0.6rem; } @@ -336,9 +331,9 @@ a.reaction { text-decoration: none; } .track-row:hover .artwork { transform: scale(1.04) rotate(-1deg); } .chevron { display: grid; place-items: center; width: 32px; height: 32px; flex-shrink: 0; border-radius: 999px; border: 0; cursor: pointer; - background: rgba(255, 255, 255, 0.06); color: var(--muted); transition: transform 0.3s var(--spring), color 0.2s, background 0.2s; + background: var(--surface-2); color: var(--muted); transition: transform 0.3s var(--spring), color 0.2s, background 0.2s; } -.chevron:hover { color: var(--text); background: rgba(255, 255, 255, 0.12); } +.chevron:hover { color: var(--text); background: #4a4868; } .chevron.open { transform: rotate(180deg); } .np-card .progress { margin-top: 0.65rem; } .np-details { overflow: hidden; } @@ -348,37 +343,37 @@ a.reaction { text-decoration: none; } .lyric { margin: 0; line-height: 1.45; transition: opacity 0.3s, font-size 0.3s; } .lyric-past { opacity: 0.4; } .lyric-next { opacity: 0.65; } -.lyric-now { font-family: var(--font-display); font-weight: 700; font-size: 1.08rem; } +.lyric-now { font-family: var(--font-display); font-weight: 600; font-size: 1.08rem; } .lyrics-note { margin: 0.9rem 0 0; font-size: 0.85rem; } .lyrics-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 0.5rem; font-size: 0.82rem; } .lyrics-source { margin-left: auto; font-size: 0.72rem; } -.progress-track { height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.14); overflow: hidden; } +.progress-track { height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.12); overflow: hidden; } .progress-fill { height: 100%; border-radius: inherit; background: var(--grad); transition: width 1s linear; } .progress-times { display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.75rem; color: var(--muted); font-variant-numeric: tabular-nums; } @media (prefers-reduced-motion: reduce) { .progress-fill { transition: none; } } .track-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.8rem; } .service-link { - font-size: 0.76rem; color: var(--text); text-decoration: none; padding: 0.2rem 0.55rem; border-radius: 999px; - background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.08); transition: background 0.2s, transform 0.25s var(--spring); + font-size: 0.76rem; color: var(--text); text-decoration: none; padding: 0.25rem 0.7rem; border-radius: 999px; + background: var(--surface-2); transition: background 0.2s, transform 0.25s var(--spring); } -.service-link:hover { background: rgba(255, 255, 255, 0.16); transform: translateY(-1px); } +.service-link:hover { background: #4a4868; transform: translateY(-1px); } .service-link.inexact { opacity: 0.55; } .profile-header { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.75rem; padding: 0.5rem 0 0.75rem; } .profile-header h1 { font-size: 2.1rem; } .profile-header .bio { margin: 0; max-width: 30rem; } .stats { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; } -.stats span { padding: 0.3rem 0.8rem; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); color: var(--muted); font-size: 0.88rem; } +.stats span { padding: 0.3rem 0.9rem; border-radius: 999px; background: var(--surface-2); color: var(--muted); font-size: 0.88rem; } .stats strong { color: var(--text); font-family: var(--font-display); } .featured blockquote { margin: 0.9rem 0 0; padding: 0.6rem 0.9rem; border-left: 3px solid transparent; font-style: italic; - border-image: var(--grad) 1; background: rgba(255, 255, 255, 0.04); + border-image: var(--grad) 1; background: var(--surface-2); } .search { position: relative; } -.search-results { list-style: none; margin: 0.5rem 0 0; padding: 0.4rem; background: var(--surface-solid); border: 1px solid var(--border); border-radius: 14px; } +.search-results { list-style: none; margin: 0.5rem 0 0; padding: 0.4rem; background: var(--surface-2); border-radius: 16px; box-shadow: var(--shadow); position: absolute; left: 0; right: 0; z-index: 25; } .search-results li { padding: 0.4rem 0.45rem; border-radius: 10px; transition: background 0.15s; } -.search-results li:hover { background: var(--surface-2); } +.search-results li:hover { background: #4a4868; } .result { display: flex; gap: 0.6rem; align-items: center; background: none; border: 0; color: inherit; font: inherit; cursor: pointer; width: 100%; text-align: left; padding: 0; } .recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } .recent-row { display: flex; align-items: center; gap: 0.8rem; padding: 0.5rem 0.4rem; text-decoration: none; border-radius: 12px; transition: background 0.15s, transform 0.25s var(--spring); } @@ -386,11 +381,11 @@ a.recent-row:hover { background: var(--surface-2); transform: translateX(3px); } .recent li + li { border-top: 1px solid var(--border); } .recent-text { display: flex; flex-direction: column; min-width: 0; flex: 1; } .recent-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.recent-title { font-weight: 600; } +.recent-title { font-weight: 500; } .recent-time { font-size: 0.8rem; white-space: nowrap; } .artwork-sm.artwork-empty { display: grid; place-items: center; background: var(--surface-2); color: var(--muted); } .qr-card { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; text-align: center; } -.qr { border-radius: 14px; box-shadow: 0 20px 50px -20px rgba(47, 140, 255, 0.6); } +.qr { border-radius: 16px; box-shadow: 0 20px 50px -20px rgba(255, 79, 139, 0.6); } .credit { margin: 0.6rem 0 0; font-size: 0.8rem; } .picked { display: flex; gap: 0.75rem; align-items: center; } @@ -411,12 +406,12 @@ a.recent-row:hover { background: var(--surface-2); transform: translateX(3px); } .hero { padding: 4.5rem 0 3rem; display: flex; flex-direction: column; gap: 1.25rem; } .hero h1 { font-size: 3.2rem; letter-spacing: -0.04em; line-height: 1.02; } .gradient-text { - background: linear-gradient(90deg, #a78bfa, #7dd3fc, #60a5fa, #a78bfa); background-size: 300% 100%; + background: linear-gradient(90deg, #ff4f8b, #e27ad8, #8fc8ff, #ff4f8b); background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: shimmer 8s linear infinite; } @keyframes shimmer { to { background-position: 300% 0; } } .hero p { color: var(--muted); font-size: 1.15rem; margin: 0; max-width: 34rem; } -.hero .eq { height: 42px; gap: 5px; color: #8b9dff; } +.hero .eq { height: 42px; gap: 5px; color: var(--accent); } .hero .eq span { width: 8px; border-radius: 4px; } @media (max-width: 480px) { @@ -428,3 +423,68 @@ a.recent-row:hover { background: var(--surface-2); transform: translateX(3px); } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } + +/* Signed in on wide screens: sidebar | page in a rounded panel | your column (components/Shell.tsx). */ +.sidebar, .rail { display: none; } +@media (min-width: 900px) { + body:has(.shell) .nav, body:has(.shell) .tabbar { display: none; } + .shell { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 100vh; } + .shell-main { min-width: 0; padding: 1rem 1rem 0 0; display: flex; flex-direction: column; } + /* The page sits in a lighter panel; cards inside it are a shade darker, like the reference. */ + .main-panel { --card: #2c2b45; --surface-2: #3b395a; flex: 1; background: var(--panel); border-radius: 36px; padding: 0.5rem 0; } + .main-panel .container { max-width: 760px; padding: 2rem 2rem 3rem; } + .shell .footer { padding: 1.25rem 1rem 1.5rem; } + + .sidebar { display: flex; flex-direction: column; gap: 2rem; position: sticky; top: 0; height: 100vh; padding: 2.2rem 1.1rem 1.6rem; } + .sidebar-brand { font-family: var(--font-display); font-weight: 600; font-size: 1.45rem; text-decoration: none; color: #b7b4ff; padding-left: 0.9rem; letter-spacing: -0.01em; } + .sidebar-brand span { color: var(--accent); } + .sidebar-nav { display: flex; flex-direction: column; gap: 0.3rem; } + .sidebar-nav a, .sidebar-logout { + display: flex; align-items: center; gap: 0.85rem; padding: 0.75rem 0.95rem; border-radius: 16px; text-decoration: none; + color: var(--muted); font-weight: 400; transition: background 0.2s, color 0.2s; + } + .sidebar-nav a:hover { color: var(--text); background: rgba(255, 255, 255, 0.04); } + .sidebar-nav a.active { color: var(--accent); background: var(--panel); font-weight: 500; } + .sidebar-badge { margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; background: var(--accent); color: #fff; font-size: 0.7rem; font-weight: 600; display: grid; place-items: center; } + .sidebar-logout { margin-top: auto; background: none; border: 0; font: inherit; font-size: 0.88rem; cursor: pointer; } + .sidebar-logout:hover { color: var(--text); } +} +@media (min-width: 1240px) { + .shell { grid-template-columns: 232px minmax(0, 1fr) 330px; } + .shell-main { padding-right: 0; } + .rail { display: flex; flex-direction: column; gap: 1.6rem; position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 2rem 1.6rem; } + .rail-me { display: flex; align-items: center; gap: 0.75rem; padding-bottom: 1.3rem; border-bottom: 1px solid var(--border); } + .rail-me .bell { margin: 0 0 0 auto; } + .rail-me-link { display: flex; align-items: center; gap: 0.75rem; min-width: 0; text-decoration: none; } + .rail-me-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; } + .rail-me-text strong { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .rail-me-text .muted { font-size: 0.8rem; } + .rail-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; } + .rail-head h2 { margin: 0; font-size: 1.1rem; } + .pill-link { font-size: 0.78rem; color: var(--muted); text-decoration: none; padding: 0.3rem 0.8rem; border-radius: 999px; background: var(--panel); } + .pill-link:hover { color: var(--text); } + .rail-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } + .rail-list li + li { border-top: 1px solid var(--border); } + .rail-row { display: flex; align-items: center; gap: 0.8rem; padding: 0.75rem 0.2rem; text-decoration: none; border-radius: 12px; transition: background 0.15s; } + .rail-row:hover { background: rgba(255, 255, 255, 0.03); } + .rail-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.35; } + .rail-row-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .rail-row-title { font-size: 0.88rem; } + .rail-row-text .muted { font-size: 0.74rem; } + .rail-list li.unread .rail-row-title { font-weight: 500; } + .rail-row-end { font-size: 0.8rem; color: var(--muted); white-space: nowrap; } + .rail-art { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--panel); } + .rail-art-empty { display: grid; place-items: center; color: var(--muted); } +} + +/* The feed's greeting and search ("Hi Chris, welcome back"). */ +.page-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } +.page-head-text { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; flex: 1; min-width: 0; } +.page-head h1 { font-size: 1.45rem; } +.page-head .muted { font-size: 1rem; } +.page-head .search { flex: 0 1 280px; min-width: 200px; } +.page-head .search input { border-radius: 999px; padding-left: 1.1rem; } +@media (max-width: 640px) { .page-head .search { flex-basis: 100%; } } +.page-section { font-size: 1.15rem; margin: 0.5rem 0 0; } +input[type='checkbox'], input[type='radio'] { width: auto; padding: 0; accent-color: var(--accent); box-shadow: none; } +.services-picker label.checkbox { white-space: nowrap; } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index eeedc9f..2bb0a0f 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,12 +1,11 @@ import type { Metadata, Viewport } from 'next'; -import { Inter, Space_Grotesk } from 'next/font/google'; +import { Poppins } from 'next/font/google'; import Link from 'next/link'; -import { Nav } from '@/components/Nav'; +import { Shell } from '@/components/Shell'; import { Providers } from '@/components/Providers'; import './globals.css'; -const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' }); -const spaceGrotesk = Space_Grotesk({ subsets: ['latin'], variable: '--font-space-grotesk', display: 'swap' }); +const poppins = Poppins({ subsets: ['latin'], weight: ['400', '500', '600', '700'], variable: '--font-poppins', display: 'swap' }); // Previews need absolute image URLs. In production the API lives at /api, so its origin is the site's. const siteUrl = process.env.SITE_URL ?? new URL(process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3737').origin; @@ -21,23 +20,26 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: '#07070b', + themeColor: '#2a2942', viewportFit: 'cover', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + -

-