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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"backgroundColor": "#0b0820",
|
||||
"backgroundColor": "#2a2942",
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"imageWidth": 150
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@@ -4,7 +4,7 @@
|
||||
"version": "1.3.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/space-grotesk": "^0.4.1",
|
||||
"@expo-google-fonts/poppins": "^0.4.1",
|
||||
"@expo/ui": "~57.0.21",
|
||||
"@musicshare/shared": "*",
|
||||
"expo": "~57.0.26",
|
||||
|
||||
@@ -5,10 +5,11 @@ export default function TabsLayout() {
|
||||
return (
|
||||
<NativeTabs
|
||||
backgroundColor={colors.bg}
|
||||
indicatorColor={colors.surface2}
|
||||
// The active tab gets a pink pill, like the reference's sidebar.
|
||||
indicatorColor={colors.accentSoft}
|
||||
// Android defaults icons and labels to black, which vanishes on the dark bar.
|
||||
iconColor={{ default: colors.muted, selected: colors.text }}
|
||||
labelStyle={{ default: { color: colors.muted }, selected: { color: colors.text } }}
|
||||
iconColor={{ default: colors.muted, selected: colors.accent }}
|
||||
labelStyle={{ default: { color: colors.muted }, selected: { color: colors.accent } }}
|
||||
labelVisibilityMode="labeled"
|
||||
tintColor={colors.accent}>
|
||||
<NativeTabs.Trigger name="index">
|
||||
|
||||
@@ -95,7 +95,19 @@ export default function FeedScreen() {
|
||||
ListHeaderComponent={
|
||||
<View style={styles.header}>
|
||||
<View style={styles.topBar}>
|
||||
<Text style={styles.brand}>musicshare</Text>
|
||||
{me ? (
|
||||
<Pressable style={styles.greeting} onPress={() => router.push('/profile')} accessibilityRole="link">
|
||||
<Avatar user={me} size={44} />
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Text style={styles.hi} numberOfLines={1}>
|
||||
Hi {me.displayName ?? me.username},
|
||||
</Text>
|
||||
<Text style={styles.welcome}>Welcome back</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Text style={styles.brand}>musicshare</Text>
|
||||
)}
|
||||
<ActivityBell />
|
||||
</View>
|
||||
<UpdateBanner />
|
||||
@@ -115,7 +127,7 @@ export default function FeedScreen() {
|
||||
)}
|
||||
{mine?.nowPlaying && (
|
||||
<View style={{ gap: space.sm }}>
|
||||
<Text style={styles.sectionLabel}>YOU</Text>
|
||||
<Text style={styles.sectionLabel}>Your song</Text>
|
||||
<NowPlayingCard np={mine.nowPlaying} preferred={me?.preferredService} showUser={false} />
|
||||
</View>
|
||||
)}
|
||||
@@ -143,7 +155,7 @@ export default function FeedScreen() {
|
||||
ListFooterComponent={
|
||||
quiet.length > 0 ? (
|
||||
<View style={{ gap: space.sm, marginTop: space.md }}>
|
||||
<Text style={styles.sectionLabel}>FOLLOWING · NOTHING PLAYED YET</Text>
|
||||
<Text style={styles.sectionLabel}>Following · nothing played yet</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.quiet}>
|
||||
{quiet.map((u) => (
|
||||
<Pressable key={u.id} onPress={() => 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' },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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<UserSummaryDTO[]>('/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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
<Text style={styles.icon}>🔔</Text>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke={colors.text} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
||||
<Path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
||||
<Path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
||||
</Svg>
|
||||
{unread > 0 && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{unread > 99 ? '99+' : unread}</Text>
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.bg }]} pointerEvents="none">
|
||||
<AnimatedImage source={require('@/assets/images/ambient-purple.png')} style={[styles.blob, { width: size, height: size }, purple]} />
|
||||
<AnimatedImage source={require('@/assets/images/ambient-blue.png')} style={[styles.blob, { width: size, height: size }, blue]} />
|
||||
</View>
|
||||
<LinearGradient
|
||||
colors={['#32304e', colors.bg, colors.bg]}
|
||||
locations={[0, 0.45, 1]}
|
||||
style={[StyleSheet.absoluteFill, { backgroundColor: colors.bg }]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
blob: { position: 'absolute', top: 0, left: 0, opacity: 0.45 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?:
|
||||
const face = user.avatarUrl ? (
|
||||
<Image source={user.avatarUrl} style={shape} contentFit="cover" />
|
||||
) : (
|
||||
<LinearGradient colors={['#4a1278', '#1872cf']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={[styles.empty, shape]}>
|
||||
<LinearGradient colors={[colors.accent, '#8b5cf6']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={[styles.empty, shape]}>
|
||||
<Text style={[styles.letter, { fontSize: size * 0.45 }]}>{(user.displayName ?? user.username).slice(0, 1).toUpperCase()}</Text>
|
||||
</LinearGradient>
|
||||
);
|
||||
@@ -31,7 +31,7 @@ export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?:
|
||||
<View style={{ width: ring, height: ring, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, spin]}>
|
||||
<LinearGradient
|
||||
colors={['#7b2ff7', '#2f8cff', colors.live, '#7b2ff7']}
|
||||
colors={[colors.accent, colors.accent2, colors.live, colors.accent]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={{ width: ring, height: ring, borderRadius: ring / 2 }}
|
||||
|
||||
@@ -5,7 +5,7 @@ import useSWR from 'swr';
|
||||
import type { ProfileDTO, TrackSearchResultDTO } from '@musicshare/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { colors, radius, space } from '@/lib/theme';
|
||||
import { colors, fonts, radius, space } from '@/lib/theme';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { useDebounced } from '@/lib/useDebounced';
|
||||
import { Body, Button, Card, Field, Heading, Row } from './ui';
|
||||
@@ -147,9 +147,9 @@ export function FeaturedPicker({ bare, onSaved }: { bare?: boolean; onSaved?: ()
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
small: { fontSize: 13 },
|
||||
title: { color: colors.text, fontWeight: '700', fontSize: 15 },
|
||||
small: { fontSize: 13, fontFamily: fonts.body },
|
||||
title: { color: colors.text, fontFamily: fonts.display, fontSize: 15 },
|
||||
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 },
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FollowListDTO, FollowListItemDTO } from '@musicshare/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { colors, space } from '@/lib/theme';
|
||||
import { colors, fonts, space } from '@/lib/theme';
|
||||
import { Avatar } from './Avatar';
|
||||
import { Body, Button } from './ui';
|
||||
|
||||
@@ -116,7 +116,7 @@ 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' },
|
||||
handle: { color: colors.muted, fontSize: 13 },
|
||||
name: { color: colors.text, fontSize: 15, fontFamily: fonts.bodyMedium },
|
||||
handle: { color: colors.muted, fontSize: 13, fontFamily: fonts.body },
|
||||
actions: { flexDirection: 'row', gap: space.sm },
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Linking, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import Animated, { LinearTransition } from 'react-native-reanimated';
|
||||
import useSWR from 'swr';
|
||||
import { currentLyricIndex, playbackPosition, type LyricLine, type LyricsDTO, type NowPlayingDTO, type TrackDTO } from '@musicshare/shared';
|
||||
import { colors, fonts, space } from '@/lib/theme';
|
||||
import { colors, fonts, space, useInk } from '@/lib/theme';
|
||||
|
||||
/** Lines shown around the current one while following along. */
|
||||
const BEFORE = 1;
|
||||
@@ -16,13 +16,15 @@ const PREVIEW = 8;
|
||||
* lines around the one being sung and follows along; "All lyrics" shows the rest.
|
||||
*/
|
||||
export function Lyrics({ track, np }: { track: TrackDTO; np?: NowPlayingDTO }) {
|
||||
const ink = useInk();
|
||||
const note = [styles.note, { color: ink.muted }];
|
||||
const { data, error, isLoading } = useSWR<LyricsDTO | null>(`/tracks/${track.id}/lyrics`);
|
||||
const [full, setFull] = useState(false);
|
||||
|
||||
if (isLoading) return <Text style={styles.note}>Finding lyrics…</Text>;
|
||||
if (error) return <Text style={styles.note}>Couldn't load lyrics right now.</Text>;
|
||||
if (!data) return <Text style={styles.note}>No lyrics found for this song.</Text>;
|
||||
if (data.instrumental && !data.synced && !data.plain) return <Text style={styles.note}>♪ Instrumental</Text>;
|
||||
if (isLoading) return <Text style={note}>Finding lyrics…</Text>;
|
||||
if (error) return <Text style={note}>Couldn't load lyrics right now.</Text>;
|
||||
if (!data) return <Text style={note}>No lyrics found for this song.</Text>;
|
||||
if (data.instrumental && !data.synced && !data.plain) return <Text style={note}>♪ Instrumental</Text>;
|
||||
|
||||
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 }) {
|
||||
</Pressable>
|
||||
<View style={styles.footer}>
|
||||
{canToggle && (
|
||||
<Text style={styles.toggle} onPress={() => setFull((v) => !v)}>
|
||||
<Text style={[styles.toggle, ink.hero && { color: '#ffffff' }]} onPress={() => setFull((v) => !v)}>
|
||||
{!full ? 'All lyrics' : synced && live ? 'Follow along' : 'Less'}
|
||||
</Text>
|
||||
)}
|
||||
<Text style={styles.source} onPress={() => Linking.openURL(data.source.url)}>
|
||||
<Text style={[styles.source, { color: ink.muted }]} onPress={() => Linking.openURL(data.source.url)}>
|
||||
Lyrics from {data.source.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<View style={[styles.card, pinned && styles.pinned]}>
|
||||
{/* The card glows with its album art; crossfades when the song changes. */}
|
||||
{np.track.artworkUrl && (
|
||||
<Animated.View key={np.track.artworkUrl} style={StyleSheet.absoluteFill} entering={FadeIn.duration(700)} exiting={FadeOut.duration(500)}>
|
||||
<Image source={np.track.artworkUrl} style={styles.backdrop} blurRadius={40} contentFit="cover" />
|
||||
</Animated.View>
|
||||
)}
|
||||
<LinearGradient colors={['rgba(7,7,11,0.05)', 'rgba(7,7,11,0.6)']} style={StyleSheet.absoluteFill} />
|
||||
<OnHero.Provider value={hero}>
|
||||
<View style={[styles.card, hero && styles.heroCard, pinned && styles.pinned]}>
|
||||
{hero && <LinearGradient colors={heroGradient} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={StyleSheet.absoluteFill} />}
|
||||
|
||||
<View style={styles.header}>
|
||||
{showUser && (
|
||||
@@ -74,11 +70,11 @@ export function NowPlayingCard({
|
||||
</Pressable>
|
||||
)}
|
||||
<View style={styles.statusRow}>
|
||||
{np.isPlaying && <Equalizer playing />}
|
||||
<Text style={[styles.status, np.isPlaying && styles.live]} numberOfLines={1}>
|
||||
{np.isPlaying && <Equalizer playing color={hero ? '#ffffff' : colors.live} />}
|
||||
<Text style={[styles.status, hero && styles.heroText, np.isPlaying && (hero ? styles.heroLive : styles.live)]} numberOfLines={1}>
|
||||
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
|
||||
<Text
|
||||
style={[styles.via, np.sourceUrl && styles.viaLink]}
|
||||
style={[styles.via, hero && styles.heroText, np.sourceUrl && styles.viaLink]}
|
||||
onPress={np.sourceUrl ? () => Linking.openURL(np.sourceUrl!) : undefined}>
|
||||
{' · '}
|
||||
{np.sourceApp ?? SERVICE_LABELS[np.source]}
|
||||
@@ -109,30 +105,33 @@ export function NowPlayingCard({
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</OnHero.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.wrap} accessibilityLabel={`${formatDuration(pos.positionMs)} of ${formatDuration(pos.durationMs)}`}>
|
||||
<View style={styles.track}>
|
||||
<Animated.View style={[styles.fill, fill]}>
|
||||
<LinearGradient colors={['#7b2ff7', '#2f8cff']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={StyleSheet.absoluteFill} />
|
||||
<View style={[styles.track, { backgroundColor: ink.faint }]}>
|
||||
<Animated.View style={[styles.fill, fill, ink.hero && { backgroundColor: '#ffffff' }]}>
|
||||
{!ink.hero && <LinearGradient colors={brandGradient} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={StyleSheet.absoluteFill} />}
|
||||
</Animated.View>
|
||||
</View>
|
||||
{!compact && (
|
||||
<View style={styles.times}>
|
||||
<Text style={styles.time}>{formatDuration(pos.positionMs)}</Text>
|
||||
<Text style={styles.time}>{formatDuration(pos.durationMs)}</Text>
|
||||
<Text style={[styles.time, { color: ink.muted }]}>{formatDuration(pos.positionMs)}</Text>
|
||||
<Text style={[styles.time, { color: ink.muted }]}>{formatDuration(pos.durationMs)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -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'] },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.services}>
|
||||
@@ -57,17 +59,17 @@ export function ServiceLinks({ track, preferred }: { track: TrackDTO; preferred:
|
||||
const { url, exact } = openUrl(track, s);
|
||||
return (
|
||||
<Pressable key={s} onPress={() => open(url)} hitSlop={6} accessibilityRole="link">
|
||||
<Text style={[styles.service, !exact && { opacity: 0.55 }]}>{SERVICE_LABELS[s]}</Text>
|
||||
<Text style={[chip, !exact && { opacity: 0.55 }]}>{SERVICE_LABELS[s]}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
{me && (
|
||||
<Pressable onPress={() => router.push(`/send/${track.id}`)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Send to friends">
|
||||
<Text style={[styles.service, styles.share]}>Send ✉</Text>
|
||||
<Text style={[chip, ink.hero ? styles.shareHero : styles.share]}>Send ✉</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable onPress={() => shareTrack(track, me?.username)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Share this song">
|
||||
<Text style={[styles.service, styles.share]}>Share ↗</Text>
|
||||
<Text style={[chip, ink.hero ? styles.shareHero : styles.share]}>Share ↗</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -88,6 +90,7 @@ export function TrackRow({
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const ink = useInk();
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Pressable style={styles.rowMain} onPress={onToggle} accessibilityRole="button" accessibilityState={{ expanded }}>
|
||||
@@ -98,7 +101,7 @@ export function TrackRow({
|
||||
</Text>
|
||||
<Text style={styles.artist} numberOfLines={1}>
|
||||
{track.artist}
|
||||
{expanded && track.album ? <Text style={styles.muted}> · {track.album}</Text> : null}
|
||||
{expanded && track.album ? <Text style={{ color: ink.muted }}> · {track.album}</Text> : null}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
@@ -109,7 +112,7 @@ export function TrackRow({
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={expanded ? 'Show less' : 'Show more'}
|
||||
style={styles.chevron}>
|
||||
<Text style={[styles.chevronText, expanded && { transform: [{ rotate: '180deg' }] }]}>⌄</Text>
|
||||
<Text style={[styles.chevronText, { color: ink.muted }, expanded && { transform: [{ rotate: '180deg' }] }]}>⌄</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 ? <Body style={{ color: colors.danger }}>{children}</Body> : null;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
point: { color: colors.text, fontSize: 14 },
|
||||
point: { color: colors.text, fontSize: 14, fontFamily: fonts.body },
|
||||
});
|
||||
|
||||
@@ -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 <Text style={[styles.title, style]} {...props} />;
|
||||
@@ -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 ? (
|
||||
<ActivityIndicator color={variant === 'secondary' ? colors.text : colors.accentText} />
|
||||
<ActivityIndicator color={textColor} />
|
||||
) : (
|
||||
<Text style={[styles.buttonText, small && styles.buttonTextSmall, variant !== 'secondary' && { color: colors.accentText }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[styles.buttonText, small && styles.buttonTextSmall, { color: textColor }]}>{title}</Text>
|
||||
);
|
||||
return (
|
||||
<Animated.View style={[press.style, inactive && { opacity: 0.55 }, style]}>
|
||||
@@ -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 && (
|
||||
<LinearGradient colors={brandGradient} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={StyleSheet.absoluteFill} />
|
||||
)}
|
||||
{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 },
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user