Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c660da
Simplify for example
SeanBarker182 Sep 12, 2025
ae2c078
fix: include modules that annotate with REACT_TURBO_MODULE
SeanBarker182 Sep 17, 2025
f0e9e97
Merge branch 'main' into feature/windows-menu
SeanBarker182 Sep 17, 2025
12c9590
use turbo module
SeanBarker182 Sep 22, 2025
9a2c3c8
Fix fabric imports
SeanBarker182 Sep 23, 2025
35f6eac
Add passthrough module
SeanBarker182 Sep 23, 2025
c687230
Group titlebar components
SeanBarker182 Sep 23, 2025
fb794cd
Code style fixes
SeanBarker182 Sep 23, 2025
a66f7c2
lint fix
SeanBarker182 Sep 23, 2025
d678ee7
Simplify module
SeanBarker182 Sep 24, 2025
d6a01be
Fix component registration
SeanBarker182 Sep 24, 2025
540d1d8
Fix memory leak issue that's caused by mounting and unmounting passt…
SeanBarker182 Sep 24, 2025
bdc9036
Merge branch 'feature/windows-menu' of https://github.com/infinitered…
SeanBarker182 Sep 24, 2025
d63a9e0
generate uuid
SeanBarker182 Sep 25, 2025
8e735a7
Add basic menu
SeanBarker182 Sep 25, 2025
0f06046
Add titlebar menu
SeanBarker182 Sep 25, 2025
3a1ae66
More stable useGlobal for windows
SeanBarker182 Sep 25, 2025
fa870f6
WIP: Add windows support for useMenuItem hook
SeanBarker182 Sep 25, 2025
0478fd9
Merge branch 'main' into feature/windows-menu
SeanBarker182 Sep 25, 2025
0745644
Rename IRMenuItemManager to IRSystemMenuManager for clarity
SeanBarker182 Sep 26, 2025
e3cc8ad
Fix native types
SeanBarker182 Sep 26, 2025
210bc9a
Break out platform implementations
SeanBarker182 Sep 26, 2025
10e6a51
improve and document the useGlobal windows hook
SeanBarker182 Sep 26, 2025
a5e2ac3
Remove RNW useKeyboard return
SeanBarker182 Sep 26, 2025
c9f3ea5
fix imports
SeanBarker182 Sep 26, 2025
fbd9f0f
rename
SeanBarker182 Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { useTheme, themed } from "./theme/theme"
import { useEffect, useMemo } from "react"
import { TimelineScreen } from "./screens/TimelineScreen"
import { useMenuItem } from "./utils/useMenuItem"
import { useSystemMenu } from "./utils/useSystemMenu/useSystemMenu"
import { Titlebar } from "./components/Titlebar/Titlebar"
import { Sidebar } from "./components/Sidebar/Sidebar"
import { useSidebar } from "./state/useSidebar"
Expand Down Expand Up @@ -74,12 +74,12 @@
},
...(__DEV__
? [
{
label: "Toggle Dev Menu",
shortcut: "cmd+shift+d",
action: () => NativeModules.DevMenu.show(),
},
]
{

Check failure on line 77 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
label: "Toggle Dev Menu",

Check failure on line 78 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
shortcut: "cmd+shift+d",

Check failure on line 79 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
action: () => NativeModules.DevMenu.show(),

Check failure on line 80 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
},

Check failure on line 81 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
]

Check failure on line 82 in app/app.tsx

View workflow job for this annotation

GitHub Actions / ci

Insert `··`
: []),
],
Window: [
Expand All @@ -101,7 +101,7 @@
[toggleSidebar],
)

useMenuItem(menuConfig)
useSystemMenu(menuConfig)

