From 8c660dad54b5e51956dd187f15687eec7cba4442 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 11 Sep 2025 20:24:59 -0400 Subject: [PATCH 01/40] Simplify for example --- .../IRMenuItemManager.windows.cpp | 80 ++++--------------- .../IRMenuItemManager.windows.h | 41 ++++------ app/utils/useMenuItem.tsx | 3 +- 3 files changed, 35 insertions(+), 89 deletions(-) diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp b/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp index 7be7d88..2df05f9 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp +++ b/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp @@ -8,72 +8,26 @@ #include "pch.h" #include "IRMenuItemManager.windows.h" +using winrt::reactotron::implementation::IRMenuItemManager; + namespace winrt::reactotron::implementation { - IRMenuItemManager::IRMenuItemManager() noexcept - { - // TurboModule initialization - } - - Microsoft::ReactNative::JSValue IRMenuItemManager::getAvailableMenus() noexcept - { - // TODO: Get available Windows application menus - Microsoft::ReactNative::JSValueArray menus; - // Stub implementation - return Microsoft::ReactNative::JSValue(std::move(menus)); - } - - Microsoft::ReactNative::JSValue IRMenuItemManager::getMenuStructure() noexcept - { - // TODO: Get Windows application menu structure - Microsoft::ReactNative::JSValueArray structure; - // Stub implementation - return Microsoft::ReactNative::JSValue(std::move(structure)); - } - - void IRMenuItemManager::createMenu(std::string menuName, Microsoft::ReactNative::ReactPromise const &promise) noexcept + void IRMenuItemManager::createMenu(std::string menuName, + ::React::ReactPromise &&result) noexcept { - // TODO: Create a new Windows menu - Microsoft::ReactNative::JSValueObject result; - result["success"] = false; - result["existed"] = false; - result["menuName"] = menuName; - promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result))); - } - - void IRMenuItemManager::addMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const &title, std::string const &keyEquivalent, Microsoft::ReactNative::ReactPromise const &promise) noexcept - { - // TODO: Add menu item at specified path in Windows - Microsoft::ReactNative::JSValueObject result; - result["success"] = false; - result["error"] = "Not implemented"; - promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result))); - } - - void IRMenuItemManager::insertMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const &title, int atIndex, std::string const &keyEquivalent, Microsoft::ReactNative::ReactPromise const &promise) noexcept - { - // TODO: Insert menu item at specified index and path in Windows - Microsoft::ReactNative::JSValueObject result; - result["success"] = false; - result["error"] = "Not implemented"; - promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result))); - } + // THE PROBLEM: onMenuItemPressed is nullptr/undefined at runtime + if (onMenuItemPressed) + { + PressEvent evt{}; + evt.menuPath = {"Test", "Event"}; + onMenuItemPressed(evt); + } - void IRMenuItemManager::removeMenuItemAtPath(Microsoft::ReactNative::JSValue path, Microsoft::ReactNative::ReactPromise const &promise) noexcept - { - // TODO: Remove menu item at specified path in Windows - Microsoft::ReactNative::JSValueObject result; - result["success"] = false; - result["error"] = "Not implemented"; - promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result))); + CreateRet ret{}; + ret.success = true; + ret.existed = false; + ret.menuName = menuName; + result.Resolve(std::move(ret)); } - void IRMenuItemManager::setMenuItemEnabledAtPath(Microsoft::ReactNative::JSValue path, bool enabled, Microsoft::ReactNative::ReactPromise const &promise) noexcept - { - // TODO: Enable/disable menu item at specified path in Windows - Microsoft::ReactNative::JSValueObject result; - result["success"] = false; - result["error"] = "Not implemented"; - promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result))); - } -} \ No newline at end of file +} // namespace winrt::reactotron::implementation diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h b/app/native/IRMenuItemManager/IRMenuItemManager.windows.h index d320e1f..de8c0de 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h +++ b/app/native/IRMenuItemManager/IRMenuItemManager.windows.h @@ -1,35 +1,28 @@ #pragma once -#include "NativeModules.h" + +#include +#include + +// Generated (DataTypes before Spec) +#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerDataTypes.g.h" +#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerSpec.g.h" namespace winrt::reactotron::implementation { REACT_MODULE(IRMenuItemManager) - struct IRMenuItemManager + struct IRMenuItemManager : reactotronCodegen::IRMenuItemManagerSpec { - IRMenuItemManager() noexcept; - - REACT_SYNC_METHOD(getAvailableMenus) - Microsoft::ReactNative::JSValue getAvailableMenus() noexcept; - - REACT_SYNC_METHOD(getMenuStructure) - Microsoft::ReactNative::JSValue getMenuStructure() noexcept; + // Only the essential types needed for the event + using PressEvent = reactotronCodegen::IRMenuItemManagerSpec_MenuItemPressedEvent; + using CreateRet = reactotronCodegen::IRMenuItemManagerSpec_createMenu_returnType; + // One simple method to test event emission REACT_METHOD(createMenu) - void createMenu(std::string menuName, Microsoft::ReactNative::ReactPromise const& promise) noexcept; - - REACT_METHOD(addMenuItemAtPath) - void addMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const& title, std::string const& keyEquivalent, Microsoft::ReactNative::ReactPromise const& promise) noexcept; - - REACT_METHOD(insertMenuItemAtPath) - void insertMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const& title, int atIndex, std::string const& keyEquivalent, Microsoft::ReactNative::ReactPromise const& promise) noexcept; - - REACT_METHOD(removeMenuItemAtPath) - void removeMenuItemAtPath(Microsoft::ReactNative::JSValue path, Microsoft::ReactNative::ReactPromise const& promise) noexcept; - - REACT_METHOD(setMenuItemEnabledAtPath) - void setMenuItemEnabledAtPath(Microsoft::ReactNative::JSValue path, bool enabled, Microsoft::ReactNative::ReactPromise const& promise) noexcept; + void createMenu(std::string menuName, ::React::ReactPromise &&result) noexcept; + // --- THE ISSUE: This event is undefined in JavaScript --- REACT_EVENT(onMenuItemPressed) - std::function onMenuItemPressed; + std::function onMenuItemPressed; }; -} \ No newline at end of file + +} // namespace winrt::reactotron::implementation diff --git a/app/utils/useMenuItem.tsx b/app/utils/useMenuItem.tsx index a680f05..7fc2536 100644 --- a/app/utils/useMenuItem.tsx +++ b/app/utils/useMenuItem.tsx @@ -57,7 +57,7 @@ import NativeIRMenuItemManager, { type MenuListEntry, SEPARATOR, } from "../native/IRMenuItemManager/NativeIRMenuItemManager" -import { Platform } from "react-native" +import { Alert, Platform } from "react-native" // Only thing to configure here is the path separator. const PATH_SEPARATOR = " > " @@ -288,7 +288,6 @@ export function useMenuItem(config?: MenuItemConfig) { }, [config, addEntries, removeMenuItems, getItemDifference]) useEffect(() => { - if (Platform.OS === "windows") return const subscription = NativeIRMenuItemManager.onMenuItemPressed(handleMenuItemPressed) discoverMenus() return () => { From ae2c078c7f307fbcf28f193e5ac3dc412422911d Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Wed, 17 Sep 2025 08:59:05 -0400 Subject: [PATCH 02/40] fix: include modules that annotate with REACT_TURBO_MODULE --- bin/generate_windows_native_files.js | 58 +++++++++++++--------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/bin/generate_windows_native_files.js b/bin/generate_windows_native_files.js index 1266bdf..582dc34 100644 --- a/bin/generate_windows_native_files.js +++ b/bin/generate_windows_native_files.js @@ -10,22 +10,22 @@ const path = require("path") // Color constants for output (matching Ruby script exactly) const colors = process.env.NO_COLOR ? { - R: "", - RB: "", - G: "", - GB: "", - BB: "", - Y: "", - YB: "", - D: "", - DD: "", - DB: "", - DDB: "", - S: "", - X: "\x1b[0m", - } + R: "", + RB: "", + G: "", + GB: "", + BB: "", + Y: "", + YB: "", + D: "", + DD: "", + DB: "", + DDB: "", + S: "", + X: "\x1b[0m", + } : process.env.PREFERS_CONTRAST === "more" - ? { + ? { R: "\x1b[91m", RB: "\x1b[91m", G: "\x1b[92m", @@ -40,7 +40,7 @@ const colors = process.env.NO_COLOR S: "\x1b[9m", X: "\x1b[0m", } - : { + : { R: "\x1b[31m", RB: "\x1b[31;1m", G: "\x1b[32m", @@ -179,7 +179,7 @@ function findWindowsNativeFiles(appPath, projectRoot) { // Detect module type by examining the header file const headerContent = fs.readFileSync(module.files.h, "utf8") - if (headerContent.includes("REACT_MODULE")) { + if (headerContent.includes("REACT_MODULE") || headerContent.includes("REACT_TURBO_MODULE")) { module.type = "turbo" } else if ( headerContent.includes("RegisterIRTabNativeComponent") || @@ -199,8 +199,7 @@ function findWindowsNativeFiles(appPath, projectRoot) { ) } else { console.log( - `${colors.YB} ⚠ Warning ${colors.X}${colors.D}${name} missing ${ - module.files.h ? ".cpp" : ".h" + `${colors.YB} ⚠ Warning ${colors.X}${colors.D}${name} missing ${module.files.h ? ".cpp" : ".h" } file${colors.X}`, ) } @@ -228,16 +227,14 @@ function generateConsolidatedFiles(modules, windowsDir, projectRoot) { fs.writeFileSync(headerPath, headerContent) const relativePath = path.relative(projectRoot, headerPath) console.log( - `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.h${colors.X} ${ - colors.DD + `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.h${colors.X} ${colors.DD }${path.dirname(relativePath)}${colors.X}`, ) changesMade = true } else { const relativePath = path.relative(projectRoot, headerPath) console.log( - `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.h${colors.X} ${ - colors.DD + `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.h${colors.X} ${colors.DD }${path.dirname(relativePath)}${colors.X}`, ) } @@ -250,16 +247,14 @@ function generateConsolidatedFiles(modules, windowsDir, projectRoot) { fs.writeFileSync(cppPath, cppContent) const relativePath = path.relative(projectRoot, cppPath) console.log( - `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.cpp${colors.X} ${ - colors.DD + `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.cpp${colors.X} ${colors.DD }${path.dirname(relativePath)}${colors.X}`, ) changesMade = true } else { const relativePath = path.relative(projectRoot, cppPath) console.log( - `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.cpp${colors.X} ${ - colors.DD + `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.cpp${colors.X} ${colors.DD }${path.dirname(relativePath)}${colors.X}`, ) } @@ -307,11 +302,10 @@ function generateHeaderTemplate(modules, windowsDir) { // // TurboModules (${turboModules.length}) will be auto-registered by AddAttributedModules() // Fabric Components (${fabricComponents.length}) require manual registration calls -${ - unknownModules.length > 0 - ? `// Unknown modules (${unknownModules.length}) - please check their implementation` - : "" -} +${unknownModules.length > 0 + ? `// Unknown modules (${unknownModules.length}) - please check their implementation` + : "" + } ${allIncludes} From 12c9590270b61ac5ce875949a71af1d2c8958e4d Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Mon, 22 Sep 2025 12:00:12 -0400 Subject: [PATCH 03/40] use turbo module --- app/native/IRMenuItemManager/IRMenuItemManager.windows.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h b/app/native/IRMenuItemManager/IRMenuItemManager.windows.h index de8c0de..e15daf9 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h +++ b/app/native/IRMenuItemManager/IRMenuItemManager.windows.h @@ -9,7 +9,7 @@ namespace winrt::reactotron::implementation { - REACT_MODULE(IRMenuItemManager) + REACT_TURBO_MODULE(IRMenuItemManager) struct IRMenuItemManager : reactotronCodegen::IRMenuItemManagerSpec { // Only the essential types needed for the event From 9a2c3c8d8f9226536100db63430bc8a4839c7303 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 23 Sep 2025 08:37:09 -0400 Subject: [PATCH 04/40] Fix fabric imports --- windows/reactotron/reactotron.cpp | 31 +++++++++++++++++++++++++-- windows/reactotron/reactotron.vcxproj | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/windows/reactotron/reactotron.cpp b/windows/reactotron/reactotron.cpp index 970b96d..4754636 100644 --- a/windows/reactotron/reactotron.cpp +++ b/windows/reactotron/reactotron.cpp @@ -7,6 +7,12 @@ #include "AutolinkedNativeModules.g.h" #include "NativeModules.h" +#include "IRNativeModules.g.h" + +#include +#include + + // A PackageProvider containing any turbo modules you define within this app project struct CompReactPackageProvider @@ -14,6 +20,8 @@ struct CompReactPackageProvider public: // IReactPackageProvider void CreatePackage(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept { AddAttributedModules(packageBuilder, true); + // Register Fabric components directly + winrt::reactotron::implementation::RegisterAllFabricComponents(packageBuilder); } }; @@ -70,8 +78,27 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE instance, HINSTANCE, PSTR // Get the AppWindow so we can configure its initial title and size auto appWindow{reactNativeWin32App.AppWindow()}; - appWindow.Title(L"Reactotron"); - appWindow.Resize({1000, 1000}); + appWindow.Title(L""); + appWindow.Resize({2000, 1500}); + + + { + using namespace winrt::Microsoft::UI::Windowing; + + auto titleBar = appWindow.TitleBar(); + if (titleBar) + { + // Extend React Native content into the title bar area + titleBar.ExtendsContentIntoTitleBar(true); + + // Make system caption buttons transparent so they overlay our content + winrt::Windows::UI::Color transparent = winrt::Windows::UI::Colors::Transparent(); + titleBar.ButtonBackgroundColor(transparent); + titleBar.ButtonInactiveBackgroundColor(transparent); + titleBar.ButtonHoverBackgroundColor(transparent); + titleBar.ButtonPressedBackgroundColor(transparent); + } + } // Get the ReactViewOptions so we can set the initial RN component to load auto viewOptions{reactNativeWin32App.ReactViewOptions()}; diff --git a/windows/reactotron/reactotron.vcxproj b/windows/reactotron/reactotron.vcxproj index 41c0023..01a66bc 100644 --- a/windows/reactotron/reactotron.vcxproj +++ b/windows/reactotron/reactotron.vcxproj @@ -104,6 +104,7 @@ + Create From 35f6eac2349a6cb563548c04cec4b14bc659c5ce Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 23 Sep 2025 13:02:43 -0400 Subject: [PATCH 05/40] Add passthrough module --- .../PassthroughView/PassthroughView.tsx | 9 ++ .../IRPassthroughView.windows.cpp | 152 ++++++++++++++++++ .../IRPassthroughView.windows.h | 38 +++++ .../IRPassthroughViewNativeComponent.ts | 13 ++ windows/reactotron/IRNativeModules.g.cpp | 1 + windows/reactotron/IRNativeModules.g.h | 4 +- windows/reactotron/reactotron.cpp | 1 - 7 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 app/components/PassthroughView/PassthroughView.tsx create mode 100644 app/native/IRPassthroughView/IRPassthroughView.windows.cpp create mode 100644 app/native/IRPassthroughView/IRPassthroughView.windows.h create mode 100644 app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts diff --git a/app/components/PassthroughView/PassthroughView.tsx b/app/components/PassthroughView/PassthroughView.tsx new file mode 100644 index 0000000..11fbf4d --- /dev/null +++ b/app/components/PassthroughView/PassthroughView.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import type { ViewProps } from 'react-native'; +import NativePassthroughView from '../../native/IRPassthroughView/IRPassthroughViewNativeComponent'; + +export interface PassthroughViewProps extends ViewProps { } + +export const PassthroughView: React.FC = (props) => { + return ; +}; \ No newline at end of file diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp new file mode 100644 index 0000000..c53fdff --- /dev/null +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp @@ -0,0 +1,152 @@ +#include "pch.h" + +#include "IRPassthroughView.windows.h" +#include +#include + +namespace winrt::reactotron::implementation { + +std::vector IRPassthroughView::s_instances; + +IRPassthroughView::IRPassthroughView() { + s_instances.push_back(this); +} + +IRPassthroughView::~IRPassthroughView() { + auto it = std::find(s_instances.begin(), s_instances.end(), this); + if (it != s_instances.end()) { + s_instances.erase(it); + UpdateAllPassthroughRegions(); + } +} + +void RegisterIRPassthroughNativeComponent( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept { + reactotronCodegen::RegisterIRPassthroughNativeComponent( + packageBuilder, + [](const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder &builder) { + // Disable default border handling to prevent visual clipping issues + builder.SetViewFeatures( + winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::Default & + ~winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::NativeBorder); + }); +} + +winrt::Microsoft::UI::Composition::Visual IRPassthroughView::CreateVisual( + const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + auto compositor = view.as().Compositor(); + + m_visual = compositor.CreateSpriteVisual(); + m_view = view; + + // Create visual that can be styled from React Native + return m_visual; +} + +void IRPassthroughView::Initialize(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + m_view = view; + + m_layoutMetricChangedRevoker = view.LayoutMetricsChanged( + winrt::auto_revoke, + [wkThis = get_weak()]( + const winrt::IInspectable & /*sender*/, const winrt::Microsoft::ReactNative::LayoutMetricsChangedArgs &args) { + if (auto strongThis = wkThis.get()) { + auto visual = strongThis->m_visual; + + // Manually position visual since we disabled default border handling + visual.Size( + {args.NewLayoutMetrics().Frame.Width * args.NewLayoutMetrics().PointScaleFactor, + args.NewLayoutMetrics().Frame.Height * args.NewLayoutMetrics().PointScaleFactor}); + visual.Offset({ + args.NewLayoutMetrics().Frame.X * args.NewLayoutMetrics().PointScaleFactor, + args.NewLayoutMetrics().Frame.Y * args.NewLayoutMetrics().PointScaleFactor, + 0.0f, + }); + + // Update passthrough regions for title bar interaction + UpdateAllPassthroughRegions(); + } + }); +} + + + +winrt::Windows::Graphics::RectInt32 IRPassthroughView::GetPassthroughRect() const noexcept { + if (!m_visual || !m_view) { + return { 0, 0, 0, 0 }; + } + + auto size = m_visual.Size(); + auto offset = m_visual.Offset(); + + // Get rectangle coordinates for passthrough regions + return { + static_cast(offset.x), + static_cast(offset.y), + static_cast(size.x), + static_cast(size.y) + }; +} + +void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { + try { + // Find the main window for our process using more reliable enumeration + HWND hwnd = nullptr; + EnumWindows([](HWND h, LPARAM p) -> BOOL { + DWORD pid = 0; + GetWindowThreadProcessId(h, &pid); + if (pid == GetCurrentProcessId() && IsWindowVisible(h) && !GetParent(h)) { + *reinterpret_cast(p) = h; + return FALSE; // Stop enumeration + } + return TRUE; // Continue enumeration + }, reinterpret_cast(&hwnd)); + + if (!hwnd) { + return; // No main window found + } + + // Convert HWND to AppWindow + auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(hwnd); + auto appWindow = winrt::Microsoft::UI::Windowing::AppWindow::GetFromWindowId(windowId); + if (!appWindow) { + return; + } + + auto nonClientInputSrc = winrt::Microsoft::UI::Input::InputNonClientPointerSource::GetForWindowId(appWindow.Id()); + if (!nonClientInputSrc) { + return; + } + + // Collect rectangles from all PassthroughView instances + std::vector passthroughRects; + + for (auto* instance : s_instances) { + if (instance && instance->m_visual && instance->m_view) { + try { + auto rect = instance->GetPassthroughRect(); + if (rect.Width > 0 && rect.Height > 0) { + passthroughRects.push_back(rect); + } + } catch (...) { + #ifdef _DEBUG + OutputDebugStringA("[IRPassthroughView] Exception accessing instance during rect collection\n"); + #endif + } + } + } + + // Configure passthrough regions for interactive elements + nonClientInputSrc.SetRegionRects( + winrt::Microsoft::UI::Input::NonClientRegionKind::Passthrough, + passthroughRects + ); + + } catch (...) { + #ifdef _DEBUG + OutputDebugStringA("[IRPassthroughView] Exception in UpdateAllPassthroughRegions\n"); + #endif + } +} + +} // namespace winrt::reactotron::implementation \ No newline at end of file diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.h b/app/native/IRPassthroughView/IRPassthroughView.windows.h new file mode 100644 index 0000000..45ff8ab --- /dev/null +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.h @@ -0,0 +1,38 @@ +#pragma once + +#include "..\..\..\windows\reactotron\codegen\react\components\AppSpec\IRPassthrough.g.h" +#include +#include +#include +#include + + +namespace winrt::reactotron::implementation +{ + + void RegisterIRPassthroughNativeComponent( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept; + + struct IRPassthroughView : winrt::implements, + reactotronCodegen::BaseIRPassthrough + { + IRPassthroughView(); + ~IRPassthroughView(); + + winrt::Microsoft::UI::Composition::Visual CreateVisual( + const winrt::Microsoft::ReactNative::ComponentView &view) noexcept override; + void Initialize(const winrt::Microsoft::ReactNative::ComponentView & /*view*/) noexcept override; + + + private: + winrt::Microsoft::ReactNative::ComponentView::LayoutMetricsChanged_revoker m_layoutMetricChangedRevoker; + winrt::Microsoft::UI::Composition::SpriteVisual m_visual{nullptr}; + winrt::Microsoft::ReactNative::ComponentView m_view{nullptr}; + + static std::vector s_instances; + static void UpdateAllPassthroughRegions() noexcept; + + winrt::Windows::Graphics::RectInt32 GetPassthroughRect() const noexcept; + }; + +} // namespace winrt::reactotron::implementation \ No newline at end of file diff --git a/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts b/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts new file mode 100644 index 0000000..f02c743 --- /dev/null +++ b/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts @@ -0,0 +1,13 @@ +import type { ViewProps } from "react-native" +import type { BubblingEventHandler } from "react-native/Libraries/Types/CodegenTypes" + +// For React Native 0.80, use this: +// import { codegenNativeComponent } from "react-native" +// For React Native 0.78, use this: +import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent" + +export interface NativeProps extends ViewProps { + // Add Passthrough specific props here if needed +} + +export default codegenNativeComponent("IRPassthrough") \ No newline at end of file diff --git a/windows/reactotron/IRNativeModules.g.cpp b/windows/reactotron/IRNativeModules.g.cpp index c3dfc1a..c58643e 100644 --- a/windows/reactotron/IRNativeModules.g.cpp +++ b/windows/reactotron/IRNativeModules.g.cpp @@ -4,6 +4,7 @@ namespace winrt::reactotron::implementation { void RegisterAllFabricComponents(winrt::Microsoft::ReactNative::IReactPackageBuilder const& packageBuilder) noexcept { // Auto-generated Fabric component registrations + RegisterIRPassthroughNativeComponent(packageBuilder); RegisterIRTabNativeComponent(packageBuilder); } } diff --git a/windows/reactotron/IRNativeModules.g.h b/windows/reactotron/IRNativeModules.g.h index 717ed49..867c32b 100644 --- a/windows/reactotron/IRNativeModules.g.h +++ b/windows/reactotron/IRNativeModules.g.h @@ -3,7 +3,7 @@ // DO NOT EDIT - This file is auto-generated // // TurboModules (9) will be auto-registered by AddAttributedModules() -// Fabric Components (1) require manual registration calls +// Fabric Components (2) require manual registration calls #include "../../app/native/IRActionMenuManager/IRActionMenuManager.windows.h" @@ -11,6 +11,7 @@ #include "../../app/native/IRFontList/IRFontList.windows.h" #include "../../app/native/IRKeyboard/IRKeyboard.windows.h" #include "../../app/native/IRMenuItemManager/IRMenuItemManager.windows.h" +#include "../../app/native/IRPassthroughView/IRPassthroughView.windows.h" #include "../../app/native/IRRunShellCommand/IRRunShellCommand.windows.h" #include "../../app/native/IRSystemInfo/IRSystemInfo.windows.h" #include "../../app/native/IRTabComponentView/IRTabComponentView.windows.h" @@ -19,6 +20,7 @@ namespace winrt::reactotron::implementation { // Fabric component registration functions + void RegisterIRPassthroughNativeComponent(winrt::Microsoft::ReactNative::IReactPackageBuilder const& packageBuilder) noexcept; void RegisterIRTabNativeComponent(winrt::Microsoft::ReactNative::IReactPackageBuilder const& packageBuilder) noexcept; // Helper function to register all Fabric components diff --git a/windows/reactotron/reactotron.cpp b/windows/reactotron/reactotron.cpp index 4754636..41543b6 100644 --- a/windows/reactotron/reactotron.cpp +++ b/windows/reactotron/reactotron.cpp @@ -13,7 +13,6 @@ #include - // A PackageProvider containing any turbo modules you define within this app project struct CompReactPackageProvider : winrt::implements { From c687230ada8276b4b370cbc563a17346cf1f3373 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 23 Sep 2025 13:07:08 -0400 Subject: [PATCH 06/40] Group titlebar components --- app/app.tsx | 14 ++++----- .../PassthroughView/PassthroughView.tsx | 9 ------ app/components/Titlebar/PassthroughView.tsx | 30 ++++++++++++++++++ app/components/{ => Titlebar}/Titlebar.tsx | 31 ++++++++++--------- 4 files changed, 54 insertions(+), 30 deletions(-) delete mode 100644 app/components/PassthroughView/PassthroughView.tsx create mode 100644 app/components/Titlebar/PassthroughView.tsx rename app/components/{ => Titlebar}/Titlebar.tsx (61%) diff --git a/app/app.tsx b/app/app.tsx index c8a0aa3..b79fd4f 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -10,7 +10,7 @@ import { useTheme, themed } from "./theme/theme" import { useEffect, useMemo } from "react" import { TimelineScreen } from "./screens/TimelineScreen" import { useMenuItem } from "./utils/useMenuItem" -import { Titlebar } from "./components/Titlebar" +import { Titlebar } from "./components/Titlebar/Titlebar" import { Sidebar } from "./components/Sidebar/Sidebar" import { useSidebar } from "./state/useSidebar" import { AppHeader } from "./components/AppHeader" @@ -73,12 +73,12 @@ function App(): React.JSX.Element { }, ...(__DEV__ ? [ - { - label: "Toggle Dev Menu", - shortcut: "cmd+shift+d", - action: () => NativeModules.DevMenu.show(), - }, - ] + { + label: "Toggle Dev Menu", + shortcut: "cmd+shift+d", + action: () => NativeModules.DevMenu.show(), + }, + ] : []), ], Window: [ diff --git a/app/components/PassthroughView/PassthroughView.tsx b/app/components/PassthroughView/PassthroughView.tsx deleted file mode 100644 index 11fbf4d..0000000 --- a/app/components/PassthroughView/PassthroughView.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import type { ViewProps } from 'react-native'; -import NativePassthroughView from '../../native/IRPassthroughView/IRPassthroughViewNativeComponent'; - -export interface PassthroughViewProps extends ViewProps { } - -export const PassthroughView: React.FC = (props) => { - return ; -}; \ No newline at end of file diff --git a/app/components/Titlebar/PassthroughView.tsx b/app/components/Titlebar/PassthroughView.tsx new file mode 100644 index 0000000..5740bfc --- /dev/null +++ b/app/components/Titlebar/PassthroughView.tsx @@ -0,0 +1,30 @@ +/** + * PassthroughView - Windows Title Bar Click-Through Component + * + * Creates regions within the extended title bar that allow mouse clicks to pass through + * to underlying interactive elements (buttons, inputs, etc.) while keeping the rest of + * the title bar draggable. + * + * This is necessary because Windows title bars with ExtendsContentIntoTitleBar(true) + * capture all mouse events by default. PassthroughView creates "punch-out" regions + * using Windows InputNonClientPointerSource passthrough regions. + * + * On macOS, this simply returns a regular View since macOS title bars don't intercept + * mouse events the same way - interactive elements in the title bar work normally. + * + * Usage: Wrap interactive elements that need to remain clickable in the title bar area. + * Example: + */ +import React from 'react'; +import { View, Platform } from 'react-native'; +import type { ViewProps } from 'react-native'; +import NativePassthroughView from '../../native/IRPassthroughView/IRPassthroughViewNativeComponent'; + +export interface PassthroughViewProps extends ViewProps { } + +export const PassthroughView: React.FC = (props) => { + return Platform.select({ + windows: , + default: , // macOS and other platforms use regular View + }); +}; \ No newline at end of file diff --git a/app/components/Titlebar.tsx b/app/components/Titlebar/Titlebar.tsx similarity index 61% rename from app/components/Titlebar.tsx rename to app/components/Titlebar/Titlebar.tsx index 9a683a3..c939596 100644 --- a/app/components/Titlebar.tsx +++ b/app/components/Titlebar/Titlebar.tsx @@ -1,8 +1,9 @@ -import { themed, useTheme } from "../theme/theme" +import { themed, useTheme } from "../../theme/theme" import { Platform, View, ViewStyle } from "react-native" -import { Icon } from "./Icon" -import ActionButton from "./ActionButton" -import { useSidebar } from "../state/useSidebar" +import { Icon } from "../Icon" +import ActionButton from "../ActionButton" +import { useSidebar } from "../../state/useSidebar" +import { PassthroughView } from "./PassthroughView" export const Titlebar = () => { const theme = useTheme() @@ -12,16 +13,18 @@ export const Titlebar = () => { - ( - - )} - onClick={toggleSidebar} - /> + + ( + + )} + onClick={toggleSidebar} + /> + ) From fb794cd3a0815a2a6394d5149ffd1e18b41dacb9 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 23 Sep 2025 13:13:28 -0400 Subject: [PATCH 07/40] Code style fixes --- app/components/Titlebar/PassthroughView.tsx | 15 ++++++--------- .../IRPassthroughViewNativeComponent.ts | 7 ++----- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/components/Titlebar/PassthroughView.tsx b/app/components/Titlebar/PassthroughView.tsx index 5740bfc..70639aa 100644 --- a/app/components/Titlebar/PassthroughView.tsx +++ b/app/components/Titlebar/PassthroughView.tsx @@ -15,16 +15,13 @@ * Usage: Wrap interactive elements that need to remain clickable in the title bar area. * Example: */ -import React from 'react'; -import { View, Platform } from 'react-native'; -import type { ViewProps } from 'react-native'; -import NativePassthroughView from '../../native/IRPassthroughView/IRPassthroughViewNativeComponent'; +import { View, Platform } from "react-native" +import type { ViewProps } from "react-native" +import NativePassthroughView from "../../native/IRPassthroughView/IRPassthroughViewNativeComponent" -export interface PassthroughViewProps extends ViewProps { } - -export const PassthroughView: React.FC = (props) => { +export const PassthroughView = (props: ViewProps) => { return Platform.select({ windows: , default: , // macOS and other platforms use regular View - }); -}; \ No newline at end of file + }) +} diff --git a/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts b/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts index f02c743..22a8581 100644 --- a/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts +++ b/app/native/IRPassthroughView/IRPassthroughViewNativeComponent.ts @@ -1,13 +1,10 @@ import type { ViewProps } from "react-native" -import type { BubblingEventHandler } from "react-native/Libraries/Types/CodegenTypes" // For React Native 0.80, use this: // import { codegenNativeComponent } from "react-native" // For React Native 0.78, use this: import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent" -export interface NativeProps extends ViewProps { - // Add Passthrough specific props here if needed -} +export interface NativeProps extends ViewProps {} -export default codegenNativeComponent("IRPassthrough") \ No newline at end of file +export default codegenNativeComponent("IRPassthrough") From a66f7c2eff83ff30706b61bba2dd591429f1f030 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 23 Sep 2025 13:17:40 -0400 Subject: [PATCH 08/40] lint fix --- app/app.tsx | 12 +++--- app/screens/HelpScreen.tsx | 2 +- app/utils/useMenuItem.tsx | 1 - bin/generate_windows_native_files.js | 56 +++++++++++++++------------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/app.tsx b/app/app.tsx index b79fd4f..c44f4b0 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -73,12 +73,12 @@ function App(): React.JSX.Element { }, ...(__DEV__ ? [ - { - label: "Toggle Dev Menu", - shortcut: "cmd+shift+d", - action: () => NativeModules.DevMenu.show(), - }, - ] + { + label: "Toggle Dev Menu", + shortcut: "cmd+shift+d", + action: () => NativeModules.DevMenu.show(), + }, + ] : []), ], Window: [ diff --git a/app/screens/HelpScreen.tsx b/app/screens/HelpScreen.tsx index 219a5c2..328db3a 100644 --- a/app/screens/HelpScreen.tsx +++ b/app/screens/HelpScreen.tsx @@ -140,7 +140,7 @@ const $keystrokeKey = themed(({ spacing, colors }) => ({ alignItems: "center", })) -const $keystroke = themed(({ colors, spacing }) => ({ +const $keystroke = themed(({ colors }) => ({ fontSize: 16, fontWeight: "bold", color: colors.mainText, diff --git a/app/utils/useMenuItem.tsx b/app/utils/useMenuItem.tsx index 7fc2536..dc094ec 100644 --- a/app/utils/useMenuItem.tsx +++ b/app/utils/useMenuItem.tsx @@ -57,7 +57,6 @@ import NativeIRMenuItemManager, { type MenuListEntry, SEPARATOR, } from "../native/IRMenuItemManager/NativeIRMenuItemManager" -import { Alert, Platform } from "react-native" // Only thing to configure here is the path separator. const PATH_SEPARATOR = " > " diff --git a/bin/generate_windows_native_files.js b/bin/generate_windows_native_files.js index 582dc34..4734388 100644 --- a/bin/generate_windows_native_files.js +++ b/bin/generate_windows_native_files.js @@ -10,22 +10,22 @@ const path = require("path") // Color constants for output (matching Ruby script exactly) const colors = process.env.NO_COLOR ? { - R: "", - RB: "", - G: "", - GB: "", - BB: "", - Y: "", - YB: "", - D: "", - DD: "", - DB: "", - DDB: "", - S: "", - X: "\x1b[0m", - } + R: "", + RB: "", + G: "", + GB: "", + BB: "", + Y: "", + YB: "", + D: "", + DD: "", + DB: "", + DDB: "", + S: "", + X: "\x1b[0m", + } : process.env.PREFERS_CONTRAST === "more" - ? { + ? { R: "\x1b[91m", RB: "\x1b[91m", G: "\x1b[92m", @@ -40,7 +40,7 @@ const colors = process.env.NO_COLOR S: "\x1b[9m", X: "\x1b[0m", } - : { + : { R: "\x1b[31m", RB: "\x1b[31;1m", G: "\x1b[32m", @@ -199,7 +199,8 @@ function findWindowsNativeFiles(appPath, projectRoot) { ) } else { console.log( - `${colors.YB} ⚠ Warning ${colors.X}${colors.D}${name} missing ${module.files.h ? ".cpp" : ".h" + `${colors.YB} ⚠ Warning ${colors.X}${colors.D}${name} missing ${ + module.files.h ? ".cpp" : ".h" } file${colors.X}`, ) } @@ -227,14 +228,16 @@ function generateConsolidatedFiles(modules, windowsDir, projectRoot) { fs.writeFileSync(headerPath, headerContent) const relativePath = path.relative(projectRoot, headerPath) console.log( - `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.h${colors.X} ${colors.DD + `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.h${colors.X} ${ + colors.DD }${path.dirname(relativePath)}${colors.X}`, ) changesMade = true } else { const relativePath = path.relative(projectRoot, headerPath) console.log( - `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.h${colors.X} ${colors.DD + `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.h${colors.X} ${ + colors.DD }${path.dirname(relativePath)}${colors.X}`, ) } @@ -247,14 +250,16 @@ function generateConsolidatedFiles(modules, windowsDir, projectRoot) { fs.writeFileSync(cppPath, cppContent) const relativePath = path.relative(projectRoot, cppPath) console.log( - `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.cpp${colors.X} ${colors.DD + `${colors.BB} ➕ Generated ${colors.X} ${colors.BB}IRNativeModules.g.cpp${colors.X} ${ + colors.DD }${path.dirname(relativePath)}${colors.X}`, ) changesMade = true } else { const relativePath = path.relative(projectRoot, cppPath) console.log( - `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.cpp${colors.X} ${colors.DD + `${colors.DB} ✔️ Exists ${colors.X}${colors.DB}IRNativeModules.g.cpp${colors.X} ${ + colors.DD }${path.dirname(relativePath)}${colors.X}`, ) } @@ -302,10 +307,11 @@ function generateHeaderTemplate(modules, windowsDir) { // // TurboModules (${turboModules.length}) will be auto-registered by AddAttributedModules() // Fabric Components (${fabricComponents.length}) require manual registration calls -${unknownModules.length > 0 - ? `// Unknown modules (${unknownModules.length}) - please check their implementation` - : "" - } +${ + unknownModules.length > 0 + ? `// Unknown modules (${unknownModules.length}) - please check their implementation` + : "" +} ${allIncludes} From d678ee7c73a5b3aa2dbdae0abafcc7b09ae1259e Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Wed, 24 Sep 2025 09:15:14 -0400 Subject: [PATCH 09/40] Simplify module --- .../IRPassthroughView.windows.cpp | 65 +++++++------------ .../IRPassthroughView.windows.h | 1 - 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp index c53fdff..e2cdffa 100644 --- a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp @@ -22,25 +22,16 @@ IRPassthroughView::~IRPassthroughView() { void RegisterIRPassthroughNativeComponent( winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept { - reactotronCodegen::RegisterIRPassthroughNativeComponent( - packageBuilder, - [](const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder &builder) { - // Disable default border handling to prevent visual clipping issues - builder.SetViewFeatures( - winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::Default & - ~winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::NativeBorder); - }); + reactotronCodegen::RegisterIRPassthroughNativeComponent(packageBuilder); } winrt::Microsoft::UI::Composition::Visual IRPassthroughView::CreateVisual( const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + // React Native Windows requires a visual for the component tree auto compositor = view.as().Compositor(); - - m_visual = compositor.CreateSpriteVisual(); + auto visual = compositor.CreateSpriteVisual(); m_view = view; - - // Create visual that can be styled from React Native - return m_visual; + return visual; } void IRPassthroughView::Initialize(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { @@ -51,19 +42,7 @@ void IRPassthroughView::Initialize(const winrt::Microsoft::ReactNative::Componen [wkThis = get_weak()]( const winrt::IInspectable & /*sender*/, const winrt::Microsoft::ReactNative::LayoutMetricsChangedArgs &args) { if (auto strongThis = wkThis.get()) { - auto visual = strongThis->m_visual; - - // Manually position visual since we disabled default border handling - visual.Size( - {args.NewLayoutMetrics().Frame.Width * args.NewLayoutMetrics().PointScaleFactor, - args.NewLayoutMetrics().Frame.Height * args.NewLayoutMetrics().PointScaleFactor}); - visual.Offset({ - args.NewLayoutMetrics().Frame.X * args.NewLayoutMetrics().PointScaleFactor, - args.NewLayoutMetrics().Frame.Y * args.NewLayoutMetrics().PointScaleFactor, - 0.0f, - }); - - // Update passthrough regions for title bar interaction + // Update passthrough regions whenever component layout changes UpdateAllPassthroughRegions(); } }); @@ -72,41 +51,43 @@ void IRPassthroughView::Initialize(const winrt::Microsoft::ReactNative::Componen winrt::Windows::Graphics::RectInt32 IRPassthroughView::GetPassthroughRect() const noexcept { - if (!m_visual || !m_view) { + if (!m_view) { return { 0, 0, 0, 0 }; } - auto size = m_visual.Size(); - auto offset = m_visual.Offset(); + // Get component position and size from React Native layout system + auto layoutMetrics = m_view.LayoutMetrics(); + auto frame = layoutMetrics.Frame; + auto scale = layoutMetrics.PointScaleFactor; - // Get rectangle coordinates for passthrough regions + // Convert to screen coordinates for Windows passthrough regions return { - static_cast(offset.x), - static_cast(offset.y), - static_cast(size.x), - static_cast(size.y) + static_cast(frame.X * scale), + static_cast(frame.Y * scale), + static_cast(frame.Width * scale), + static_cast(frame.Height * scale) }; } void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { try { - // Find the main window for our process using more reliable enumeration + // Find the application window using process enumeration HWND hwnd = nullptr; EnumWindows([](HWND h, LPARAM p) -> BOOL { DWORD pid = 0; GetWindowThreadProcessId(h, &pid); if (pid == GetCurrentProcessId() && IsWindowVisible(h) && !GetParent(h)) { *reinterpret_cast(p) = h; - return FALSE; // Stop enumeration + return FALSE; } - return TRUE; // Continue enumeration + return TRUE; }, reinterpret_cast(&hwnd)); if (!hwnd) { - return; // No main window found + return; } - // Convert HWND to AppWindow + // Get Windows App SDK window components auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(hwnd); auto appWindow = winrt::Microsoft::UI::Windowing::AppWindow::GetFromWindowId(windowId); if (!appWindow) { @@ -118,11 +99,11 @@ void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { return; } - // Collect rectangles from all PassthroughView instances + // Collect all PassthroughView rectangles std::vector passthroughRects; for (auto* instance : s_instances) { - if (instance && instance->m_visual && instance->m_view) { + if (instance && instance->m_view) { try { auto rect = instance->GetPassthroughRect(); if (rect.Width > 0 && rect.Height > 0) { @@ -136,7 +117,7 @@ void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { } } - // Configure passthrough regions for interactive elements + // Apply passthrough regions to Windows title bar nonClientInputSrc.SetRegionRects( winrt::Microsoft::UI::Input::NonClientRegionKind::Passthrough, passthroughRects diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.h b/app/native/IRPassthroughView/IRPassthroughView.windows.h index 45ff8ab..e51488b 100644 --- a/app/native/IRPassthroughView/IRPassthroughView.windows.h +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.h @@ -26,7 +26,6 @@ namespace winrt::reactotron::implementation private: winrt::Microsoft::ReactNative::ComponentView::LayoutMetricsChanged_revoker m_layoutMetricChangedRevoker; - winrt::Microsoft::UI::Composition::SpriteVisual m_visual{nullptr}; winrt::Microsoft::ReactNative::ComponentView m_view{nullptr}; static std::vector s_instances; From d6a01be2a4acdc106be0fbe12a6398261333b939 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Wed, 24 Sep 2025 09:59:05 -0400 Subject: [PATCH 10/40] Fix component registration --- .gitignore | 5 ++++- app/native/IRPassthroughView/IRPassthroughView.windows.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 445e421..7c11bae 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,7 @@ macos/.xcode.env.local msbuild.binlog -.claude/* \ No newline at end of file +.claude/* + +# Windows reserved device names +nul \ No newline at end of file diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp index e2cdffa..e844c7c 100644 --- a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp @@ -22,7 +22,7 @@ IRPassthroughView::~IRPassthroughView() { void RegisterIRPassthroughNativeComponent( winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept { - reactotronCodegen::RegisterIRPassthroughNativeComponent(packageBuilder); + reactotronCodegen::RegisterIRPassthroughNativeComponent(packageBuilder, nullptr); } winrt::Microsoft::UI::Composition::Visual IRPassthroughView::CreateVisual( From 540d1d8c7b4bad241d6d646278fc7bf65e21e2a0 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Wed, 24 Sep 2025 11:26:00 -0400 Subject: [PATCH 11/40] Fix memory leak issue that's caused by mounting and unmounting passthroughviews --- .../IRPassthroughView.windows.cpp | 100 ++++++++++++------ .../IRPassthroughView.windows.h | 5 + 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp index e844c7c..b3e29fd 100644 --- a/app/native/IRPassthroughView/IRPassthroughView.windows.cpp +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.cpp @@ -13,9 +13,13 @@ IRPassthroughView::IRPassthroughView() { } IRPassthroughView::~IRPassthroughView() { + // WARNING: This destructor is rarely called in React Native Windows Fabric due to component lifecycle issues. + // The proper cleanup happens in UnmountChildComponentView() instead. + // Keep this as a safety fallback in case the lifecycle method isn't called. auto it = std::find(s_instances.begin(), s_instances.end(), this); if (it != s_instances.end()) { s_instances.erase(it); + DebugLog("Destructor fallback cleanup - this should rarely happen"); UpdateAllPassthroughRegions(); } } @@ -37,17 +41,32 @@ winrt::Microsoft::UI::Composition::Visual IRPassthroughView::CreateVisual( void IRPassthroughView::Initialize(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { m_view = view; + // Subscribe to layout changes to keep Windows passthrough regions in sync with React Native layout m_layoutMetricChangedRevoker = view.LayoutMetricsChanged( winrt::auto_revoke, [wkThis = get_weak()]( const winrt::IInspectable & /*sender*/, const winrt::Microsoft::ReactNative::LayoutMetricsChangedArgs &args) { if (auto strongThis = wkThis.get()) { - // Update passthrough regions whenever component layout changes UpdateAllPassthroughRegions(); } }); } +void IRPassthroughView::UnmountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept { + // CRITICAL: This is the proper React Native Windows Fabric lifecycle method for component cleanup. + // Unlike C++ destructors, this method is reliably called when React Native unmounts components. + if (view == m_view) { + // Remove this instance from the global instances list and update Windows passthrough regions + auto it = std::find(s_instances.begin(), s_instances.end(), this); + if (it != s_instances.end()) { + s_instances.erase(it); + DebugLog("Component unmounted - cleaned up passthrough region"); + UpdateAllPassthroughRegions(); + } + } +} + winrt::Windows::Graphics::RectInt32 IRPassthroughView::GetPassthroughRect() const noexcept { @@ -55,23 +74,28 @@ winrt::Windows::Graphics::RectInt32 IRPassthroughView::GetPassthroughRect() cons return { 0, 0, 0, 0 }; } - // Get component position and size from React Native layout system - auto layoutMetrics = m_view.LayoutMetrics(); - auto frame = layoutMetrics.Frame; - auto scale = layoutMetrics.PointScaleFactor; - - // Convert to screen coordinates for Windows passthrough regions - return { - static_cast(frame.X * scale), - static_cast(frame.Y * scale), - static_cast(frame.Width * scale), - static_cast(frame.Height * scale) - }; + try { + // Get component position and size from React Native layout system + auto layoutMetrics = m_view.LayoutMetrics(); + auto frame = layoutMetrics.Frame; + auto scale = layoutMetrics.PointScaleFactor; + + // Convert React Native coordinates to Windows screen coordinates for passthrough regions + return { + static_cast(frame.X * scale), + static_cast(frame.Y * scale), + static_cast(frame.Width * scale), + static_cast(frame.Height * scale) + }; + } catch (...) { + DebugLog("Exception getting passthrough rect - component may be unmounted"); + return { 0, 0, 0, 0 }; + } } void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { try { - // Find the application window using process enumeration + // Find the main application window by enumerating all windows for this process HWND hwnd = nullptr; EnumWindows([](HWND h, LPARAM p) -> BOOL { DWORD pid = 0; @@ -84,50 +108,58 @@ void IRPassthroughView::UpdateAllPassthroughRegions() noexcept { }, reinterpret_cast(&hwnd)); if (!hwnd) { + DebugLog("Application window not found"); return; } - // Get Windows App SDK window components + // Get Windows App SDK components needed for passthrough region management auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(hwnd); auto appWindow = winrt::Microsoft::UI::Windowing::AppWindow::GetFromWindowId(windowId); if (!appWindow) { + DebugLog("Failed to get AppWindow from window handle"); return; } auto nonClientInputSrc = winrt::Microsoft::UI::Input::InputNonClientPointerSource::GetForWindowId(appWindow.Id()); if (!nonClientInputSrc) { + DebugLog("Failed to get InputNonClientPointerSource"); return; } - // Collect all PassthroughView rectangles - std::vector passthroughRects; + // CRITICAL: Clear only passthrough regions, not all regions (which would break title bar dragging) + // This prevents accumulation of stale regions from unmounted components + nonClientInputSrc.ClearRegionRects(winrt::Microsoft::UI::Input::NonClientRegionKind::Passthrough); + // Collect rectangles from all currently mounted PassthroughView instances + std::vector passthroughRects; for (auto* instance : s_instances) { if (instance && instance->m_view) { - try { - auto rect = instance->GetPassthroughRect(); - if (rect.Width > 0 && rect.Height > 0) { - passthroughRects.push_back(rect); - } - } catch (...) { - #ifdef _DEBUG - OutputDebugStringA("[IRPassthroughView] Exception accessing instance during rect collection\n"); - #endif + auto rect = instance->GetPassthroughRect(); + if (rect.Width > 0 && rect.Height > 0) { + passthroughRects.push_back(rect); } } } - // Apply passthrough regions to Windows title bar - nonClientInputSrc.SetRegionRects( - winrt::Microsoft::UI::Input::NonClientRegionKind::Passthrough, - passthroughRects - ); + // Apply the complete new set of passthrough regions to Windows + // SetRegionRects replaces ALL existing passthrough regions with this new set + if (!passthroughRects.empty()) { + nonClientInputSrc.SetRegionRects( + winrt::Microsoft::UI::Input::NonClientRegionKind::Passthrough, + passthroughRects + ); + } } catch (...) { - #ifdef _DEBUG - OutputDebugStringA("[IRPassthroughView] Exception in UpdateAllPassthroughRegions\n"); - #endif + DebugLog("Exception in UpdateAllPassthroughRegions"); } } +void IRPassthroughView::DebugLog(const std::string& message) noexcept { +#ifdef _DEBUG + std::string fullMessage = "[IRPassthroughView] " + message + "\n"; + OutputDebugStringA(fullMessage.c_str()); +#endif +} + } // namespace winrt::reactotron::implementation \ No newline at end of file diff --git a/app/native/IRPassthroughView/IRPassthroughView.windows.h b/app/native/IRPassthroughView/IRPassthroughView.windows.h index e51488b..099999e 100644 --- a/app/native/IRPassthroughView/IRPassthroughView.windows.h +++ b/app/native/IRPassthroughView/IRPassthroughView.windows.h @@ -22,6 +22,8 @@ namespace winrt::reactotron::implementation winrt::Microsoft::UI::Composition::Visual CreateVisual( const winrt::Microsoft::ReactNative::ComponentView &view) noexcept override; void Initialize(const winrt::Microsoft::ReactNative::ComponentView & /*view*/) noexcept override; + void UnmountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept override; private: @@ -32,6 +34,9 @@ namespace winrt::reactotron::implementation static void UpdateAllPassthroughRegions() noexcept; winrt::Windows::Graphics::RectInt32 GetPassthroughRect() const noexcept; + + // Debug logging helper + static void DebugLog(const std::string& message) noexcept; }; } // namespace winrt::reactotron::implementation \ No newline at end of file From d63a9e0fc01e85889af215555b44e967efbf3b95 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 25 Sep 2025 12:01:23 -0400 Subject: [PATCH 12/40] generate uuid --- app/utils/random/IRRandom.windows.cpp | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/utils/random/IRRandom.windows.cpp b/app/utils/random/IRRandom.windows.cpp index 88f1a74..ddb7090 100644 --- a/app/utils/random/IRRandom.windows.cpp +++ b/app/utils/random/IRRandom.windows.cpp @@ -7,6 +7,9 @@ #include "pch.h" #include "IRRandom.windows.h" +#include +#include +#include namespace winrt::reactotron::implementation { @@ -17,7 +20,28 @@ namespace winrt::reactotron::implementation std::string IRRandom::getUUID() noexcept { - // TODO: Generate UUID on Windows using CoCreateGuid or similar - return "00000000-0000-0000-0000-000000000000"; + GUID guid; + HRESULT result = CoCreateGuid(&guid); + + if (FAILED(result)) + { + return "00000000-0000-0000-0000-000000000000"; + } + + std::ostringstream stream; + stream << std::hex << std::uppercase << std::setfill('0') + << std::setw(8) << guid.Data1 << "-" + << std::setw(4) << guid.Data2 << "-" + << std::setw(4) << guid.Data3 << "-" + << std::setw(2) << static_cast(guid.Data4[0]) + << std::setw(2) << static_cast(guid.Data4[1]) << "-" + << std::setw(2) << static_cast(guid.Data4[2]) + << std::setw(2) << static_cast(guid.Data4[3]) + << std::setw(2) << static_cast(guid.Data4[4]) + << std::setw(2) << static_cast(guid.Data4[5]) + << std::setw(2) << static_cast(guid.Data4[6]) + << std::setw(2) << static_cast(guid.Data4[7]); + + return stream.str(); } } \ No newline at end of file From 8e735a78502267525968f6975767143383ba02d6 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 25 Sep 2025 16:56:09 -0400 Subject: [PATCH 13/40] Add basic menu --- app/components/Menu/MenuDropdown.tsx | 110 ++++++++++++++++ app/components/Menu/MenuDropdownItem.tsx | 145 ++++++++++++++++++++++ app/components/Menu/MenuOverlay.tsx | 44 +++++++ app/components/Menu/menuSettings.ts | 13 ++ app/components/Menu/types.ts | 13 ++ app/components/Menu/useMenuPositioning.ts | 50 ++++++++ app/components/Menu/useSubmenuState.ts | 37 ++++++ 7 files changed, 412 insertions(+) create mode 100644 app/components/Menu/MenuDropdown.tsx create mode 100644 app/components/Menu/MenuDropdownItem.tsx create mode 100644 app/components/Menu/MenuOverlay.tsx create mode 100644 app/components/Menu/menuSettings.ts create mode 100644 app/components/Menu/types.ts create mode 100644 app/components/Menu/useMenuPositioning.ts create mode 100644 app/components/Menu/useSubmenuState.ts diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx new file mode 100644 index 0000000..fbc4feb --- /dev/null +++ b/app/components/Menu/MenuDropdown.tsx @@ -0,0 +1,110 @@ +import { View, type ViewStyle, type TextStyle } from "react-native" +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 } from "./types" +import { MenuItem, SEPARATOR } from "../../utils/useMenuItem" +import { getUUID } from "../../utils/random/getUUID" +import { Separator } from "../Separator" + +type DropdownMenuItem = MenuItem & { + submenu?: (DropdownMenuItem | typeof SEPARATOR)[] +} + +interface MenuDropdownProps { + items: (DropdownMenuItem | typeof SEPARATOR)[] + position: Position + onItemPress: (item: MenuItem) => void + isSubmenu?: boolean +} + +const MenuDropdownComponent = ({ + items, + position, + onItemPress, + isSubmenu, +}: MenuDropdownProps) => { + const portalName = useRef( + `${isSubmenu ? 'submenu' : 'dropdown'}-${getUUID()}` + ).current + const { openSubmenu, submenuPosition, handleItemHover } = useSubmenuState(position) + + const isSeparator = (item: MenuItem | typeof SEPARATOR): item is typeof SEPARATOR => { + return item === SEPARATOR + } + + // Find the submenu item if one is open + const submenuItem = openSubmenu + ? items.find(item => !isSeparator(item) && item.label === openSubmenu) as DropdownMenuItem | undefined + : undefined + + const dropdownContent = useMemo(() => ( + + {items.map((item, index) => { + if (isSeparator(item)) return + + return ( + + ) + })} + + ), [items, isSubmenu, position.x, position.y, onItemPress, handleItemHover]) + + return ( + <> + + {dropdownContent} + + {/* Render submenu */} + {submenuItem?.submenu && ( + + )} + + ) +} + +export const MenuDropdown = memo(MenuDropdownComponent) + +const $dropdown = themed(({ 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(({ colors, spacing }) => ({ + position: "absolute", + backgroundColor: colors.cardBackground, + borderColor: colors.keyline, + borderWidth: 1, + borderRadius: 4, + minWidth: menuSettings.submenuMinWidth, + paddingVertical: spacing.xs, + zIndex: menuSettings.zIndex.submenu, +})) + + diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx new file mode 100644 index 0000000..b498f29 --- /dev/null +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -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 "../../utils/useMenuItem" + +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(null) + const hoverTimeoutRef = useRef(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 ( + [ + $dropdownItem(), + ((pressed || hoveredItem === item.label) && enabled) && $dropdownItemHovered(), + !enabled && $dropdownItemDisabled, + ]} + > + + {item.label} + + + {item.shortcut && ( + + {formatShortcut(item.shortcut)} + + )} + {item.submenu && ( + + ▶ + + )} + + + ) +} + +export const MenuDropdownItem = memo(MenuDropdownItemComponent) + +function formatShortcut(shortcut: string): string { + return shortcut + .replace(/cmd/gi, "Ctrl") + .replace(/shift/gi, "Shift") + .replace(/\+/g, "+") + .split("+") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join("+") +} + +const $dropdownItem = themed(({ spacing }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: 4, + minHeight: menuSettings.itemMinHeight, +})) + +const $dropdownItemHovered = themed(({ colors }) => ({ + backgroundColor: colors.neutralVery, +})) + +const $dropdownItemDisabled = { + opacity: 0.5, +} + +const $dropdownItemText = themed(({ colors, typography }) => ({ + color: colors.mainText, + fontSize: typography.caption, +})) + +const $dropdownItemTextDisabled = themed((theme) => ({ + color: theme.colors.neutral, +})) + +const $shortcut = themed(({ colors, typography, spacing }) => ({ + color: colors.neutral, + fontSize: typography.small, + marginLeft: spacing.md, +})) + +const $submenuArrow = themed(({ colors, typography, spacing }) => ({ + color: colors.neutral, + fontSize: typography.small, + marginLeft: spacing.sm, +})) + +const $rightContent: ViewStyle = { + flexDirection: "row", + alignItems: "center", +} \ No newline at end of file diff --git a/app/components/Menu/MenuOverlay.tsx b/app/components/Menu/MenuOverlay.tsx new file mode 100644 index 0000000..65e6768 --- /dev/null +++ b/app/components/Menu/MenuOverlay.tsx @@ -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 ( + + + + ) +} + +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, +}) \ No newline at end of file diff --git a/app/components/Menu/menuSettings.ts b/app/components/Menu/menuSettings.ts new file mode 100644 index 0000000..79a034f --- /dev/null +++ b/app/components/Menu/menuSettings.ts @@ -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 \ No newline at end of file diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts new file mode 100644 index 0000000..b9e0e96 --- /dev/null +++ b/app/components/Menu/types.ts @@ -0,0 +1,13 @@ +export interface Position { + x: number + y: number +} + +export interface MenuItemWithSubmenu { + label: string + shortcut?: string + enabled?: boolean + position?: number + action?: () => void + submenu?: (MenuItemWithSubmenu | 'menu-item-separator')[] +} \ No newline at end of file diff --git a/app/components/Menu/useMenuPositioning.ts b/app/components/Menu/useMenuPositioning.ts new file mode 100644 index 0000000..cac99d6 --- /dev/null +++ b/app/components/Menu/useMenuPositioning.ts @@ -0,0 +1,50 @@ +import { useCallback } from "react" +import { menuSettings } from "./menuSettings" +import type { Position } from "./types" + +export interface PositioningStrategy { + calculateSubmenuPosition: ( + basePosition: Position, + itemIndex: number, + parentWidth?: number + ) => Position + calculateContextMenuPosition?: ( + clickPosition: Position, + menuSize?: { width: number; height: number }, + screenSize?: { width: number; height: number } + ) => Position +} + +const defaultStrategy: PositioningStrategy = { + calculateSubmenuPosition: (basePosition, itemIndex, parentWidth = menuSettings.submenuOffsetX) => ({ + x: basePosition.x + parentWidth, + y: basePosition.y + itemIndex * menuSettings.itemHeight + menuSettings.submenuOffsetY, + }), + + calculateContextMenuPosition: (clickPosition, menuSize, screenSize) => { + // Basic positioning - can be enhanced for screen edge detection + return { + x: clickPosition.x, + y: clickPosition.y, + } + }, +} + +export const useMenuPositioning = (strategy: PositioningStrategy = defaultStrategy) => { + const calculateSubmenuPosition = useCallback( + (basePosition: Position, itemIndex: number, parentWidth?: number) => + strategy.calculateSubmenuPosition(basePosition, itemIndex, parentWidth), + [strategy] + ) + + const calculateContextMenuPosition = useCallback( + (clickPosition: Position, menuSize?: { width: number; height: number }, screenSize?: { width: number; height: number }) => + strategy.calculateContextMenuPosition?.(clickPosition, menuSize, screenSize) ?? clickPosition, + [strategy] + ) + + return { + calculateSubmenuPosition, + calculateContextMenuPosition, + } +} \ No newline at end of file diff --git a/app/components/Menu/useSubmenuState.ts b/app/components/Menu/useSubmenuState.ts new file mode 100644 index 0000000..1c105ca --- /dev/null +++ b/app/components/Menu/useSubmenuState.ts @@ -0,0 +1,37 @@ +import { useState, useCallback } from "react" +import { menuSettings } from "./menuSettings" +import { type Position } from "./types" + +export const useSubmenuState = (basePosition: Position) => { + const [openSubmenu, setOpenSubmenu] = useState(null) + const [submenuPosition, setSubmenuPosition] = useState({ x: 0, y: 0 }) + + const openSubmenuAt = useCallback((itemLabel: string, index: number) => { + setOpenSubmenu(itemLabel) + setSubmenuPosition({ + x: basePosition.x + menuSettings.submenuOffsetX, + y: basePosition.y + index * menuSettings.itemHeight + menuSettings.submenuOffsetY, + }) + }, [basePosition.x, basePosition.y]) + + const closeSubmenu = useCallback(() => { + setOpenSubmenu(null) + }, []) + + const handleItemHover = useCallback((itemLabel: string, index: number, hasSubmenu: boolean) => { + if (hasSubmenu) { + openSubmenuAt(itemLabel, index) + } else { + if (openSubmenu) { + closeSubmenu() + } + } + }, [openSubmenu, openSubmenuAt, closeSubmenu]) + + return { + openSubmenu, + submenuPosition, + handleItemHover, + closeSubmenu, + } +} \ No newline at end of file From 0f06046b86c7553643764b213cbb5c48fbfa3c05 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 25 Sep 2025 17:00:31 -0400 Subject: [PATCH 14/40] Add titlebar menu --- app/components/Titlebar/Titlebar.tsx | 6 ++ app/components/Titlebar/TitlebarMenu.tsx | 81 ++++++++++++++++++++ app/components/Titlebar/TitlebarMenuItem.tsx | 60 +++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 app/components/Titlebar/TitlebarMenu.tsx create mode 100644 app/components/Titlebar/TitlebarMenuItem.tsx diff --git a/app/components/Titlebar/Titlebar.tsx b/app/components/Titlebar/Titlebar.tsx index c939596..6c4e029 100644 --- a/app/components/Titlebar/Titlebar.tsx +++ b/app/components/Titlebar/Titlebar.tsx @@ -4,6 +4,7 @@ import { Icon } from "../Icon" import ActionButton from "../ActionButton" import { useSidebar } from "../../state/useSidebar" import { PassthroughView } from "./PassthroughView" +import { TitlebarMenu } from "./TitlebarMenu" export const Titlebar = () => { const theme = useTheme() @@ -13,6 +14,11 @@ export const Titlebar = () => { + {Platform.OS === "windows" && ( + + + + )} ( diff --git a/app/components/Titlebar/TitlebarMenu.tsx b/app/components/Titlebar/TitlebarMenu.tsx new file mode 100644 index 0000000..aff8c1c --- /dev/null +++ b/app/components/Titlebar/TitlebarMenu.tsx @@ -0,0 +1,81 @@ +import { View, ViewStyle } from "react-native" +import { useState, useCallback, useRef } from "react" +import { themed } from "../../theme/theme" +import { TitlebarMenuItem } from "./TitlebarMenuItem" +import { MenuDropdown } from "../Menu/MenuDropdown" +import { MenuOverlay } from "../Menu/MenuOverlay" +import type { Position } from "../Menu/types" +import { PassthroughView } from "./PassthroughView" +import { useMenuItem } from "../../utils/useMenuItem" + +export const TitlebarMenu = () => { + const { menuStructure, menuItems, handleMenuItemPressed } = useMenuItem() + const [openMenu, setOpenMenu] = useState(null) + const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 }) + const menuRefs = useRef>(new Map()) + + const handleMenuClick = useCallback((menuName: string) => { + const menuRef = menuRefs.current.get(menuName) + if (!menuRef) return + menuRef.measureInWindow((x, y, _, height) => { + setDropdownPosition({ x, y: y + height }) + setOpenMenu((prev) => (prev === menuName ? null : menuName)) + }) + }, []) + + const handleMenuHover = useCallback( + (menuName: string) => { + if (openMenu === null || openMenu === menuName) return + handleMenuClick(menuName) + }, + [openMenu, handleMenuClick], + ) + + const handleClose = () => setOpenMenu(null) + + // TODO: Add hotkey handling + + return ( + + {menuStructure.map((menu) => ( + { + if (ref) menuRefs.current.set(menu.title, ref) + }} + > + handleMenuClick(menu.title)} + onHoverIn={() => handleMenuHover(menu.title)} + /> + + ))} + {openMenu && menuItems[openMenu] && ( + <> + {/* Single overlay for all menu interactions */} + + { + handleMenuItemPressed({ menuPath: [openMenu, item.label] }) + handleClose() + }} + /> + + )} + + ) +} + +const $menuBar = themed(() => ({ + flexDirection: "row", + alignItems: "center", + height: "100%", + paddingHorizontal: 4, +})) \ No newline at end of file diff --git a/app/components/Titlebar/TitlebarMenuItem.tsx b/app/components/Titlebar/TitlebarMenuItem.tsx new file mode 100644 index 0000000..2fd03ce --- /dev/null +++ b/app/components/Titlebar/TitlebarMenuItem.tsx @@ -0,0 +1,60 @@ +import { Pressable, Text, type TextStyle, type ViewStyle } from "react-native" +import { useCallback, useState } from "react" +import { themed } from "../../theme/theme" + +interface TitlebarMenuItemProps { + title: string + isOpen?: boolean + onPress: () => void + onHoverIn?: () => void + onHoverOut?: () => void +} + +export const TitlebarMenuItem = ({ title, isOpen, onPress, onHoverIn, onHoverOut }: TitlebarMenuItemProps) => { + const [isHovered, setIsHovered] = useState(false) + + const handleHover = useCallback((isHovered: boolean) => { + setIsHovered(isHovered) + if (isHovered) { + onHoverIn?.() + } else { + onHoverOut?.() + } + }, [onHoverIn, onHoverOut]) + + return ( + { + handleHover(true) + }} + onHoverOut={() => { + handleHover(false) + }} + style={({ pressed }) => [ + $menuItem(), + (pressed || isOpen || isHovered) && $menuItemHovered(), + ]} + > + {title} + + ) +} + +const $menuItem = themed(({ spacing }) => ({ + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: 4, + justifyContent: "center", +})) + +const $menuItemHovered = themed(({ colors }) => ({ + backgroundColor: colors.neutralVery, + opacity: 0.8, +})) + +const $menuItemText = themed(({ colors, typography }) => ({ + color: colors.mainText, + fontSize: typography.caption, + fontWeight: "400", +})) \ No newline at end of file From 3a1ae6696b951851dc7ba2e7eb7ca3967d4c023d Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 25 Sep 2025 17:00:51 -0400 Subject: [PATCH 15/40] More stable useGlobal for windows --- app/state/useGlobal.windows.ts | 141 +++++++++++++++++---------------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/app/state/useGlobal.windows.ts b/app/state/useGlobal.windows.ts index b13f1ac..edbc94b 100644 --- a/app/state/useGlobal.windows.ts +++ b/app/state/useGlobal.windows.ts @@ -1,87 +1,92 @@ -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react" +// globalStore.ts +import { useSyncExternalStore, useCallback } from "react"; +import { unstable_batchedUpdates } from "react-native"; -type UseGlobalOptions = Record +type Id = string; +type Listener = () => void; +type SetValue = T | ((prev: T) => T); -const globals: Record = {} -const components_to_rerender: Record>[]> = {} +const globals = new Map(); +const listeners = new Map>(); -type SetValueFn = (prev: T) => T -type SetValue = T | SetValueFn +// Initialize global value if it doesn't exist, but don't modify during snapshot reads +function initializeGlobal(id: Id, initialValue: T): void { + if (!globals.has(id)) { + globals.set(id, initialValue); + } +} + +function getSnapshot(id: Id): T { + return globals.get(id) as T; +} -/** - * Trying for the simplest possible global state management. - * Use anywhere and it'll share the same state globally, and rerender any component that uses it. - * - * const [value, setValue] = useGlobal("my-state", "initial-value") - * - */ -export function useGlobal( - id: string, - initialValue: T, - options: UseGlobalOptions = {}, -): [T, (value: SetValue) => void] { - // This is a dummy state to rerender any component that uses this global. - const [_v, setRender] = useState([]) +function subscribe(id: Id, cb: Listener): () => void { + let set = listeners.get(id); + if (!set) listeners.set(id, (set = new Set())); + set.add(cb); + return () => { + const s = listeners.get(id); + if (!s) return; + s.delete(cb); + if (s.size === 0) listeners.delete(id); + }; +} + +function notify(id: Id) { + const s = listeners.get(id); + if (!s || s.size === 0) return; + unstable_batchedUpdates(() => { + for (const l of s) l(); + }); +} - // Subscribe & unsubscribe from state changes for this ID. - useEffect(() => { - components_to_rerender[id] ||= [] - components_to_rerender[id].push(setRender) - return () => { - if (!components_to_rerender[id]) return - components_to_rerender[id] = components_to_rerender[id].filter( - (listener) => listener !== setRender, - ) - } - }, [id]) +export function useGlobal(id: Id, initialValue: T): [T, (v: SetValue) => void] { + // Initialize the global value once, outside of the snapshot function + initializeGlobal(id, initialValue); - // We use the withGlobal hook to do the actual work. - const [value] = withGlobal(id, initialValue, options) + const value = useSyncExternalStore( + (cb) => subscribe(id, cb), + () => getSnapshot(id) + ); - // We use a callback to ensure that the setValue function is stable. - const setValue = useCallback(buildSetValue(id), [id]) + // Memoize the setter function to prevent unnecessary re-renders + const set = useCallback((next: SetValue) => { + const current = getSnapshot(id); + const resolved = typeof next === "function" ? (next as (p: T) => T)(current) : next; + globals.set(id, resolved); + notify(id); + }, [id]); - return [value, setValue] + return [value, set]; } -/** - * For global state used outside of a component. Can be used in a component with - * the same id string, using useGlobal. - */ +// Imperative access (outside components) export function withGlobal( - id: string, - initialValue: T, - _: UseGlobalOptions = {}, -): [T, (value: SetValue | null) => void] { - // Initialize this global if it doesn't exist. - if (globals[id] === undefined) globals[id] = initialValue + id: Id, + initialValue: T +): [T, (v: SetValue | null) => void] { + // Initialize the global value + initializeGlobal(id, initialValue); - return [globals[id] as T, buildSetValue(id)] + const setter = (v: SetValue | null) => { + if (v === null) return resetGlobal(id); + const current = getSnapshot(id); + const resolved = typeof v === "function" ? (v as (p: T) => T)(current) : v; + globals.set(id, resolved); + notify(id); + }; + return [getSnapshot(id), setter]; } -function buildSetValue(id: string) { - return (value: SetValue | null) => { - // Call the setter function if it's a function. - if (typeof value === "function") value = (value as SetValueFn)(globals[id] as T) - if (value === null) { - delete globals[id] - } else { - globals[id] = value - } - components_to_rerender[id] ||= [] - components_to_rerender[id].forEach((rerender) => rerender([])) - } +export function resetGlobal(id: Id, rerender = true) { + globals.delete(id); + if (rerender) notify(id); } -/** - * Clear all globals and reset the storage entirely. - * Optionally rerender all components that use useGlobal. - */ -export function clearGlobals(rerender: boolean = true): void { - Object.keys(globals).forEach((key) => delete globals[key]) +export function clearGlobals(rerender = true) { + globals.clear(); if (rerender) { - Object.keys(components_to_rerender).forEach((key) => { - components_to_rerender[key].forEach((rerender) => rerender([])) - }) + // Only notify ids that have listeners; avoids stale maps + for (const id of listeners.keys()) notify(id); } } From fa870f66b1856ea52e7b040b854667e2654dcadc Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 25 Sep 2025 17:01:21 -0400 Subject: [PATCH 16/40] WIP: Add windows support for useMenuItem hook --- app/utils/useMenuItem.tsx | 167 ++++++++++++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 35 deletions(-) diff --git a/app/utils/useMenuItem.tsx b/app/utils/useMenuItem.tsx index dc094ec..133efbe 100644 --- a/app/utils/useMenuItem.tsx +++ b/app/utils/useMenuItem.tsx @@ -51,6 +51,8 @@ */ import { useEffect, useRef, useCallback, useState } from "react" +import { Platform } from "react-native" +import { useGlobal } from "../state/useGlobal" import NativeIRMenuItemManager, { type MenuItemPressedEvent, type MenuStructure, @@ -68,7 +70,8 @@ export interface MenuItem { shortcut?: string enabled?: boolean position?: number - action: () => void + action?: () => void // Make action optional for items with submenus + submenu?: MenuListEntry[] // Add submenu support } export interface MenuItemConfig { @@ -91,6 +94,21 @@ export function useMenuItem(config?: MenuItemConfig) { const previousConfigRef = useRef(null) const [availableMenus, setAvailableMenus] = useState([]) const [menuStructure, setMenuStructure] = useState([]) + const [windowsMenuItems, setWindowsMenuItems] = useState>({}) + + // Global state for Windows menu persistence + const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( + "windows-menu-config", + null, + ) + const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( + "windows-menu-structure", + [], + ) + const [globalMenuItems, setGlobalMenuItems] = useGlobal>( + "windows-menu-items", + {}, + ) const handleMenuItemPressed = useCallback((event: MenuItemPressedEvent) => { const key = joinPath(event.menuPath) @@ -100,16 +118,46 @@ export function useMenuItem(config?: MenuItemConfig) { const discoverMenus = useCallback(async () => { try { - const menus = NativeIRMenuItemManager.getAvailableMenus() - const structure = NativeIRMenuItemManager.getMenuStructure() - setAvailableMenus(menus) - setMenuStructure(structure) - return menus + if (Platform.OS === "windows") { + // For Windows, use global state + const configToUse = config || globalMenuConfig + if (configToUse?.items) { + const winStructure: MenuStructure = Object.keys(configToUse.items).map((title) => ({ + title, + enabled: true, + path: [title], + items: [], + children: [], + })) + + // Update global state if we have a config + if (config && config !== globalMenuConfig) { + setGlobalMenuConfig(config) + setGlobalMenuStructure(winStructure) + setGlobalMenuItems(config.items as Record) + } + + // Always use global state for consistency + setMenuStructure(globalMenuStructure.length > 0 ? globalMenuStructure : winStructure) + setWindowsMenuItems( + Object.keys(globalMenuItems).length > 0 + ? globalMenuItems + : (configToUse.items as Record), + ) + } + return [] + } else { + const menus = NativeIRMenuItemManager.getAvailableMenus() + const structure = NativeIRMenuItemManager.getMenuStructure() + setAvailableMenus(menus) + setMenuStructure(structure) + return menus + } } catch (error) { console.error("Failed to discover menus:", error) return [] } - }, []) + }, [config, globalMenuConfig, globalMenuStructure, globalMenuItems]) const addEntries = useCallback(async (parentKey: string, entries: MenuListEntry[]) => { const parentPath = parsePathKey(parentKey) @@ -248,49 +296,78 @@ export function useMenuItem(config?: MenuItemConfig) { const updateMenus = async () => { if (!config) return - const previousConfig = previousConfigRef.current - - if (config.remove?.length) { - for (const entry of config.remove) { - await removeMenuItemByName(entry) + if (Platform.OS === "windows") { + // For Windows, update global state and action map + if (config.items) { + // Store actions in actionsRef + for (const [parentKey, entries] of Object.entries(config.items)) { + for (const entry of entries) { + if (!isSeparator(entry)) { + const item = entry as MenuItem + const leafPath = [parentKey, item.label] + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } + } + } + } + // Update global state + setGlobalMenuConfig(config) + } + previousConfigRef.current = config + await discoverMenus() + } else { + // Original macOS logic + const previousConfig = previousConfigRef.current + + if (config.remove?.length) { + for (const entry of config.remove) { + await removeMenuItemByName(entry) + } } - } - if (config.items) { - for (const [parentKey, entries] of Object.entries(config.items)) { - const previousEntries = previousConfig?.items?.[parentKey] || [] - const { toRemove, toUpdate } = getItemDifference(previousEntries, entries) + if (config.items) { + for (const [parentKey, entries] of Object.entries(config.items)) { + const previousEntries = previousConfig?.items?.[parentKey] || [] + const { toRemove, toUpdate } = getItemDifference(previousEntries, entries) - if (toRemove.length) await removeMenuItems(parentKey, toRemove) + if (toRemove.length) await removeMenuItems(parentKey, toRemove) - await addEntries(parentKey, entries) + await addEntries(parentKey, entries) - for (const item of toUpdate) { - const leafPath = [...parsePathKey(parentKey), item.label] - actionsRef.current.set(joinPath(leafPath), item.action) - if (item.enabled !== undefined) { - try { - await NativeIRMenuItemManager.setMenuItemEnabledAtPath(leafPath, item.enabled) - } catch (e) { - console.error(`Failed to update ${joinPath(leafPath)}:`, e) + for (const item of toUpdate) { + const leafPath = [...parsePathKey(parentKey), item.label] + actionsRef.current.set(joinPath(leafPath), item.action) + if (item.enabled !== undefined) { + try { + await NativeIRMenuItemManager.setMenuItemEnabledAtPath(leafPath, item.enabled) + } catch (e) { + console.error(`Failed to update ${joinPath(leafPath)}:`, e) + } } } } } - } - previousConfigRef.current = config - await discoverMenus() + previousConfigRef.current = config + await discoverMenus() + } } updateMenus() }, [config, addEntries, removeMenuItems, getItemDifference]) useEffect(() => { - const subscription = NativeIRMenuItemManager.onMenuItemPressed(handleMenuItemPressed) - discoverMenus() - return () => { - subscription.remove() + if (Platform.OS === "windows") { + // For Windows, just discover menus from config + discoverMenus() + } else { + // For macOS, use native menu manager + const subscription = NativeIRMenuItemManager.onMenuItemPressed(handleMenuItemPressed) + discoverMenus() + return () => { + subscription.remove() + } } }, [handleMenuItemPressed, discoverMenus]) @@ -337,13 +414,33 @@ export function useMenuItem(config?: MenuItemConfig) { [addEntries], ) + // For Windows, populate actions from global state if no config provided + useEffect(() => { + if (Platform.OS === "windows" && !config && globalMenuConfig?.items) { + // Restore actions from global config + for (const [parentKey, entries] of Object.entries(globalMenuConfig.items)) { + for (const entry of entries) { + if (!isSeparator(entry)) { + const item = entry as MenuItem + const leafPath = [parentKey, item.label] + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } + } + } + } + } + }, [config, globalMenuConfig]) + return { availableMenus, - menuStructure, + menuStructure: Platform.OS === "windows" ? globalMenuStructure : menuStructure, + menuItems: Platform.OS === "windows" ? globalMenuItems : {}, discoverMenus, addMenuItem, removeMenuItemByName, setMenuItemEnabled, getAllMenuPaths, + handleMenuItemPressed, } } From 0745644b8e8900714689da30564ef1ae688d0e12 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 09:19:51 -0400 Subject: [PATCH 17/40] Rename IRMenuItemManager to IRSystemMenuManager for clarity --- app/app.tsx | 4 +- app/components/Menu/MenuDropdown.tsx | 13 +-- app/components/Menu/MenuDropdownItem.tsx | 2 +- app/components/Menu/types.ts | 14 ++- app/components/Titlebar/TitlebarMenu.tsx | 4 +- .../IRMenuItemManager/IRMenuItemManager.h | 8 -- .../IRSystemMenuManager/IRSystemMenuManager.h | 8 ++ .../IRSystemMenuManager.mm} | 6 +- .../IRSystemMenuManager.windows.cpp} | 8 +- .../IRSystemMenuManager.windows.h} | 12 +- .../NativeIRSystemMenuManager.ts} | 24 ++-- .../{useMenuItem.tsx => useSystemMenu.tsx} | 110 +++++++++--------- windows/reactotron/IRNativeModules.g.h | 2 +- 13 files changed, 107 insertions(+), 108 deletions(-) delete mode 100644 app/native/IRMenuItemManager/IRMenuItemManager.h create mode 100644 app/native/IRSystemMenuManager/IRSystemMenuManager.h rename app/native/{IRMenuItemManager/IRMenuItemManager.mm => IRSystemMenuManager/IRSystemMenuManager.mm} (98%) rename app/native/{IRMenuItemManager/IRMenuItemManager.windows.cpp => IRSystemMenuManager/IRSystemMenuManager.windows.cpp} (78%) rename app/native/{IRMenuItemManager/IRMenuItemManager.windows.h => IRSystemMenuManager/IRSystemMenuManager.windows.h} (57%) rename app/native/{IRMenuItemManager/NativeIRMenuItemManager.ts => IRSystemMenuManager/NativeIRSystemMenuManager.ts} (71%) rename app/utils/{useMenuItem.tsx => useSystemMenu.tsx} (77%) diff --git a/app/app.tsx b/app/app.tsx index 17bdb65..0b5378a 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -9,7 +9,7 @@ import { connectToServer } from "./state/connectToServer" 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" import { Titlebar } from "./components/Titlebar/Titlebar" import { Sidebar } from "./components/Sidebar/Sidebar" import { useSidebar } from "./state/useSidebar" @@ -101,7 +101,7 @@ function App(): React.JSX.Element { [toggleSidebar], ) - useMenuItem(menuConfig) + useSystemMenu(menuConfig) setTimeout(() => { fetch("https://www.google.com") diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx index fbc4feb..19e998a 100644 --- a/app/components/Menu/MenuDropdown.tsx +++ b/app/components/Menu/MenuDropdown.tsx @@ -5,17 +5,12 @@ import { Portal } from "../Portal" import { MenuDropdownItem } from "./MenuDropdownItem" import { useSubmenuState } from "./useSubmenuState" import { menuSettings } from "./menuSettings" -import { type Position } from "./types" -import { MenuItem, SEPARATOR } from "../../utils/useMenuItem" +import { type Position, type DropdownMenuItem, type MenuItem, MENU_SEPARATOR } from "./types" import { getUUID } from "../../utils/random/getUUID" import { Separator } from "../Separator" -type DropdownMenuItem = MenuItem & { - submenu?: (DropdownMenuItem | typeof SEPARATOR)[] -} - interface MenuDropdownProps { - items: (DropdownMenuItem | typeof SEPARATOR)[] + items: (DropdownMenuItem | typeof MENU_SEPARATOR)[] position: Position onItemPress: (item: MenuItem) => void isSubmenu?: boolean @@ -32,8 +27,8 @@ const MenuDropdownComponent = ({ ).current const { openSubmenu, submenuPosition, handleItemHover } = useSubmenuState(position) - const isSeparator = (item: MenuItem | typeof SEPARATOR): item is typeof SEPARATOR => { - return item === SEPARATOR + const isSeparator = (item: MenuItem | typeof MENU_SEPARATOR): item is typeof MENU_SEPARATOR => { + return item === MENU_SEPARATOR } // Find the submenu item if one is open diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index b498f29..8628f1f 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -2,7 +2,7 @@ import { Pressable, Text, View, type ViewStyle, type TextStyle } from "react-nat import { useState, useRef, memo, useCallback } from "react" import { themed } from "../../theme/theme" import { menuSettings } from "./menuSettings" -import type { MenuItem } from "../../utils/useMenuItem" +import type { MenuItem } from "./types" interface MenuDropdownItemProps { item: MenuItem diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts index b9e0e96..486ef78 100644 --- a/app/components/Menu/types.ts +++ b/app/components/Menu/types.ts @@ -3,11 +3,17 @@ export interface Position { y: number } -export interface MenuItemWithSubmenu { +// Generic menu item interface for UI components +export interface MenuItem { label: string shortcut?: string enabled?: boolean - position?: number action?: () => void - submenu?: (MenuItemWithSubmenu | 'menu-item-separator')[] -} \ No newline at end of file + 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 \ No newline at end of file diff --git a/app/components/Titlebar/TitlebarMenu.tsx b/app/components/Titlebar/TitlebarMenu.tsx index aff8c1c..ee446e3 100644 --- a/app/components/Titlebar/TitlebarMenu.tsx +++ b/app/components/Titlebar/TitlebarMenu.tsx @@ -6,10 +6,10 @@ import { MenuDropdown } from "../Menu/MenuDropdown" import { MenuOverlay } from "../Menu/MenuOverlay" import type { Position } from "../Menu/types" import { PassthroughView } from "./PassthroughView" -import { useMenuItem } from "../../utils/useMenuItem" +import { useSystemMenu } from "../../utils/useSystemMenu" export const TitlebarMenu = () => { - const { menuStructure, menuItems, handleMenuItemPressed } = useMenuItem() + const { menuStructure, menuItems, handleMenuItemPressed } = useSystemMenu() const [openMenu, setOpenMenu] = useState(null) const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 }) const menuRefs = useRef>(new Map()) diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.h b/app/native/IRMenuItemManager/IRMenuItemManager.h deleted file mode 100644 index c042f99..0000000 --- a/app/native/IRMenuItemManager/IRMenuItemManager.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface IRMenuItemManager : NativeIRMenuItemManagerSpecBase -@end - -NS_ASSUME_NONNULL_END diff --git a/app/native/IRSystemMenuManager/IRSystemMenuManager.h b/app/native/IRSystemMenuManager/IRSystemMenuManager.h new file mode 100644 index 0000000..6c379d1 --- /dev/null +++ b/app/native/IRSystemMenuManager/IRSystemMenuManager.h @@ -0,0 +1,8 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IRSystemMenuManager : NativeIRSystemMenuManagerSpecBase +@end + +NS_ASSUME_NONNULL_END diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.mm b/app/native/IRSystemMenuManager/IRSystemMenuManager.mm similarity index 98% rename from app/native/IRMenuItemManager/IRMenuItemManager.mm rename to app/native/IRSystemMenuManager/IRSystemMenuManager.mm index 82df0bd..e2610e6 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.mm +++ b/app/native/IRSystemMenuManager/IRSystemMenuManager.mm @@ -1,16 +1,16 @@ -#import "IRMenuItemManager.h" +#import "IRSystemMenuManager.h" #import #import static NSString * const separatorString = @"menu-item-separator"; -@implementation IRMenuItemManager { +@implementation IRSystemMenuManager { } RCT_EXPORT_MODULE() - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + return std::make_shared(params); } #pragma mark - API diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.cpp similarity index 78% rename from app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp rename to app/native/IRSystemMenuManager/IRSystemMenuManager.windows.cpp index 2df05f9..85220ee 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp +++ b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.cpp @@ -1,18 +1,18 @@ // -// IRMenuItemManager.cpp +// IRSystemMenuManager.cpp // Reactotron-Windows // // Windows TurboModule implementation of menu item management // #include "pch.h" -#include "IRMenuItemManager.windows.h" +#include "IRSystemMenuManager.windows.h" -using winrt::reactotron::implementation::IRMenuItemManager; +using winrt::reactotron::implementation::IRSystemMenuManager; namespace winrt::reactotron::implementation { - void IRMenuItemManager::createMenu(std::string menuName, + void IRSystemMenuManager::createMenu(std::string menuName, ::React::ReactPromise &&result) noexcept { // THE PROBLEM: onMenuItemPressed is nullptr/undefined at runtime diff --git a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h similarity index 57% rename from app/native/IRMenuItemManager/IRMenuItemManager.windows.h rename to app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h index e15daf9..88018f3 100644 --- a/app/native/IRMenuItemManager/IRMenuItemManager.windows.h +++ b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h @@ -4,17 +4,17 @@ #include // Generated (DataTypes before Spec) -#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerDataTypes.g.h" -#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerSpec.g.h" +#include "..\..\..\windows\reactotron\codegen\NativeIRSystemMenuManagerDataTypes.g.h" +#include "..\..\..\windows\reactotron\codegen\NativeIRSystemMenuManagerSpec.g.h" namespace winrt::reactotron::implementation { - REACT_TURBO_MODULE(IRMenuItemManager) - struct IRMenuItemManager : reactotronCodegen::IRMenuItemManagerSpec + REACT_TURBO_MODULE(IRSystemMenuManager) + struct IRSystemMenuManager : reactotronCodegen::IRSystemMenuManagerSpec { // Only the essential types needed for the event - using PressEvent = reactotronCodegen::IRMenuItemManagerSpec_MenuItemPressedEvent; - using CreateRet = reactotronCodegen::IRMenuItemManagerSpec_createMenu_returnType; + using PressEvent = reactotronCodegen::IRSystemMenuManagerSpec_MenuItemPressedEvent; + using CreateRet = reactotronCodegen::IRSystemMenuManagerSpec_createMenu_returnType; // One simple method to test event emission REACT_METHOD(createMenu) diff --git a/app/native/IRMenuItemManager/NativeIRMenuItemManager.ts b/app/native/IRSystemMenuManager/NativeIRSystemMenuManager.ts similarity index 71% rename from app/native/IRMenuItemManager/NativeIRMenuItemManager.ts rename to app/native/IRSystemMenuManager/NativeIRSystemMenuManager.ts index fa17dfe..af59d58 100644 --- a/app/native/IRMenuItemManager/NativeIRMenuItemManager.ts +++ b/app/native/IRSystemMenuManager/NativeIRSystemMenuManager.ts @@ -6,41 +6,41 @@ import { TurboModuleRegistry } from "react-native" export const SEPARATOR = "menu-item-separator" as const // Path shape: ["View", "Zen Mode"] -export interface MenuItemPressedEvent { +export interface SystemMenuItemPressedEvent { menuPath: string[] } // Native -> JS: Tree node describing a menu item returned by getMenuStructure() -export interface MenuNode { +export interface SystemMenuNode { title: string enabled: boolean path: string[] // TODO: This creates an infinite loop when building for windows - // children?: MenuNode[] + // children?: SystemMenuNode[] children?: any } // Native -> JS: Top-level entry from getMenuStructure() -export interface MenuEntry { +export interface SystemMenuEntry { title: string - items: MenuNode[] + items: SystemMenuNode[] } -export type MenuStructure = MenuEntry[] +export type SystemMenuStructure = SystemMenuEntry[] -// JS -> Native: For building menu -export interface MenuItem { +// JS -> Native: For building menu (legacy - use SystemMenuItem instead) +export interface SystemNativeMenuItem { label: string shortcut?: string enabled?: boolean action: () => void } -export type MenuListEntry = MenuItem | typeof SEPARATOR +export type SystemMenuListEntry = SystemNativeMenuItem | typeof SEPARATOR export interface Spec extends TurboModule { getAvailableMenus(): string[] - getMenuStructure(): MenuStructure + getMenuStructure(): SystemMenuStructure createMenu(menuName: string): Promise<{ success: boolean; existed: boolean; menuName: string }> addMenuItemAtPath( parentPath: string[], @@ -60,7 +60,7 @@ export interface Spec extends TurboModule { path: string[], enabled: boolean, ): Promise<{ success: boolean; error?: string }> - readonly onMenuItemPressed: EventEmitter + readonly onMenuItemPressed: EventEmitter } -export default TurboModuleRegistry.getEnforcing("IRMenuItemManager") +export default TurboModuleRegistry.getEnforcing("IRSystemMenuManager") diff --git a/app/utils/useMenuItem.tsx b/app/utils/useSystemMenu.tsx similarity index 77% rename from app/utils/useMenuItem.tsx rename to app/utils/useSystemMenu.tsx index 133efbe..a61a74a 100644 --- a/app/utils/useMenuItem.tsx +++ b/app/utils/useSystemMenu.tsx @@ -53,29 +53,30 @@ import { useEffect, useRef, useCallback, useState } from "react" import { Platform } from "react-native" import { useGlobal } from "../state/useGlobal" -import NativeIRMenuItemManager, { - type MenuItemPressedEvent, - type MenuStructure, - type MenuListEntry, +import NativeIRSystemMenuManager, { + type SystemMenuItemPressedEvent, + type SystemMenuStructure, SEPARATOR, -} from "../native/IRMenuItemManager/NativeIRMenuItemManager" +} from "../native/IRSystemMenuManager/NativeIRSystemMenuManager" // Only thing to configure here is the path separator. const PATH_SEPARATOR = " > " export { SEPARATOR } // Menu separator -export interface MenuItem { +export interface SystemMenuItem { label: string shortcut?: string enabled?: boolean position?: number action?: () => void // Make action optional for items with submenus - submenu?: MenuListEntry[] // Add submenu support + submenu?: SystemMenuListEntry[] // Add submenu support } -export interface MenuItemConfig { - items?: Record +export type SystemMenuListEntry = SystemMenuItem | typeof SEPARATOR + +export interface SystemMenuConfig { + items?: Record remove?: string[] } @@ -87,30 +88,29 @@ const parsePathKey = (key: string): string[] => const joinPath = (p: string[]) => p.join(PATH_SEPARATOR) -const isSeparator = (e: MenuListEntry): e is typeof SEPARATOR => e === SEPARATOR +const isSeparator = (e: SystemMenuListEntry): e is typeof SEPARATOR => e === SEPARATOR -export function useMenuItem(config?: MenuItemConfig) { +export function useSystemMenu(config?: SystemMenuConfig) { const actionsRef = useRef void>>(new Map()) - const previousConfigRef = useRef(null) + const previousConfigRef = useRef(null) const [availableMenus, setAvailableMenus] = useState([]) - const [menuStructure, setMenuStructure] = useState([]) - const [windowsMenuItems, setWindowsMenuItems] = useState>({}) + const [menuStructure, setMenuStructure] = useState([]) // Global state for Windows menu persistence - const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( + const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( "windows-menu-config", null, ) - const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( + const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( "windows-menu-structure", [], ) - const [globalMenuItems, setGlobalMenuItems] = useGlobal>( + const [globalMenuItems, setGlobalMenuItems] = useGlobal>( "windows-menu-items", {}, ) - const handleMenuItemPressed = useCallback((event: MenuItemPressedEvent) => { + const handleMenuItemPressed = useCallback((event: SystemMenuItemPressedEvent) => { const key = joinPath(event.menuPath) const action = actionsRef.current.get(key) if (action) action() @@ -122,7 +122,7 @@ export function useMenuItem(config?: MenuItemConfig) { // For Windows, use global state const configToUse = config || globalMenuConfig if (configToUse?.items) { - const winStructure: MenuStructure = Object.keys(configToUse.items).map((title) => ({ + const winStructure: SystemMenuStructure = Object.keys(configToUse.items).map((title) => ({ title, enabled: true, path: [title], @@ -134,21 +134,16 @@ export function useMenuItem(config?: MenuItemConfig) { if (config && config !== globalMenuConfig) { setGlobalMenuConfig(config) setGlobalMenuStructure(winStructure) - setGlobalMenuItems(config.items as Record) + setGlobalMenuItems(config.items as Record) } // Always use global state for consistency setMenuStructure(globalMenuStructure.length > 0 ? globalMenuStructure : winStructure) - setWindowsMenuItems( - Object.keys(globalMenuItems).length > 0 - ? globalMenuItems - : (configToUse.items as Record), - ) } return [] } else { - const menus = NativeIRMenuItemManager.getAvailableMenus() - const structure = NativeIRMenuItemManager.getMenuStructure() + const menus = NativeIRSystemMenuManager.getAvailableMenus() + const structure = NativeIRSystemMenuManager.getMenuStructure() setAvailableMenus(menus) setMenuStructure(structure) return menus @@ -159,11 +154,11 @@ export function useMenuItem(config?: MenuItemConfig) { } }, [config, globalMenuConfig, globalMenuStructure, globalMenuItems]) - const addEntries = useCallback(async (parentKey: string, entries: MenuListEntry[]) => { + const addEntries = useCallback(async (parentKey: string, entries: SystemMenuListEntry[]) => { const parentPath = parsePathKey(parentKey) try { - await NativeIRMenuItemManager.removeMenuItemAtPath([...parentPath, SEPARATOR]) + await NativeIRSystemMenuManager.removeMenuItemAtPath([...parentPath, SEPARATOR]) } catch (e) { console.warn(`Failed to clear separators for "${parentKey}":`, e) } @@ -171,28 +166,29 @@ export function useMenuItem(config?: MenuItemConfig) { for (const entry of entries) { if (isSeparator(entry)) { try { - await NativeIRMenuItemManager.addMenuItemAtPath(parentPath, SEPARATOR, "") + await NativeIRSystemMenuManager.addMenuItemAtPath(parentPath, SEPARATOR, "") } catch (e) { console.error(`Failed to add separator under "${parentKey}":`, e) } continue } - const item = entry as MenuItem + const item = entry as SystemMenuItem const leafPath = [...parentPath, item.label] const actionKey = joinPath(leafPath) - actionsRef.current.set(actionKey, item.action) + + if (item.action) actionsRef.current.set(actionKey, item.action) try { if (typeof item.position === "number") { - await NativeIRMenuItemManager.insertMenuItemAtPath( + await NativeIRSystemMenuManager.insertMenuItemAtPath( parentPath, item.label, item.position, item.shortcut ?? "", ) } else { - await NativeIRMenuItemManager.addMenuItemAtPath( + await NativeIRSystemMenuManager.addMenuItemAtPath( parentPath, item.label, item.shortcut ?? "", @@ -200,7 +196,7 @@ export function useMenuItem(config?: MenuItemConfig) { } if (item.enabled !== undefined) { - await NativeIRMenuItemManager.setMenuItemEnabledAtPath(leafPath, item.enabled) + await NativeIRSystemMenuManager.setMenuItemEnabledAtPath(leafPath, item.enabled) } } catch (error) { console.error(`Failed to add "${item.label}" under "${parentKey}":`, error) @@ -208,12 +204,12 @@ export function useMenuItem(config?: MenuItemConfig) { } }, []) - const removeMenuItems = useCallback(async (parentKey: string, items: MenuItem[]) => { + const removeMenuItems = useCallback(async (parentKey: string, items: SystemMenuItem[]) => { const parentPath = parsePathKey(parentKey) for (const item of items) { const leafPath = [...parentPath, item.label] try { - await NativeIRMenuItemManager.removeMenuItemAtPath(leafPath) + await NativeIRSystemMenuManager.removeMenuItemAtPath(leafPath) } catch (error) { console.error(`Failed to remove menu item ${joinPath(leafPath)}:`, error) } finally { @@ -225,7 +221,7 @@ export function useMenuItem(config?: MenuItemConfig) { const removeMenuItemByName = useCallback(async (nameOrPath: string) => { const path = parsePathKey(nameOrPath) try { - await NativeIRMenuItemManager.removeMenuItemAtPath(path) + await NativeIRSystemMenuManager.removeMenuItemAtPath(path) actionsRef.current.delete(joinPath(path)) } catch (error) { console.error(`Failed to remove menu item/menu ${nameOrPath}:`, error) @@ -235,7 +231,7 @@ export function useMenuItem(config?: MenuItemConfig) { const setMenuItemEnabled = useCallback(async (pathOrKey: string | string[], enabled: boolean) => { const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) try { - await NativeIRMenuItemManager.setMenuItemEnabledAtPath(path, enabled) + await NativeIRSystemMenuManager.setMenuItemEnabledAtPath(path, enabled) } catch (error) { console.error(`Failed to set enabled for ${joinPath(path)}:`, error) } @@ -243,7 +239,7 @@ export function useMenuItem(config?: MenuItemConfig) { const getAllMenuPaths = useCallback(async (): Promise => { try { - const structure = NativeIRMenuItemManager.getMenuStructure() + const structure = NativeIRSystemMenuManager.getMenuStructure() const out: string[] = [] const walk = (nodes?: any[]) => { if (!nodes) return @@ -261,16 +257,16 @@ export function useMenuItem(config?: MenuItemConfig) { }, []) const getItemDifference = useCallback( - (oldEntries: MenuListEntry[] = [], newEntries: MenuListEntry[] = []) => { - const oldItems = oldEntries.filter((e): e is MenuItem => !isSeparator(e)) - const newItems = newEntries.filter((e): e is MenuItem => !isSeparator(e)) + (oldEntries: SystemMenuListEntry[] = [], newEntries: SystemMenuListEntry[] = []) => { + const oldItems = oldEntries.filter((e): e is SystemMenuItem => !isSeparator(e)) + const newItems = newEntries.filter((e): e is SystemMenuItem => !isSeparator(e)) - const byLabel = (xs: MenuItem[]) => new Map(xs.map((x) => [x.label, x])) + const byLabel = (xs: SystemMenuItem[]) => new Map(xs.map((x) => [x.label, x])) const oldMap = byLabel(oldItems) const newMap = byLabel(newItems) - const toRemove: MenuItem[] = [] - const toUpdate: MenuItem[] = [] + const toRemove: SystemMenuItem[] = [] + const toUpdate: SystemMenuItem[] = [] for (const [label, item] of newMap) { if (oldMap.has(label)) { @@ -303,7 +299,7 @@ export function useMenuItem(config?: MenuItemConfig) { for (const [parentKey, entries] of Object.entries(config.items)) { for (const entry of entries) { if (!isSeparator(entry)) { - const item = entry as MenuItem + const item = entry as SystemMenuItem const leafPath = [parentKey, item.label] if (item.action) { actionsRef.current.set(joinPath(leafPath), item.action) @@ -337,10 +333,12 @@ export function useMenuItem(config?: MenuItemConfig) { for (const item of toUpdate) { const leafPath = [...parsePathKey(parentKey), item.label] - actionsRef.current.set(joinPath(leafPath), item.action) + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } if (item.enabled !== undefined) { try { - await NativeIRMenuItemManager.setMenuItemEnabledAtPath(leafPath, item.enabled) + await NativeIRSystemMenuManager.setMenuItemEnabledAtPath(leafPath, item.enabled) } catch (e) { console.error(`Failed to update ${joinPath(leafPath)}:`, e) } @@ -363,7 +361,7 @@ export function useMenuItem(config?: MenuItemConfig) { discoverMenus() } else { // For macOS, use native menu manager - const subscription = NativeIRMenuItemManager.onMenuItemPressed(handleMenuItemPressed) + const subscription = NativeIRSystemMenuManager.onMenuItemPressed(handleMenuItemPressed) discoverMenus() return () => { subscription.remove() @@ -380,22 +378,22 @@ export function useMenuItem(config?: MenuItemConfig) { const pairs = Object.entries(previousConfigRef.current.items ?? config.items) const cleanup = async () => { for (const [parentKey, entries] of pairs) { - const itemsOnly = entries.filter((e): e is MenuItem => !isSeparator(e)) + const itemsOnly = entries.filter((e): e is SystemMenuItem => !isSeparator(e)) await removeMenuItems(parentKey, itemsOnly) - await NativeIRMenuItemManager.removeMenuItemAtPath([ + await NativeIRSystemMenuManager.removeMenuItemAtPath([ ...parsePathKey(parentKey), SEPARATOR, ]) const parentPath = parsePathKey(parentKey) if (parentPath.length === 1) { const top = parentPath[0] - const structure = NativeIRMenuItemManager.getMenuStructure() + const structure = NativeIRSystemMenuManager.getMenuStructure() const entry = structure.find( (e) => e.title.localeCompare(top, undefined, { sensitivity: "accent" }) === 0, ) if (!entry || !entry.items || entry.items.length === 0) { try { - await NativeIRMenuItemManager.removeMenuItemAtPath([top]) + await NativeIRSystemMenuManager.removeMenuItemAtPath([top]) } catch (e) { console.warn(`Couldn't remove top-level menu "${top}":`, e) } @@ -408,7 +406,7 @@ export function useMenuItem(config?: MenuItemConfig) { }, []) const addMenuItem = useCallback( - async (parentKey: string, item: MenuItem) => { + async (parentKey: string, item: SystemMenuItem) => { await addEntries(parentKey, [item]) }, [addEntries], @@ -421,7 +419,7 @@ export function useMenuItem(config?: MenuItemConfig) { for (const [parentKey, entries] of Object.entries(globalMenuConfig.items)) { for (const entry of entries) { if (!isSeparator(entry)) { - const item = entry as MenuItem + const item = entry as SystemMenuItem const leafPath = [parentKey, item.label] if (item.action) { actionsRef.current.set(joinPath(leafPath), item.action) diff --git a/windows/reactotron/IRNativeModules.g.h b/windows/reactotron/IRNativeModules.g.h index 867c32b..4c3745f 100644 --- a/windows/reactotron/IRNativeModules.g.h +++ b/windows/reactotron/IRNativeModules.g.h @@ -10,10 +10,10 @@ #include "../../app/native/IRClipboard/IRClipboard.windows.h" #include "../../app/native/IRFontList/IRFontList.windows.h" #include "../../app/native/IRKeyboard/IRKeyboard.windows.h" -#include "../../app/native/IRMenuItemManager/IRMenuItemManager.windows.h" #include "../../app/native/IRPassthroughView/IRPassthroughView.windows.h" #include "../../app/native/IRRunShellCommand/IRRunShellCommand.windows.h" #include "../../app/native/IRSystemInfo/IRSystemInfo.windows.h" +#include "../../app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h" #include "../../app/native/IRTabComponentView/IRTabComponentView.windows.h" #include "../../app/utils/experimental/IRExperimental.windows.h" #include "../../app/utils/random/IRRandom.windows.h" From e3cc8ad19ec003cc6a0b0d77c95d6c98ac2cbb25 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 09:46:15 -0400 Subject: [PATCH 18/40] Fix native types --- app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h index 88018f3..32edc65 100644 --- a/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h +++ b/app/native/IRSystemMenuManager/IRSystemMenuManager.windows.h @@ -13,7 +13,7 @@ namespace winrt::reactotron::implementation struct IRSystemMenuManager : reactotronCodegen::IRSystemMenuManagerSpec { // Only the essential types needed for the event - using PressEvent = reactotronCodegen::IRSystemMenuManagerSpec_MenuItemPressedEvent; + using PressEvent = reactotronCodegen::IRSystemMenuManagerSpec_SystemMenuItemPressedEvent; using CreateRet = reactotronCodegen::IRSystemMenuManagerSpec_createMenu_returnType; // One simple method to test event emission From 210bc9a56ea95224666ebad4fb873a10e445dabf Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 09:46:31 -0400 Subject: [PATCH 19/40] Break out platform implementations --- app/utils/useSystemMenu/types.ts | 21 ++ .../useSystemMenu.macos.ts} | 218 ++++----------- .../useSystemMenu/useSystemMenu.windows.ts | 256 ++++++++++++++++++ app/utils/useSystemMenu/utils.ts | 13 + 4 files changed, 344 insertions(+), 164 deletions(-) create mode 100644 app/utils/useSystemMenu/types.ts rename app/utils/{useSystemMenu.tsx => useSystemMenu/useSystemMenu.macos.ts} (60%) create mode 100644 app/utils/useSystemMenu/useSystemMenu.windows.ts create mode 100644 app/utils/useSystemMenu/utils.ts diff --git a/app/utils/useSystemMenu/types.ts b/app/utils/useSystemMenu/types.ts new file mode 100644 index 0000000..19f3871 --- /dev/null +++ b/app/utils/useSystemMenu/types.ts @@ -0,0 +1,21 @@ +export { SEPARATOR } from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" +export type { + SystemMenuItemPressedEvent, + SystemMenuStructure, +} from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" + +export interface SystemMenuItem { + label: string + shortcut?: string + enabled?: boolean + position?: number + action?: () => void + submenu?: SystemMenuListEntry[] +} + +export type SystemMenuListEntry = SystemMenuItem | typeof SEPARATOR + +export interface SystemMenuConfig { + items?: Record + remove?: string[] +} \ No newline at end of file diff --git a/app/utils/useSystemMenu.tsx b/app/utils/useSystemMenu/useSystemMenu.macos.ts similarity index 60% rename from app/utils/useSystemMenu.tsx rename to app/utils/useSystemMenu/useSystemMenu.macos.ts index a61a74a..d6ae931 100644 --- a/app/utils/useSystemMenu.tsx +++ b/app/utils/useSystemMenu/useSystemMenu.macos.ts @@ -42,7 +42,7 @@ * removeMenuItemByName, * setMenuItemEnabled, * getAllMenuPaths - * } = useMenuItem() + * } = useSystemMenu() * * useEffect(() => { * removeMenuItemByName("Format") @@ -51,44 +51,16 @@ */ import { useEffect, useRef, useCallback, useState } from "react" -import { Platform } from "react-native" -import { useGlobal } from "../state/useGlobal" -import NativeIRSystemMenuManager, { +import NativeIRSystemMenuManager from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" +import { + SEPARATOR, + type SystemMenuItem, + type SystemMenuConfig, + type SystemMenuListEntry, type SystemMenuItemPressedEvent, type SystemMenuStructure, - SEPARATOR, -} from "../native/IRSystemMenuManager/NativeIRSystemMenuManager" - -// Only thing to configure here is the path separator. -const PATH_SEPARATOR = " > " - -export { SEPARATOR } // Menu separator - -export interface SystemMenuItem { - label: string - shortcut?: string - enabled?: boolean - position?: number - action?: () => void // Make action optional for items with submenus - submenu?: SystemMenuListEntry[] // Add submenu support -} - -export type SystemMenuListEntry = SystemMenuItem | typeof SEPARATOR - -export interface SystemMenuConfig { - items?: Record - remove?: string[] -} - -const parsePathKey = (key: string): string[] => - key - .split(PATH_SEPARATOR) - .map((s) => s.trim()) - .filter(Boolean) - -const joinPath = (p: string[]) => p.join(PATH_SEPARATOR) - -const isSeparator = (e: SystemMenuListEntry): e is typeof SEPARATOR => e === SEPARATOR +} from "./types" +import { parsePathKey, joinPath, isSeparator } from "./utils" export function useSystemMenu(config?: SystemMenuConfig) { const actionsRef = useRef void>>(new Map()) @@ -96,20 +68,6 @@ export function useSystemMenu(config?: SystemMenuConfig) { const [availableMenus, setAvailableMenus] = useState([]) const [menuStructure, setMenuStructure] = useState([]) - // Global state for Windows menu persistence - const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( - "windows-menu-config", - null, - ) - const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( - "windows-menu-structure", - [], - ) - const [globalMenuItems, setGlobalMenuItems] = useGlobal>( - "windows-menu-items", - {}, - ) - const handleMenuItemPressed = useCallback((event: SystemMenuItemPressedEvent) => { const key = joinPath(event.menuPath) const action = actionsRef.current.get(key) @@ -118,45 +76,21 @@ export function useSystemMenu(config?: SystemMenuConfig) { const discoverMenus = useCallback(async () => { try { - if (Platform.OS === "windows") { - // For Windows, use global state - const configToUse = config || globalMenuConfig - if (configToUse?.items) { - const winStructure: SystemMenuStructure = Object.keys(configToUse.items).map((title) => ({ - title, - enabled: true, - path: [title], - items: [], - children: [], - })) - - // Update global state if we have a config - if (config && config !== globalMenuConfig) { - setGlobalMenuConfig(config) - setGlobalMenuStructure(winStructure) - setGlobalMenuItems(config.items as Record) - } - - // Always use global state for consistency - setMenuStructure(globalMenuStructure.length > 0 ? globalMenuStructure : winStructure) - } - return [] - } else { - const menus = NativeIRSystemMenuManager.getAvailableMenus() - const structure = NativeIRSystemMenuManager.getMenuStructure() - setAvailableMenus(menus) - setMenuStructure(structure) - return menus - } + const menus = NativeIRSystemMenuManager.getAvailableMenus() + const structure = NativeIRSystemMenuManager.getMenuStructure() + setAvailableMenus(menus) + setMenuStructure(structure) + return menus } catch (error) { console.error("Failed to discover menus:", error) return [] } - }, [config, globalMenuConfig, globalMenuStructure, globalMenuItems]) + }, []) const addEntries = useCallback(async (parentKey: string, entries: SystemMenuListEntry[]) => { const parentPath = parsePathKey(parentKey) + // Clear any existing separators before adding new ones to avoid duplication try { await NativeIRSystemMenuManager.removeMenuItemAtPath([...parentPath, SEPARATOR]) } catch (e) { @@ -241,6 +175,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { try { const structure = NativeIRSystemMenuManager.getMenuStructure() const out: string[] = [] + // Recursively walk the menu tree structure const walk = (nodes?: any[]) => { if (!nodes) return for (const n of nodes) { @@ -292,84 +227,55 @@ export function useSystemMenu(config?: SystemMenuConfig) { const updateMenus = async () => { if (!config) return - if (Platform.OS === "windows") { - // For Windows, update global state and action map - if (config.items) { - // Store actions in actionsRef - for (const [parentKey, entries] of Object.entries(config.items)) { - for (const entry of entries) { - if (!isSeparator(entry)) { - const item = entry as SystemMenuItem - const leafPath = [parentKey, item.label] - if (item.action) { - actionsRef.current.set(joinPath(leafPath), item.action) - } - } - } - } - // Update global state - setGlobalMenuConfig(config) - } - previousConfigRef.current = config - await discoverMenus() - } else { - // Original macOS logic - const previousConfig = previousConfigRef.current - - if (config.remove?.length) { - for (const entry of config.remove) { - await removeMenuItemByName(entry) - } + const previousConfig = previousConfigRef.current + + if (config.remove?.length) { + for (const entry of config.remove) { + await removeMenuItemByName(entry) } + } - if (config.items) { - for (const [parentKey, entries] of Object.entries(config.items)) { - const previousEntries = previousConfig?.items?.[parentKey] || [] - const { toRemove, toUpdate } = getItemDifference(previousEntries, entries) + if (config.items) { + for (const [parentKey, entries] of Object.entries(config.items)) { + const previousEntries = previousConfig?.items?.[parentKey] || [] + const { toRemove, toUpdate } = getItemDifference(previousEntries, entries) - if (toRemove.length) await removeMenuItems(parentKey, toRemove) + if (toRemove.length) await removeMenuItems(parentKey, toRemove) - await addEntries(parentKey, entries) + await addEntries(parentKey, entries) - for (const item of toUpdate) { - const leafPath = [...parsePathKey(parentKey), item.label] - if (item.action) { - actionsRef.current.set(joinPath(leafPath), item.action) - } - if (item.enabled !== undefined) { - try { - await NativeIRSystemMenuManager.setMenuItemEnabledAtPath(leafPath, item.enabled) - } catch (e) { - console.error(`Failed to update ${joinPath(leafPath)}:`, e) - } + for (const item of toUpdate) { + const leafPath = [...parsePathKey(parentKey), item.label] + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } + if (item.enabled !== undefined) { + try { + await NativeIRSystemMenuManager.setMenuItemEnabledAtPath(leafPath, item.enabled) + } catch (e) { + console.error(`Failed to update ${joinPath(leafPath)}:`, e) } } } } - - previousConfigRef.current = config - await discoverMenus() } + + previousConfigRef.current = config + await discoverMenus() } updateMenus() - }, [config, addEntries, removeMenuItems, getItemDifference]) + }, [config, addEntries, removeMenuItems, getItemDifference, removeMenuItemByName, discoverMenus]) useEffect(() => { - if (Platform.OS === "windows") { - // For Windows, just discover menus from config - discoverMenus() - } else { - // For macOS, use native menu manager - const subscription = NativeIRSystemMenuManager.onMenuItemPressed(handleMenuItemPressed) - discoverMenus() - return () => { - subscription.remove() - } + const subscription = NativeIRSystemMenuManager.onMenuItemPressed(handleMenuItemPressed) + discoverMenus() + return () => { + subscription.remove() } }, [handleMenuItemPressed, discoverMenus]) - // Clean up old menu items + // Clean up old menu items when component unmounts useEffect(() => { return () => { if (!previousConfigRef.current || !config || !config.items) { @@ -380,11 +286,13 @@ export function useSystemMenu(config?: SystemMenuConfig) { for (const [parentKey, entries] of pairs) { const itemsOnly = entries.filter((e): e is SystemMenuItem => !isSeparator(e)) await removeMenuItems(parentKey, itemsOnly) + // Remove any remaining separators await NativeIRSystemMenuManager.removeMenuItemAtPath([ ...parsePathKey(parentKey), SEPARATOR, ]) const parentPath = parsePathKey(parentKey) + // If this was a top-level menu we created and it's now empty, remove it entirely if (parentPath.length === 1) { const top = parentPath[0] const structure = NativeIRSystemMenuManager.getMenuStructure() @@ -403,7 +311,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { } cleanup() } - }, []) + }, [removeMenuItems]) const addMenuItem = useCallback( async (parentKey: string, item: SystemMenuItem) => { @@ -412,28 +320,10 @@ export function useSystemMenu(config?: SystemMenuConfig) { [addEntries], ) - // For Windows, populate actions from global state if no config provided - useEffect(() => { - if (Platform.OS === "windows" && !config && globalMenuConfig?.items) { - // Restore actions from global config - for (const [parentKey, entries] of Object.entries(globalMenuConfig.items)) { - for (const entry of entries) { - if (!isSeparator(entry)) { - const item = entry as SystemMenuItem - const leafPath = [parentKey, item.label] - if (item.action) { - actionsRef.current.set(joinPath(leafPath), item.action) - } - } - } - } - } - }, [config, globalMenuConfig]) - return { availableMenus, - menuStructure: Platform.OS === "windows" ? globalMenuStructure : menuStructure, - menuItems: Platform.OS === "windows" ? globalMenuItems : {}, + menuStructure, + menuItems: {} as Record, discoverMenus, addMenuItem, removeMenuItemByName, @@ -441,4 +331,4 @@ export function useSystemMenu(config?: SystemMenuConfig) { getAllMenuPaths, handleMenuItemPressed, } -} +} \ No newline at end of file diff --git a/app/utils/useSystemMenu/useSystemMenu.windows.ts b/app/utils/useSystemMenu/useSystemMenu.windows.ts new file mode 100644 index 0000000..76440f7 --- /dev/null +++ b/app/utils/useSystemMenu/useSystemMenu.windows.ts @@ -0,0 +1,256 @@ +/* + * Windows Menu Management (Global State Facade) + * + * Add, delete, and update Windows menu items using global state persistence. + * This implementation provides a facade over global state since Windows doesn't + * have native menu management APIs like macOS. + * + * ────────────────────────────── + * Declarative Usage (via config) + * ────────────────────────────── + * + * const menuConfig = { + * items: { + * "File": [ + * { + * label: "New Project", + * shortcut: "ctrl+n", + * action: () => console.log("New project created"), + * }, + * SEPARATOR, + * { + * label: "Save", + * enabled: false, + * action: () => console.log("Save action"), + * }, + * ], + * "View": [ + * { + * label: "Toggle Sidebar", + * shortcut: "ctrl+b", + * action: () => console.log("Sidebar toggled"), + * }, + * ], + * }, + * remove: ["Help", "Format"], + * } + * + * ─────────────────────────── + * Imperative Usage (via hook) + * ─────────────────────────── + * + * const { + * addMenuItem, + * removeMenuItemByName, + * setMenuItemEnabled, + * getAllMenuPaths, + * menuItems, + * menuStructure + * } = useSystemMenu() + * + * useEffect(() => { + * addMenuItem("Tools", { + * label: "Clear Cache", + * action: () => console.log("Cache cleared") + * }) + * getAllMenuPaths().then(paths => console.log({ paths })) + * }, [addMenuItem, getAllMenuPaths]) + * + * // Note: Windows implementation stores menu state globally for persistence + * // across component unmounts. Actions are stored in actionsRef for execution. + */ + +import { useEffect, useRef, useCallback, useState } from "react" +import { useGlobal } from "../../state/useGlobal" +import { + SEPARATOR, + type SystemMenuItem, + type SystemMenuConfig, + type SystemMenuListEntry, + type SystemMenuItemPressedEvent, + type SystemMenuStructure, +} from "./types" +import { parsePathKey, joinPath, isSeparator } from "./utils" + +export function useSystemMenu(config?: SystemMenuConfig) { + const actionsRef = useRef void>>(new Map()) + + // Global state for Windows menu persistence across component unmounts + const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( + "windows-menu-config", + null, + ) + const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( + "windows-menu-structure", + [], + ) + const [globalMenuItems, setGlobalMenuItems] = useGlobal>( + "windows-menu-items", + {}, + ) + + const handleMenuItemPressed = useCallback((event: SystemMenuItemPressedEvent) => { + const key = joinPath(event.menuPath) + const action = actionsRef.current.get(key) + if (action) action() + }, []) + + const discoverMenus = useCallback(async () => { + const configToUse = config || globalMenuConfig + if (!configToUse?.items || !config || config === globalMenuConfig) return [] + + // Create a simple structure from config items for Windows titlebar rendering + const winStructure: SystemMenuStructure = Object.keys(configToUse.items).map((title) => ({ + title, + enabled: true, + path: [title], + items: [], + children: [], + })) + + // Update global state if we have a new config + setGlobalMenuConfig(config) + setGlobalMenuStructure(winStructure) + setGlobalMenuItems(config.items as Record) + + return [] // Windows doesn't have native menu discovery + }, [config, globalMenuConfig, setGlobalMenuConfig, setGlobalMenuStructure, setGlobalMenuItems]) + + const addMenuItem = useCallback( + async (parentKey: string, item: SystemMenuItem) => { + const leafPath = [parentKey, item.label] + const actionKey = joinPath(leafPath) + + // Store action in memory for execution when menu item is pressed + if (item.action) { + actionsRef.current.set(actionKey, item.action) + } + + // Add item to global state for UI rendering + setGlobalMenuItems((prev) => ({ + ...prev, + [parentKey]: [...(prev[parentKey] || []), item], + })) + }, + [setGlobalMenuItems], + ) + + const removeMenuItemByName = useCallback( + async (nameOrPath: string) => { + const path = parsePathKey(nameOrPath) + const key = joinPath(path) + // Remove action from memory + actionsRef.current.delete(key) + + // Remove from global state based on path depth + if (path.length === 1) { + // Remove entire top-level menu + setGlobalMenuItems((prev) => { + const updated = { ...prev } + delete updated[path[0]] + return updated + }) + } else if (path.length === 2) { + // Remove specific menu item + const [parentKey, itemLabel] = path + setGlobalMenuItems((prev) => ({ + ...prev, + [parentKey]: (prev[parentKey] || []).filter((item) => item.label !== itemLabel), + })) + } + }, + [setGlobalMenuItems], + ) + + const setMenuItemEnabled = useCallback( + async (pathOrKey: string | string[], enabled: boolean) => { + const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) + + // Update enabled state in global state (Windows only supports in-memory state updates) + if (path.length >= 2) { + const [parentKey, itemLabel] = path + setGlobalMenuItems((prev) => ({ + ...prev, + [parentKey]: (prev[parentKey] || []).map((item) => + item.label === itemLabel ? { ...item, enabled } : item, + ), + })) + } + }, + [setGlobalMenuItems], + ) + + const getAllMenuPaths = useCallback(async (): Promise => { + const paths: string[] = [] + for (const [parentKey, entries] of Object.entries(globalMenuItems)) { + for (const entry of entries) { + if (!isSeparator(entry)) { + paths.push(joinPath([parentKey, entry.label])) + } + } + } + return paths + }, [globalMenuItems]) + + // Update menus when config changes + useEffect(() => { + const updateMenus = async () => { + if (!config) return + + if (config.items) { + // Store actions in memory for execution + for (const [parentKey, entries] of Object.entries(config.items)) { + for (const entry of entries) { + if (!isSeparator(entry)) { + const item = entry as SystemMenuItem + const leafPath = [parentKey, item.label] + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } + } + } + } + // Update global state for UI persistence + setGlobalMenuConfig(config) + setGlobalMenuItems(config.items as Record) + } + await discoverMenus() + } + + updateMenus() + }, [config, setGlobalMenuConfig, setGlobalMenuItems, discoverMenus]) + + // Restore actions from global state when no config is provided (component remount) + useEffect(() => { + if (!config && globalMenuConfig?.items) { + // Restore actions from persisted global config + for (const [parentKey, entries] of Object.entries(globalMenuConfig.items)) { + for (const entry of entries) { + if (!isSeparator(entry)) { + const item = entry as SystemMenuItem + const leafPath = [parentKey, item.label] + if (item.action) { + actionsRef.current.set(joinPath(leafPath), item.action) + } + } + } + } + } + }, [config, globalMenuConfig]) + + useEffect(() => { + discoverMenus() + }, [discoverMenus]) + + return { + availableMenus: [], // Windows doesn't have native menu discovery + menuStructure: globalMenuStructure, + menuItems: globalMenuItems, + discoverMenus, + addMenuItem, + removeMenuItemByName, + setMenuItemEnabled, + getAllMenuPaths, + handleMenuItemPressed, + } +} \ No newline at end of file diff --git a/app/utils/useSystemMenu/utils.ts b/app/utils/useSystemMenu/utils.ts new file mode 100644 index 0000000..a20a844 --- /dev/null +++ b/app/utils/useSystemMenu/utils.ts @@ -0,0 +1,13 @@ +import { SEPARATOR, type SystemMenuListEntry } from "./types" + +export const PATH_SEPARATOR = " > " + +export const parsePathKey = (key: string): string[] => + key + .split(PATH_SEPARATOR) + .map((s) => s.trim()) + .filter(Boolean) + +export const joinPath = (p: string[]) => p.join(PATH_SEPARATOR) + +export const isSeparator = (e: SystemMenuListEntry): e is typeof SEPARATOR => e === SEPARATOR \ No newline at end of file From 10e6a5193f269c87d0ee440c795e8b2db4c81781 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 11:00:19 -0400 Subject: [PATCH 20/40] improve and document the useGlobal windows hook --- app/state/useGlobal.windows.ts | 249 ++++++++++++++++++++++++++++----- 1 file changed, 215 insertions(+), 34 deletions(-) diff --git a/app/state/useGlobal.windows.ts b/app/state/useGlobal.windows.ts index edbc94b..cd22dd1 100644 --- a/app/state/useGlobal.windows.ts +++ b/app/state/useGlobal.windows.ts @@ -1,92 +1,273 @@ -// globalStore.ts -import { useSyncExternalStore, useCallback } from "react"; +/** + * Global State Management System (React Native Windows) + * + * A lightweight global state solution built on React's useSyncExternalStore. + * Provides both hook-based and imperative access to shared state values. + * + * Features: + * - Type-safe global state with "initialize on first read" semantics + * - Efficient subscription system with automatic cleanup + * - Batched updates to minimize re-renders + * - Support for both React components (useGlobal) and external code (withGlobal) + * - Memory leak prevention via Set cleanup + * - No-op persistence stubs (API-stable; can be wired later) + * + * Usage: + * const [count, setCount] = useGlobal('counter', 0); + * const [data, setData] = withGlobal('userData', {}); + * const [persistedData, setPersisted] = useGlobal('key', {}, { persist: true }); // persist currently no-op + * + * Notes: + * - `initialValue` is used as the value if the key is missing. For components, + * we DO NOT write during render; we write once post-mount if still missing. + * - If different `initialValue`s are passed for the same `id`, the first + * established value "wins" (subsequent differing defaults are ignored). + */ + +import { useSyncExternalStore, useEffect, useCallback } from "react"; import { unstable_batchedUpdates } from "react-native"; type Id = string; type Listener = () => void; type SetValue = T | ((prev: T) => T); +type UseGlobalOptions = { persist?: boolean }; +/* ----------------------------------------------------------------------------- + * Internal Stores + * -------------------------------------------------------------------------- */ +// Central storage for all global state values, keyed by unique identifiers const globals = new Map(); +// Subscription system: maps each global ID to a set of listener functions const listeners = new Map>(); -// Initialize global value if it doesn't exist, but don't modify during snapshot reads -function initializeGlobal(id: Id, initialValue: T): void { - if (!globals.has(id)) { - globals.set(id, initialValue); - } +/* ----------------------------------------------------------------------------- + * Persistence Stubs (no-op) + * RN Web doesn't have react-native-mmkv support out of the box; plan to wire later + * -------------------------------------------------------------------------- */ +function loadPersistedGlobals(): void { + // No-op: persistence not implemented +} + +function debouncePersist(_delay: number = 300): void { + // No-op: persistence not implemented } +/* ----------------------------------------------------------------------------- + * Helpers + * -------------------------------------------------------------------------- */ + +/** + * Read a snapshot for an id, returning `initialValue` when missing. + * Pure read: NEVER writes during render. + */ +function getSnapshotWithDefault(id: Id, initialValue: T): T { + return (globals.has(id) ? (globals.get(id) as T) : initialValue); +} + +/** + * Read a snapshot without default (used by imperative API and setters). + */ function getSnapshot(id: Id): T { return globals.get(id) as T; } +/** + * Subscribe a component to changes for a specific global ID. + * Returns an unsubscribe function that cleans up both the listener and empty sets. + */ function subscribe(id: Id, cb: Listener): () => void { let set = listeners.get(id); if (!set) listeners.set(id, (set = new Set())); set.add(cb); + + // Return cleanup function that prevents memory leaks return () => { const s = listeners.get(id); if (!s) return; s.delete(cb); + // Clean up empty listener sets to prevent memory leaks if (s.size === 0) listeners.delete(id); }; } +/** + * Notify all subscribers of a global value change. + * Uses batched updates to prevent excessive re-renders when multiple globals change. + * Iterates over a copy to be resilient to listeners mutating subscriptions. + */ function notify(id: Id) { const s = listeners.get(id); if (!s || s.size === 0) return; + unstable_batchedUpdates(() => { - for (const l of s) l(); + for (const l of Array.from(s)) l(); }); } -export function useGlobal(id: Id, initialValue: T): [T, (v: SetValue) => void] { - // Initialize the global value once, outside of the snapshot function - initializeGlobal(id, initialValue); +/** + * Create a setter function that handles state updates (persistence is no-op). + * - Supports functional updates like React.useState + * - Skips notifications if value is Object.is-equal (no-op update) + * - Accepts `null` to reset/delete the value (used by imperative API) + */ +function buildSetValue(id: Id, persist: boolean) { + return (value: SetValue | null) => { + const prev = globals.get(id) as T | undefined; + + // Handle null value as reset (imperative API) + if (value === null) { + if (!globals.has(id)) return; // nothing to reset + globals.delete(id); + // persistence cleanup would go here (no-op for now) + notify(id); + return; + } + + // Resolve functional updater + const next = + typeof value === "function" + ? (value as (prev: T) => T)(getSnapshot(id)) + : value; + + // Avoid unnecessary notifications/re-renders on no-op updates + if (Object.is(prev, next)) return; + globals.set(id, next); + + // Would save to persistent storage if implemented (no-op for now) + if (persist) debouncePersist(); + + // Notify all subscribers for re-renders + notify(id); + }; +} + +/* ----------------------------------------------------------------------------- + * Public API + * -------------------------------------------------------------------------- */ + +/** + * React hook for accessing and updating global state (component-friendly API) + * + * RN-only: No SSR getServerSnapshot is provided. + * + * @param id - Unique identifier for the global value + * @param initialValue - Default value to use if the global doesn't exist yet + * @param options - Configuration options including persistence (NOTE: persist is no-op) + * @returns Tuple of [currentValue, setter] similar to useState + */ +export function useGlobal( + id: Id, + initialValue: T, + { persist = false }: UseGlobalOptions = {} +): [T, (v: SetValue) => void] { + // Read via useSyncExternalStore; ensure the snapshot read is PURE (no writes) const value = useSyncExternalStore( - (cb) => subscribe(id, cb), - () => getSnapshot(id) + (cb) => subscribe(id, cb), // subscribe + () => getSnapshotWithDefault(id, initialValue) // getSnapshot (client) ); - // Memoize the setter function to prevent unnecessary re-renders - const set = useCallback((next: SetValue) => { - const current = getSnapshot(id); - const resolved = typeof next === "function" ? (next as (p: T) => T)(current) : next; - globals.set(id, resolved); - notify(id); + /** + * Post-mount initialization: + * If the key is still missing, establish it with `initialValue`. + * This avoids writes during render while ensuring the key exists thereafter. + * No notify needed: subscribers already read `initialValue` on first render. + */ + useEffect(() => { + if (!globals.has(id)) { + globals.set(id, initialValue); + // Optionally, a dev-only warning could detect mismatched defaults for same id. + } + // We intentionally do not depend on initialValue here: + // changing the default later should not rewrite established globals. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); + // Memoize the setter; enforce non-null signature for hook users + const setAny = useCallback(buildSetValue(id, persist), [id, persist]); + const set = useCallback<(v: SetValue) => void>((v) => setAny(v), [setAny]); + return [value, set]; } -// Imperative access (outside components) +/** + * Imperative access to global state (for use outside React components) + * + * Useful for accessing global state in utility functions, event handlers, + * or other non-React code that needs to read/write global values. + * + * @param id - Unique identifier for the global value + * @param initialValue - Default value to use if the global doesn't exist yet + * @param options - Configuration options including persistence (NOTE: persist is no-op) + * @returns Tuple of [currentValue, setter] where setter accepts null to reset + */ export function withGlobal( id: Id, - initialValue: T + initialValue: T, + { persist = false }: UseGlobalOptions = {} ): [T, (v: SetValue | null) => void] { - // Initialize the global value - initializeGlobal(id, initialValue); - - const setter = (v: SetValue | null) => { - if (v === null) return resetGlobal(id); - const current = getSnapshot(id); - const resolved = typeof v === "function" ? (v as (p: T) => T)(current) : v; - globals.set(id, resolved); - notify(id); - }; - return [getSnapshot(id), setter]; + // Imperative path can initialize synchronously without render concerns + if (!globals.has(id)) globals.set(id, initialValue); + return [getSnapshot(id), buildSetValue(id, persist)]; } +/** + * Reset a specific global value back to its initial state (delete the key). + * + * @param id - The global identifier to reset + * @param rerender - Whether to trigger re-renders (default: true) + */ export function resetGlobal(id: Id, rerender = true) { + if (!globals.has(id)) return; globals.delete(id); + // Note: No persistence cleanup needed since persistence is not implemented if (rerender) notify(id); } +/** + * Clear all global state values. + * + * Useful for testing or app-wide state resets. Only notifies globals + * that have active listeners to avoid unnecessary work. + * + * @param rerender - Whether to trigger re-renders (default: true) + */ export function clearGlobals(rerender = true) { + // Clear in-memory state + const hadAny = globals.size > 0; globals.clear(); - if (rerender) { - // Only notify ids that have listeners; avoids stale maps + // Note: No persistent storage to clear since persistence is not implemented + + if (rerender && hadAny) { + // Only notify ids that currently have listeners for (const id of listeners.keys()) notify(id); } } + +/* ----------------------------------------------------------------------------- + * Optional Developer Ergonomics (handy for tests & utilities) + * -------------------------------------------------------------------------- */ + +/** + * Read a global value without subscribing. Returns undefined if missing. + */ +export const getGlobal = (id: Id): T | undefined => + (globals.get(id) as T | undefined); + +/** + * Set a global value without subscribing. (Non-null only.) + */ +export const setGlobal = (id: Id, v: SetValue): void => + buildSetValue(id, false)(v); + +/** + * Check whether a global key exists. + */ +export const hasGlobal = (id: Id): boolean => globals.has(id); + +/* ----------------------------------------------------------------------------- + * Module Initialization + * -------------------------------------------------------------------------- */ + +// Load persisted globals on module initialization (no-op for now) +loadPersistedGlobals(); From a5e2ac3881b766627f2e18da388ca0184a11abbc Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 11:00:40 -0400 Subject: [PATCH 21/40] Remove RNW useKeyboard return --- app/utils/system.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/utils/system.ts b/app/utils/system.ts index dcc66be..18b1e3a 100644 --- a/app/utils/system.ts +++ b/app/utils/system.ts @@ -47,7 +47,6 @@ export function useKeyboardEvents(onKeyboardEvent: (event: KeyboardEvent) => voi const keyboardSubscription = useRef(null) useEffect(() => { - if (Platform.OS === "windows") return _keyboardSubscribers++ if (_keyboardSubscribers === 1) IRKeyboard.startListening() keyboardSubscription.current = IRKeyboard.onKeyboardEvent(onKeyboardEvent) From c9f3ea50e8388d53dc11e9a936ea8deb3a9dbe02 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 11:00:57 -0400 Subject: [PATCH 22/40] fix imports --- app/app.tsx | 14 +++++++------- app/components/Titlebar/TitlebarMenu.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/app.tsx b/app/app.tsx index 0b5378a..20c6898 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -9,7 +9,7 @@ import { connectToServer } from "./state/connectToServer" import { useTheme, themed } from "./theme/theme" import { useEffect, useMemo } from "react" import { TimelineScreen } from "./screens/TimelineScreen" -import { useSystemMenu } from "./utils/useSystemMenu" +import { useSystemMenu } from "./utils/useSystemMenu/useSystemMenu" import { Titlebar } from "./components/Titlebar/Titlebar" import { Sidebar } from "./components/Sidebar/Sidebar" import { useSidebar } from "./state/useSidebar" @@ -74,12 +74,12 @@ function App(): React.JSX.Element { }, ...(__DEV__ ? [ - { - label: "Toggle Dev Menu", - shortcut: "cmd+shift+d", - action: () => NativeModules.DevMenu.show(), - }, - ] + { + label: "Toggle Dev Menu", + shortcut: "cmd+shift+d", + action: () => NativeModules.DevMenu.show(), + }, + ] : []), ], Window: [ diff --git a/app/components/Titlebar/TitlebarMenu.tsx b/app/components/Titlebar/TitlebarMenu.tsx index ee446e3..60b48ce 100644 --- a/app/components/Titlebar/TitlebarMenu.tsx +++ b/app/components/Titlebar/TitlebarMenu.tsx @@ -6,7 +6,7 @@ import { MenuDropdown } from "../Menu/MenuDropdown" import { MenuOverlay } from "../Menu/MenuOverlay" import type { Position } from "../Menu/types" import { PassthroughView } from "./PassthroughView" -import { useSystemMenu } from "../../utils/useSystemMenu" +import { useSystemMenu } from "../../utils/useSystemMenu/useSystemMenu" export const TitlebarMenu = () => { const { menuStructure, menuItems, handleMenuItemPressed } = useSystemMenu() From fbd9f0f7402ef8f3e5cc3b66e7c3660a3da59cfa Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Fri, 26 Sep 2025 11:01:11 -0400 Subject: [PATCH 23/40] rename --- .../useSystemMenu/{useSystemMenu.macos.ts => useSystemMenu.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/utils/useSystemMenu/{useSystemMenu.macos.ts => useSystemMenu.ts} (100%) diff --git a/app/utils/useSystemMenu/useSystemMenu.macos.ts b/app/utils/useSystemMenu/useSystemMenu.ts similarity index 100% rename from app/utils/useSystemMenu/useSystemMenu.macos.ts rename to app/utils/useSystemMenu/useSystemMenu.ts From 5f7f3bb34ca8bf3167c7ebe01dbae6d80e960ca1 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 12:58:09 -0500 Subject: [PATCH 24/40] Use a content island to listen to keyboard --- app/native/IRKeyboard/IRKeyboard.windows.cpp | 183 ++++++++++++++++--- app/native/IRKeyboard/IRKeyboard.windows.h | 55 +++--- 2 files changed, 188 insertions(+), 50 deletions(-) diff --git a/app/native/IRKeyboard/IRKeyboard.windows.cpp b/app/native/IRKeyboard/IRKeyboard.windows.cpp index 386b6b3..7cb3e6c 100644 --- a/app/native/IRKeyboard/IRKeyboard.windows.cpp +++ b/app/native/IRKeyboard/IRKeyboard.windows.cpp @@ -1,33 +1,168 @@ #include "pch.h" #include "IRKeyboard.windows.h" + #include +#include +#include +#include +#include + +namespace { + // Bit tested by GetKeyState for "currently down". + constexpr auto KEYBOARD_STATE_PRESSED = 0x8000; + + // Global WinAppSDK input state. Keep lifetimes/threading aligned with RNW's UI thread. + winrt::reactotron::implementation::IRKeyboard* g_keyboardInstance = nullptr; + winrt::Microsoft::UI::Input::InputKeyboardSource g_keyboardSource{ nullptr }; + winrt::Microsoft::UI::Content::ContentIsland g_contentIsland{ nullptr }; + winrt::event_token g_keyDownToken{}; + winrt::event_token g_keyUpToken{}; + + inline bool IsKeyPressed(int vk) noexcept { return (GetKeyState(vk) & KEYBOARD_STATE_PRESSED) != 0; } + + // Virtual-key → label. These labels are consumed cross-platform; changing them can break shortcuts. + const std::unordered_map VirtualKeyNames = { + // Navigation / editing + {VK_BACK,"Backspace"},{VK_TAB,"Tab"},{VK_RETURN,"\r"},{VK_ESCAPE,"\u001b"}, + {VK_SPACE,"Space"},{VK_DELETE,"Delete"},{VK_HOME,"Home"},{VK_END,"End"}, + {VK_PRIOR,"PageUp"},{VK_NEXT,"PageDown"},{VK_INSERT,"Insert"}, + + // Arrows + {VK_LEFT,"ArrowLeft"},{VK_UP,"ArrowUp"},{VK_RIGHT,"ArrowRight"},{VK_DOWN,"ArrowDown"}, + + // Function keys + {VK_F1,"F1"},{VK_F2,"F2"},{VK_F3,"F3"},{VK_F4,"F4"},{VK_F5,"F5"},{VK_F6,"F6"}, + {VK_F7,"F7"},{VK_F8,"F8"},{VK_F9,"F9"},{VK_F10,"F10"},{VK_F11,"F11"},{VK_F12,"F12"}, + + // Numpad digits + {VK_NUMPAD0,"0"},{VK_NUMPAD1,"1"},{VK_NUMPAD2,"2"},{VK_NUMPAD3,"3"},{VK_NUMPAD4,"4"}, + {VK_NUMPAD5,"5"},{VK_NUMPAD6,"6"},{VK_NUMPAD7,"7"},{VK_NUMPAD8,"8"},{VK_NUMPAD9,"9"}, + + // Modifiers normalized to single labels + {VK_CONTROL,"Control"},{VK_LCONTROL,"Control"},{VK_RCONTROL,"Control"}, + {VK_MENU,"Alt"},{VK_LMENU,"Alt"},{VK_RMENU,"Alt"}, + {VK_SHIFT,"Shift"},{VK_LSHIFT,"Shift"},{VK_RSHIFT,"Shift"}, + {VK_LWIN,"Meta"},{VK_RWIN,"Meta"}, + }; + + // VK → human label or character(s). Order matters: table → ASCII A-Z/0-9 → ToUnicode → "Unknown". + std::string TranslateVirtualKeyToString(DWORD vk) noexcept { + if (const auto it = VirtualKeyNames.find(vk); it != VirtualKeyNames.end()) return it->second; + + if ((vk >= 'A' && vk <= 'Z') || (vk >= '0' && vk <= '9')) return std::string(1, static_cast(vk)); + + BYTE keyboardState[256]; + GetKeyboardState(keyboardState); + + WCHAR unicodeBuffer[8] = {}; + const UINT scanCode = MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC, GetKeyboardLayout(0)); + const int result = ToUnicode(vk, scanCode, keyboardState, unicodeBuffer, 8, 0); + + if (result > 0) { + char utf8[16] = {}; + WideCharToMultiByte(CP_UTF8, 0, unicodeBuffer, result, utf8, 16, nullptr, nullptr); + return std::string(utf8); + } + return "Unknown"; + } -using namespace winrt::reactotron::implementation; + // KeyDown only: consumers derive state from live modifiers, not event-latched ones. + void OnKeyDown(winrt::Microsoft::UI::Input::InputKeyboardSource const&, + winrt::Microsoft::UI::Input::KeyEventArgs const& args) noexcept { + if (!g_keyboardInstance || !g_keyboardInstance->m_isListening) return; -namespace -{ - inline bool IsKeyDown(int vk) noexcept { return (GetKeyState(vk) & 0x8000) != 0; } + try { + const auto vk = static_cast(args.VirtualKey()); + reactotronCodegen::IRKeyboardSpec_KeyboardEvent e{}; + e.type = "keydown"; + e.key = TranslateVirtualKeyToString(vk); + e.characters = e.key; + e.keyCode = static_cast(vk); + + // Modifier policy: report actual keys pressed + e.modifiers = reactotronCodegen::IRKeyboardSpec_KeyboardEvent_modifiers{ + IsKeyPressed(VK_CONTROL), // ctrl + IsKeyPressed(VK_MENU), // alt + IsKeyPressed(VK_SHIFT), // shift + IsKeyPressed(VK_LWIN) || IsKeyPressed(VK_RWIN) // cmd (Windows key) + }; + + if (g_keyboardInstance->onKeyboardEvent) g_keyboardInstance->onKeyboardEvent(std::move(e)); + } catch (...) { + #ifdef _DEBUG + OutputDebugStringA("[IRKeyboard] Exception in OnKeyDown"); + #endif + } + } + + // Present for completeness and future use. Currently a no-op; subscription remains intentional. + void OnKeyUp(winrt::Microsoft::UI::Input::InputKeyboardSource const&, + winrt::Microsoft::UI::Input::KeyEventArgs const&) noexcept {} + + // Binds to the current island and wires events. Call on the island's UI thread. + void InitializeKeyboardCapture(winrt::reactotron::implementation::IRKeyboard* instance) noexcept { + if (!instance || !instance->m_isListening) return; + + try { + const auto islands = winrt::Microsoft::UI::Content::ContentIsland::FindAllForCurrentThread(); + if (islands.size() == 0) return; + + g_contentIsland = islands[0]; + g_keyboardSource = winrt::Microsoft::UI::Input::InputKeyboardSource::GetForIsland(g_contentIsland); + if (!g_keyboardSource) return; + + g_keyboardInstance = instance; + g_keyDownToken = g_keyboardSource.KeyDown({ OnKeyDown }); + g_keyUpToken = g_keyboardSource.KeyUp({ OnKeyUp }); + + // Encourage focus so keystrokes route to the foreground window (best-effort, not guaranteed). + if (HWND hwnd = GetActiveWindow()) { + SetForegroundWindow(hwnd); + SetFocus(hwnd); + SendMessage(hwnd, WM_SETFOCUS, 0, 0); + } + } catch (...) { + // Non-fatal: input can be retried on the next startListening(). + } + } +} // namespace + +void winrt::reactotron::implementation::IRKeyboard::Initialize( + winrt::Microsoft::ReactNative::ReactContext const& reactContext) noexcept { + // ReactContext is used only to marshal to the owning UI dispatcher. + m_reactContext = reactContext; } -bool IRKeyboard::ctrl() noexcept { return IsKeyDown(VK_CONTROL); } -bool IRKeyboard::alt() noexcept { return IsKeyDown(VK_MENU); } -bool IRKeyboard::shift() noexcept { return IsKeyDown(VK_SHIFT); } -bool IRKeyboard::cmd() noexcept { return IsKeyDown(VK_LWIN) || IsKeyDown(VK_RWIN); } - -void IRKeyboard::startListening() noexcept -{ - m_isListening = true; - - // Emit a synthetic event to verify wiring - reactotronCodegen::IRKeyboardSpec_KeyboardEvent evt{}; - evt.type = "keydown"; - evt.key = "A"; - evt.characters = "a"; - evt.keyCode = 65; - evt.modifiers = reactotronCodegen::IRKeyboardSpec_KeyboardEvent_modifiers{ctrl(), alt(), shift(), cmd()}; - - if (onKeyboardEvent) - onKeyboardEvent(std::move(evt)); +// These helpers intentionally query live (GetKeyState) modifier state. +bool winrt::reactotron::implementation::IRKeyboard::ctrl() noexcept { return IsKeyPressed(VK_CONTROL); } +bool winrt::reactotron::implementation::IRKeyboard::alt() noexcept { return IsKeyPressed(VK_MENU); } +bool winrt::reactotron::implementation::IRKeyboard::shift() noexcept { return IsKeyPressed(VK_SHIFT); } +bool winrt::reactotron::implementation::IRKeyboard::cmd() noexcept { return IsKeyPressed(VK_LWIN) || IsKeyPressed(VK_RWIN); } + +void winrt::reactotron::implementation::IRKeyboard::startListening() noexcept { + if (m_isListening || !m_reactContext) return; + + m_isListening = true; // matches original timing; some consumers check this before the dispatch posts + m_reactContext.UIDispatcher().Post([this]() { + InitializeKeyboardCapture(this); + }); } -void IRKeyboard::stopListening() noexcept { m_isListening = false; } +void winrt::reactotron::implementation::IRKeyboard::stopListening() noexcept { + if (!m_isListening) return; + m_isListening = false; + + // Remove handlers if present; tokens must be cleared even on exceptions. + if (g_keyboardSource) { + try { + if (g_keyDownToken.value != 0) { g_keyboardSource.KeyDown(g_keyDownToken); g_keyDownToken = {}; } + if (g_keyUpToken.value != 0) { g_keyboardSource.KeyUp(g_keyUpToken); g_keyUpToken = {}; } + } catch (...) { + // Non-fatal cleanup failure; state below still resets. + } + g_keyboardSource = nullptr; + } + + if (g_keyboardInstance == this) g_keyboardInstance = nullptr; + g_contentIsland = nullptr; +} diff --git a/app/native/IRKeyboard/IRKeyboard.windows.h b/app/native/IRKeyboard/IRKeyboard.windows.h index 537af25..6761821 100644 --- a/app/native/IRKeyboard/IRKeyboard.windows.h +++ b/app/native/IRKeyboard/IRKeyboard.windows.h @@ -1,34 +1,37 @@ #pragma once -#include "NativeModules.h" #include "..\..\..\windows\reactotron\codegen\NativeIRKeyboardDataTypes.g.h" #include "..\..\..\windows\reactotron\codegen\NativeIRKeyboardSpec.g.h" +#include "NativeModules.h" + +namespace winrt::reactotron::implementation { +REACT_TURBO_MODULE(IRKeyboard) +struct IRKeyboard : reactotronCodegen::IRKeyboardSpec { + IRKeyboard() noexcept = default; -namespace winrt::reactotron::implementation -{ - REACT_MODULE(IRKeyboard, L"IRKeyboard") - struct IRKeyboard - { - IRKeyboard() noexcept = default; + REACT_INIT(Initialize) + void Initialize( + winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; - REACT_SYNC_METHOD(ctrl) - bool ctrl() noexcept; - REACT_SYNC_METHOD(alt) - bool alt() noexcept; - REACT_SYNC_METHOD(shift) - bool shift() noexcept; - REACT_SYNC_METHOD(cmd) - bool cmd() noexcept; + REACT_SYNC_METHOD(ctrl) + bool ctrl() noexcept; + REACT_SYNC_METHOD(alt) + bool alt() noexcept; + REACT_SYNC_METHOD(shift) + bool shift() noexcept; + REACT_SYNC_METHOD(cmd) + bool cmd() noexcept; - REACT_METHOD(startListening) - void startListening() noexcept; - REACT_METHOD(stopListening) - void stopListening() noexcept; + REACT_METHOD(startListening) + void startListening() noexcept; + REACT_METHOD(stopListening) + void stopListening() noexcept; - REACT_EVENT(onKeyboardEvent) - std::function onKeyboardEvent; + REACT_EVENT(onKeyboardEvent) + std::function + onKeyboardEvent; - private: - bool m_isListening = false; - HHOOK m_keyboardHook = nullptr; - }; -} + // Public members needed for event-driven keyboard capture + bool m_isListening = false; + winrt::Microsoft::ReactNative::ReactContext m_reactContext{nullptr}; +}; +} // namespace winrt::reactotron::implementation From ff2c72d9368e25dcdac6f4bc8030f41caee687b7 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:01:51 -0500 Subject: [PATCH 25/40] Make shortcuts platform specific --- app/app.tsx | 50 ++-- app/contexts/ShortcutsContext.tsx | 84 +++++++ app/utils/useSystemMenu/types.ts | 7 +- app/utils/useSystemMenu/useSystemMenu.ts | 9 +- .../useSystemMenu/useSystemMenu.windows.ts | 227 ++++++++---------- app/utils/useSystemMenu/utils.ts | 78 +++++- 6 files changed, 302 insertions(+), 153 deletions(-) create mode 100644 app/contexts/ShortcutsContext.tsx diff --git a/app/app.tsx b/app/app.tsx index 20c6898..de7c9bf 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -4,7 +4,7 @@ * * @format */ -import { DevSettings, NativeModules, StatusBar, View, type ViewStyle } from "react-native" +import { DevSettings, NativeModules, Pressable, StatusBar, View, Text, type ViewStyle } from "react-native" import { connectToServer } from "./state/connectToServer" import { useTheme, themed } from "./theme/theme" import { useEffect, useMemo } from "react" @@ -20,6 +20,7 @@ import { HelpScreen } from "./screens/HelpScreen" import { TimelineItem } from "./types" import { PortalHost } from "./components/Portal" import { StateScreen } from "./screens/StateScreen" +import { ShortcutsProvider } from "./contexts/ShortcutsContext" if (__DEV__) { // This is for debugging Reactotron with ... Reactotron! @@ -44,39 +45,39 @@ function App(): React.JSX.Element { View: [ { label: "Toggle Sidebar", - shortcut: "cmd+b", - action: toggleSidebar, + shortcut: { windows: "ctrl+b", macos: "cmd+b" }, + action: () => toggleSidebar(), }, { label: "Logs Tab", - shortcut: "cmd+1", + shortcut: { windows: "ctrl+1", macos: "cmd+1" }, action: () => setActiveItem("logs"), }, { label: "Network Tab", - shortcut: "cmd+2", + shortcut: { windows: "ctrl+2", macos: "cmd+2" }, action: () => setActiveItem("network"), }, { label: "Performance Tab", - shortcut: "cmd+3", + shortcut: { windows: "ctrl+3", macos: "cmd+3" }, action: () => setActiveItem("performance"), }, { label: "Plugins Tab", - shortcut: "cmd+4", + shortcut: { windows: "ctrl+4", macos: "cmd+4" }, action: () => setActiveItem("plugins"), }, { label: "Help Tab", - shortcut: "cmd+5", + shortcut: { windows: "ctrl+5", macos: "cmd+5" }, action: () => setActiveItem("help"), }, ...(__DEV__ ? [ { label: "Toggle Dev Menu", - shortcut: "cmd+shift+d", + shortcut: { windows: "ctrl+shift+d", macos: "cmd+shift+d" }, action: () => NativeModules.DevMenu.show(), }, ] @@ -85,20 +86,20 @@ function App(): React.JSX.Element { Window: [ { label: "Reload", - shortcut: "cmd+shift+r", + shortcut: { windows: "ctrl+shift+r", macos: "cmd+shift+r" }, action: () => DevSettings.reload(), }, ], Tools: [ { label: "Clear Timeline Items", - shortcut: "cmd+k", + shortcut: { windows: "ctrl+k", macos: "cmd+k" }, action: () => setTimelineItems([]), }, ], }, }), - [toggleSidebar], + [toggleSidebar, setActiveItem, setTimelineItems], ) useSystemMenu(menuConfig) @@ -128,18 +129,23 @@ function App(): React.JSX.Element { } return ( - - - - - - - - {renderActiveItem()} + + + + + + + + + {renderActiveItem()} + toggleSidebar()}> + Toggle Sidebar + + + - - + ) } diff --git a/app/contexts/ShortcutsContext.tsx b/app/contexts/ShortcutsContext.tsx new file mode 100644 index 0000000..c6a3407 --- /dev/null +++ b/app/contexts/ShortcutsContext.tsx @@ -0,0 +1,84 @@ +import React, { createContext, useContext, useCallback } from "react" +import { useGlobal } from "../state/useGlobal" +import { useKeyboardEvents } from "../utils/system" +import { parseShortcut, matchesKeyCombo, type KeyCombination } from "../utils/useSystemMenu/utils" + +// Global state for shortcuts - shared across all instances +type ShortcutRegistry = Record void> +type ShortcutCombinations = Record + +interface ShortcutsContextType { + registerShortcut: (shortcut: string, action: () => void) => void + unregisterShortcut: (shortcut: string) => void + clearAllShortcuts: () => void +} + +const ShortcutsContext = createContext(null) + +export function ShortcutsProvider({ children }: { children: React.ReactNode }) { + const [shortcuts, setShortcuts] = useGlobal("global-shortcuts", {}) + const [combinations, setCombinations] = useGlobal("global-shortcut-combinations", {}) + + const registerShortcut = useCallback((shortcut: string, action: () => void) => { + if (!shortcut || !action) return + + const combination = parseShortcut(shortcut) + if (!combination) { + console.warn(`Invalid shortcut format: ${shortcut}`) + return + } + + // Register globally (will overwrite if already exists - automatic deduplication!) + setShortcuts(prev => ({ ...prev, [shortcut]: action })) + setCombinations(prev => ({ ...prev, [shortcut]: combination })) + }, [setShortcuts, setCombinations]) + + const unregisterShortcut = useCallback((shortcut: string) => { + setShortcuts(prev => { + const { [shortcut]: _, ...rest } = prev + return rest + }) + setCombinations(prev => { + const { [shortcut]: _, ...rest } = prev + return rest + }) + }, [setShortcuts, setCombinations]) + + const clearAllShortcuts = useCallback(() => { + setShortcuts({}) + setCombinations({}) + }, [setShortcuts, setCombinations]) + + const handleKeyboardEvent = useCallback((event: any) => { + // Only handle keydown events + if (event.type !== "keydown") return + + // Check all registered shortcuts for a match + for (const [shortcut, combination] of Object.entries(combinations)) { + if (matchesKeyCombo(event, combination)) { + const action = shortcuts[shortcut] + if (action) { + action() + return // Stop after first match + } + } + } + }, [shortcuts, combinations]) + + // Set up the global keyboard listener + useKeyboardEvents(handleKeyboardEvent, [handleKeyboardEvent]) + + return ( + + {children} + + ) +} + +export function useShortcuts() { + const context = useContext(ShortcutsContext) + if (!context) { + throw new Error("useShortcuts must be used within a ShortcutsProvider") + } + return context +} \ No newline at end of file diff --git a/app/utils/useSystemMenu/types.ts b/app/utils/useSystemMenu/types.ts index 19f3871..1d99d61 100644 --- a/app/utils/useSystemMenu/types.ts +++ b/app/utils/useSystemMenu/types.ts @@ -4,9 +4,14 @@ export type { SystemMenuStructure, } from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" +export type PlatformShortcut = { + windows?: string + macos?: string +} + export interface SystemMenuItem { label: string - shortcut?: string + shortcut?: string | PlatformShortcut enabled?: boolean position?: number action?: () => void diff --git a/app/utils/useSystemMenu/useSystemMenu.ts b/app/utils/useSystemMenu/useSystemMenu.ts index d6ae931..3e9fa2a 100644 --- a/app/utils/useSystemMenu/useSystemMenu.ts +++ b/app/utils/useSystemMenu/useSystemMenu.ts @@ -113,19 +113,24 @@ export function useSystemMenu(config?: SystemMenuConfig) { if (item.action) actionsRef.current.set(actionKey, item.action) + // Resolve platform-specific shortcut for macOS + const resolvedShortcut = typeof item.shortcut === "object" + ? (item.shortcut.macos ?? "") + : (item.shortcut ?? "") + try { if (typeof item.position === "number") { await NativeIRSystemMenuManager.insertMenuItemAtPath( parentPath, item.label, item.position, - item.shortcut ?? "", + resolvedShortcut, ) } else { await NativeIRSystemMenuManager.addMenuItemAtPath( parentPath, item.label, - item.shortcut ?? "", + resolvedShortcut, ) } diff --git a/app/utils/useSystemMenu/useSystemMenu.windows.ts b/app/utils/useSystemMenu/useSystemMenu.windows.ts index 76440f7..beaf55a 100644 --- a/app/utils/useSystemMenu/useSystemMenu.windows.ts +++ b/app/utils/useSystemMenu/useSystemMenu.windows.ts @@ -60,13 +60,12 @@ * // across component unmounts. Actions are stored in actionsRef for execution. */ -import { useEffect, useRef, useCallback, useState } from "react" +import { useEffect, useRef, useCallback } from "react" import { useGlobal } from "../../state/useGlobal" +import { useShortcuts } from "../../contexts/ShortcutsContext" import { - SEPARATOR, type SystemMenuItem, type SystemMenuConfig, - type SystemMenuListEntry, type SystemMenuItemPressedEvent, type SystemMenuStructure, } from "./types" @@ -74,33 +73,21 @@ import { parsePathKey, joinPath, isSeparator } from "./utils" export function useSystemMenu(config?: SystemMenuConfig) { const actionsRef = useRef void>>(new Map()) + const { registerShortcut, unregisterShortcut, clearAllShortcuts } = useShortcuts() - // Global state for Windows menu persistence across component unmounts - const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( - "windows-menu-config", - null, - ) - const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( - "windows-menu-structure", - [], - ) - const [globalMenuItems, setGlobalMenuItems] = useGlobal>( - "windows-menu-items", - {}, - ) + const [globalMenuConfig, setGlobalMenuConfig] = useGlobal("windows-menu-config", null) + const [globalMenuStructure, setGlobalMenuStructure] = useGlobal("windows-menu-structure", []) + const [globalMenuItems, setGlobalMenuItems] = useGlobal>("windows-menu-items", {}) const handleMenuItemPressed = useCallback((event: SystemMenuItemPressedEvent) => { - const key = joinPath(event.menuPath) - const action = actionsRef.current.get(key) + const action = actionsRef.current.get(joinPath(event.menuPath)) if (action) action() }, []) const discoverMenus = useCallback(async () => { - const configToUse = config || globalMenuConfig - if (!configToUse?.items || !config || config === globalMenuConfig) return [] + if (!config?.items || config === globalMenuConfig) return [] - // Create a simple structure from config items for Windows titlebar rendering - const winStructure: SystemMenuStructure = Object.keys(configToUse.items).map((title) => ({ + const menuStructure: SystemMenuStructure = Object.keys(config.items).map(title => ({ title, enabled: true, path: [title], @@ -108,142 +95,138 @@ export function useSystemMenu(config?: SystemMenuConfig) { children: [], })) - // Update global state if we have a new config setGlobalMenuConfig(config) - setGlobalMenuStructure(winStructure) + setGlobalMenuStructure(menuStructure) setGlobalMenuItems(config.items as Record) - return [] // Windows doesn't have native menu discovery + return [] }, [config, globalMenuConfig, setGlobalMenuConfig, setGlobalMenuStructure, setGlobalMenuItems]) - const addMenuItem = useCallback( - async (parentKey: string, item: SystemMenuItem) => { - const leafPath = [parentKey, item.label] - const actionKey = joinPath(leafPath) + const addMenuItem = useCallback(async (parentKey: string, item: SystemMenuItem) => { + const actionKey = joinPath([parentKey, item.label]) - // Store action in memory for execution when menu item is pressed - if (item.action) { - actionsRef.current.set(actionKey, item.action) - } + if (item.action) { + actionsRef.current.set(actionKey, item.action) + } - // Add item to global state for UI rendering - setGlobalMenuItems((prev) => ({ + setGlobalMenuItems(prev => ({ + ...prev, + [parentKey]: [...(prev[parentKey] || []), item], + })) + }, [setGlobalMenuItems]) + + const removeMenuItemByName = useCallback(async (nameOrPath: string) => { + const path = parsePathKey(nameOrPath) + actionsRef.current.delete(joinPath(path)) + + if (path.length === 1) { + // Remove entire top-level menu + setGlobalMenuItems(prev => { + const { [path[0]]: _, ...rest } = prev + return rest + }) + } else if (path.length === 2) { + const [parentKey, itemLabel] = path + setGlobalMenuItems(prev => ({ ...prev, - [parentKey]: [...(prev[parentKey] || []), item], + [parentKey]: (prev[parentKey] || []).filter(item => item.label !== itemLabel), })) - }, - [setGlobalMenuItems], - ) - - const removeMenuItemByName = useCallback( - async (nameOrPath: string) => { - const path = parsePathKey(nameOrPath) - const key = joinPath(path) - // Remove action from memory - actionsRef.current.delete(key) - - // Remove from global state based on path depth - if (path.length === 1) { - // Remove entire top-level menu - setGlobalMenuItems((prev) => { - const updated = { ...prev } - delete updated[path[0]] - return updated - }) - } else if (path.length === 2) { - // Remove specific menu item - const [parentKey, itemLabel] = path - setGlobalMenuItems((prev) => ({ - ...prev, - [parentKey]: (prev[parentKey] || []).filter((item) => item.label !== itemLabel), - })) - } - }, - [setGlobalMenuItems], - ) + } + }, [setGlobalMenuItems, globalMenuItems]) - const setMenuItemEnabled = useCallback( - async (pathOrKey: string | string[], enabled: boolean) => { - const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) + const setMenuItemEnabled = useCallback(async (pathOrKey: string | string[], enabled: boolean) => { + const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) - // Update enabled state in global state (Windows only supports in-memory state updates) - if (path.length >= 2) { - const [parentKey, itemLabel] = path - setGlobalMenuItems((prev) => ({ - ...prev, - [parentKey]: (prev[parentKey] || []).map((item) => - item.label === itemLabel ? { ...item, enabled } : item, - ), - })) - } - }, - [setGlobalMenuItems], - ) + if (path.length >= 2) { + const [parentKey, itemLabel] = path + setGlobalMenuItems(prev => ({ + ...prev, + [parentKey]: (prev[parentKey] || []).map(item => + item.label === itemLabel ? { ...item, enabled } : item + ), + })) + } + }, [setGlobalMenuItems]) const getAllMenuPaths = useCallback(async (): Promise => { - const paths: string[] = [] - for (const [parentKey, entries] of Object.entries(globalMenuItems)) { - for (const entry of entries) { - if (!isSeparator(entry)) { - paths.push(joinPath([parentKey, entry.label])) - } - } - } - return paths + return Object.entries(globalMenuItems).flatMap(([parentKey, entries]) => + entries.filter(entry => !isSeparator(entry)) + .map(entry => joinPath([parentKey, entry.label])) + ) }, [globalMenuItems]) - // Update menus when config changes useEffect(() => { const updateMenus = async () => { - if (!config) return + if (!config?.items) return + + // Clear all existing actions and shortcuts first + actionsRef.current.clear() + clearAllShortcuts() - if (config.items) { - // Store actions in memory for execution - for (const [parentKey, entries] of Object.entries(config.items)) { - for (const entry of entries) { - if (!isSeparator(entry)) { - const item = entry as SystemMenuItem - const leafPath = [parentKey, item.label] - if (item.action) { - actionsRef.current.set(joinPath(leafPath), item.action) + Object.entries(config.items).forEach(([parentKey, entries]) => { + entries.forEach(entry => { + if (!isSeparator(entry)) { + const item = entry as SystemMenuItem + if (item.action) { + actionsRef.current.set(joinPath([parentKey, item.label]), item.action) + // Register shortcut if present + if (item.shortcut) { + const resolvedShortcut = typeof item.shortcut === "object" + ? item.shortcut.windows + : item.shortcut + if (resolvedShortcut) { + registerShortcut(resolvedShortcut, item.action) + } } } } - } - // Update global state for UI persistence - setGlobalMenuConfig(config) - setGlobalMenuItems(config.items as Record) - } + }) + }) + + setGlobalMenuConfig(config) + setGlobalMenuItems(config.items as Record) await discoverMenus() } updateMenus() - }, [config, setGlobalMenuConfig, setGlobalMenuItems, discoverMenus]) + }, [config, setGlobalMenuConfig, setGlobalMenuItems, discoverMenus, clearAllShortcuts, registerShortcut]) - // Restore actions from global state when no config is provided (component remount) + // This effect should only restore actions when there's no config but global config exists + // It should NOT run when config is provided useEffect(() => { - if (!config && globalMenuConfig?.items) { - // Restore actions from persisted global config - for (const [parentKey, entries] of Object.entries(globalMenuConfig.items)) { - for (const entry of entries) { - if (!isSeparator(entry)) { - const item = entry as SystemMenuItem - const leafPath = [parentKey, item.label] - if (item.action) { - actionsRef.current.set(joinPath(leafPath), item.action) + if (config || !globalMenuConfig?.items) return + + // Clear and re-register to ensure no duplicates + actionsRef.current.clear() + clearAllShortcuts() + + Object.entries(globalMenuConfig.items).forEach(([parentKey, entries]) => { + entries.forEach(entry => { + if (!isSeparator(entry)) { + const item = entry as SystemMenuItem + if (item.action) { + actionsRef.current.set(joinPath([parentKey, item.label]), item.action) + // Register shortcut if present + if (item.shortcut) { + const resolvedShortcut = typeof item.shortcut === "object" + ? item.shortcut.windows + : item.shortcut + if (resolvedShortcut) { + registerShortcut(resolvedShortcut, item.action) + } } } } - } - } - }, [config, globalMenuConfig]) + }) + }) + }, [config, globalMenuConfig, clearAllShortcuts, registerShortcut]) useEffect(() => { discoverMenus() }, [discoverMenus]) return { - availableMenus: [], // Windows doesn't have native menu discovery + availableMenus: [], menuStructure: globalMenuStructure, menuItems: globalMenuItems, discoverMenus, diff --git a/app/utils/useSystemMenu/utils.ts b/app/utils/useSystemMenu/utils.ts index a20a844..99b8936 100644 --- a/app/utils/useSystemMenu/utils.ts +++ b/app/utils/useSystemMenu/utils.ts @@ -3,11 +3,77 @@ import { SEPARATOR, type SystemMenuListEntry } from "./types" export const PATH_SEPARATOR = " > " export const parsePathKey = (key: string): string[] => - key - .split(PATH_SEPARATOR) - .map((s) => s.trim()) - .filter(Boolean) + key.split(PATH_SEPARATOR).map(s => s.trim()).filter(Boolean) -export const joinPath = (p: string[]) => p.join(PATH_SEPARATOR) +export const joinPath = (path: string[]): string => path.join(PATH_SEPARATOR) -export const isSeparator = (e: SystemMenuListEntry): e is typeof SEPARATOR => e === SEPARATOR \ No newline at end of file +export const isSeparator = (entry: SystemMenuListEntry): entry is typeof SEPARATOR => + entry === SEPARATOR + +export interface KeyCombination { + ctrl: boolean + alt: boolean + shift: boolean + cmd: boolean + key: string +} + +/** + * Parse a shortcut string like "ctrl+shift+n" into a key combination object + * @param shortcut - Shortcut string (e.g., "ctrl+n", "ctrl+shift+f", "alt+f4") + * @returns KeyCombination object for matching against keyboard events + */ +export function parseShortcut(shortcut: string): KeyCombination | null { + if (!shortcut?.trim()) return null + + const parts = shortcut.toLowerCase().split("+").map(s => s.trim()).filter(Boolean) + if (parts.length === 0) return null + + const combination: KeyCombination = { ctrl: false, alt: false, shift: false, cmd: false, key: "" } + + for (const part of parts) { + switch (part) { + case "ctrl": + case "control": + combination.ctrl = true + break + case "alt": + case "option": + combination.alt = true + break + case "shift": + combination.shift = true + break + case "cmd": + case "command": + case "win": + case "windows": + combination.cmd = true + break + default: + if (!combination.key) combination.key = part.toUpperCase() + break + } + } + + return combination.key ? combination : null +} + +/** + * Check if a keyboard event matches a key combination + * @param event - Keyboard event from native keyboard hook + * @param combination - Parsed key combination to match against + * @returns true if the event matches the combination + */ +export function matchesKeyCombo(event: any, combination: KeyCombination): boolean { + if (!event?.modifiers || !combination) return false + + const { modifiers } = event + return ( + modifiers.ctrl === combination.ctrl && + modifiers.alt === combination.alt && + modifiers.shift === combination.shift && + modifiers.cmd === combination.cmd && + event.key?.toUpperCase() === combination.key.toUpperCase() + ) +} \ No newline at end of file From 29df03024ea30fcebd1caaabb289b0185376c8db Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:27:38 -0500 Subject: [PATCH 26/40] accept an initialValue parameter --- app/state/useGlobal.windows.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/state/useGlobal.windows.ts b/app/state/useGlobal.windows.ts index cd22dd1..d8ebaab 100644 --- a/app/state/useGlobal.windows.ts +++ b/app/state/useGlobal.windows.ts @@ -110,7 +110,7 @@ function notify(id: Id) { * - Skips notifications if value is Object.is-equal (no-op update) * - Accepts `null` to reset/delete the value (used by imperative API) */ -function buildSetValue(id: Id, persist: boolean) { +function buildSetValue(id: Id, persist: boolean, initialValue?: T) { return (value: SetValue | null) => { const prev = globals.get(id) as T | undefined; @@ -123,14 +123,17 @@ function buildSetValue(id: Id, persist: boolean) { return; } - // Resolve functional updater + // Resolve functional updater - use prev if exists, otherwise use initialValue as fallback + const current = prev !== undefined ? prev : initialValue; const next = typeof value === "function" - ? (value as (prev: T) => T)(getSnapshot(id)) + ? (value as (prev: T) => T)(current as T) : value; + // Avoid unnecessary notifications/re-renders on no-op updates - if (Object.is(prev, next)) return; + // BUT: prev might be undefined while next is defined (first set) + if (prev !== undefined && Object.is(prev, next)) return; globals.set(id, next); @@ -184,7 +187,7 @@ export function useGlobal( }, [id]); // Memoize the setter; enforce non-null signature for hook users - const setAny = useCallback(buildSetValue(id, persist), [id, persist]); + const setAny = useCallback(buildSetValue(id, persist, initialValue), [id, persist, initialValue]); const set = useCallback<(v: SetValue) => void>((v) => setAny(v), [setAny]); return [value, set]; @@ -208,7 +211,7 @@ export function withGlobal( ): [T, (v: SetValue | null) => void] { // Imperative path can initialize synchronously without render concerns if (!globals.has(id)) globals.set(id, initialValue); - return [getSnapshot(id), buildSetValue(id, persist)]; + return [getSnapshot(id), buildSetValue(id, persist, initialValue)]; } /** @@ -258,7 +261,7 @@ export const getGlobal = (id: Id): T | undefined => * Set a global value without subscribing. (Non-null only.) */ export const setGlobal = (id: Id, v: SetValue): void => - buildSetValue(id, false)(v); + buildSetValue(id, false, undefined)(v); /** * Check whether a global key exists. From 3127ce352446e790577c841582656e7d28bcd31e Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:28:49 -0500 Subject: [PATCH 27/40] fix shortcut registration --- app/app.tsx | 113 +++--------------- app/components/Menu/MenuDropdownItem.tsx | 3 +- app/components/Menu/types.ts | 4 +- app/components/SystemMenu.tsx | 86 +++++++++++++ .../useSystemMenu/useSystemMenu.windows.ts | 63 +++------- 5 files changed, 126 insertions(+), 143 deletions(-) create mode 100644 app/components/SystemMenu.tsx diff --git a/app/app.tsx b/app/app.tsx index de7c9bf..a5a7412 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -4,23 +4,21 @@ * * @format */ -import { DevSettings, NativeModules, Pressable, StatusBar, View, Text, type ViewStyle } from "react-native" +import { StatusBar, View, type ViewStyle } from "react-native" import { connectToServer } from "./state/connectToServer" import { useTheme, themed } from "./theme/theme" -import { useEffect, useMemo } from "react" +import { useEffect } from "react" import { TimelineScreen } from "./screens/TimelineScreen" -import { useSystemMenu } from "./utils/useSystemMenu/useSystemMenu" import { Titlebar } from "./components/Titlebar/Titlebar" import { Sidebar } from "./components/Sidebar/Sidebar" -import { useSidebar } from "./state/useSidebar" import { AppHeader } from "./components/AppHeader" -import { useGlobal, withGlobal } from "./state/useGlobal" +import { useGlobal } from "./state/useGlobal" import { MenuItemId } from "./components/Sidebar/SidebarMenu" import { HelpScreen } from "./screens/HelpScreen" -import { TimelineItem } from "./types" import { PortalHost } from "./components/Portal" import { StateScreen } from "./screens/StateScreen" import { ShortcutsProvider } from "./contexts/ShortcutsContext" +import { SystemMenu } from "./components/SystemMenu" if (__DEV__) { // This is for debugging Reactotron with ... Reactotron! @@ -30,87 +28,11 @@ if (__DEV__) { function App(): React.JSX.Element { const { colors } = useTheme() - const { toggleSidebar } = useSidebar() - const [activeItem, setActiveItem] = useGlobal("sidebar-active-item", "logs", { + const [activeItem] = useGlobal("sidebar-active-item", "logs", { persist: true, }) - const [, setTimelineItems] = withGlobal("timelineItems", [], { - persist: true, - }) - - const menuConfig = useMemo( - () => ({ - remove: ["File", "Edit", "Format"], - items: { - View: [ - { - label: "Toggle Sidebar", - shortcut: { windows: "ctrl+b", macos: "cmd+b" }, - action: () => toggleSidebar(), - }, - { - label: "Logs Tab", - shortcut: { windows: "ctrl+1", macos: "cmd+1" }, - action: () => setActiveItem("logs"), - }, - { - label: "Network Tab", - shortcut: { windows: "ctrl+2", macos: "cmd+2" }, - action: () => setActiveItem("network"), - }, - { - label: "Performance Tab", - shortcut: { windows: "ctrl+3", macos: "cmd+3" }, - action: () => setActiveItem("performance"), - }, - { - label: "Plugins Tab", - shortcut: { windows: "ctrl+4", macos: "cmd+4" }, - action: () => setActiveItem("plugins"), - }, - { - label: "Help Tab", - shortcut: { windows: "ctrl+5", macos: "cmd+5" }, - action: () => setActiveItem("help"), - }, - ...(__DEV__ - ? [ - { - label: "Toggle Dev Menu", - shortcut: { windows: "ctrl+shift+d", macos: "cmd+shift+d" }, - action: () => NativeModules.DevMenu.show(), - }, - ] - : []), - ], - Window: [ - { - label: "Reload", - shortcut: { windows: "ctrl+shift+r", macos: "cmd+shift+r" }, - action: () => DevSettings.reload(), - }, - ], - Tools: [ - { - label: "Clear Timeline Items", - shortcut: { windows: "ctrl+k", macos: "cmd+k" }, - action: () => setTimelineItems([]), - }, - ], - }, - }), - [toggleSidebar, setActiveItem, setTimelineItems], - ) - useSystemMenu(menuConfig) - setTimeout(() => { - fetch("https://www.google.com") - .then((res) => res.text()) - .then((text) => { - console.tron.log("text", text) - }) - }, 1000) // Connect to the server when the app mounts. // This will update global state with the server's state @@ -130,21 +52,20 @@ function App(): React.JSX.Element { return ( - - - - - - - - {renderActiveItem()} - toggleSidebar()}> - Toggle Sidebar - + + + + + + + + + {renderActiveItem()} + + - - + ) } diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index 8628f1f..b1c31d0 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -70,7 +70,7 @@ const MenuDropdownItemComponent = ({ - {formatShortcut(item.shortcut)} + {formatShortcut(item.shortcut.windows || "")} )} {item.submenu && ( @@ -91,6 +91,7 @@ const MenuDropdownItemComponent = ({ export const MenuDropdownItem = memo(MenuDropdownItemComponent) function formatShortcut(shortcut: string): string { + if (!shortcut) return "" return shortcut .replace(/cmd/gi, "Ctrl") .replace(/shift/gi, "Shift") diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts index 486ef78..83b7c63 100644 --- a/app/components/Menu/types.ts +++ b/app/components/Menu/types.ts @@ -1,3 +1,5 @@ +import { PlatformShortcut } from "app/utils/useSystemMenu/types" + export interface Position { x: number y: number @@ -6,7 +8,7 @@ export interface Position { // Generic menu item interface for UI components export interface MenuItem { label: string - shortcut?: string + shortcut?: PlatformShortcut enabled?: boolean action?: () => void submenu?: (MenuItem | typeof MENU_SEPARATOR)[] diff --git a/app/components/SystemMenu.tsx b/app/components/SystemMenu.tsx new file mode 100644 index 0000000..70b0111 --- /dev/null +++ b/app/components/SystemMenu.tsx @@ -0,0 +1,86 @@ +import { useMemo } from "react" +import { DevSettings, NativeModules } from "react-native" +import { useSidebar } from "../state/useSidebar" +import { useGlobal, withGlobal } from "../state/useGlobal" +import { useSystemMenu } from "../utils/useSystemMenu/useSystemMenu" +import { TimelineItem } from "app/types" +import { MenuItemId } from "./Sidebar/SidebarMenu" + +export function SystemMenu({ children }: { children: React.ReactNode }) { + const { toggleSidebar } = useSidebar() + const [_, setActiveItem] = useGlobal("sidebar-active-item", "logs", { + persist: true, + }) + const [__, setTimelineItems] = withGlobal("timelineItems", [], { + persist: true, + }) + + const menuConfig = useMemo( + () => ({ + remove: ["File", "Edit", "Format"], + items: { + View: [ + { + label: "Toggle Sidebar", + shortcut: { windows: "ctrl+b", macos: "cmd+b" }, + action: () => toggleSidebar(), + }, + { + label: "Logs Tab", + shortcut: { windows: "ctrl+1", macos: "cmd+1" }, + action: () => setActiveItem("logs"), + }, + { + label: "Network Tab", + shortcut: { windows: "ctrl+2", macos: "cmd+2" }, + action: () => setActiveItem("network"), + }, + { + label: "Performance Tab", + shortcut: { windows: "ctrl+3", macos: "cmd+3" }, + action: () => setActiveItem("performance"), + }, + { + label: "Plugins Tab", + shortcut: { windows: "ctrl+4", macos: "cmd+4" }, + action: () => setActiveItem("plugins"), + }, + { + label: "Help Tab", + shortcut: { windows: "ctrl+5", macos: "cmd+5" }, + action: () => setActiveItem("help"), + }, + ...(__DEV__ + ? [ + { + label: "Toggle Dev Menu", + shortcut: { windows: "ctrl+shift+d", macos: "cmd+shift+d" }, + action: () => NativeModules.DevMenu.show(), + }, + ] + : []), + ], + Window: [ + { + label: "Reload", + shortcut: { windows: "ctrl+shift+r", macos: "cmd+shift+r" }, + action: () => DevSettings.reload(), + }, + ], + Tools: [ + { + label: "Clear Timeline Items", + shortcut: { windows: "ctrl+k", macos: "cmd+k" }, + action: () => setTimelineItems([]), + }, + ], + }, + }), + [toggleSidebar, setActiveItem, setTimelineItems], + ) + + useSystemMenu(menuConfig) + + return <>{children} +} + diff --git a/app/utils/useSystemMenu/useSystemMenu.windows.ts b/app/utils/useSystemMenu/useSystemMenu.windows.ts index beaf55a..b632833 100644 --- a/app/utils/useSystemMenu/useSystemMenu.windows.ts +++ b/app/utils/useSystemMenu/useSystemMenu.windows.ts @@ -156,51 +156,13 @@ export function useSystemMenu(config?: SystemMenuConfig) { }, [globalMenuItems]) useEffect(() => { - const updateMenus = async () => { - if (!config?.items) return - - // Clear all existing actions and shortcuts first - actionsRef.current.clear() - clearAllShortcuts() - - Object.entries(config.items).forEach(([parentKey, entries]) => { - entries.forEach(entry => { - if (!isSeparator(entry)) { - const item = entry as SystemMenuItem - if (item.action) { - actionsRef.current.set(joinPath([parentKey, item.label]), item.action) - // Register shortcut if present - if (item.shortcut) { - const resolvedShortcut = typeof item.shortcut === "object" - ? item.shortcut.windows - : item.shortcut - if (resolvedShortcut) { - registerShortcut(resolvedShortcut, item.action) - } - } - } - } - }) - }) - - setGlobalMenuConfig(config) - setGlobalMenuItems(config.items as Record) - await discoverMenus() - } + if (!config?.items) return - updateMenus() - }, [config, setGlobalMenuConfig, setGlobalMenuItems, discoverMenus, clearAllShortcuts, registerShortcut]) - - // This effect should only restore actions when there's no config but global config exists - // It should NOT run when config is provided - useEffect(() => { - if (config || !globalMenuConfig?.items) return - - // Clear and re-register to ensure no duplicates + // Clear all existing actions and shortcuts first (only on initial mount) actionsRef.current.clear() clearAllShortcuts() - Object.entries(globalMenuConfig.items).forEach(([parentKey, entries]) => { + Object.entries(config.items).forEach(([parentKey, entries]) => { entries.forEach(entry => { if (!isSeparator(entry)) { const item = entry as SystemMenuItem @@ -219,11 +181,22 @@ export function useSystemMenu(config?: SystemMenuConfig) { } }) }) - }, [config, globalMenuConfig, clearAllShortcuts, registerShortcut]) - useEffect(() => { - discoverMenus() - }, [discoverMenus]) + // Update global state directly without calling discoverMenus to avoid redundancy + const menuStructure: SystemMenuStructure = Object.keys(config.items).map(title => ({ + title, + enabled: true, + path: [title], + items: [], + children: [], + })) + + setGlobalMenuConfig(config) + setGlobalMenuStructure(menuStructure) + setGlobalMenuItems(config.items as Record) + }, []) + + return { availableMenus: [], From 9532238c96fcc345114a14c5959804f34758c82c Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:33:57 -0500 Subject: [PATCH 28/40] update menu config --- app/components/SystemMenu.tsx | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/app/components/SystemMenu.tsx b/app/components/SystemMenu.tsx index 70b0111..4e99a24 100644 --- a/app/components/SystemMenu.tsx +++ b/app/components/SystemMenu.tsx @@ -1,10 +1,11 @@ -import { useMemo } from "react" +import { useMemo, useState } from "react" import { DevSettings, NativeModules } from "react-native" import { useSidebar } from "../state/useSidebar" import { useGlobal, withGlobal } from "../state/useGlobal" import { useSystemMenu } from "../utils/useSystemMenu/useSystemMenu" import { TimelineItem } from "app/types" import { MenuItemId } from "./Sidebar/SidebarMenu" +import { AboutModal } from "./AboutModal" export function SystemMenu({ children }: { children: React.ReactNode }) { const { toggleSidebar } = useSidebar() @@ -14,47 +15,60 @@ export function SystemMenu({ children }: { children: React.ReactNode }) { const [__, setTimelineItems] = withGlobal("timelineItems", [], { persist: true, }) + const [aboutVisible, setAboutVisible] = useState(false) const menuConfig = useMemo( () => ({ - remove: ["File", "Edit", "Format"], + remove: ["File", "Edit", "Format", "Reactotron > About Reactotron"], items: { + Reactotron: [ + { + label: "About Reactotron", + position: 0, + action: () => setAboutVisible(true), + }, + ], View: [ { label: "Toggle Sidebar", - shortcut: { windows: "ctrl+b", macos: "cmd+b" }, - action: () => toggleSidebar(), + shortcut: "cmd+b", + action: toggleSidebar, }, { label: "Logs Tab", - shortcut: { windows: "ctrl+1", macos: "cmd+1" }, + shortcut: "cmd+1", action: () => setActiveItem("logs"), }, { label: "Network Tab", - shortcut: { windows: "ctrl+2", macos: "cmd+2" }, + shortcut: "cmd+2", action: () => setActiveItem("network"), }, { label: "Performance Tab", - shortcut: { windows: "ctrl+3", macos: "cmd+3" }, + shortcut: "cmd+3", action: () => setActiveItem("performance"), }, { label: "Plugins Tab", - shortcut: { windows: "ctrl+4", macos: "cmd+4" }, + shortcut: "cmd+4", action: () => setActiveItem("plugins"), }, + { + label: "Custom Commands Tab", + shortcut: "cmd+5", + action: () => setActiveItem("customCommands"), + }, { label: "Help Tab", - shortcut: { windows: "ctrl+5", macos: "cmd+5" }, + shortcut: "cmd+6", action: () => setActiveItem("help"), }, ...(__DEV__ ? [ { label: "Toggle Dev Menu", - shortcut: { windows: "ctrl+shift+d", macos: "cmd+shift+d" }, + shortcut: "cmd+shift+d", action: () => NativeModules.DevMenu.show(), }, ] @@ -63,24 +77,26 @@ export function SystemMenu({ children }: { children: React.ReactNode }) { Window: [ { label: "Reload", - shortcut: { windows: "ctrl+shift+r", macos: "cmd+shift+r" }, + shortcut: "cmd+shift+r", action: () => DevSettings.reload(), }, ], Tools: [ { label: "Clear Timeline Items", - shortcut: { windows: "ctrl+k", macos: "cmd+k" }, + shortcut: "cmd+k", action: () => setTimelineItems([]), }, ], }, }), - [toggleSidebar, setActiveItem, setTimelineItems], + [toggleSidebar, setActiveItem], ) useSystemMenu(menuConfig) - return <>{children} + return (<>{children} + + setAboutVisible(false)} />) } From ad64da635e0f234c2811c0a753dafc2de97a0f63 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:37:34 -0500 Subject: [PATCH 29/40] Update SystemMenu shortcuts to be platform-specific for macOS and Windows --- app/components/SystemMenu.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/SystemMenu.tsx b/app/components/SystemMenu.tsx index 4e99a24..13750c6 100644 --- a/app/components/SystemMenu.tsx +++ b/app/components/SystemMenu.tsx @@ -31,44 +31,44 @@ export function SystemMenu({ children }: { children: React.ReactNode }) { View: [ { label: "Toggle Sidebar", - shortcut: "cmd+b", + shortcut: { macos: "cmd+b", windows: "ctrl+b" }, action: toggleSidebar, }, { label: "Logs Tab", - shortcut: "cmd+1", + shortcut: { macos: "cmd+1", windows: "ctrl+1" }, action: () => setActiveItem("logs"), }, { label: "Network Tab", - shortcut: "cmd+2", + shortcut: { macos: "cmd+2", windows: "ctrl+2" }, action: () => setActiveItem("network"), }, { label: "Performance Tab", - shortcut: "cmd+3", + shortcut: { macos: "cmd+3", windows: "ctrl+3" }, action: () => setActiveItem("performance"), }, { label: "Plugins Tab", - shortcut: "cmd+4", + shortcut: { macos: "cmd+4", windows: "ctrl+4" }, action: () => setActiveItem("plugins"), }, { label: "Custom Commands Tab", - shortcut: "cmd+5", + shortcut: { macos: "cmd+5", windows: "ctrl+5" }, action: () => setActiveItem("customCommands"), }, { label: "Help Tab", - shortcut: "cmd+6", + shortcut: { macos: "cmd+6", windows: "ctrl+6" }, action: () => setActiveItem("help"), }, ...(__DEV__ ? [ { label: "Toggle Dev Menu", - shortcut: "cmd+shift+d", + shortcut: { macos: "cmd+shift+d", windows: "ctrl+shift+d" }, action: () => NativeModules.DevMenu.show(), }, ] @@ -77,14 +77,14 @@ export function SystemMenu({ children }: { children: React.ReactNode }) { Window: [ { label: "Reload", - shortcut: "cmd+shift+r", + shortcut: { macos: "cmd+shift+r", windows: "ctrl+shift+r" }, action: () => DevSettings.reload(), }, ], Tools: [ { label: "Clear Timeline Items", - shortcut: "cmd+k", + shortcut: { macos: "cmd+k", windows: "ctrl+k" }, action: () => setTimelineItems([]), }, ], From 69d737f05ffb2b3a8cff69ba4ce99504e9d1617c Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:51:20 -0500 Subject: [PATCH 30/40] Fix linting issues --- app/app.tsx | 4 - app/components/Menu/MenuDropdown.tsx | 71 ++++--- app/components/Menu/MenuDropdownItem.tsx | 26 +-- app/components/Menu/MenuOverlay.tsx | 12 +- app/components/Menu/menuSettings.ts | 4 +- app/components/Menu/types.ts | 2 +- app/components/Menu/useMenuPositioning.ts | 22 ++- app/components/Menu/useSubmenuState.ts | 38 ++-- app/components/SystemMenu.tsx | 177 +++++++++--------- app/components/Titlebar/TitlebarMenu.tsx | 11 +- app/components/Titlebar/TitlebarMenuItem.tsx | 34 ++-- app/contexts/ShortcutsContext.tsx | 84 +++++---- app/state/useGlobal.ts | 2 +- app/state/useGlobal.windows.ts | 129 ++++++------- app/utils/system.ts | 2 +- app/utils/useSystemMenu/types.ts | 6 +- app/utils/useSystemMenu/useSystemMenu.ts | 15 +- .../useSystemMenu/useSystemMenu.windows.ts | 130 +++++++------ app/utils/useSystemMenu/utils.ts | 20 +- 19 files changed, 408 insertions(+), 381 deletions(-) diff --git a/app/app.tsx b/app/app.tsx index c1c23eb..d6d6846 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -24,10 +24,6 @@ function App(): React.JSX.Element { const { colors } = useTheme() const [activeItem] = useGlobal("sidebar-active-item", "logs") - - - - // Connect to the server when the app mounts. // This will update global state with the server's state // and handle all websocket events. diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx index 19e998a..0f1a9d0 100644 --- a/app/components/Menu/MenuDropdown.tsx +++ b/app/components/Menu/MenuDropdown.tsx @@ -1,4 +1,4 @@ -import { View, type ViewStyle, type TextStyle } from "react-native" +import { View, type ViewStyle } from "react-native" import { useRef, useMemo, memo } from "react" import { themed } from "../../theme/theme" import { Portal } from "../Portal" @@ -16,15 +16,8 @@ interface MenuDropdownProps { isSubmenu?: boolean } -const MenuDropdownComponent = ({ - items, - position, - onItemPress, - isSubmenu, -}: MenuDropdownProps) => { - const portalName = useRef( - `${isSubmenu ? 'submenu' : 'dropdown'}-${getUUID()}` - ).current +const MenuDropdownComponent = ({ items, position, onItemPress, isSubmenu }: MenuDropdownProps) => { + const portalName = useRef(`${isSubmenu ? "submenu" : "dropdown"}-${getUUID()}`).current const { openSubmenu, submenuPosition, handleItemHover } = useSubmenuState(position) const isSeparator = (item: MenuItem | typeof MENU_SEPARATOR): item is typeof MENU_SEPARATOR => { @@ -33,38 +26,38 @@ const MenuDropdownComponent = ({ // Find the submenu item if one is open const submenuItem = openSubmenu - ? items.find(item => !isSeparator(item) && item.label === openSubmenu) as DropdownMenuItem | undefined + ? (items.find((item) => !isSeparator(item) && item.label === openSubmenu) as + | DropdownMenuItem + | undefined) : undefined - const dropdownContent = useMemo(() => ( - - {items.map((item, index) => { - if (isSeparator(item)) return + const dropdownContent = useMemo( + () => ( + + {items.map((item, index) => { + if (isSeparator(item)) return - return ( - - ) - })} - - ), [items, isSubmenu, position.x, position.y, onItemPress, handleItemHover]) + return ( + + ) + })} + + ), + [items, isSubmenu, position.x, position.y, onItemPress, handleItemHover], + ) return ( <> - - {dropdownContent} - + {dropdownContent} {/* Render submenu */} {submenuItem?.submenu && ( (({ colors, spacing }) => ({ zIndex: menuSettings.zIndex.submenu, })) - +const $menuPosition = (position: Position, isSubmenu: boolean | undefined) => ({ + left: position.x, + top: position.y, + zIndex: isSubmenu ? 10001 : 10000, +}) diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index b1c31d0..3c602e1 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -35,7 +35,7 @@ const MenuDropdownItemComponent = ({ const handleHoverOut = useCallback(() => { // Use a small timeout to prevent flickering between items hoverTimeoutRef.current = setTimeout(() => { - setHoveredItem((current) => current === item.label ? null : current) + setHoveredItem((current) => (current === item.label ? null : current)) }, 10) }, [item.label]) @@ -53,35 +53,21 @@ const MenuDropdownItemComponent = ({ disabled={!enabled} style={({ pressed }) => [ $dropdownItem(), - ((pressed || hoveredItem === item.label) && enabled) && $dropdownItemHovered(), + (pressed || hoveredItem === item.label) && enabled && $dropdownItemHovered(), !enabled && $dropdownItemDisabled, ]} > - + {item.label} {item.shortcut && ( - + {formatShortcut(item.shortcut.windows || "")} )} {item.submenu && ( - - ▶ - + )} @@ -143,4 +129,4 @@ const $submenuArrow = themed(({ colors, typography, spacing }) => ({ const $rightContent: ViewStyle = { flexDirection: "row", alignItems: "center", -} \ No newline at end of file +} diff --git a/app/components/Menu/MenuOverlay.tsx b/app/components/Menu/MenuOverlay.tsx index 65e6768..cf2b967 100644 --- a/app/components/Menu/MenuOverlay.tsx +++ b/app/components/Menu/MenuOverlay.tsx @@ -16,11 +16,10 @@ interface MenuOverlayProps { export const MenuOverlay = ({ onPress, - portalName = 'menu-overlay', + portalName = "menu-overlay", style, excludeArea, }: MenuOverlayProps) => { - return ( @@ -29,11 +28,14 @@ export const MenuOverlay = ({ } interface OverlayStyleArgs { - excludeArea?: { top?: number, left?: number, right?: number, bottom?: number } + excludeArea?: { top?: number; left?: number; right?: number; bottom?: number } style?: ViewStyle } -const overlayStyle: (args: OverlayStyleArgs) => ViewStyle = ({ excludeArea, style }: OverlayStyleArgs) => ({ +const overlayStyle: (args: OverlayStyleArgs) => ViewStyle = ({ + excludeArea, + style, +}: OverlayStyleArgs) => ({ position: "absolute", top: excludeArea?.top ?? 0, left: excludeArea?.left ?? 0, @@ -41,4 +43,4 @@ const overlayStyle: (args: OverlayStyleArgs) => ViewStyle = ({ excludeArea, styl bottom: excludeArea?.bottom ?? 0, zIndex: menuSettings.zIndex.menuOverlay, ...style, -}) \ No newline at end of file +}) diff --git a/app/components/Menu/menuSettings.ts b/app/components/Menu/menuSettings.ts index 79a034f..abe0a90 100644 --- a/app/components/Menu/menuSettings.ts +++ b/app/components/Menu/menuSettings.ts @@ -9,5 +9,5 @@ export const menuSettings = { menuOverlay: 9999, dropdown: 10000, submenu: 10001, - } -} as const \ No newline at end of file + }, +} as const diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts index 83b7c63..0b02bed 100644 --- a/app/components/Menu/types.ts +++ b/app/components/Menu/types.ts @@ -18,4 +18,4 @@ export interface MenuItem { export type DropdownMenuItem = MenuItem // Menu separator constant -export const MENU_SEPARATOR = 'menu-item-separator' as const \ No newline at end of file +export const MENU_SEPARATOR = "menu-item-separator" as const diff --git a/app/components/Menu/useMenuPositioning.ts b/app/components/Menu/useMenuPositioning.ts index cac99d6..150c5ff 100644 --- a/app/components/Menu/useMenuPositioning.ts +++ b/app/components/Menu/useMenuPositioning.ts @@ -6,22 +6,26 @@ export interface PositioningStrategy { calculateSubmenuPosition: ( basePosition: Position, itemIndex: number, - parentWidth?: number + parentWidth?: number, ) => Position calculateContextMenuPosition?: ( clickPosition: Position, menuSize?: { width: number; height: number }, - screenSize?: { width: number; height: number } + screenSize?: { width: number; height: number }, ) => Position } const defaultStrategy: PositioningStrategy = { - calculateSubmenuPosition: (basePosition, itemIndex, parentWidth = menuSettings.submenuOffsetX) => ({ + calculateSubmenuPosition: ( + basePosition, + itemIndex, + parentWidth = menuSettings.submenuOffsetX, + ) => ({ x: basePosition.x + parentWidth, y: basePosition.y + itemIndex * menuSettings.itemHeight + menuSettings.submenuOffsetY, }), - calculateContextMenuPosition: (clickPosition, menuSize, screenSize) => { + calculateContextMenuPosition: (clickPosition: Position) => { // Basic positioning - can be enhanced for screen edge detection return { x: clickPosition.x, @@ -34,17 +38,17 @@ export const useMenuPositioning = (strategy: PositioningStrategy = defaultStrate const calculateSubmenuPosition = useCallback( (basePosition: Position, itemIndex: number, parentWidth?: number) => strategy.calculateSubmenuPosition(basePosition, itemIndex, parentWidth), - [strategy] + [strategy], ) const calculateContextMenuPosition = useCallback( - (clickPosition: Position, menuSize?: { width: number; height: number }, screenSize?: { width: number; height: number }) => - strategy.calculateContextMenuPosition?.(clickPosition, menuSize, screenSize) ?? clickPosition, - [strategy] + (clickPosition: Position) => + strategy.calculateContextMenuPosition?.(clickPosition) ?? clickPosition, + [strategy], ) return { calculateSubmenuPosition, calculateContextMenuPosition, } -} \ No newline at end of file +} diff --git a/app/components/Menu/useSubmenuState.ts b/app/components/Menu/useSubmenuState.ts index 1c105ca..4cb5778 100644 --- a/app/components/Menu/useSubmenuState.ts +++ b/app/components/Menu/useSubmenuState.ts @@ -6,27 +6,33 @@ export const useSubmenuState = (basePosition: Position) => { const [openSubmenu, setOpenSubmenu] = useState(null) const [submenuPosition, setSubmenuPosition] = useState({ x: 0, y: 0 }) - const openSubmenuAt = useCallback((itemLabel: string, index: number) => { - setOpenSubmenu(itemLabel) - setSubmenuPosition({ - x: basePosition.x + menuSettings.submenuOffsetX, - y: basePosition.y + index * menuSettings.itemHeight + menuSettings.submenuOffsetY, - }) - }, [basePosition.x, basePosition.y]) + const openSubmenuAt = useCallback( + (itemLabel: string, index: number) => { + setOpenSubmenu(itemLabel) + setSubmenuPosition({ + x: basePosition.x + menuSettings.submenuOffsetX, + y: basePosition.y + index * menuSettings.itemHeight + menuSettings.submenuOffsetY, + }) + }, + [basePosition.x, basePosition.y], + ) const closeSubmenu = useCallback(() => { setOpenSubmenu(null) }, []) - const handleItemHover = useCallback((itemLabel: string, index: number, hasSubmenu: boolean) => { - if (hasSubmenu) { - openSubmenuAt(itemLabel, index) - } else { - if (openSubmenu) { - closeSubmenu() + const handleItemHover = useCallback( + (itemLabel: string, index: number, hasSubmenu: boolean) => { + if (hasSubmenu) { + openSubmenuAt(itemLabel, index) + } else { + if (openSubmenu) { + closeSubmenu() + } } - } - }, [openSubmenu, openSubmenuAt, closeSubmenu]) + }, + [openSubmenu, openSubmenuAt, closeSubmenu], + ) return { openSubmenu, @@ -34,4 +40,4 @@ export const useSubmenuState = (basePosition: Position) => { handleItemHover, closeSubmenu, } -} \ No newline at end of file +} diff --git a/app/components/SystemMenu.tsx b/app/components/SystemMenu.tsx index 13750c6..b2b0bae 100644 --- a/app/components/SystemMenu.tsx +++ b/app/components/SystemMenu.tsx @@ -8,95 +8,98 @@ import { MenuItemId } from "./Sidebar/SidebarMenu" import { AboutModal } from "./AboutModal" export function SystemMenu({ children }: { children: React.ReactNode }) { - const { toggleSidebar } = useSidebar() - const [_, setActiveItem] = useGlobal("sidebar-active-item", "logs", { - persist: true, - }) - const [__, setTimelineItems] = withGlobal("timelineItems", [], { - persist: true, - }) - const [aboutVisible, setAboutVisible] = useState(false) + const { toggleSidebar } = useSidebar() + const [_, setActiveItem] = useGlobal("sidebar-active-item", "logs", { + persist: true, + }) + const [__, setTimelineItems] = withGlobal("timelineItems", [], { + persist: true, + }) + const [aboutVisible, setAboutVisible] = useState(false) - const menuConfig = useMemo( - () => ({ - remove: ["File", "Edit", "Format", "Reactotron > About Reactotron"], - items: { - Reactotron: [ - { - label: "About Reactotron", - position: 0, - action: () => setAboutVisible(true), - }, - ], - View: [ - { - label: "Toggle Sidebar", - shortcut: { macos: "cmd+b", windows: "ctrl+b" }, - action: toggleSidebar, - }, - { - label: "Logs Tab", - shortcut: { macos: "cmd+1", windows: "ctrl+1" }, - action: () => setActiveItem("logs"), - }, - { - label: "Network Tab", - shortcut: { macos: "cmd+2", windows: "ctrl+2" }, - action: () => setActiveItem("network"), - }, - { - label: "Performance Tab", - shortcut: { macos: "cmd+3", windows: "ctrl+3" }, - action: () => setActiveItem("performance"), - }, - { - label: "Plugins Tab", - shortcut: { macos: "cmd+4", windows: "ctrl+4" }, - action: () => setActiveItem("plugins"), - }, - { - label: "Custom Commands Tab", - shortcut: { macos: "cmd+5", windows: "ctrl+5" }, - action: () => setActiveItem("customCommands"), - }, - { - label: "Help Tab", - shortcut: { macos: "cmd+6", windows: "ctrl+6" }, - action: () => setActiveItem("help"), - }, - ...(__DEV__ - ? [ - { - label: "Toggle Dev Menu", - shortcut: { macos: "cmd+shift+d", windows: "ctrl+shift+d" }, - action: () => NativeModules.DevMenu.show(), - }, - ] - : []), - ], - Window: [ - { - label: "Reload", - shortcut: { macos: "cmd+shift+r", windows: "ctrl+shift+r" }, - action: () => DevSettings.reload(), - }, - ], - Tools: [ - { - label: "Clear Timeline Items", - shortcut: { macos: "cmd+k", windows: "ctrl+k" }, - action: () => setTimelineItems([]), - }, - ], - }, - }), - [toggleSidebar, setActiveItem], - ) + const menuConfig = useMemo( + () => ({ + remove: ["File", "Edit", "Format", "Reactotron > About Reactotron"], + items: { + Reactotron: [ + { + label: "About Reactotron", + position: 0, + action: () => setAboutVisible(true), + }, + ], + View: [ + { + label: "Toggle Sidebar", + shortcut: { macos: "cmd+b", windows: "ctrl+b" }, + action: toggleSidebar, + }, + { + label: "Logs Tab", + shortcut: { macos: "cmd+1", windows: "ctrl+1" }, + action: () => setActiveItem("logs"), + }, + { + label: "Network Tab", + shortcut: { macos: "cmd+2", windows: "ctrl+2" }, + action: () => setActiveItem("network"), + }, + { + label: "Performance Tab", + shortcut: { macos: "cmd+3", windows: "ctrl+3" }, + action: () => setActiveItem("performance"), + }, + { + label: "Plugins Tab", + shortcut: { macos: "cmd+4", windows: "ctrl+4" }, + action: () => setActiveItem("plugins"), + }, + { + label: "Custom Commands Tab", + shortcut: { macos: "cmd+5", windows: "ctrl+5" }, + action: () => setActiveItem("customCommands"), + }, + { + label: "Help Tab", + shortcut: { macos: "cmd+6", windows: "ctrl+6" }, + action: () => setActiveItem("help"), + }, + ...(__DEV__ + ? [ + { + label: "Toggle Dev Menu", + shortcut: { macos: "cmd+shift+d", windows: "ctrl+shift+d" }, + action: () => NativeModules.DevMenu.show(), + }, + ] + : []), + ], + Window: [ + { + label: "Reload", + shortcut: { macos: "cmd+shift+r", windows: "ctrl+shift+r" }, + action: () => DevSettings.reload(), + }, + ], + Tools: [ + { + label: "Clear Timeline Items", + shortcut: { macos: "cmd+k", windows: "ctrl+k" }, + action: () => setTimelineItems([]), + }, + ], + }, + }), + [toggleSidebar, setActiveItem], + ) - useSystemMenu(menuConfig) + useSystemMenu(menuConfig) - return (<>{children} + return ( + <> + {children} - setAboutVisible(false)} />) + setAboutVisible(false)} /> + + ) } - diff --git a/app/components/Titlebar/TitlebarMenu.tsx b/app/components/Titlebar/TitlebarMenu.tsx index 60b48ce..6b729a2 100644 --- a/app/components/Titlebar/TitlebarMenu.tsx +++ b/app/components/Titlebar/TitlebarMenu.tsx @@ -4,7 +4,7 @@ import { themed } from "../../theme/theme" import { TitlebarMenuItem } from "./TitlebarMenuItem" import { MenuDropdown } from "../Menu/MenuDropdown" import { MenuOverlay } from "../Menu/MenuOverlay" -import type { Position } from "../Menu/types" +import type { DropdownMenuItem, Position } from "../Menu/types" import { PassthroughView } from "./PassthroughView" import { useSystemMenu } from "../../utils/useSystemMenu/useSystemMenu" @@ -55,12 +55,9 @@ export const TitlebarMenu = () => { {openMenu && menuItems[openMenu] && ( <> {/* Single overlay for all menu interactions */} - + { handleMenuItemPressed({ menuPath: [openMenu, item.label] }) @@ -78,4 +75,4 @@ const $menuBar = themed(() => ({ alignItems: "center", height: "100%", paddingHorizontal: 4, -})) \ No newline at end of file +})) diff --git a/app/components/Titlebar/TitlebarMenuItem.tsx b/app/components/Titlebar/TitlebarMenuItem.tsx index 2fd03ce..b8d21e6 100644 --- a/app/components/Titlebar/TitlebarMenuItem.tsx +++ b/app/components/Titlebar/TitlebarMenuItem.tsx @@ -10,17 +10,26 @@ interface TitlebarMenuItemProps { onHoverOut?: () => void } -export const TitlebarMenuItem = ({ title, isOpen, onPress, onHoverIn, onHoverOut }: TitlebarMenuItemProps) => { +export const TitlebarMenuItem = ({ + title, + isOpen, + onPress, + onHoverIn, + onHoverOut, +}: TitlebarMenuItemProps) => { const [isHovered, setIsHovered] = useState(false) - const handleHover = useCallback((isHovered: boolean) => { - setIsHovered(isHovered) - if (isHovered) { - onHoverIn?.() - } else { - onHoverOut?.() - } - }, [onHoverIn, onHoverOut]) + const handleHover = useCallback( + (isHovered: boolean) => { + setIsHovered(isHovered) + if (isHovered) { + onHoverIn?.() + } else { + onHoverOut?.() + } + }, + [onHoverIn, onHoverOut], + ) return ( { handleHover(false) }} - style={({ pressed }) => [ - $menuItem(), - (pressed || isOpen || isHovered) && $menuItemHovered(), - ]} + style={({ pressed }) => [$menuItem(), (pressed || isOpen || isHovered) && $menuItemHovered()]} > {title} @@ -57,4 +63,4 @@ const $menuItemText = themed(({ colors, typography }) => ({ color: colors.mainText, fontSize: typography.caption, fontWeight: "400", -})) \ No newline at end of file +})) diff --git a/app/contexts/ShortcutsContext.tsx b/app/contexts/ShortcutsContext.tsx index c6a3407..c69cd2e 100644 --- a/app/contexts/ShortcutsContext.tsx +++ b/app/contexts/ShortcutsContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useCallback } from "react" +import { createContext, useContext, useCallback } from "react" import { useGlobal } from "../state/useGlobal" import { useKeyboardEvents } from "../utils/system" import { parseShortcut, matchesKeyCombo, type KeyCombination } from "../utils/useSystemMenu/utils" @@ -17,53 +17,65 @@ const ShortcutsContext = createContext(null) export function ShortcutsProvider({ children }: { children: React.ReactNode }) { const [shortcuts, setShortcuts] = useGlobal("global-shortcuts", {}) - const [combinations, setCombinations] = useGlobal("global-shortcut-combinations", {}) + const [combinations, setCombinations] = useGlobal( + "global-shortcut-combinations", + {}, + ) - const registerShortcut = useCallback((shortcut: string, action: () => void) => { - if (!shortcut || !action) return + const registerShortcut = useCallback( + (shortcut: string, action: () => void) => { + if (!shortcut || !action) return - const combination = parseShortcut(shortcut) - if (!combination) { - console.warn(`Invalid shortcut format: ${shortcut}`) - return - } + const combination = parseShortcut(shortcut) + if (!combination) { + console.warn(`Invalid shortcut format: ${shortcut}`) + return + } - // Register globally (will overwrite if already exists - automatic deduplication!) - setShortcuts(prev => ({ ...prev, [shortcut]: action })) - setCombinations(prev => ({ ...prev, [shortcut]: combination })) - }, [setShortcuts, setCombinations]) + // Register globally (will overwrite if already exists - automatic deduplication!) + setShortcuts((prev) => ({ ...prev, [shortcut]: action })) + setCombinations((prev) => ({ ...prev, [shortcut]: combination })) + }, + [setShortcuts, setCombinations], + ) - const unregisterShortcut = useCallback((shortcut: string) => { - setShortcuts(prev => { - const { [shortcut]: _, ...rest } = prev - return rest - }) - setCombinations(prev => { - const { [shortcut]: _, ...rest } = prev - return rest - }) - }, [setShortcuts, setCombinations]) + const unregisterShortcut = useCallback( + (shortcut: string) => { + setShortcuts((prev) => { + const { [shortcut]: _, ...rest } = prev + return rest + }) + setCombinations((prev) => { + const { [shortcut]: _, ...rest } = prev + return rest + }) + }, + [setShortcuts, setCombinations], + ) const clearAllShortcuts = useCallback(() => { setShortcuts({}) setCombinations({}) }, [setShortcuts, setCombinations]) - const handleKeyboardEvent = useCallback((event: any) => { - // Only handle keydown events - if (event.type !== "keydown") return + const handleKeyboardEvent = useCallback( + (event: any) => { + // Only handle keydown events + if (event.type !== "keydown") return - // Check all registered shortcuts for a match - for (const [shortcut, combination] of Object.entries(combinations)) { - if (matchesKeyCombo(event, combination)) { - const action = shortcuts[shortcut] - if (action) { - action() - return // Stop after first match + // Check all registered shortcuts for a match + for (const [shortcut, combination] of Object.entries(combinations)) { + if (matchesKeyCombo(event, combination)) { + const action = shortcuts[shortcut] + if (action) { + action() + return // Stop after first match + } } } - } - }, [shortcuts, combinations]) + }, + [shortcuts, combinations], + ) // Set up the global keyboard listener useKeyboardEvents(handleKeyboardEvent, [handleKeyboardEvent]) @@ -81,4 +93,4 @@ export function useShortcuts() { throw new Error("useShortcuts must be used within a ShortcutsProvider") } return context -} \ No newline at end of file +} diff --git a/app/state/useGlobal.ts b/app/state/useGlobal.ts index c2b20e4..1badd6a 100644 --- a/app/state/useGlobal.ts +++ b/app/state/useGlobal.ts @@ -113,7 +113,7 @@ function buildSetValue(id: string, persist: boolean) { } export function deleteGlobal(id: string): void { - delete globals[id] + delete _globals[id] } /** diff --git a/app/state/useGlobal.windows.ts b/app/state/useGlobal.windows.ts index d8ebaab..c4edad5 100644 --- a/app/state/useGlobal.windows.ts +++ b/app/state/useGlobal.windows.ts @@ -24,21 +24,21 @@ * established value "wins" (subsequent differing defaults are ignored). */ -import { useSyncExternalStore, useEffect, useCallback } from "react"; -import { unstable_batchedUpdates } from "react-native"; +import { useSyncExternalStore, useEffect, useCallback } from "react" +import { unstable_batchedUpdates } from "react-native" -type Id = string; -type Listener = () => void; -type SetValue = T | ((prev: T) => T); -type UseGlobalOptions = { persist?: boolean }; +type Id = string +type Listener = () => void +type SetValue = T | ((prev: T) => T) +type UseGlobalOptions = { persist?: boolean } /* ----------------------------------------------------------------------------- * Internal Stores * -------------------------------------------------------------------------- */ // Central storage for all global state values, keyed by unique identifiers -const globals = new Map(); +const globals = new Map() // Subscription system: maps each global ID to a set of listener functions -const listeners = new Map>(); +const listeners = new Map>() /* ----------------------------------------------------------------------------- * Persistence Stubs (no-op) @@ -61,14 +61,14 @@ function debouncePersist(_delay: number = 300): void { * Pure read: NEVER writes during render. */ function getSnapshotWithDefault(id: Id, initialValue: T): T { - return (globals.has(id) ? (globals.get(id) as T) : initialValue); + return globals.has(id) ? (globals.get(id) as T) : initialValue } /** * Read a snapshot without default (used by imperative API and setters). */ function getSnapshot(id: Id): T { - return globals.get(id) as T; + return globals.get(id) as T } /** @@ -76,18 +76,18 @@ function getSnapshot(id: Id): T { * Returns an unsubscribe function that cleans up both the listener and empty sets. */ function subscribe(id: Id, cb: Listener): () => void { - let set = listeners.get(id); - if (!set) listeners.set(id, (set = new Set())); - set.add(cb); + let set = listeners.get(id) + if (!set) listeners.set(id, (set = new Set())) + set.add(cb) // Return cleanup function that prevents memory leaks return () => { - const s = listeners.get(id); - if (!s) return; - s.delete(cb); + const s = listeners.get(id) + if (!s) return + s.delete(cb) // Clean up empty listener sets to prevent memory leaks - if (s.size === 0) listeners.delete(id); - }; + if (s.size === 0) listeners.delete(id) + } } /** @@ -96,12 +96,12 @@ function subscribe(id: Id, cb: Listener): () => void { * Iterates over a copy to be resilient to listeners mutating subscriptions. */ function notify(id: Id) { - const s = listeners.get(id); - if (!s || s.size === 0) return; + const s = listeners.get(id) + if (!s || s.size === 0) return unstable_batchedUpdates(() => { - for (const l of Array.from(s)) l(); - }); + for (const l of Array.from(s)) l() + }) } /** @@ -112,37 +112,33 @@ function notify(id: Id) { */ function buildSetValue(id: Id, persist: boolean, initialValue?: T) { return (value: SetValue | null) => { - const prev = globals.get(id) as T | undefined; + const prev = globals.get(id) as T | undefined // Handle null value as reset (imperative API) if (value === null) { - if (!globals.has(id)) return; // nothing to reset - globals.delete(id); + if (!globals.has(id)) return // nothing to reset + globals.delete(id) // persistence cleanup would go here (no-op for now) - notify(id); - return; + notify(id) + return } // Resolve functional updater - use prev if exists, otherwise use initialValue as fallback - const current = prev !== undefined ? prev : initialValue; - const next = - typeof value === "function" - ? (value as (prev: T) => T)(current as T) - : value; - + const current = prev !== undefined ? prev : initialValue + const next = typeof value === "function" ? (value as (prev: T) => T)(current as T) : value // Avoid unnecessary notifications/re-renders on no-op updates // BUT: prev might be undefined while next is defined (first set) - if (prev !== undefined && Object.is(prev, next)) return; + if (prev !== undefined && Object.is(prev, next)) return - globals.set(id, next); + globals.set(id, next) // Would save to persistent storage if implemented (no-op for now) - if (persist) debouncePersist(); + if (persist) debouncePersist() // Notify all subscribers for re-renders - notify(id); - }; + notify(id) + } } /* ----------------------------------------------------------------------------- @@ -162,13 +158,13 @@ function buildSetValue(id: Id, persist: boolean, initialValue?: T) { export function useGlobal( id: Id, initialValue: T, - { persist = false }: UseGlobalOptions = {} + { persist = false }: UseGlobalOptions = {}, ): [T, (v: SetValue) => void] { // Read via useSyncExternalStore; ensure the snapshot read is PURE (no writes) const value = useSyncExternalStore( - (cb) => subscribe(id, cb), // subscribe - () => getSnapshotWithDefault(id, initialValue) // getSnapshot (client) - ); + (cb) => subscribe(id, cb), // subscribe + () => getSnapshotWithDefault(id, initialValue), // getSnapshot (client) + ) /** * Post-mount initialization: @@ -178,19 +174,19 @@ export function useGlobal( */ useEffect(() => { if (!globals.has(id)) { - globals.set(id, initialValue); - // Optionally, a dev-only warning could detect mismatched defaults for same id. + globals.set(id, initialValue) } - // We intentionally do not depend on initialValue here: - // changing the default later should not rewrite established globals. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); + }, [id]) // Memoize the setter; enforce non-null signature for hook users - const setAny = useCallback(buildSetValue(id, persist, initialValue), [id, persist, initialValue]); - const set = useCallback<(v: SetValue) => void>((v) => setAny(v), [setAny]); - - return [value, set]; + const setAny = useCallback(buildSetValue(id, persist, initialValue), [ + id, + persist, + initialValue, + ]) + const set = useCallback<(v: SetValue) => void>((v) => setAny(v), [setAny]) + + return [value, set] } /** @@ -207,11 +203,11 @@ export function useGlobal( export function withGlobal( id: Id, initialValue: T, - { persist = false }: UseGlobalOptions = {} + { persist = false }: UseGlobalOptions = {}, ): [T, (v: SetValue | null) => void] { // Imperative path can initialize synchronously without render concerns - if (!globals.has(id)) globals.set(id, initialValue); - return [getSnapshot(id), buildSetValue(id, persist, initialValue)]; + if (!globals.has(id)) globals.set(id, initialValue) + return [getSnapshot(id), buildSetValue(id, persist, initialValue)] } /** @@ -221,10 +217,10 @@ export function withGlobal( * @param rerender - Whether to trigger re-renders (default: true) */ export function resetGlobal(id: Id, rerender = true) { - if (!globals.has(id)) return; - globals.delete(id); + if (!globals.has(id)) return + globals.delete(id) // Note: No persistence cleanup needed since persistence is not implemented - if (rerender) notify(id); + if (rerender) notify(id) } /** @@ -237,13 +233,13 @@ export function resetGlobal(id: Id, rerender = true) { */ export function clearGlobals(rerender = true) { // Clear in-memory state - const hadAny = globals.size > 0; - globals.clear(); + const hadAny = globals.size > 0 + globals.clear() // Note: No persistent storage to clear since persistence is not implemented if (rerender && hadAny) { // Only notify ids that currently have listeners - for (const id of listeners.keys()) notify(id); + for (const id of listeners.keys()) notify(id) } } @@ -254,23 +250,22 @@ export function clearGlobals(rerender = true) { /** * Read a global value without subscribing. Returns undefined if missing. */ -export const getGlobal = (id: Id): T | undefined => - (globals.get(id) as T | undefined); +export const getGlobal = (id: Id): T | undefined => globals.get(id) as T | undefined /** * Set a global value without subscribing. (Non-null only.) */ -export const setGlobal = (id: Id, v: SetValue): void => - buildSetValue(id, false, undefined)(v); +export const setGlobal = (id: Id, v: SetValue): void => + buildSetValue(id, false, undefined)(v) /** * Check whether a global key exists. */ -export const hasGlobal = (id: Id): boolean => globals.has(id); +export const hasGlobal = (id: Id): boolean => globals.has(id) /* ----------------------------------------------------------------------------- * Module Initialization * -------------------------------------------------------------------------- */ // Load persisted globals on module initialization (no-op for now) -loadPersistedGlobals(); +loadPersistedGlobals() diff --git a/app/utils/system.ts b/app/utils/system.ts index 18b1e3a..d054f8f 100644 --- a/app/utils/system.ts +++ b/app/utils/system.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react" import IRRunShellCommand from "../native/IRRunShellCommand/NativeIRRunShellCommand" import IRSystemInfo, { SystemInfo } from "../native/IRSystemInfo/NativeIRSystemInfo" import IRKeyboard, { KeyboardEvent } from "../native/IRKeyboard/NativeIRKeyboard" -import { Platform, type EventSubscription } from "react-native" +import { type EventSubscription } from "react-native" /** * Get the current memory usage of the app in MB via a shell command. diff --git a/app/utils/useSystemMenu/types.ts b/app/utils/useSystemMenu/types.ts index 1d99d61..b2941d8 100644 --- a/app/utils/useSystemMenu/types.ts +++ b/app/utils/useSystemMenu/types.ts @@ -1,4 +1,4 @@ -export { SEPARATOR } from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" +import { MENU_SEPARATOR } from "../../components/Menu/types" export type { SystemMenuItemPressedEvent, SystemMenuStructure, @@ -18,9 +18,9 @@ export interface SystemMenuItem { submenu?: SystemMenuListEntry[] } -export type SystemMenuListEntry = SystemMenuItem | typeof SEPARATOR +export type SystemMenuListEntry = SystemMenuItem | typeof MENU_SEPARATOR export interface SystemMenuConfig { items?: Record remove?: string[] -} \ No newline at end of file +} diff --git a/app/utils/useSystemMenu/useSystemMenu.ts b/app/utils/useSystemMenu/useSystemMenu.ts index 3e9fa2a..ae40f84 100644 --- a/app/utils/useSystemMenu/useSystemMenu.ts +++ b/app/utils/useSystemMenu/useSystemMenu.ts @@ -53,7 +53,6 @@ import { useEffect, useRef, useCallback, useState } from "react" import NativeIRSystemMenuManager from "../../native/IRSystemMenuManager/NativeIRSystemMenuManager" import { - SEPARATOR, type SystemMenuItem, type SystemMenuConfig, type SystemMenuListEntry, @@ -61,6 +60,7 @@ import { type SystemMenuStructure, } from "./types" import { parsePathKey, joinPath, isSeparator } from "./utils" +import { MENU_SEPARATOR } from "../../components/Menu/types" export function useSystemMenu(config?: SystemMenuConfig) { const actionsRef = useRef void>>(new Map()) @@ -92,7 +92,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { // Clear any existing separators before adding new ones to avoid duplication try { - await NativeIRSystemMenuManager.removeMenuItemAtPath([...parentPath, SEPARATOR]) + await NativeIRSystemMenuManager.removeMenuItemAtPath([...parentPath, MENU_SEPARATOR]) } catch (e) { console.warn(`Failed to clear separators for "${parentKey}":`, e) } @@ -100,7 +100,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { for (const entry of entries) { if (isSeparator(entry)) { try { - await NativeIRSystemMenuManager.addMenuItemAtPath(parentPath, SEPARATOR, "") + await NativeIRSystemMenuManager.addMenuItemAtPath(parentPath, MENU_SEPARATOR, "") } catch (e) { console.error(`Failed to add separator under "${parentKey}":`, e) } @@ -114,9 +114,8 @@ export function useSystemMenu(config?: SystemMenuConfig) { if (item.action) actionsRef.current.set(actionKey, item.action) // Resolve platform-specific shortcut for macOS - const resolvedShortcut = typeof item.shortcut === "object" - ? (item.shortcut.macos ?? "") - : (item.shortcut ?? "") + const resolvedShortcut = + typeof item.shortcut === "object" ? item.shortcut.macos ?? "" : item.shortcut ?? "" try { if (typeof item.position === "number") { @@ -294,7 +293,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { // Remove any remaining separators await NativeIRSystemMenuManager.removeMenuItemAtPath([ ...parsePathKey(parentKey), - SEPARATOR, + MENU_SEPARATOR, ]) const parentPath = parsePathKey(parentKey) // If this was a top-level menu we created and it's now empty, remove it entirely @@ -336,4 +335,4 @@ export function useSystemMenu(config?: SystemMenuConfig) { getAllMenuPaths, handleMenuItemPressed, } -} \ No newline at end of file +} diff --git a/app/utils/useSystemMenu/useSystemMenu.windows.ts b/app/utils/useSystemMenu/useSystemMenu.windows.ts index b632833..d23de6e 100644 --- a/app/utils/useSystemMenu/useSystemMenu.windows.ts +++ b/app/utils/useSystemMenu/useSystemMenu.windows.ts @@ -73,11 +73,20 @@ import { parsePathKey, joinPath, isSeparator } from "./utils" export function useSystemMenu(config?: SystemMenuConfig) { const actionsRef = useRef void>>(new Map()) - const { registerShortcut, unregisterShortcut, clearAllShortcuts } = useShortcuts() - - const [globalMenuConfig, setGlobalMenuConfig] = useGlobal("windows-menu-config", null) - const [globalMenuStructure, setGlobalMenuStructure] = useGlobal("windows-menu-structure", []) - const [globalMenuItems, setGlobalMenuItems] = useGlobal>("windows-menu-items", {}) + const { registerShortcut, clearAllShortcuts } = useShortcuts() + + const [globalMenuConfig, setGlobalMenuConfig] = useGlobal( + "windows-menu-config", + null, + ) + const [globalMenuStructure, setGlobalMenuStructure] = useGlobal( + "windows-menu-structure", + [], + ) + const [globalMenuItems, setGlobalMenuItems] = useGlobal>( + "windows-menu-items", + {}, + ) const handleMenuItemPressed = useCallback((event: SystemMenuItemPressedEvent) => { const action = actionsRef.current.get(joinPath(event.menuPath)) @@ -87,7 +96,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { const discoverMenus = useCallback(async () => { if (!config?.items || config === globalMenuConfig) return [] - const menuStructure: SystemMenuStructure = Object.keys(config.items).map(title => ({ + const menuStructure: SystemMenuStructure = Object.keys(config.items).map((title) => ({ title, enabled: true, path: [title], @@ -102,56 +111,66 @@ export function useSystemMenu(config?: SystemMenuConfig) { return [] }, [config, globalMenuConfig, setGlobalMenuConfig, setGlobalMenuStructure, setGlobalMenuItems]) - const addMenuItem = useCallback(async (parentKey: string, item: SystemMenuItem) => { - const actionKey = joinPath([parentKey, item.label]) - - if (item.action) { - actionsRef.current.set(actionKey, item.action) - } - - setGlobalMenuItems(prev => ({ - ...prev, - [parentKey]: [...(prev[parentKey] || []), item], - })) - }, [setGlobalMenuItems]) + const addMenuItem = useCallback( + async (parentKey: string, item: SystemMenuItem) => { + const actionKey = joinPath([parentKey, item.label]) - const removeMenuItemByName = useCallback(async (nameOrPath: string) => { - const path = parsePathKey(nameOrPath) - actionsRef.current.delete(joinPath(path)) + if (item.action) { + actionsRef.current.set(actionKey, item.action) + } - if (path.length === 1) { - // Remove entire top-level menu - setGlobalMenuItems(prev => { - const { [path[0]]: _, ...rest } = prev - return rest - }) - } else if (path.length === 2) { - const [parentKey, itemLabel] = path - setGlobalMenuItems(prev => ({ + setGlobalMenuItems((prev) => ({ ...prev, - [parentKey]: (prev[parentKey] || []).filter(item => item.label !== itemLabel), + [parentKey]: [...(prev[parentKey] || []), item], })) - } - }, [setGlobalMenuItems, globalMenuItems]) - - const setMenuItemEnabled = useCallback(async (pathOrKey: string | string[], enabled: boolean) => { - const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) - - if (path.length >= 2) { - const [parentKey, itemLabel] = path - setGlobalMenuItems(prev => ({ - ...prev, - [parentKey]: (prev[parentKey] || []).map(item => - item.label === itemLabel ? { ...item, enabled } : item - ), - })) - } - }, [setGlobalMenuItems]) + }, + [setGlobalMenuItems], + ) + + const removeMenuItemByName = useCallback( + async (nameOrPath: string) => { + const path = parsePathKey(nameOrPath) + actionsRef.current.delete(joinPath(path)) + + if (path.length === 1) { + // Remove entire top-level menu + setGlobalMenuItems((prev) => { + const { [path[0]]: _, ...rest } = prev + return rest + }) + } else if (path.length === 2) { + const [parentKey, itemLabel] = path + setGlobalMenuItems((prev) => ({ + ...prev, + [parentKey]: (prev[parentKey] || []).filter((item) => item.label !== itemLabel), + })) + } + }, + [setGlobalMenuItems, globalMenuItems], + ) + + const setMenuItemEnabled = useCallback( + async (pathOrKey: string | string[], enabled: boolean) => { + const path = Array.isArray(pathOrKey) ? pathOrKey : parsePathKey(pathOrKey) + + if (path.length >= 2) { + const [parentKey, itemLabel] = path + setGlobalMenuItems((prev) => ({ + ...prev, + [parentKey]: (prev[parentKey] || []).map((item) => + item.label === itemLabel ? { ...item, enabled } : item, + ), + })) + } + }, + [setGlobalMenuItems], + ) const getAllMenuPaths = useCallback(async (): Promise => { return Object.entries(globalMenuItems).flatMap(([parentKey, entries]) => - entries.filter(entry => !isSeparator(entry)) - .map(entry => joinPath([parentKey, entry.label])) + entries + .filter((entry) => !isSeparator(entry)) + .map((entry) => joinPath([parentKey, entry.label])), ) }, [globalMenuItems]) @@ -163,16 +182,15 @@ export function useSystemMenu(config?: SystemMenuConfig) { clearAllShortcuts() Object.entries(config.items).forEach(([parentKey, entries]) => { - entries.forEach(entry => { + entries.forEach((entry) => { if (!isSeparator(entry)) { const item = entry as SystemMenuItem if (item.action) { actionsRef.current.set(joinPath([parentKey, item.label]), item.action) // Register shortcut if present if (item.shortcut) { - const resolvedShortcut = typeof item.shortcut === "object" - ? item.shortcut.windows - : item.shortcut + const resolvedShortcut = + typeof item.shortcut === "object" ? item.shortcut.windows : item.shortcut if (resolvedShortcut) { registerShortcut(resolvedShortcut, item.action) } @@ -183,7 +201,7 @@ export function useSystemMenu(config?: SystemMenuConfig) { }) // Update global state directly without calling discoverMenus to avoid redundancy - const menuStructure: SystemMenuStructure = Object.keys(config.items).map(title => ({ + const menuStructure: SystemMenuStructure = Object.keys(config.items).map((title) => ({ title, enabled: true, path: [title], @@ -196,8 +214,6 @@ export function useSystemMenu(config?: SystemMenuConfig) { setGlobalMenuItems(config.items as Record) }, []) - - return { availableMenus: [], menuStructure: globalMenuStructure, @@ -209,4 +225,4 @@ export function useSystemMenu(config?: SystemMenuConfig) { getAllMenuPaths, handleMenuItemPressed, } -} \ No newline at end of file +} diff --git a/app/utils/useSystemMenu/utils.ts b/app/utils/useSystemMenu/utils.ts index 99b8936..9a3cc35 100644 --- a/app/utils/useSystemMenu/utils.ts +++ b/app/utils/useSystemMenu/utils.ts @@ -1,14 +1,18 @@ -import { SEPARATOR, type SystemMenuListEntry } from "./types" +import { type SystemMenuListEntry } from "./types" +import { MENU_SEPARATOR } from "../../components/Menu/types" export const PATH_SEPARATOR = " > " export const parsePathKey = (key: string): string[] => - key.split(PATH_SEPARATOR).map(s => s.trim()).filter(Boolean) + key + .split(PATH_SEPARATOR) + .map((s) => s.trim()) + .filter(Boolean) export const joinPath = (path: string[]): string => path.join(PATH_SEPARATOR) -export const isSeparator = (entry: SystemMenuListEntry): entry is typeof SEPARATOR => - entry === SEPARATOR +export const isSeparator = (entry: SystemMenuListEntry): entry is typeof MENU_SEPARATOR => + entry === MENU_SEPARATOR export interface KeyCombination { ctrl: boolean @@ -26,7 +30,11 @@ export interface KeyCombination { export function parseShortcut(shortcut: string): KeyCombination | null { if (!shortcut?.trim()) return null - const parts = shortcut.toLowerCase().split("+").map(s => s.trim()).filter(Boolean) + const parts = shortcut + .toLowerCase() + .split("+") + .map((s) => s.trim()) + .filter(Boolean) if (parts.length === 0) return null const combination: KeyCombination = { ctrl: false, alt: false, shift: false, cmd: false, key: "" } @@ -76,4 +84,4 @@ export function matchesKeyCombo(event: any, combination: KeyCombination): boolea modifiers.cmd === combination.cmd && event.key?.toUpperCase() === combination.key.toUpperCase() ) -} \ No newline at end of file +} From 00d4f2c5c783e313cbf0981e3928b881679668cd Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Tue, 4 Nov 2025 13:53:43 -0500 Subject: [PATCH 31/40] Remove redundant shortcut formatting replacement in MenuDropdownItem component --- app/components/Menu/MenuDropdownItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index 3c602e1..e804b5b 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -81,7 +81,6 @@ function formatShortcut(shortcut: string): string { return shortcut .replace(/cmd/gi, "Ctrl") .replace(/shift/gi, "Shift") - .replace(/\+/g, "+") .split("+") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join("+") From e326df8d51560b32146634885718f69c0e19fe34 Mon Sep 17 00:00:00 2001 From: Jamon Holmgren Date: Tue, 4 Nov 2025 18:06:14 -0800 Subject: [PATCH 32/40] Add Windows setup script and update readme --- .gitignore | 6 ++++-- README.md | 30 +++++++++++++++++++++++++++++- bin/setup-windows.ps1 | 11 +++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 bin/setup-windows.ps1 diff --git a/.gitignore b/.gitignore index 7c11bae..94dd65c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,7 @@ msbuild.binlog .claude/* -# Windows reserved device names -nul \ No newline at end of file +# Windows +nul +ReactNativeWindows.dsc.yaml +WindowsDevTools.dsc.yaml diff --git a/README.md b/README.md index c08aff0..3a4b572 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,15 @@ npm run macos-release #### System Requirements -First, install the system requirements for React Native Windows: https://microsoft.github.io/react-native-windows/docs/rnw-dependencies +First, install the system requirements for React Native Windows by running the following in an elevated Powershell session: + +```powershell +.\bin\windows-setup.ps1 +``` + +This should install everything for you. + +**Alternative**: Follow the official installation steps (results vary): https://microsoft.github.io/react-native-windows/docs/rnw-dependencies **Alternative**: If you experience issues with the official `rnw-dependencies.ps1` script, consider using Josh Yoes' improved setup process: https://github.com/joshuayoes/ReactNativeWindowsSandbox @@ -60,6 +68,26 @@ npm run windows npm run windows-release ``` +**Troubleshooting**: If you run into a build error with ctype.h or ROSLYNCODETASKFACTORYCSHARPCOMPILER, try making a symlink like this (it's weird, I know): + +```powershell +mklink /D "C:\Program Files\Windows Kits\10\lib" "C:\Program Files (x86)\Windows Kits\10\lib" +``` + +Somehow, `C:\Program Files\` instead of `C:\Program Files (x86)\` is being used, which breaks things. This makes a symlink to fix that. + +You may also need to add this path to the `reactotron.vcxproj`: + +``` + + + + $(ProjectDir)..\..\app;%(AdditionalIncludeDirectories);C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt + + + +``` + ### Cross-Platform Native Development Both platforms use unified commands for native module development: diff --git a/bin/setup-windows.ps1 b/bin/setup-windows.ps1 new file mode 100644 index 0000000..dc1d487 --- /dev/null +++ b/bin/setup-windows.ps1 @@ -0,0 +1,11 @@ +echo "Downloading the RN windows dependencies manifest..." +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/joshuayoes/ReactNativeWindowsSandbox/main/ReactNativeWindows.dsc.yaml" -OutFile "ReactNativeWindows.dsc.yaml" +echo "Installing RN windows deps..." +winget configure ReactNativeWindows.dsc.yaml +echo "Downloading the windows dev tools manifest..." +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/joshuayoes/ReactNativeWindowsSandbox/main/WindowsDevTools.dsc.yaml" -OutFile "WindowsDevTools.dsc.yaml" +echo "Installing windows dev tools..." +winget configure WindowsDevTools.dsc.yaml +echo "Installing react-native-windows dependencies..." +.\node_modules\react-native-windows\scripts\rnw-dependencies.ps1 +echo "Windows setup complete!" From 22561ae34b25f4affe43deff6ece5a9b03f8ce54 Mon Sep 17 00:00:00 2001 From: Jamon Holmgren Date: Tue, 4 Nov 2025 18:09:06 -0800 Subject: [PATCH 33/40] Lock react-native versions and only run postinstall on macos --- package-lock.json | 581 +++++++++++++----- package.json | 8 +- windows/reactotron.Package/packages.lock.json | 204 ++---- windows/reactotron/packages.lock.json | 104 ++-- 4 files changed, 524 insertions(+), 373 deletions(-) diff --git a/package-lock.json b/package-lock.json index 253ae42..0c69009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,14 +7,13 @@ "": { "name": "reactotron", "version": "4.0.0-alpha.1", - "hasInstallScript": true, "dependencies": { "@expo-google-fonts/space-grotesk": "^0.3.0", "@legendapp/list": "^1.1.4", "react": "19.0.0", "react-native-macos": "^0.78.2", "react-native-mmkv": "^3.3.0", - "react-native-windows": "^0.78.5", + "react-native-windows": "0.78.5", "reactotron-core-contract": "^0.3.2" }, "devDependencies": { @@ -60,6 +59,54 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2711,12 +2758,11 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.9.tgz", - "integrity": "sha512-T8s5qROH7caBNiFrUpN8vgC6wg7QysVPryZKprgl3kLQQPpoMFM6ffIYvUWD74KM9fWWLU7vzFFNBWDBsrTyWg==", - "license": "MIT", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.10.tgz", + "integrity": "sha512-5fSZmkGwWkH+mrIA5M1GYPZdPM+SjXwCCl2Am7VhFoVwOBJNhRnwvIpAdzw6sFjiebN/rz+/YH0NdxztGZSa9Q==", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.9", + "@microsoft/applicationinsights-core-js": "3.3.10", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", "@nevware21/ts-async": ">= 0.5.4 < 2.x", @@ -2724,12 +2770,11 @@ } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.9.tgz", - "integrity": "sha512-BvxI4CW8Ws+gfXKy+Y/9pmEXp88iU1GYVjkUfqXP7La59VHARTumlG5iIgMVvaifOrvSW7G6knvQM++0tEfMBQ==", - "license": "MIT", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.10.tgz", + "integrity": "sha512-VSLjc9cT+Y+eTiSfYltJHJCejn8oYr0E6Pq2BMhOEO7F6IyLGYIxzKKvo78ze9x+iHX7KPTATcZ+PFgjGXuNqg==", "dependencies": { - "@microsoft/1ds-core-js": "4.3.9", + "@microsoft/1ds-core-js": "4.3.10", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", "@nevware21/ts-async": ">= 0.5.4 < 2.x", @@ -2737,10 +2782,9 @@ } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.9.tgz", - "integrity": "sha512-xliiE9H09xCycndlua4QjajN8q5k/ET6VCv+e0Jjodxr9+cmoOP/6QY9dun9ptokuwR8TK0qOaIJ8z4fgslVSA==", - "license": "MIT", + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.10.tgz", + "integrity": "sha512-5yKeyassZTq2l+SAO4npu6LPnbS++UD+M+Ghjm9uRzoBwD8tumFx0/F8AkSVqbniSREd+ztH/2q2foewa2RZyg==", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", @@ -2755,7 +2799,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", - "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } @@ -2764,7 +2807,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", - "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } @@ -2773,7 +2815,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", - "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } @@ -2781,8 +2822,7 @@ "node_modules/@nevware21/ts-utils": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.5.tgz", - "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", - "license": "MIT" + "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==" }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", @@ -3189,20 +3229,18 @@ } }, "node_modules/@react-native-windows/cli": { - "version": "0.78.9", - "resolved": "https://registry.npmjs.org/@react-native-windows/cli/-/cli-0.78.9.tgz", - "integrity": "sha512-lpG5tHeEvTbr/6OGaCRdrU3hHXe+EnVM7euCEsyIInoo1QA5qx8Hz33D+kO0t4ewWTI/Xr3iZIqdXeujHDAUTg==", - "license": "MIT", + "version": "0.78.2", + "resolved": "https://registry.npmjs.org/@react-native-windows/cli/-/cli-0.78.2.tgz", + "integrity": "sha512-F5l0P7j1LpsWWkenIa118GSQhBZXLWU2V30E+xePh52Yp2H4WmGnvG41Ie8LiTLXs5BRwmzuGbOIgjYbHhK2pw==", "dependencies": { - "@react-native-windows/codegen": "0.78.3", - "@react-native-windows/fs": "0.78.1", - "@react-native-windows/package-utils": "0.78.1", - "@react-native-windows/telemetry": "0.78.2", + "@react-native-windows/codegen": "0.78.1", + "@react-native-windows/fs": "0.78.0", + "@react-native-windows/package-utils": "0.78.0", + "@react-native-windows/telemetry": "0.78.0", "@xmldom/xmldom": "^0.7.7", "chalk": "^4.1.0", "cli-spinners": "^2.2.0", "envinfo": "^7.5.0", - "execa": "^5.0.0", "find-up": "^4.1.0", "glob": "^7.1.1", "lodash": "^4.17.15", @@ -3227,7 +3265,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", "engines": { "node": ">=6" } @@ -3236,7 +3273,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -3248,7 +3284,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" }, @@ -3260,7 +3295,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -3268,14 +3302,12 @@ "node_modules/@react-native-windows/cli/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@react-native-windows/cli/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -3284,7 +3316,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -3297,7 +3328,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", "engines": { "node": ">=4" } @@ -3306,7 +3336,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -3318,7 +3347,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "license": "MIT", "dependencies": { "chalk": "^2.0.1" }, @@ -3330,7 +3358,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3344,7 +3371,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "license": "MIT", "engines": { "node": ">=4" } @@ -3353,7 +3379,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" }, @@ -3365,7 +3390,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", - "license": "MIT", "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", @@ -3382,7 +3406,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3396,7 +3419,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -3411,7 +3433,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -3423,7 +3444,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "license": "MIT", "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" @@ -3433,10 +3453,9 @@ } }, "node_modules/@react-native-windows/cli/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "bin": { "semver": "bin/semver.js" }, @@ -3448,7 +3467,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" }, @@ -3460,7 +3478,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -3469,12 +3486,11 @@ } }, "node_modules/@react-native-windows/codegen": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native-windows/codegen/-/codegen-0.78.3.tgz", - "integrity": "sha512-SANoBXhFEcpwJuLrTwJPXzdchvni6Me1kFg6/KWX/QSWTRPy7tyHUKTN4X1QqwxHCLEOtwsgbrfq15Y49pfzeA==", - "license": "MIT", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native-windows/codegen/-/codegen-0.78.1.tgz", + "integrity": "sha512-qZZMrgTCkjVK93GQ39hrbEPxnbHV0F+v7gVf9xlndqYcgyL5MqlEP29Nx9TEQsi/bZ9MqJpQJqz1ydtJIl3jYA==", "dependencies": { - "@react-native-windows/fs": "0.78.1", + "@react-native-windows/fs": "0.78.0", "chalk": "^4.1.0", "globby": "^11.1.0", "mustache": "^4.0.1", @@ -3495,7 +3511,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -3506,7 +3521,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3516,7 +3530,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -3534,18 +3547,16 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/@react-native-windows/find-repo-root": { - "version": "0.78.1", - "resolved": "https://registry.npmjs.org/@react-native-windows/find-repo-root/-/find-repo-root-0.78.1.tgz", - "integrity": "sha512-2O0yMNcERh5D3bq8jWo6BCAckZEEbB+EDOqtLm8AQb9VVVvI1CNyA7RSk6rzQFVEPvLDSp4rxsI4FlbS7LNqhg==", - "license": "MIT", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native-windows/find-repo-root/-/find-repo-root-0.78.0.tgz", + "integrity": "sha512-ts3zioNoo8M0Fct1m1w657Bn7KZ5ILGaprW0HBUUWNmc3NPBg1qfKMq0dZ1ivwpBTwKEcqyCrfJ7MMLFMhH7qQ==", "dependencies": { - "@react-native-windows/fs": "0.78.1", + "@react-native-windows/fs": "0.78.0", "find-up": "^4.1.0" }, "engines": { @@ -3556,7 +3567,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -3569,7 +3579,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -3581,7 +3590,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -3596,7 +3604,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -3605,22 +3612,20 @@ } }, "node_modules/@react-native-windows/fs": { - "version": "0.78.1", - "resolved": "https://registry.npmjs.org/@react-native-windows/fs/-/fs-0.78.1.tgz", - "integrity": "sha512-GYIElnKFv2TUhbYvduNrXclHNqWFZxelttslv2f14jocF7gwjHYJg3tjQJtf5lk1KRlMYddl9EO+VR+tCgOr7A==", - "license": "MIT", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native-windows/fs/-/fs-0.78.0.tgz", + "integrity": "sha512-7RasiN+xySObTujqkpujTsY2ZMv95JVvvY5CytoJuy1NFoKRyXyxBsJtAVWDNx3tGpl1E2xE7H8ObCiT/mnVuQ==", "dependencies": { "graceful-fs": "^4.2.8" } }, "node_modules/@react-native-windows/package-utils": { - "version": "0.78.1", - "resolved": "https://registry.npmjs.org/@react-native-windows/package-utils/-/package-utils-0.78.1.tgz", - "integrity": "sha512-dyAmdJ4iNJcbq+SZfqzaZPglLEvs9/KsRN9J37QrK6gx/r0xEt3dAJVmDEMnZ4oW9znTfA+lku6i8SQN1XOU9w==", - "license": "MIT", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native-windows/package-utils/-/package-utils-0.78.0.tgz", + "integrity": "sha512-6KaHvQRUxZsm52HgLhdzU/ySoNroJH+m/trSRxsIDx1x1VKUhr6SzwNYSfx5ec7TaGt6gYtQwYLYVj7L+hwdIw==", "dependencies": { - "@react-native-windows/find-repo-root": "0.78.1", - "@react-native-windows/fs": "0.78.1", + "@react-native-windows/find-repo-root": "0.78.0", + "@react-native-windows/fs": "0.78.0", "get-monorepo-packages": "^1.2.0", "lodash": "^4.17.15" }, @@ -3629,14 +3634,14 @@ } }, "node_modules/@react-native-windows/telemetry": { - "version": "0.78.2", - "resolved": "https://registry.npmjs.org/@react-native-windows/telemetry/-/telemetry-0.78.2.tgz", - "integrity": "sha512-IkgZBNp/Nsf7lb2Cs1+TJrwUFR2x7sITkX4IrDcDvMvLVw0QQGPIDwE7c4hm/DKDXQa4/Dz3SWwCH76qLScKKg==", - "license": "MIT", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native-windows/telemetry/-/telemetry-0.78.0.tgz", + "integrity": "sha512-q7Y9gZUFuLHi4GOMmNkgVO4UWdy7lQEh9wcN8GEHiW0HDyMM7vzeo8EYPr1iUcM//xuCayj3FuYwhP+W17C+4g==", "dependencies": { + "@azure/core-auth": "1.5.0", "@microsoft/1ds-core-js": "^4.3.0", "@microsoft/1ds-post-js": "^4.3.0", - "@react-native-windows/fs": "0.78.1", + "@react-native-windows/fs": "0.78.0", "@xmldom/xmldom": "^0.7.7", "ci-info": "^3.2.0", "envinfo": "^7.8.1", @@ -3656,6 +3661,7 @@ "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.78.2.tgz", "integrity": "sha512-VHqQqjj1rnh2KQeS3yx4IfFSxIIIDi1jR4yUeC438Q6srwxDohR4W0UkXuSIz0imhlems5eS7yZTjdgSpWHRUQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3758,6 +3764,7 @@ "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.78.2.tgz", "integrity": "sha512-xqEnpqxvBlm02mRY58L0NBjF25MTHmbaeA2qBx5VtheH/pXL6MHUbtwB1Q2dJrg9XcK0Np1i9h7N5h9gFwA2Mg==", "license": "MIT", + "peer": true, "dependencies": { "@react-native/dev-middleware": "0.78.2", "@react-native/metro-babel-transformer": "0.78.2", @@ -3787,6 +3794,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -3795,13 +3803,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -3814,6 +3824,7 @@ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.78.2.tgz", "integrity": "sha512-qNJT679OU/cdAKmZxfBFjqTG+ZC5i/4sLyvbcQjFFypunGSOaWl3mMQFQQdCBIQN+DFDPVSUXTPZQK1uI2j/ow==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=18" } @@ -3823,6 +3834,7 @@ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.78.2.tgz", "integrity": "sha512-/u0pGiWVgvx09cYNO4/Okj8v1ZNt4K941pQJPhdwg5AHYuggVHNJjROukXJzZiElYFcJhMfOuxwksiIyx/GAkA==", "license": "MIT", + "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.78.2", @@ -3846,6 +3858,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -3855,6 +3868,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -3866,13 +3880,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@react-native/dev-middleware/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -3940,6 +3956,7 @@ "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.78.2.tgz", "integrity": "sha512-LHgmdrbyK9fcBDdxtn2GLOoDAE+aFHtDHgu6vUZ5CSCi9CMd5Krq8IWAmWjeq+BQr+D1rwSXDAHtOrfJ6qOolA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3991,7 +4008,8 @@ "version": "0.78.2", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.78.2.tgz", "integrity": "sha512-CA/3ynRO6/g1LDbqU8ewrv0js/1lU4+j04L7qz6btXbLTDk1UkF+AfpGRJGbIVY9UmFBJ7l1AOmzwutrWb3Txw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@react-native/typescript-config": { "version": "0.78.2", @@ -4001,10 +4019,9 @@ "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { - "version": "0.78.2", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.78.2.tgz", - "integrity": "sha512-y/wVRUz1ImR2hKKUXFroTdSBiL0Dd+oudzqcGKp/M8Ybrw9MQ0m2QCXxtyONtDn8qkEGceqllwTCKq5WQwJcew==", - "license": "MIT", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.78.0.tgz", + "integrity": "sha512-ibETs3AwpkkRcORRANvZeEFjzvN41W02X882sBzoxC5XdHiZ2DucXo4fjKF7i86MhYCFLfNSIYbwupx1D1iFmg==", "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -4490,6 +4507,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz", + "integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4509,7 +4539,6 @@ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", "deprecated": "this version is no longer supported, please update to at least 0.8.*", - "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -4577,6 +4606,14 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4763,7 +4800,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6099,7 +6135,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -7545,7 +7580,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-monorepo-packages/-/get-monorepo-packages-1.3.0.tgz", "integrity": "sha512-A/s881nNcKhoM7RgkvYFTOtGO+dy4EWbyRaatncPEhhlJAaZRlpfHwuT68p5GJenEt81nnjJOwGg0WKLkR5ZdQ==", - "license": "MIT", "dependencies": { "globby": "^7.1.1", "load-json-file": "^4.0.0" @@ -7555,7 +7589,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "license": "MIT", "dependencies": { "array-uniq": "^1.0.1" }, @@ -7567,7 +7600,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "license": "MIT", "dependencies": { "path-type": "^3.0.0" }, @@ -7579,7 +7611,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", - "license": "MIT", "dependencies": { "array-union": "^1.0.1", "dir-glob": "^2.0.0", @@ -7595,14 +7626,12 @@ "node_modules/get-monorepo-packages/node_modules/ignore": { "version": "3.3.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "license": "MIT" + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" }, "node_modules/get-monorepo-packages/node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -7614,7 +7643,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7946,6 +7974,30 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8093,7 +8145,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "license": "MIT", "engines": { "node": ">= 0.10" } @@ -8111,7 +8162,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", - "license": "MIT", "engines": { "node": ">=8" }, @@ -9903,7 +9953,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", - "license": "MIT", "dependencies": { "invert-kv": "^3.0.0" }, @@ -9969,7 +10018,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", @@ -9984,7 +10032,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "license": "MIT", "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -9997,7 +10044,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", "engines": { "node": ">=4" } @@ -10253,7 +10299,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "license": "MIT", "dependencies": { "p-defer": "^1.0.0" }, @@ -10291,7 +10336,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", - "license": "MIT", "dependencies": { "map-age-cleaner": "^0.1.3", "mimic-fn": "^2.1.0", @@ -10855,7 +10899,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", "bin": { "mustache": "bin/mustache" } @@ -10885,8 +10928,7 @@ "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "license": "MIT" + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node_modules/nocache": { "version": "3.0.4", @@ -10974,7 +11016,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11180,7 +11221,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", - "license": "MIT", "dependencies": { "execa": "^4.0.0", "lcid": "^3.0.0", @@ -11197,7 +11237,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -11220,7 +11259,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -11235,7 +11273,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "license": "Apache-2.0", "engines": { "node": ">=8.12.0" } @@ -11272,7 +11309,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "license": "MIT", "engines": { "node": ">=4" } @@ -11281,7 +11317,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", "engines": { "node": ">=4" } @@ -11290,7 +11325,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "license": "MIT", "engines": { "node": ">=6" } @@ -11587,7 +11621,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", "engines": { "node": ">=4" } @@ -11807,7 +11840,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -12400,25 +12432,24 @@ } }, "node_modules/react-native-windows": { - "version": "0.78.14", - "resolved": "https://registry.npmjs.org/react-native-windows/-/react-native-windows-0.78.14.tgz", - "integrity": "sha512-Fo5FTf2b1YVhMDK021uYHFiYQTwdQJYgFw60yjnRfC6CMBv5e2CYfh9LaT8cRHSnKqSyJ5nVqeGYHcJHon4xPA==", - "license": "MIT", + "version": "0.78.5", + "resolved": "https://registry.npmjs.org/react-native-windows/-/react-native-windows-0.78.5.tgz", + "integrity": "sha512-ItdDzbifrMxj/6dVkrp2AtzjeAuaqH9cRe92qPXX6BtYfKeL5nw4eKRUXtySMaiyonP3spsWJvgvkC5ktR4Rug==", "dependencies": { "@babel/runtime": "^7.0.0", "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "^15.0.0", "@react-native-community/cli-platform-android": "^15.0.0", "@react-native-community/cli-platform-ios": "^15.0.0", - "@react-native-windows/cli": "0.78.9", + "@react-native-windows/cli": "0.78.2", "@react-native/assets": "1.0.0", - "@react-native/assets-registry": "0.78.2", - "@react-native/codegen": "0.78.2", - "@react-native/community-cli-plugin": "0.78.2", - "@react-native/gradle-plugin": "0.78.2", - "@react-native/js-polyfills": "0.78.2", - "@react-native/normalize-colors": "0.78.2", - "@react-native/virtualized-lists": "0.78.2", + "@react-native/assets-registry": "0.78.0", + "@react-native/codegen": "0.78.0", + "@react-native/community-cli-plugin": "0.78.0", + "@react-native/gradle-plugin": "0.78.0", + "@react-native/js-polyfills": "0.78.0", + "@react-native/normalize-colors": "0.78.0", + "@react-native/virtualized-lists": "0.78.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -12433,14 +12464,15 @@ "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "memoize-one": "^5.0.0", - "metro-runtime": "^0.81.3", - "metro-source-map": "^0.81.3", + "metro-runtime": "^0.81.0", + "metro-source-map": "^0.81.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.0.1", "react-refresh": "^0.14.0", + "react-shallow-renderer": "^16.15.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.25.0", "semver": "^7.1.3", @@ -12683,6 +12715,215 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/react-native-windows/node_modules/@react-native/assets-registry": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.78.0.tgz", + "integrity": "sha512-PPHlTRuP9litTYkbFNkwveQFto3I94QRWPBBARU0cH/4ks4EkfCfb/Pdb3AHgtJi58QthSHKFvKTQnAWyHPs7w==", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.78.0.tgz", + "integrity": "sha512-+Sy9Uine0QAbQRxMl6kBlkzKW0qHQk8hghCoKswRWt1ZfxaMA3rezobD5mtSwt/Yhadds9cGbMFWfFJM3Tynsg==", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.78.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/babel-preset": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.78.0.tgz", + "integrity": "sha512-q44ZbR0JXdPvNrjNw75VmiVXXoJhZIx8dTUBVgnZx/UHBQuhPu0e8pAuo56E2mZVkF7FK0s087/Zji8n5OSxbQ==", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.78.0", + "babel-plugin-syntax-hermes-parser": "0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/codegen": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.78.0.tgz", + "integrity": "sha512-8iVT2VYhkalLFUWoQRGSluZZHEG93StfwQGwQ+wk1vOUlOfoT/Xqglt6DvGXIyM9gaMCr6fJBFQVrU+FrXEFYA==", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.25.1", + "invariant": "^2.2.4", + "jscodeshift": "^17.0.0", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/community-cli-plugin": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.78.0.tgz", + "integrity": "sha512-LpfEU+F1hZGcxIf07aBrjlImA0hh8v76V4wTJOgxxqGDUjjQ/X6h9V+bMXne60G9gwccTtvs1G0xiKWNUPI0VQ==", + "dependencies": { + "@react-native/dev-middleware": "0.78.0", + "@react-native/metro-babel-transformer": "0.78.0", + "chalk": "^4.0.0", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", + "readline": "^1.3.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli-server-api": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli-server-api": { + "optional": true + } + } + }, + "node_modules/react-native-windows/node_modules/@react-native/debugger-frontend": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.78.0.tgz", + "integrity": "sha512-KQYD9QlxES/VdmXh9EEvtZCJK1KAemLlszQq4dpLU1stnue5N8dnCY6A7PpStMf5UtAMk7tiniQhaicw0uVHgQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/dev-middleware": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.78.0.tgz", + "integrity": "sha512-zEafAZdOz4s37Jh5Xcv4hJE5qZ6uNxgrTLcpjDOJnQG6dO34/BoZeXvDrjomQFNn6ogdysR51mKJStaQ3ixp5A==", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.78.0", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/gradle-plugin": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.78.0.tgz", + "integrity": "sha512-WvwgfmVs1QfFl1FOL514kz2Fs5Nkg2BGgpE8V0ild8b/UT6jCD8qh2dTI5kL0xdT0d2Xd2BxfuFN0xCLkMC+SA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/js-polyfills": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.78.0.tgz", + "integrity": "sha512-YZ9XtS77s/df7548B6dszX89ReehnA7hiab/axc30j/Mgk7Wv2woOjBKnAA4+rZ0ITLtxNwyJIMaRAc9kGznXw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/metro-babel-transformer": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.78.0.tgz", + "integrity": "sha512-Hy/dl+zytLCRD9dp32ukcRS1Bn0gZH0h0i3AbriS6OGYgUgjAUFhXOKzZ15/G1SEq2sng91MNo/hMvo4uXoc5A==", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.78.0", + "hermes-parser": "0.25.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/react-native-windows/node_modules/@react-native/normalize-colors": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.78.0.tgz", + "integrity": "sha512-FkeLvLLaMYlGsSntixTUvlNtc1OHij4TYRtymMNPWqBKFAMXJB/qe45VxXNzWP+jD0Ok6yXineQFtktKcHk9PA==" + }, "node_modules/react-native-windows/node_modules/@types/yargs": { "version": "15.0.19", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", @@ -12701,6 +12942,25 @@ "node": ">=18" } }, + "node_modules/react-native-windows/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/react-native-windows/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/react-native-windows/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -12713,6 +12973,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/react-native-windows/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/react-native-windows/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -12745,6 +13010,18 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-native-windows/node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-native-windows/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -13542,7 +13819,6 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "license": "BSD-3-Clause", "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", @@ -13967,7 +14243,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14481,7 +14756,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", "integrity": "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==", - "license": "MIT", "dependencies": { "execa": "^1.0.0", "mem": "^4.3.0" @@ -14494,7 +14768,6 @@ "version": "6.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -14510,7 +14783,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "license": "MIT", "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -14528,7 +14800,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -14540,7 +14811,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14549,7 +14819,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "license": "MIT", "dependencies": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", @@ -14563,7 +14832,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "license": "MIT", "dependencies": { "path-key": "^2.0.0" }, @@ -14575,7 +14843,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "license": "MIT", "engines": { "node": ">=4" } @@ -14584,7 +14851,6 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -14593,7 +14859,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -14605,7 +14870,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14614,7 +14878,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -14860,7 +15123,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.1.tgz", "integrity": "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==", - "license": "MIT", "dependencies": { "xml-parser-xo": "^3.2.0" }, @@ -14872,7 +15134,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/xml-parser/-/xml-parser-1.2.1.tgz", "integrity": "sha512-lPUzzmS0zdwcNtyNndCl2IwH172ozkUDqmfmH3FcuDzHVl552Kr6oNfsvteHabqTWhsrMgpijqZ/yT7Wo1/Pzw==", - "license": "MIT", "dependencies": { "debug": "^2.2.0" } @@ -14881,7 +15142,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==", - "license": "MIT", "engines": { "node": ">= 10" } @@ -14890,7 +15150,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -14898,14 +15157,12 @@ "node_modules/xml-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/xpath": { "version": "0.0.27", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", - "license": "MIT", "engines": { "node": ">=0.6.0" } diff --git a/package.json b/package.json index b36a5eb..8707110 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,15 @@ "ci": "npm run lint", "start": "REACT_NATIVE_PATH=./node_modules/react-native-macos RCT_SCRIPT_RN_DIR=$REACT_NATIVE_PATH RCT_NEW_ARCH_ENABLED=1 ./node_modules/react-native-macos/scripts/packager.sh start", "test": "jest", - "postinstall": "ln -sf $(pwd)/node_modules/react-native-macos $(pwd)/node_modules/react-native && patch-package", + "postinstall": "if [ \"$(uname)\" = \"Darwin\" ]; then ln -sf $(pwd)/node_modules/react-native-macos $(pwd)/node_modules/react-native; fi && patch-package", "node-process": "node -e \"require('./standalone-server').startReactotronServer({ port: 9292 })\"" }, "dependencies": { "@expo-google-fonts/space-grotesk": "^0.3.0", "@legendapp/list": "^1.1.4", "react": "19.0.0", - "react-native-macos": "^0.78.2", - "react-native-windows": "^0.78.5", + "react-native-macos": "0.78.2", + "react-native-windows": "0.78.5", "react-native-mmkv": "^3.3.0", "reactotron-core-contract": "^0.3.2" }, @@ -164,4 +164,4 @@ "template": "cpp-app" } } -} +} \ No newline at end of file diff --git a/windows/reactotron.Package/packages.lock.json b/windows/reactotron.Package/packages.lock.json index 4dde1ed..6921ced 100644 --- a/windows/reactotron.Package/packages.lock.json +++ b/windows/reactotron.Package/packages.lock.json @@ -4,46 +4,18 @@ "UAP,Version=v10.0.17763": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "boost": { - "type": "Transitive", - "resolved": "1.83.0", - "contentHash": "cy53VNMzysEMvhBixDe8ujPk67Fcj3v6FPHQnH91NYJNLHpc6jxa2xq9ruCaaJjE4M3YrGSHDi4uUSTGBWw6EQ==" - }, - "Microsoft.JavaScript.Hermes": { - "type": "Transitive", - "resolved": "0.0.0-2505.2001-0e4bc3b9", - "contentHash": "VNSUBgaGzJ/KkK3Br0b9FORkCgKqke54hi48vG42xRACIlxN+uLFMz0hRo+KHogz+Fsn+ltXicGwQsDVpmaCMg==" - }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.ReactNative.Cxx": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "d1bRbaozh3h0F9+Urg8QMQmlB1OFhOPumDjP2d51KqROeylSbp8Wo5pNRich8VGUKpM6QurVXNsjcydYqi47Dg==", - "dependencies": { - "Microsoft.ReactNative": "0.78.14-Fabric" - } - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" }, "Microsoft.Windows.SDK.BuildTools": { "type": "Transitive", @@ -51,204 +23,126 @@ "contentHash": "7ZL2sFSioYm1Ry067Kw1hg0SCcW5kuVezC2SwjGbcPE61Nn+gTbH86T73G3LcEOVj0S3IZzNuE/29gZvOLS7VA==" }, "reactotron": { - "type": "Project", - "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2505.2001-0e4bc3b9, )", - "Microsoft.ReactNative": "[0.78.14-Fabric, )", - "Microsoft.ReactNative.Cxx": "[0.78.14-Fabric, )", - "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", - "Microsoft.WindowsAppSDK": "[1.7.250401001, )", - "boost": "[1.83.0, )" - } + "type": "Project" } }, "UAP,Version=v10.0.17763/win10-arm": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-arm-aot": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-arm64-aot": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-x64": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-x64-aot": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-x86": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "UAP,Version=v10.0.17763/win10-x86-aot": { "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, - "Microsoft.ReactNative": { - "type": "Transitive", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" - }, - "Microsoft.VCRTForwarders.140": { - "type": "Transitive", - "resolved": "1.0.2-rc", - "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" - }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } } } diff --git a/windows/reactotron/packages.lock.json b/windows/reactotron/packages.lock.json index 8cf1d4c..068c537 100644 --- a/windows/reactotron/packages.lock.json +++ b/windows/reactotron/packages.lock.json @@ -10,23 +10,23 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2505.2001-0e4bc3b9, )", - "resolved": "0.0.0-2505.2001-0e4bc3b9", - "contentHash": "VNSUBgaGzJ/KkK3Br0b9FORkCgKqke54hi48vG42xRACIlxN+uLFMz0hRo+KHogz+Fsn+ltXicGwQsDVpmaCMg==" + "requested": "[0.1.23, )", + "resolved": "0.1.23", + "contentHash": "cA9t1GjY4Yo0JD1AfA//e1lOwk48hLANfuX6GXrikmEBNZVr2TIX5ONJt5tqCnpZyLz6xGiPDgTfFNKbSfb21g==" }, "Microsoft.ReactNative": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "oN/M8Ob7LVMlCIgyshPWk50dKK8XJuA6pw0IFH8fP6v3+s2OEcvsH2dhxS5GMtJjffeP/y5sBMaY7p+bAKjf7A==" }, "Microsoft.ReactNative.Cxx": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "d1bRbaozh3h0F9+Urg8QMQmlB1OFhOPumDjP2d51KqROeylSbp8Wo5pNRich8VGUKpM6QurVXNsjcydYqi47Dg==", + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "NpMlcXQiSo8wQiTQ34t9v/5fUcxibBSfVMaAEc7G0C72lIetijXjagufbwL8Q+z/1F3zEMH3LdmpUHTFc/+Kfw==", "dependencies": { - "Microsoft.ReactNative": "0.78.14-Fabric" + "Microsoft.ReactNative": "0.78.5-Fabric" } }, "Microsoft.VCRTForwarders.140": { @@ -43,18 +43,18 @@ }, "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" }, "Microsoft.Windows.SDK.BuildTools": { "type": "Transitive", @@ -65,9 +65,9 @@ "native,Version=v0.0/win": { "Microsoft.ReactNative": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "oN/M8Ob7LVMlCIgyshPWk50dKK8XJuA6pw0IFH8fP6v3+s2OEcvsH2dhxS5GMtJjffeP/y5sBMaY7p+bAKjf7A==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -77,26 +77,26 @@ }, "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "native,Version=v0.0/win-arm64": { "Microsoft.ReactNative": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "oN/M8Ob7LVMlCIgyshPWk50dKK8XJuA6pw0IFH8fP6v3+s2OEcvsH2dhxS5GMtJjffeP/y5sBMaY7p+bAKjf7A==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -106,26 +106,26 @@ }, "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "native,Version=v0.0/win-x64": { "Microsoft.ReactNative": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "oN/M8Ob7LVMlCIgyshPWk50dKK8XJuA6pw0IFH8fP6v3+s2OEcvsH2dhxS5GMtJjffeP/y5sBMaY7p+bAKjf7A==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -135,26 +135,26 @@ }, "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } }, "native,Version=v0.0/win-x86": { "Microsoft.ReactNative": { "type": "Direct", - "requested": "[0.78.14-Fabric, )", - "resolved": "0.78.14-Fabric", - "contentHash": "KcsI2MLHH44fxo3xv8x24Et0SHVGY9qg7QVkLVkQdovPCAkJWnYvSqOl8lxNW1WCQ3zGeRhbs6WgNCAvWhMjVQ==" + "requested": "[0.78.5-Fabric, )", + "resolved": "0.78.5-Fabric", + "contentHash": "oN/M8Ob7LVMlCIgyshPWk50dKK8XJuA6pw0IFH8fP6v3+s2OEcvsH2dhxS5GMtJjffeP/y5sBMaY7p+bAKjf7A==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -164,18 +164,18 @@ }, "Microsoft.WindowsAppSDK": { "type": "Direct", - "requested": "[1.7.250401001, )", - "resolved": "1.7.250401001", - "contentHash": "kPsJ2LZoo3Xs/6FtIWMZRGnQ2ZMx9zDa0ZpqRGz1qwZr0gwwlXZJTmngaA1Ym2AHmIa05NtX2jEE2He8CzfhTg==", + "requested": "[1.6.240923002, )", + "resolved": "1.6.240923002", + "contentHash": "7PfOz2scXU+AAM/GYge+f6s7k3DVI+R1P8MNPZQr56GOPCGw+csvcg3S5KZg47z/o04kNvWH3GKtWT1ML9tpZw==", "dependencies": { - "Microsoft.Web.WebView2": "1.0.2903.40", + "Microsoft.Web.WebView2": "1.0.2651.64", "Microsoft.Windows.SDK.BuildTools": "10.0.22621.756" } }, "Microsoft.Web.WebView2": { "type": "Transitive", - "resolved": "1.0.2903.40", - "contentHash": "THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==" + "resolved": "1.0.2651.64", + "contentHash": "f5sc/vcAoTCTEW7Nqzp4galAuTRguZViw8ksn+Nx2uskEBPm0/ubzy6gVjvXS/P96jLS89C8T9I0hPc417xpNg==" } } } From f0dd6feb766f32ce4845dc0a8135eac40841cfc4 Mon Sep 17 00:00:00 2001 From: Jamon Holmgren Date: Tue, 4 Nov 2025 18:09:20 -0800 Subject: [PATCH 34/40] Autolinking stuff --- windows/reactotron/reactotron.vcxproj.filters | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows/reactotron/reactotron.vcxproj.filters b/windows/reactotron/reactotron.vcxproj.filters index efabed3..5b57ad6 100644 --- a/windows/reactotron/reactotron.vcxproj.filters +++ b/windows/reactotron/reactotron.vcxproj.filters @@ -44,6 +44,12 @@ Source Files + + Source Files + + + Source Files + From 064e5d7e4faf75b0729ff514931dab405308c45b Mon Sep 17 00:00:00 2001 From: Jamon Holmgren Date: Tue, 4 Nov 2025 18:09:40 -0800 Subject: [PATCH 35/40] Manually include ucrt (for Jamon's machine, oof) --- windows/reactotron/reactotron.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/reactotron/reactotron.vcxproj b/windows/reactotron/reactotron.vcxproj index 01a66bc..aa3a995 100644 --- a/windows/reactotron/reactotron.vcxproj +++ b/windows/reactotron/reactotron.vcxproj @@ -131,7 +131,7 @@ - $(ProjectDir)..\..\app;%(AdditionalIncludeDirectories) + $(ProjectDir)..\..\app;%(AdditionalIncludeDirectories);C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt From 59f7bddfc294dab6dff7cdc4627807a5ea995144 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 6 Nov 2025 08:58:22 -0500 Subject: [PATCH 36/40] Update lock file --- package-lock.json | 168 +++++++++++++++++++++++----------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c69009..166128f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,12 @@ "": { "name": "reactotron", "version": "4.0.0-alpha.1", + "hasInstallScript": true, "dependencies": { "@expo-google-fonts/space-grotesk": "^0.3.0", "@legendapp/list": "^1.1.4", "react": "19.0.0", - "react-native-macos": "^0.78.2", + "react-native-macos": "0.78.2", "react-native-mmkv": "^3.3.0", "react-native-windows": "0.78.5", "reactotron-core-contract": "^0.3.2" @@ -3186,30 +3187,6 @@ "resolved": "https://registry.npmjs.org/@react-native-mac/virtualized-lists/-/virtualized-lists-0.78.2.tgz", "integrity": "sha512-BtT/qFrSbAyts9Ugnk1lLlqqTphDA/VumRsDRA7rz7GWaKtBJhGiH4/Bd0n94BeoUE+sQ0tZxD4YiTKPVqxZmA==", "license": "MIT", - "peer": true, - "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/react": "^19.0.0", - "react": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@react-native-macos/virtualized-lists": { - "version": "0.78.6", - "resolved": "https://registry.npmjs.org/@react-native-macos/virtualized-lists/-/virtualized-lists-0.78.6.tgz", - "integrity": "sha512-As43Mg/QWVUXKCmNlZhCfcBNplKxcIuY7jrjE0aN6yNBE8mV66MnWjfneTFHqZ0Z3ax34shtPan13/94IZW3pA==", - "license": "MIT", "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -12061,19 +12038,20 @@ } }, "node_modules/react-native-macos": { - "version": "0.78.6", - "resolved": "https://registry.npmjs.org/react-native-macos/-/react-native-macos-0.78.6.tgz", - "integrity": "sha512-oP6NNr1oAJt4tckOr5nascd5PQxKPQidvrlAQu185NtclMQL1BQhyHaKTvEosgLqKnLeN/iIHmlljuCnoeDAxw==", + "version": "0.78.2", + "resolved": "https://registry.npmjs.org/react-native-macos/-/react-native-macos-0.78.2.tgz", + "integrity": "sha512-q6PsExdr0bVQBqYxLCAoOXQUhoKqqMztyapinh/r0obRhGE7A2StL89Pjaly+fOu0tBj1UtTfM6eTeprT18e0Q==", "license": "MIT", "dependencies": { "@jest/create-cache-key-function": "^29.6.3", - "@react-native-macos/virtualized-lists": "0.78.6", - "@react-native/assets-registry": "0.78.3", - "@react-native/codegen": "0.78.3", - "@react-native/community-cli-plugin": "0.78.3", - "@react-native/gradle-plugin": "0.78.3", - "@react-native/js-polyfills": "0.78.3", - "@react-native/normalize-colors": "0.78.3", + "@react-native-mac/virtualized-lists": "0.78.2", + "@react-native/assets-registry": "0.78.1", + "@react-native/codegen": "0.78.1", + "@react-native/community-cli-plugin": "0.78.1", + "@react-native/gradle-plugin": "0.78.1", + "@react-native/js-polyfills": "0.78.1", + "@react-native/normalize-colors": "0.78.1", + "@react-native/virtualized-lists": "0.78.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -12088,8 +12066,8 @@ "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "memoize-one": "^5.0.0", - "metro-runtime": "^0.81.3", - "metro-source-map": "^0.81.3", + "metro-runtime": "^0.81.0", + "metro-source-map": "^0.81.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", @@ -12111,8 +12089,7 @@ }, "peerDependencies": { "@types/react": "^19.0.0", - "react": "^19.0.0", - "react-native": "0.78.3" + "react": "^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -12121,31 +12098,31 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/assets-registry": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.78.3.tgz", - "integrity": "sha512-gQGoxEq7CuY/LjnHjORrNnJzUkx0YH7r/U1bvdznaaZ4CLcRFa1nKZEmZMv0h9moVqzr7GUbphJzS+RwqoGYIg==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.78.1.tgz", + "integrity": "sha512-SegfYQFuut05EQIQIVB/6QMGaxJ29jEtPmzFWJdIp/yc2mmhIq7MfWRjwOe6qbONzIdp6Ca8p835hiGiAGyeKQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/react-native-macos/node_modules/@react-native/babel-plugin-codegen": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.78.3.tgz", - "integrity": "sha512-yKs7KR9CzqGaM8mZi4vdjgaNgqomj094U325h2GWqsdj9+m/lf8e/Crd9sLDFtK0W2UCbcVw2L+M8okqXJ3oHw==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.78.1.tgz", + "integrity": "sha512-rD0tnct/yPEtoOc8eeFHIf8ZJJJEzLkmqLs8HZWSkt3w9VYWngqLXZxiDGqv0ngXjunAlC/Hpq+ULMVOvOnByw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.78.3" + "@react-native/codegen": "0.78.1" }, "engines": { "node": ">=18" } }, "node_modules/react-native-macos/node_modules/@react-native/babel-preset": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.78.3.tgz", - "integrity": "sha512-L1DRY8CYbrnpFoqVgeRW1FO8ZfgagYd3nx0M+9oaqG/VFX5rrfoMt011ZDeoYpmNayZS7klkqCFQLXVWAMPNBA==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.78.1.tgz", + "integrity": "sha512-yTVcHmEdNQH4Ju7lhvbiQaGxBpMcalgkBy/IvHowXKk/ex3nY1PolF16/mBG1BrefcUA/rtJpqTtk2Ii+7T/Lw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -12189,7 +12166,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.78.3", + "@react-native/babel-plugin-codegen": "0.78.1", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -12202,9 +12179,9 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/codegen": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.78.3.tgz", - "integrity": "sha512-p6mbFm6vvDskMj3zBzFIhHc85i2G/f47HwkFLJYSdWUITrPaVlXLSjSoCQPhYSNqrMv2g376OZZ+QXjp50XnTQ==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.78.1.tgz", + "integrity": "sha512-kGG5qAM9JdFtxzUwe7c6CyJbsU2PnaTrtCHA2dF8VEiNX1K3yd9yKPzfkxA7HPvmHoAn3ga1941O79BStWcM3A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", @@ -12223,19 +12200,19 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/community-cli-plugin": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.78.3.tgz", - "integrity": "sha512-Ax4mYFHxWH7xDsfPr7UR+WHBXAv3rXNzROEc7xVNsbNtpNVTHSqawUfDzH8jCO4rJEYQU18RARHwhBIXKwLFew==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.78.1.tgz", + "integrity": "sha512-S6vF4oWpFqThpt/dBLrqLQw5ED2M1kg5mVtiL6ZqpoYIg+/e0vg7LZ8EXNbcdMDH4obRnm2xbOd+qlC7mOzNBg==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.78.3", - "@react-native/metro-babel-transformer": "0.78.3", + "@react-native/dev-middleware": "0.78.1", + "@react-native/metro-babel-transformer": "0.78.1", "chalk": "^4.0.0", "debug": "^2.2.0", "invariant": "^2.2.4", - "metro": "^0.81.3", - "metro-config": "^0.81.3", - "metro-core": "^0.81.3", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", "readline": "^1.3.0", "semver": "^7.1.3" }, @@ -12252,22 +12229,22 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/debugger-frontend": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.78.3.tgz", - "integrity": "sha512-ImYGtEI9zsF/pietY45M8vd3OVWEkECbOngOhul0GVHECBsSHuOaQ/8PoxWl9Rps+8p1048aIMsPT9QzEtGwtQ==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.78.1.tgz", + "integrity": "sha512-xev/B++QLxSDpEBWsc74GyCuq9XOHYTBwcGSpsuhOJDUha6WZIbEEvZe3LpVW+OiFso4oGIdnVSQntwippZdWw==", "license": "BSD-3-Clause", "engines": { "node": ">=18" } }, "node_modules/react-native-macos/node_modules/@react-native/dev-middleware": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.78.3.tgz", - "integrity": "sha512-7upCJUYTFt3AwDQqByWDmTdlHYU93AdU+rsndis2xsJI4h7DrEjKtvvEgFOJG+jGHcyct9vNu1S+Jj2g8DRguQ==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.78.1.tgz", + "integrity": "sha512-l8p7/dXa1vWPOdj0iuACkex8lgbLpYyPZ3QXGkocMcpl0bQ24K7hf3Bj02tfptP5PAm16b2RuEi04sjIGHUzzg==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.78.3", + "@react-native/debugger-frontend": "0.78.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -12284,31 +12261,31 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/gradle-plugin": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.78.3.tgz", - "integrity": "sha512-Nrg3TRd/kjE+qOvukqeP5GqD1/oMd25X2yv370lWHBt9d0RJ0d008almkb5fHxQa+vKPeiAEhK726qCX8YXvIQ==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.78.1.tgz", + "integrity": "sha512-v8GJU+8DzQDWO3iuTFI1nbuQ/kzuqbXv07VVtSIMLbdofHzuuQT14DGBacBkrIDKBDTVaBGAc/baDNsyxCghng==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/react-native-macos/node_modules/@react-native/js-polyfills": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.78.3.tgz", - "integrity": "sha512-RvWAV2qU+XgMRVF+WIJQIqKdfrth1ghhdzAoKkXpXRKgWPps/6ZSCFgxkSjYaxAwXREOEx8/HunSmXDCsW+0ag==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.78.1.tgz", + "integrity": "sha512-Ogcv4QOA1o3IyErrf/i4cDnP+nfNcIfGTgw6iNQyAPry1xjPOz4ziajskLpWG/3ADeneIZuyZppKB4A28rZSvg==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/react-native-macos/node_modules/@react-native/metro-babel-transformer": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.78.3.tgz", - "integrity": "sha512-VSzAJ5G7uD1F5nG6NagHZFq6Q6dpsCU6LH+2j7iTsXZ9QUSds54f+WP5RC0UHZcVkQavSfqzu3+wj4pYGv5Pzg==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.78.1.tgz", + "integrity": "sha512-jQWf69D+QTMvSZSWLR+cr3VUF16rGB6sbD+bItD8Czdfn3hajzfMoHJTkVFP7991cjK5sIVekNiQIObou8JSQw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.78.3", + "@react-native/babel-preset": "0.78.1", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" }, @@ -12320,11 +12297,34 @@ } }, "node_modules/react-native-macos/node_modules/@react-native/normalize-colors": { - "version": "0.78.3", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.78.3.tgz", - "integrity": "sha512-/Nbuhc65xSVE3KFCejQEI9pgF+uwArj6EMHMVCkRtUqkM88Ng+f+8E7PyNN0hDUnj2Vr30FwBczdwm1kQLTWZA==", + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.78.1.tgz", + "integrity": "sha512-h4wARnY4iBFgigN1NjnaKFtcegWwQyE9+CEBVG4nHmwMtr8lZBmc7ZKIM6hUc6lxqY/ugHg48aSQSynss7mJUg==", "license": "MIT" }, + "node_modules/react-native-macos/node_modules/@react-native/virtualized-lists": { + "version": "0.78.1", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.78.1.tgz", + "integrity": "sha512-v0jqDNMFXpnRnSlkDVvwNxXgPhifzzTFlxTSnHj9erKJsKpE26gSU5qB4hmJkEsscLG/ygdJ1c88aqinSh/wRA==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-native-macos/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -12410,9 +12410,9 @@ "license": "MIT" }, "node_modules/react-native-macos/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" From a93a9304883625d29a45aa7a27385aa69d546061 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 13 Nov 2025 20:19:32 -0500 Subject: [PATCH 37/40] Refactor MenuDropdownItem component to use 'disabled' property instead of 'enabled' for item state management --- app/components/Menu/MenuDropdownItem.tsx | 16 ++++++++-------- app/components/Menu/types.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index e804b5b..8f1949a 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -19,7 +19,7 @@ const MenuDropdownItemComponent = ({ }: MenuDropdownItemProps) => { const [hoveredItem, setHoveredItem] = useState(null) const hoverTimeoutRef = useRef(null) - const enabled = item.enabled !== false + const disabled = item.disabled === false const handleHoverIn = useCallback(() => { // Clear any pending hover clear @@ -40,7 +40,7 @@ const MenuDropdownItemComponent = ({ }, [item.label]) const handlePress = useCallback(() => { - if (!item.action || !enabled) return + if (!item.action || disabled) return item.action() onItemPress(item) }, [item, onItemPress]) @@ -50,24 +50,24 @@ const MenuDropdownItemComponent = ({ onHoverIn={handleHoverIn} onHoverOut={handleHoverOut} onPress={handlePress} - disabled={!enabled} + disabled={disabled} style={({ pressed }) => [ $dropdownItem(), - (pressed || hoveredItem === item.label) && enabled && $dropdownItemHovered(), - !enabled && $dropdownItemDisabled, + (pressed || hoveredItem === item.label) && !disabled && $dropdownItemHovered(), + disabled && $dropdownItemDisabled, ]} > - + {item.label} {item.shortcut && ( - + {formatShortcut(item.shortcut.windows || "")} )} {item.submenu && ( - + )} diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts index 0b02bed..bbd4c28 100644 --- a/app/components/Menu/types.ts +++ b/app/components/Menu/types.ts @@ -1,4 +1,4 @@ -import { PlatformShortcut } from "app/utils/useSystemMenu/types" +import { PlatformShortcut } from "../../utils/useSystemMenu/types" export interface Position { x: number @@ -9,7 +9,7 @@ export interface Position { export interface MenuItem { label: string shortcut?: PlatformShortcut - enabled?: boolean + disabled?: boolean action?: () => void submenu?: (MenuItem | typeof MENU_SEPARATOR)[] } From 0f6bdd8104c88c15cfd8d0974e57775142035eb1 Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 13 Nov 2025 20:19:44 -0500 Subject: [PATCH 38/40] Refactor MenuDropdown to utilize a separate utility function for identifying menu separators --- app/components/Menu/MenuDropdown.tsx | 5 +---- app/components/Menu/utils.ts | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 app/components/Menu/utils.ts diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx index 0f1a9d0..954092c 100644 --- a/app/components/Menu/MenuDropdown.tsx +++ b/app/components/Menu/MenuDropdown.tsx @@ -8,6 +8,7 @@ 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" +import { isSeparator } from "./utils" interface MenuDropdownProps { items: (DropdownMenuItem | typeof MENU_SEPARATOR)[] @@ -20,10 +21,6 @@ const MenuDropdownComponent = ({ items, position, onItemPress, isSubmenu }: Menu const portalName = useRef(`${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 diff --git a/app/components/Menu/utils.ts b/app/components/Menu/utils.ts new file mode 100644 index 0000000..9bbe6ff --- /dev/null +++ b/app/components/Menu/utils.ts @@ -0,0 +1,7 @@ +import { MenuItem, MENU_SEPARATOR } from "./types" + +export const isSeparator = ( + item: MenuItem | typeof MENU_SEPARATOR, +): item is typeof MENU_SEPARATOR => { + return item === MENU_SEPARATOR +} From 75daecb47399f6df0c89ae1c8e778b22606dcacf Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 13 Nov 2025 20:29:38 -0500 Subject: [PATCH 39/40] still call onItemPress even if item.action isn't present --- app/components/Menu/MenuDropdownItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx index 8f1949a..773d81c 100644 --- a/app/components/Menu/MenuDropdownItem.tsx +++ b/app/components/Menu/MenuDropdownItem.tsx @@ -40,8 +40,8 @@ const MenuDropdownItemComponent = ({ }, [item.label]) const handlePress = useCallback(() => { - if (!item.action || disabled) return - item.action() + if (disabled) return + if (item.action) item.action() onItemPress(item) }, [item, onItemPress]) From 18f145196f5db2f7f817484aaaf8a0fc73c4553a Mon Sep 17 00:00:00 2001 From: Sean Barker Date: Thu, 13 Nov 2025 20:35:59 -0500 Subject: [PATCH 40/40] Move separator constant to utils --- app/components/Menu/MenuDropdown.tsx | 4 ++-- app/components/Menu/types.ts | 4 +--- app/components/Menu/utils.ts | 5 ++++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx index 954092c..a2e7c65 100644 --- a/app/components/Menu/MenuDropdown.tsx +++ b/app/components/Menu/MenuDropdown.tsx @@ -5,10 +5,10 @@ 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" -import { isSeparator } from "./utils" +import { isSeparator, MENU_SEPARATOR } from "./utils" +import type { Position, DropdownMenuItem, MenuItem } from "./types" interface MenuDropdownProps { items: (DropdownMenuItem | typeof MENU_SEPARATOR)[] diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts index bbd4c28..64a4446 100644 --- a/app/components/Menu/types.ts +++ b/app/components/Menu/types.ts @@ -1,4 +1,5 @@ import { PlatformShortcut } from "../../utils/useSystemMenu/types" +import { MENU_SEPARATOR } from "./utils" export interface Position { x: number @@ -16,6 +17,3 @@ export interface MenuItem { // 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 diff --git a/app/components/Menu/utils.ts b/app/components/Menu/utils.ts index 9bbe6ff..d4ccf9b 100644 --- a/app/components/Menu/utils.ts +++ b/app/components/Menu/utils.ts @@ -1,4 +1,7 @@ -import { MenuItem, MENU_SEPARATOR } from "./types" +import { MenuItem } from "./types" + +// Menu separator constant +export const MENU_SEPARATOR = "menu-item-separator" as const export const isSeparator = ( item: MenuItem | typeof MENU_SEPARATOR,