Skip to content

Commit 3ece1dd

Browse files
Feature/application sidebar (#15)
* Add custom titlebar script * Add custom titlebar * Remove commented out useEffect * Minor code style fixes * Switch to Pressable (TouchableOpacity styles don't show on mac) * Remove header for now * Add icon for sidebar * change cursor * Add panel open icon * Add basic sidebar support * add support for cmd + b to toggle sidebar * Add content * use white background for now * Add icons * Style improvements * add reload * Add reactotron logos * toggle dev menu command * Add navigation color * Fix border * Add keyline color * Reduced motion util hook * cleaned up sidebar * Dark mode styles * Adapt to theme hook changes * Switch to pressable
1 parent 51c392e commit 3ece1dd

38 files changed

+530
-70
lines changed

app/app.tsx

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,58 @@
44
*
55
* @format
66
*/
7-
import { StatusBar, View, type ViewStyle } from "react-native"
7+
import { DevSettings, NativeModules, StatusBar, View, type ViewStyle } from "react-native"
88
import { connectToServer } from "./state/connectToServer"
99
import { useTheme, themed } from "./theme/theme"
10-
import { useEffect } from "react"
10+
import { useEffect, useMemo } from "react"
1111
import { TimelineScreen } from "./screens/TimelineScreen"
12-
import { AppHeader } from "./components/AppHeader"
1312
import { useMenuItem } from "./utils/useMenuItem"
1413
import { Titlebar } from "./components/Titlebar"
14+
import { Sidebar } from "./components/Sidebar/Sidebar"
15+
import { useSidebar } from "./state/useSidebar"
16+
import { AppHeader } from "./components/AppHeader"
1517

1618
if (__DEV__) {
1719
// This is for debugging Reactotron with ... Reactotron!
1820
// Load Reactotron client in development only.
1921
require("./devtools/ReactotronConfig.ts")
2022
}
2123

22-
const menuConfig = {
23-
remove: ["File", "Edit", "Format"],
24-
}
25-
2624
function App(): React.JSX.Element {
2725
const { colors } = useTheme()
26+
const { toggleSidebar } = useSidebar()
27+
28+
const menuConfig = useMemo(
29+
() => ({
30+
remove: ["File", "Edit", "Format"],
31+
items: {
32+
View: [
33+
{
34+
label: "Toggle Sidebar",
35+
shortcut: "cmd+b",
36+
action: toggleSidebar,
37+
},
38+
...(__DEV__
39+
? [
40+
{
41+
label: "Toggle Dev Menu",
42+
shortcut: "cmd+shift+d",
43+
action: () => NativeModules.DevMenu.show(),
44+
},
45+
]
46+
: []),
47+
],
48+
Window: [
49+
{
50+
label: "Reload",
51+
shortcut: "cmd+shift+r",
52+
action: () => DevSettings.reload(),
53+
},
54+
],
55+
},
56+
}),
57+
[toggleSidebar],
58+
)
2859

2960
useMenuItem(menuConfig)
3061

@@ -45,9 +76,12 @@ function App(): React.JSX.Element {
4576
<View style={$container()}>
4677
<Titlebar />
4778
<StatusBar barStyle={"dark-content"} backgroundColor={colors.background} />
48-
<AppHeader />
49-
<View style={$contentContainer}>
50-
<TimelineScreen />
79+
<View style={$mainContent}>
80+
<Sidebar />
81+
<View style={$contentContainer}>
82+
<AppHeader />
83+
<TimelineScreen />
84+
</View>
5185
</View>
5286
</View>
5387
)
@@ -58,6 +92,11 @@ const $container = themed<ViewStyle>(({ colors }) => ({
5892
backgroundColor: colors.background,
5993
}))
6094

95+
const $mainContent: ViewStyle = {
96+
flex: 1,
97+
flexDirection: "row",
98+
}
99+
61100
const $contentContainer: ViewStyle = {
62101
flex: 1,
63102
}

app/components/ActionButton.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GestureResponderEvent, TouchableOpacity, ViewStyle } from "react-native"
1+
import { GestureResponderEvent, ViewStyle, Pressable } from "react-native"
22
import { themed } from "../theme/theme"
33

44
interface ActionButtonProps {
@@ -9,9 +9,15 @@ interface ActionButtonProps {
99

1010
function ActionButton({ icon: Icon, onClick, style }: ActionButtonProps) {
1111
return (
12-
<TouchableOpacity style={[$container(), style]} onPress={onClick} activeOpacity={0.7}>
12+
<Pressable
13+
style={({ pressed }) => [$container(), style, $pressed(pressed)]}
14+
onPress={onClick}
15+
// TODO: Add hover support https://github.com/microsoft/react-native-macos/issues/2313
16+
// onHoverIn={() => setHovered(true)}
17+
// onHoverOut={() => setHovered(false)}
18+
>
1319
<Icon size={24} />
14-
</TouchableOpacity>
20+
</Pressable>
1521
)
1622
}
1723

@@ -20,6 +26,11 @@ const $container = themed<ViewStyle>(({ spacing }) => ({
2026
padding: spacing.sm,
2127
justifyContent: "center",
2228
alignItems: "center",
29+
cursor: "pointer",
2330
}))
2431

32+
const $pressed = (pressed: boolean) => ({
33+
opacity: pressed ? 0.5 : 1,
34+
})
35+
2536
export default ActionButton

app/components/DetailPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ function DetailSection({ title, children }: { title: string; children: React.Rea
200200

201201
const $container = themed<ViewStyle>(({ colors }) => ({
202202
flex: 1,
203-
backgroundColor: colors.cardBackground,
203+
backgroundColor: colors.background,
204204
borderLeftWidth: 1,
205205
borderLeftColor: colors.border,
206206
}))
@@ -209,7 +209,7 @@ const $emptyContainer = themed<ViewStyle>(({ colors }) => ({
209209
flex: 1,
210210
justifyContent: "center",
211211
alignItems: "center",
212-
backgroundColor: colors.cardBackground,
212+
backgroundColor: colors.background,
213213
borderLeftWidth: 1,
214214
borderLeftColor: colors.border,
215215
}))

app/components/Icon.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Image, ImageStyle, StyleProp, View, ViewProps, ViewStyle } from "react-native"
2+
3+
import { useTheme, useThemeName } from "../theme/theme"
4+
5+
export type IconTypes = keyof typeof iconRegistry
6+
7+
type BaseIconProps = {
8+
/**
9+
* The name of the icon
10+
*/
11+
icon: IconTypes
12+
13+
/**
14+
* An optional tint color for the icon
15+
*/
16+
color?: string
17+
18+
/**
19+
* An optional size for the icon. If not provided, the icon will be sized to the icon's resolution.
20+
*/
21+
size?: number
22+
23+
/**
24+
* Style overrides for the icon image
25+
*/
26+
style?: StyleProp<ImageStyle>
27+
28+
/**
29+
* Style overrides for the icon container
30+
*/
31+
containerStyle?: StyleProp<ViewStyle>
32+
}
33+
34+
type IconProps = Omit<ViewProps, "style"> & BaseIconProps
35+
36+
/**
37+
* A component to render a registered icon.
38+
* It is wrapped in a <View />, use `ActionButton` if you want to react to input
39+
* @param {IconProps} props - The props for the `Icon` component.
40+
* @returns {JSX.Element} The rendered `Icon` component.
41+
*/
42+
export function Icon(props: IconProps) {
43+
const {
44+
icon,
45+
color,
46+
size,
47+
style: $imageStyleOverride,
48+
containerStyle: $containerStyleOverride,
49+
...viewProps
50+
} = props
51+
52+
const theme = useTheme()
53+
54+
const $imageStyle: StyleProp<ImageStyle> = [
55+
$imageStyleBase,
56+
{ tintColor: color ?? theme.colors.mainText },
57+
size !== undefined && { width: size, height: size },
58+
$imageStyleOverride,
59+
]
60+
61+
return (
62+
<View {...viewProps} style={$containerStyleOverride}>
63+
<Image style={$imageStyle} source={iconRegistry[icon]} />
64+
</View>
65+
)
66+
}
67+
68+
export const iconRegistry = {
69+
panelLeftClose: require("../../assets/icons/panelLeftClose.png"),
70+
panelLeftOpen: require("../../assets/icons/panelLeftOpen.png"),
71+
plug: require("../../assets/icons/plug.png"),
72+
scrollText: require("../../assets/icons/scrollText.png"),
73+
chevronsLeftRightEllipsis: require("../../assets/icons/chevronsLeftRightEllipsis.png"),
74+
circleGauge: require("../../assets/icons/circleGauge.png"),
75+
}
76+
77+
const $imageStyleBase: ImageStyle = {
78+
resizeMode: "contain",
79+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { themed, useThemeName, withTheme } from "../../theme/theme"
2+
import { Animated, ImageStyle, View, ViewStyle } from "react-native-macos"
3+
4+
type AnimatedReactotronLogoProps = {
5+
progress: Animated.Value // 0 = collapsed, 1 = expanded (animated in the sidebar)
6+
mounted: boolean // tells us if the text/wordmark is mounted (only while expanded or animating open)
7+
}
8+
9+
export const AnimatedReactotronLogo = ({ progress, mounted }: AnimatedReactotronLogoProps) => {
10+
const [themeName] = useThemeName()
11+
const logoScale = progress.interpolate({ inputRange: [0, 1], outputRange: [0.78, 1] })
12+
const logoTextOpacity = progress // fade out the reactotron text when collapsed
13+
14+
return (
15+
<View style={$logoRow()}>
16+
<Animated.Image
17+
source={require("../../../assets/images/reactotronLogo.png")}
18+
style={[$logo, { transform: [{ scale: logoScale }] }]}
19+
resizeMode="contain"
20+
/>
21+
{mounted && (
22+
<Animated.Image
23+
key={`${themeName}-reactotronText`}
24+
source={require("../../../assets/images/reactotronText.png")}
25+
style={[$logoText(), { opacity: logoTextOpacity }]}
26+
resizeMode="contain"
27+
/>
28+
)}
29+
</View>
30+
)
31+
}
32+
33+
const $logo = {
34+
width: 36,
35+
height: 36,
36+
}
37+
38+
const $logoText = themed<ImageStyle>((theme) => ({
39+
height: 36 * 0.54, // this magic number is the aspect ratio of the logo text compared to the logo
40+
tintColor: theme.colors.mainText,
41+
}))
42+
43+
const $logoRow = themed<ViewStyle>(({ spacing }) => ({
44+
flexDirection: "row",
45+
alignItems: "center",
46+
gap: spacing.xs,
47+
}))

app/components/Sidebar/Sidebar.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react"
2+
import { Animated, View, ViewStyle, StyleSheet } from "react-native"
3+
import { themed, useTheme, useThemeName, withTheme } from "../../theme/theme"
4+
import { AnimatedReactotronLogo } from "./AnimatedReactotronLogo"
5+
import { useSidebarAnimationProgress } from "./useSidebarAnimationProgress"
6+
import { SidebarMenu } from "./SidebarMenu"
7+
8+
// Expanded sidebar width in px
9+
const EXPANDED_WIDTH = 250
10+
11+
// Collapsed sidebar width in px
12+
const COLLAPSED_WIDTH = 60
13+
14+
export const Sidebar = () => {
15+
const theme = useTheme()
16+
const { progress, mounted } = useSidebarAnimationProgress()
17+
18+
const animatedWidth = progress.interpolate({
19+
inputRange: [0, 1],
20+
outputRange: [COLLAPSED_WIDTH, EXPANDED_WIDTH],
21+
})
22+
23+
return (
24+
<Animated.View style={{ width: animatedWidth, overflow: "hidden" }}>
25+
<View style={$container()}>
26+
<View style={$content()}>
27+
<AnimatedReactotronLogo progress={progress} mounted={mounted} />
28+
<SidebarMenu progress={progress} mounted={mounted} collapsedWidth={COLLAPSED_WIDTH} />
29+
</View>
30+
</View>
31+
</Animated.View>
32+
)
33+
}
34+
35+
const $container = themed<ViewStyle>((theme) => ({
36+
flex: 1,
37+
overflow: "hidden",
38+
backgroundColor: theme.colors.navigation,
39+
borderWidth: StyleSheet.hairlineWidth,
40+
borderColor: theme.colors.keyline,
41+
// RN macOS doesn't seem to support borderBottomWidth so it's all or nothing
42+
// This makes it so we don't get a double border from the Titlebar border
43+
marginTop: -StyleSheet.hairlineWidth,
44+
}))
45+
46+
const $content = themed<ViewStyle>((theme) => ({
47+
flex: 1,
48+
padding: theme.spacing.sm,
49+
}))

0 commit comments

Comments
 (0)