setTimeout(() => {
fetch("https://www.google.com")
Expand Down
105 changes: 105 additions & 0 deletions app/components/Menu/MenuDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { View, type ViewStyle, type TextStyle } from "react-native"

Check failure on line 1 in app/components/Menu/MenuDropdown.tsx

View workflow job for this annotation

GitHub Actions / ci

'TextStyle' is defined but never used
import { useRef, useMemo, memo } from "react"
import { themed } from "../../theme/theme"
import { Portal } from "../Portal"
import { MenuDropdownItem } from "./MenuDropdownItem"
import { useSubmenuState } from "./useSubmenuState"
import { menuSettings } from "./menuSettings"
import { type Position, type DropdownMenuItem, type MenuItem, MENU_SEPARATOR } from "./types"
import { getUUID } from "../../utils/random/getUUID"
import { Separator } from "../Separator"

interface MenuDropdownProps {
items: (DropdownMenuItem | typeof MENU_SEPARATOR)[]
position: Position
onItemPress: (item: MenuItem) => void
isSubmenu?: boolean
}

const MenuDropdownComponent = ({

Check failure on line 19 in app/components/Menu/MenuDropdown.tsx

View workflow job for this annotation

GitHub Actions / ci

Replace `⏎··items,⏎··position,⏎··onItemPress,⏎··isSubmenu,⏎` with `·items,·position,·onItemPress,·isSubmenu·`
items,
position,
onItemPress,
isSubmenu,
}: MenuDropdownProps) => {
const portalName = useRef(

Check failure on line 25 in app/components/Menu/MenuDropdown.tsx

View workflow job for this annotation

GitHub Actions / ci

Replace `⏎····`${isSubmenu·?·'submenu'·:·'dropdown'}-${getUUID()}`⏎··` with ``${isSubmenu·?·"submenu"·:·"dropdown"}-${getUUID()}``
`${isSubmenu ? 'submenu' : 'dropdown'}-${getUUID()}`
).current
const { openSubmenu, submenuPosition, handleItemHover } = useSubmenuState(position)

const isSeparator = (item: MenuItem | typeof MENU_SEPARATOR): item is typeof MENU_SEPARATOR => {
return item === MENU_SEPARATOR
}

// Find the submenu item if one is open
const submenuItem = openSubmenu
? items.find(item => !isSeparator(item) && item.label === openSubmenu) as DropdownMenuItem | undefined

Check failure on line 36 in app/components/Menu/MenuDropdown.tsx

View workflow job for this annotation

GitHub Actions / ci

Replace `items.find(item·=>·!isSeparator(item)·&&·item.label·===·openSubmenu)·as·DropdownMenuItem·|·undefined` with `(items.find((item)·=>·!isSeparator(item)·&&·item.label·===·openSubmenu)·as⏎········|·DropdownMenuItem⏎········|·undefined)`
: undefined

const dropdownContent = useMemo(() => (
<View
style={[
isSubmenu ? $submenuDropdown() : $dropdown(),
{ left: position.x, top: position.y, zIndex: isSubmenu ? 10001 : 10000 }
]}
accessibilityRole="menu"
>
{items.map((item, index) => {
if (isSeparator(item)) return <Separator key={`separator-${index}`} />

return (
<MenuDropdownItem
key={item.label}
item={item as MenuItem}
index={index}
onItemPress={onItemPress}
onItemHover={handleItemHover}
/>
)
})}
</View>
), [items, isSubmenu, position.x, position.y, onItemPress, handleItemHover])

return (
<>
<Portal name={portalName}>
{dropdownContent}
</Portal>
{/* Render submenu */}
{submenuItem?.submenu && (
<MenuDropdown
items={submenuItem.submenu}
position={submenuPosition}
onItemPress={onItemPress}
isSubmenu={true}
/>
)}
</>
)
}

export const MenuDropdown = memo(MenuDropdownComponent)

const $dropdown = themed<ViewStyle>(({ colors, spacing }) => ({
position: "absolute",
backgroundColor: colors.cardBackground,
borderColor: colors.keyline,
borderWidth: 1,
borderRadius: 4,
minWidth: menuSettings.dropdownMinWidth,
paddingVertical: spacing.xs,
zIndex: menuSettings.zIndex.dropdown,
}))

const $submenuDropdown = themed<ViewStyle>(({ colors, spacing }) => ({
position: "absolute",
backgroundColor: colors.cardBackground,
borderColor: colors.keyline,
borderWidth: 1,
borderRadius: 4,
minWidth: menuSettings.submenuMinWidth,
paddingVertical: spacing.xs,
zIndex: menuSettings.zIndex.submenu,
}))


145 changes: 145 additions & 0 deletions app/components/Menu/MenuDropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Pressable, Text, View, type ViewStyle, type TextStyle } from "react-native"
import { useState, useRef, memo, useCallback } from "react"
import { themed } from "../../theme/theme"
import { menuSettings } from "./menuSettings"
import type { MenuItem } from "./types"

interface MenuDropdownItemProps {
item: MenuItem
index: number
onItemPress: (item: MenuItem) => void
onItemHover: (itemLabel: string, index: number, hasSubmenu: boolean) => void
}

const MenuDropdownItemComponent = ({
item,
index,
onItemPress,
onItemHover,
}: MenuDropdownItemProps) => {
const [hoveredItem, setHoveredItem] = useState<string | null>(null)
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const enabled = item.enabled !== false

const handleHoverIn = useCallback(() => {
// Clear any pending hover clear
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current)
hoverTimeoutRef.current = null
}
setHoveredItem(item.label)
const hasSubmenu = !!item.submenu
onItemHover(item.label, index, hasSubmenu)
}, [item.label, item.submenu, index, onItemHover])

const handleHoverOut = useCallback(() => {
// Use a small timeout to prevent flickering between items
hoverTimeoutRef.current = setTimeout(() => {
setHoveredItem((current) => current === item.label ? null : current)
}, 10)
}, [item.label])

const handlePress = useCallback(() => {
if (!item.action || !enabled) return
item.action()
onItemPress(item)
}, [item, onItemPress])

return (
<Pressable
onHoverIn={handleHoverIn}
onHoverOut={handleHoverOut}
onPress={handlePress}
disabled={!enabled}
style={({ pressed }) => [
$dropdownItem(),
((pressed || hoveredItem === item.label) && enabled) && $dropdownItemHovered(),
!enabled && $dropdownItemDisabled,
]}
>
<Text
style={[
$dropdownItemText(),
!enabled && $dropdownItemTextDisabled(),
]}
>
{item.label}
</Text>
<View style={$rightContent}>
{item.shortcut && (
<Text
style={[$shortcut(), !enabled && $dropdownItemTextDisabled()]}
>
{formatShortcut(item.shortcut)}
</Text>
)}
{item.submenu && (
<Text
style={[
$submenuArrow(),
!enabled && $dropdownItemTextDisabled(),
]}
>
</Text>
)}
</View>
</Pressable>
)
}

export const MenuDropdownItem = memo(MenuDropdownItemComponent)

function formatShortcut(shortcut: string): string {
return shortcut
.replace(/cmd/gi, "Ctrl")
.replace(/shift/gi, "Shift")
.replace(/\+/g, "+")

Check warning

Code scanning / CodeQL

Replacement of a substring with itself Medium

This replaces '+' with itself.

Copilot Autofix

AI 23 days ago

To fix this issue, simply remove the redundant line .replace(/\+/g, "+") from the formatShortcut function in app/components/Menu/MenuDropdownItem.tsx. Removing this will not affect the logic, as the split and map steps will still operate as needed. No further changes, imports, or method updates are required.

Suggested changeset 1
app/components/Menu/MenuDropdownItem.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx
--- a/app/components/Menu/MenuDropdownItem.tsx
+++ b/app/components/Menu/MenuDropdownItem.tsx
@@ -94,7 +94,6 @@
   return shortcut
     .replace(/cmd/gi, "Ctrl")
     .replace(/shift/gi, "Shift")
-    .replace(/\+/g, "+")
     .split("+")
     .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
     .join("+")
EOF
@@ -94,7 +94,6 @@
return shortcut
.replace(/cmd/gi, "Ctrl")
.replace(/shift/gi, "Shift")
.replace(/\+/g, "+")
.split("+")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("+")
Copilot is powered by AI and may make mistakes. Always verify output.
.split("+")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("+")
}

const $dropdownItem = themed<ViewStyle>(({ spacing }) => ({
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: 4,
minHeight: menuSettings.itemMinHeight,
}))

const $dropdownItemHovered = themed<ViewStyle>(({ colors }) => ({
backgroundColor: colors.neutralVery,
}))

const $dropdownItemDisabled = {
opacity: 0.5,
}

const $dropdownItemText = themed<TextStyle>(({ colors, typography }) => ({
color: colors.mainText,
fontSize: typography.caption,
}))

const $dropdownItemTextDisabled = themed<TextStyle>((theme) => ({
color: theme.colors.neutral,
}))

const $shortcut = themed<TextStyle>(({ colors, typography, spacing }) => ({
color: colors.neutral,
fontSize: typography.small,
marginLeft: spacing.md,
}))

const $submenuArrow = themed<TextStyle>(({ colors, typography, spacing }) => ({
color: colors.neutral,
fontSize: typography.small,
marginLeft: spacing.sm,
}))

const $rightContent: ViewStyle = {
flexDirection: "row",
alignItems: "center",
}
44 changes: 44 additions & 0 deletions app/components/Menu/MenuOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Pressable, type ViewStyle } from "react-native"
import { Portal } from "../Portal"
import { menuSettings } from "./menuSettings"

interface MenuOverlayProps {
onPress: () => void
portalName?: string
style?: ViewStyle
excludeArea?: {
top?: number
left?: number
right?: number
bottom?: number
}
}

export const MenuOverlay = ({
onPress,
portalName = 'menu-overlay',
style,
excludeArea,
}: MenuOverlayProps) => {

return (
<Portal name={portalName}>
<Pressable style={overlayStyle({ excludeArea, style })} onPress={onPress} />
</Portal>
)
}

interface OverlayStyleArgs {
excludeArea?: { top?: number, left?: number, right?: number, bottom?: number }
style?: ViewStyle
}

const overlayStyle: (args: OverlayStyleArgs) => ViewStyle = ({ excludeArea, style }: OverlayStyleArgs) => ({
position: "absolute",
top: excludeArea?.top ?? 0,
left: excludeArea?.left ?? 0,
right: excludeArea?.right ?? 0,
bottom: excludeArea?.bottom ?? 0,
zIndex: menuSettings.zIndex.menuOverlay,
...style,
})
13 changes: 13 additions & 0 deletions app/components/Menu/menuSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const menuSettings = {
dropdownMinWidth: 200,
submenuMinWidth: 150,
itemMinHeight: 28,
itemHeight: 32,
submenuOffsetX: 200,
submenuOffsetY: -5,
zIndex: {
menuOverlay: 9999,
dropdown: 10000,
submenu: 10001,
}
} as const
19 changes: 19 additions & 0 deletions app/components/Menu/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Position {
x: number
y: number
}

// Generic menu item interface for UI components
export interface MenuItem {
label: string
shortcut?: string
enabled?: boolean
action?: () => void
submenu?: (MenuItem | typeof MENU_SEPARATOR)[]
}

// Type alias for dropdown menu items (same as MenuItem)
export type DropdownMenuItem = MenuItem

// Menu separator constant
export const MENU_SEPARATOR = 'menu-item-separator' as const
Loading