From ee35556ae5e2667415082a63f40e98e3ab63ab6d Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 00:39:14 +0100 Subject: [PATCH 01/34] =?UTF-8?q?feat:=20implements=20a=20new=20custom=20m?= =?UTF-8?q?enu=20style=20instead=20of=20the=20native=20Android=20=E2=80=9C?= =?UTF-8?q?MenuView=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/components/ActionMenu.tsx | 282 +++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 ui/components/ActionMenu.tsx diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx new file mode 100644 index 00000000..288d21fd --- /dev/null +++ b/ui/components/ActionMenu.tsx @@ -0,0 +1,282 @@ +import React, { ReactNode, useState, useRef, cloneElement, isValidElement } from "react"; +import { + Modal, + Platform, + Pressable, + TouchableOpacity, + View, + StyleSheet, + Text, + LayoutRectangle, +} from "react-native"; +import { useTheme } from "@react-navigation/native"; + +let NativeMenuView: any = null; +try { + NativeMenuView = require("@react-native-menu/menu").MenuView; +} catch {} + +interface MenuAction { + id: string; + title: string; + subtitle?: string; + state?: "on" | "off" | "mixed"; + image?: string; + imageColor?: string; + destructive?: boolean; + disabled?: boolean; + subactions?: MenuAction[]; + displayInline?: boolean; +} + +interface ActionMenuProps { + actions: MenuAction[]; + children: ReactNode; + onPressAction: (event: { nativeEvent: { event: string } }) => void; + title?: string; +} + +function MenuItem({ + action, + textColor, + subtitleColor, + primaryColor, + onPress, +}: { + action: MenuAction; + textColor: string; + subtitleColor: string; + primaryColor: string; + onPress: () => void; +}) { + const isOn = action.state === "on"; + const hasSubactions = action.subactions && action.subactions.length > 0; + + return ( + + + + + {action.title} + + {action.subtitle && ( + + {action.subtitle} + + )} + + {hasSubactions && ( + + )} + {isOn && !hasSubactions && ( + + )} + + + ); +} + +export default function ActionMenu({ + actions = [], + children, + onPressAction, + title, +}: ActionMenuProps) { + const { colors, dark } = useTheme(); + const textColor = colors.text; + const subtitleColor = dark ? `${colors.text}80` : `${colors.text}80`; + const primaryColor = colors.primary; + const cardColor = colors.card; + + const triggerRef = useRef(null); + const [visible, setVisible] = useState(false); + const [submenu, setSubmenu] = useState(null); + const [position, setPosition] = useState(null); + + // iOS + if (Platform.OS === "ios" && NativeMenuView) { + return ( + + {children} + + ); + } + + // Android + function open() { + setVisible(true); + setTimeout(() => { + triggerRef.current?.measureInWindow((x, y, width, height) => { + setPosition({ x, y, width, height }); + }); + }, 0); + } + + function close() { + setVisible(false); + setSubmenu(null); + } + + function handlePress(action: MenuAction) { + if (action.subactions && action.subactions.length > 0) { + setSubmenu(action); + } else { + onPressAction({ nativeEvent: { event: action.id } }); + close(); + } + } + + function getMenuPosition() { + if (!position) { + return { alignSelf: "center" as const }; + } + return { + position: "absolute" as const, + top: position.y + position.height + 8, + left: Math.min(position.x, 100), + }; + } + + const currentActions = submenu?.subactions ?? actions; + + const trigger = isValidElement(children) + ? cloneElement(children as React.ReactElement, { + onPress: () => { + (children as any).props?.onPress?.(); + open(); + }, + }) + : children; + + return ( + + {trigger} + + + + + {submenu && ( + setSubmenu(null)} + style={styles.header} + > + + ‹ {submenu.title} + + + )} + {currentActions.map((action) => ( + handlePress(action)} + /> + ))} + + + + + ); +} + +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.35)", + }, + container: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + menu: { + minWidth: 260, + maxWidth: 320, + borderRadius: 14, + padding: 6, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 8, + }, + header: { + paddingVertical: 10, + paddingHorizontal: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "rgba(128,128,128,0.2)", + marginBottom: 4, + }, + back: { + fontSize: 16, + fontWeight: "600", + }, + item: { + flexDirection: "row", + alignItems: "center", + minHeight: 48, + paddingVertical: 12, + paddingHorizontal: 14, + borderRadius: 10, + }, + itemSelected: { + backgroundColor: "rgba(0,102,204,0.12)", + }, + itemContent: { + flex: 1, + marginRight: 10, + }, + itemTitle: { + fontSize: 16, + }, + itemSubtitle: { + fontSize: 13, + marginTop: 2, + }, + arrow: { + fontSize: 22, + fontWeight: "400", + opacity: 0.7, + marginLeft: 4, + }, + check: { + fontSize: 18, + fontWeight: "600", + marginLeft: 6, + }, + disabled: { + opacity: 0.4, + }, +}); From 7f984a0cf8abbf33f75be45c2d880bafd3e3813e Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 00:44:01 +0100 Subject: [PATCH 02/34] =?UTF-8?q?feat:=20Modification=20of=20=E2=80=9CMenu?= =?UTF-8?q?View=E2=80=9D=20by=20the=20new=20component=20=E2=80=9CActionMen?= =?UTF-8?q?u=E2=80=9D=20in=20Averages.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/grades/atoms/Averages.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/(tabs)/grades/atoms/Averages.tsx b/app/(tabs)/grades/atoms/Averages.tsx index 0fddad49..596744d5 100644 --- a/app/(tabs)/grades/atoms/Averages.tsx +++ b/app/(tabs)/grades/atoms/Averages.tsx @@ -1,5 +1,5 @@ import { Papicons } from "@getpapillon/papicons"; -import { MenuView } from "@react-native-menu/menu"; +import ActionMenu from "@/ui/components/ActionMenu"; import { useTheme } from "@react-navigation/native"; import { useRouter } from "expo-router"; import { t } from "i18next"; @@ -211,9 +211,10 @@ const Averages = ({ grades, realAverage, color, scale = 20 }: { grades: Grade[], - ({ id: "setAlg:" + algo.key, @@ -263,7 +264,7 @@ const Averages = ({ grades, realAverage, color, scale = 20 }: { grades: Grade[], - + From 4f34adf029af91f79d706a3477f244287e651dab Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 00:44:41 +0100 Subject: [PATCH 03/34] =?UTF-8?q?feat:=20Modification=20of=20=E2=80=9CMenu?= =?UTF-8?q?View=E2=80=9D=20by=20the=20new=20component=20=E2=80=9CActionMen?= =?UTF-8?q?u=E2=80=9D=20in=20index.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/grades/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/(tabs)/grades/index.tsx b/app/(tabs)/grades/index.tsx index 61d91e9b..d716c88e 100644 --- a/app/(tabs)/grades/index.tsx +++ b/app/(tabs)/grades/index.tsx @@ -1,6 +1,6 @@ import { Papicons } from '@getpapillon/papicons'; import { LegendList } from '@legendapp/list'; -import { MenuView } from '@react-native-menu/menu'; +import ActionMenu from '@/ui/components/ActionMenu'; import { useTheme } from '@react-navigation/native'; import { useNavigation } from 'expo-router'; import { t } from 'i18next'; @@ -413,7 +413,7 @@ const GradesView: React.FC = () => { onHeightChanged={setHeaderHeight} /* Nom de la période */ title={ - { const actionId = nativeEvent.event; @@ -450,7 +450,7 @@ const GradesView: React.FC = () => { loading={loading} chevron={periods.length > 1} /> - + } /* Filtres */ trailing={ @@ -534,4 +534,5 @@ const GradesView: React.FC = () => { ) }; -export default GradesView; \ No newline at end of file +export default GradesView; + From 160c7daa1064b4cdc57da3a09c5fd008517738bc Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 10:50:55 +0100 Subject: [PATCH 04/34] fix: Add the changes suggested by copilot in PR --- ui/components/ActionMenu.tsx | 67 +++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 288d21fd..b19aa8f8 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -8,6 +8,7 @@ import { StyleSheet, Text, LayoutRectangle, + Dimensions, } from "react-native"; import { useTheme } from "@react-navigation/native"; @@ -17,7 +18,7 @@ try { } catch {} interface MenuAction { - id: string; + id?: string; title: string; subtitle?: string; state?: "on" | "off" | "mixed"; @@ -27,6 +28,11 @@ interface MenuAction { disabled?: boolean; subactions?: MenuAction[]; displayInline?: boolean; + attributes?: { + destructive?: boolean; + disabled?: boolean; + state?: "on" | "off" | "mixed"; + }; } interface ActionMenuProps { @@ -49,18 +55,21 @@ function MenuItem({ primaryColor: string; onPress: () => void; }) { - const isOn = action.state === "on"; + const state = action.state ?? action.attributes?.state; + const isOn = state === "on"; const hasSubactions = action.subactions && action.subactions.length > 0; + const destructive = action.destructive ?? action.attributes?.destructive ?? false; + const disabled = action.disabled ?? action.attributes?.disabled ?? false; return ( - + @@ -71,7 +80,7 @@ function MenuItem({ style={[ styles.itemSubtitle, { color: subtitleColor }, - action.disabled && styles.disabled, + disabled && styles.disabled, ]} numberOfLines={2} > @@ -102,9 +111,9 @@ export default function ActionMenu({ const primaryColor = colors.primary; const cardColor = colors.card; - const triggerRef = useRef(null); + const triggerRef = useRef(null); const [visible, setVisible] = useState(false); - const [submenu, setSubmenu] = useState(null); + const [submenuStack, setSubmenuStack] = useState([]); const [position, setPosition] = useState(null); // iOS @@ -132,30 +141,48 @@ export default function ActionMenu({ function close() { setVisible(false); - setSubmenu(null); + setSubmenuStack([]); } function handlePress(action: MenuAction) { if (action.subactions && action.subactions.length > 0) { - setSubmenu(action); - } else { - onPressAction({ nativeEvent: { event: action.id } }); - close(); + setSubmenuStack((prev) => [...prev, action]); + return; } + onPressAction({ nativeEvent: { event: action.id ?? "" } }); + close(); + } + + function handleBack() { + setSubmenuStack((prev) => prev.slice(0, -1)); } function getMenuPosition() { if (!position) { return { alignSelf: "center" as const }; } + const window = Dimensions.get("window"); + const margin = 16; + const menuWidth = 320; + const estimatedMenuHeight = 260; + const left = Math.max(margin, Math.min(position.x, window.width - margin - menuWidth)); + + const belowTop = position.y + position.height + 8; + const spaceBelow = window.height - margin - belowTop; + const canShowBelow = spaceBelow >= estimatedMenuHeight; + const top = canShowBelow + ? belowTop + : Math.max(margin, position.y - estimatedMenuHeight - 8); + return { position: "absolute" as const, - top: position.y + position.height + 8, - left: Math.min(position.x, 100), + top, + left, }; } - const currentActions = submenu?.subactions ?? actions; + const currentSubmenu = submenuStack[submenuStack.length - 1]; + const currentActions = currentSubmenu?.subactions ?? actions; const trigger = isValidElement(children) ? cloneElement(children as React.ReactElement, { @@ -167,7 +194,7 @@ export default function ActionMenu({ : children; return ( - + {trigger} @@ -179,13 +206,13 @@ export default function ActionMenu({ { backgroundColor: cardColor }, ]} > - {submenu && ( + {currentSubmenu && ( setSubmenu(null)} + onPress={handleBack} style={styles.header} > - ‹ {submenu.title} + ‹ {currentSubmenu.title} )} From e697683e91519e93e8c5093faaf644e035c1486b Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 10:52:37 +0100 Subject: [PATCH 05/34] fix: Remove id from Averages.tsx because it is no longer necessary and was not present before --- app/(tabs)/grades/atoms/Averages.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/(tabs)/grades/atoms/Averages.tsx b/app/(tabs)/grades/atoms/Averages.tsx index 596744d5..9a6fa5ad 100644 --- a/app/(tabs)/grades/atoms/Averages.tsx +++ b/app/(tabs)/grades/atoms/Averages.tsx @@ -214,7 +214,6 @@ const Averages = ({ grades, realAverage, color, scale = 20 }: { grades: Grade[], ({ id: "setAlg:" + algo.key, From fab33fbba7c6b771824f546c0fc672da54f737a5 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 11:29:18 +0100 Subject: [PATCH 06/34] fix(ui): Replace MenuView with ActionMenu in ChipButton and correct interactions. --- ui/components/ActionMenu.tsx | 42 +++++++++++++++++++----------------- ui/components/ChipButton.tsx | 11 +++++----- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index b19aa8f8..a5b38688 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, useRef, cloneElement, isValidElement } from "react"; +import React, { ReactNode, useState, useRef } from "react"; import { Modal, Platform, @@ -9,6 +9,7 @@ import { Text, LayoutRectangle, Dimensions, + ColorValue, } from "react-native"; import { useTheme } from "@react-navigation/native"; @@ -23,7 +24,7 @@ interface MenuAction { subtitle?: string; state?: "on" | "off" | "mixed"; image?: string; - imageColor?: string; + imageColor?: number | ColorValue; destructive?: boolean; disabled?: boolean; subactions?: MenuAction[]; @@ -38,7 +39,7 @@ interface MenuAction { interface ActionMenuProps { actions: MenuAction[]; children: ReactNode; - onPressAction: (event: { nativeEvent: { event: string } }) => void; + onPressAction?: (event: { nativeEvent: { event: string } }) => void; title?: string; } @@ -105,6 +106,7 @@ export default function ActionMenu({ onPressAction, title, }: ActionMenuProps) { + const handleActionPress = onPressAction ?? (() => {}); const { colors, dark } = useTheme(); const textColor = colors.text; const subtitleColor = dark ? `${colors.text}80` : `${colors.text}80`; @@ -120,7 +122,7 @@ export default function ActionMenu({ if (Platform.OS === "ios" && NativeMenuView) { return ( @@ -144,12 +146,12 @@ export default function ActionMenu({ setSubmenuStack([]); } - function handlePress(action: MenuAction) { + function handlePress(action: MenuAction, fallbackId: string) { if (action.subactions && action.subactions.length > 0) { setSubmenuStack((prev) => [...prev, action]); return; } - onPressAction({ nativeEvent: { event: action.id ?? "" } }); + handleActionPress({ nativeEvent: { event: action.id ?? fallbackId } }); close(); } @@ -184,18 +186,18 @@ export default function ActionMenu({ const currentSubmenu = submenuStack[submenuStack.length - 1]; const currentActions = currentSubmenu?.subactions ?? actions; - const trigger = isValidElement(children) - ? cloneElement(children as React.ReactElement, { - onPress: () => { - (children as any).props?.onPress?.(); - open(); - }, - }) - : children; - return ( - - {trigger} + { + if (!visible) { + e.stopPropagation(); + open(); + } + }} + > + {children} @@ -216,14 +218,14 @@ export default function ActionMenu({ )} - {currentActions.map((action) => ( + {currentActions.map((action, index) => ( handlePress(action)} + onPress={() => handlePress(action, `action-${submenuStack.length}-${index}`)} /> ))} diff --git a/ui/components/ChipButton.tsx b/ui/components/ChipButton.tsx index b2cb2a54..50a8616a 100644 --- a/ui/components/ChipButton.tsx +++ b/ui/components/ChipButton.tsx @@ -1,5 +1,5 @@ import { Papicons } from "@getpapillon/papicons"; -import { MenuAction, MenuView } from '@react-native-menu/menu'; +import { MenuAction } from '@react-native-menu/menu'; import { useTheme } from "@react-navigation/native"; import { LiquidGlassView } from '@sbaiahmed1/react-native-blur'; import React from "react"; @@ -11,6 +11,7 @@ import { Dynamic } from "./Dynamic"; import Icon from "./Icon"; import Stack from "./Stack"; import Typography from "./Typography"; +import ActionMenu from "./ActionMenu"; const ChipButton: React.FC void; @@ -43,7 +44,7 @@ const ChipButton: React.FC - + {icon && @@ -69,7 +70,7 @@ const ChipButton: React.FC } - + ); @@ -93,7 +94,7 @@ const ChipButton: React.FC - + {icon && @@ -119,7 +120,7 @@ const ChipButton: React.FC } - + ); From c8a2f8f33fa116e2f608e39847b22bfe26d3a2c4 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 14:27:52 +0100 Subject: [PATCH 07/34] fix(ui/action-menu): add error handling in ActionMenu.tsx and loads MenuView only on iOS --- ui/components/ActionMenu.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index a5b38688..a56ce567 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -14,9 +14,15 @@ import { import { useTheme } from "@react-navigation/native"; let NativeMenuView: any = null; -try { - NativeMenuView = require("@react-native-menu/menu").MenuView; -} catch {} +if (Platform.OS === "ios") { + try { + const mod = require("@react-native-menu/menu"); + NativeMenuView = mod?.MenuView ?? null; + } catch (err: unknown) { + console.warn("ActionMenu: impossible de charger @react-native-menu/menu MenuView:", err); + NativeMenuView = null; + } +} interface MenuAction { id?: string; From 6fec72faef778ce6d909e610b72465b4ebd747ea Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 14:31:16 +0100 Subject: [PATCH 08/34] fix(ui/action-menu): Replace any with strict typing for NativeMenuView in ActionMenu --- ui/components/ActionMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index a56ce567..2ff0e3b3 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, useState, useRef } from "react"; +import type { ComponentType } from "react"; import { Modal, Platform, @@ -13,7 +14,7 @@ import { } from "react-native"; import { useTheme } from "@react-navigation/native"; -let NativeMenuView: any = null; +let NativeMenuView: ComponentType> | null = null; if (Platform.OS === "ios") { try { const mod = require("@react-native-menu/menu"); From 66e2bd27e145e0da5da72817db448e4128cc8ace Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 14:38:47 +0100 Subject: [PATCH 09/34] fix(ui/action-menu): reusing native types from @react-native-menu --- ui/components/ActionMenu.tsx | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 2ff0e3b3..0f913d30 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -1,5 +1,6 @@ import React, { ReactNode, useState, useRef } from "react"; import type { ComponentType } from "react"; +import type { MenuAction as NativeMenuAction, NativeActionEvent } from "@react-native-menu/menu"; import { Modal, Platform, @@ -10,7 +11,6 @@ import { Text, LayoutRectangle, Dimensions, - ColorValue, } from "react-native"; import { useTheme } from "@react-navigation/native"; @@ -25,28 +25,12 @@ if (Platform.OS === "ios") { } } -interface MenuAction { - id?: string; - title: string; - subtitle?: string; - state?: "on" | "off" | "mixed"; - image?: string; - imageColor?: number | ColorValue; - destructive?: boolean; - disabled?: boolean; - subactions?: MenuAction[]; - displayInline?: boolean; - attributes?: { - destructive?: boolean; - disabled?: boolean; - state?: "on" | "off" | "mixed"; - }; -} +type MenuAction = NativeMenuAction; interface ActionMenuProps { actions: MenuAction[]; children: ReactNode; - onPressAction?: (event: { nativeEvent: { event: string } }) => void; + onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; title?: string; } @@ -63,11 +47,11 @@ function MenuItem({ primaryColor: string; onPress: () => void; }) { - const state = action.state ?? action.attributes?.state; + const state = action.state; const isOn = state === "on"; const hasSubactions = action.subactions && action.subactions.length > 0; - const destructive = action.destructive ?? action.attributes?.destructive ?? false; - const disabled = action.disabled ?? action.attributes?.disabled ?? false; + const destructive = action.attributes?.destructive ?? false; + const disabled = action.attributes?.disabled ?? false; return ( From c29394ea610e2c12055d12208fbe18c1cd2427d3 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 14:59:27 +0100 Subject: [PATCH 10/34] refactor(ui/action-menu): simplify legacy flag extraction and boolean checks --- ui/components/ActionMenu.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 0f913d30..ae41d1c3 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -47,11 +47,10 @@ function MenuItem({ primaryColor: string; onPress: () => void; }) { - const state = action.state; - const isOn = state === "on"; - const hasSubactions = action.subactions && action.subactions.length > 0; - const destructive = action.attributes?.destructive ?? false; - const disabled = action.attributes?.disabled ?? false; + const isOn = action.state === "on"; + const hasSubactions = Boolean(action.subactions?.length); + const destructive = Boolean((action as unknown as { destructive?: boolean }).destructive ?? action.attributes?.destructive); + const disabled = Boolean((action as unknown as { disabled?: boolean }).disabled ?? action.attributes?.disabled); return ( From 435bf5641cf4774ef54476cdd2a2b5cf70acbf04 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 15:05:00 +0100 Subject: [PATCH 11/34] refactor(ui/action-menu): simplify state and legacy flag handling --- ui/components/ActionMenu.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index ae41d1c3..e89c3b0f 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -47,10 +47,12 @@ function MenuItem({ primaryColor: string; onPress: () => void; }) { + // Reduced flags: compute isOn from action.state (attributes does not expose state in the types). const isOn = action.state === "on"; const hasSubactions = Boolean(action.subactions?.length); - const destructive = Boolean((action as unknown as { destructive?: boolean }).destructive ?? action.attributes?.destructive); - const disabled = Boolean((action as unknown as { disabled?: boolean }).disabled ?? action.attributes?.disabled); + const legacy = action as unknown as { destructive?: boolean; disabled?: boolean }; + const destructive = Boolean(action.attributes?.destructive ?? legacy.destructive); + const disabled = Boolean(action.attributes?.disabled ?? legacy.disabled); return ( From 58cde3ce57e04928809a49b772c711b41d6ed6c7 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 15:33:39 +0100 Subject: [PATCH 12/34] refactor(ui/action-menu): use Stack for item layout --- ui/components/ActionMenu.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index e89c3b0f..96cd13b7 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -13,6 +13,7 @@ import { Dimensions, } from "react-native"; import { useTheme } from "@react-navigation/native"; +import Stack from "@/ui/components/Stack"; let NativeMenuView: ComponentType> | null = null; if (Platform.OS === "ios") { @@ -56,7 +57,7 @@ function MenuItem({ return ( - + )} - + ); } @@ -265,8 +266,6 @@ const styles = StyleSheet.create({ fontWeight: "600", }, item: { - flexDirection: "row", - alignItems: "center", minHeight: 48, paddingVertical: 12, paddingHorizontal: 14, From 5e31489e4cbb6daa92187e045c39e34aa78c349a Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 15:48:43 +0100 Subject: [PATCH 13/34] refactor(ui/action-menu): change borderBottomColor --- ui/components/ActionMenu.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 96cd13b7..f61789bd 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -14,6 +14,7 @@ import { } from "react-native"; import { useTheme } from "@react-navigation/native"; import Stack from "@/ui/components/Stack"; +import { runsIOS26 } from "@/ui/utils/IsLiquidGlass"; let NativeMenuView: ComponentType> | null = null; if (Platform.OS === "ios") { @@ -105,6 +106,7 @@ export default function ActionMenu({ const subtitleColor = dark ? `${colors.text}80` : `${colors.text}80`; const primaryColor = colors.primary; const cardColor = colors.card; + const borderColor = colors.border; const triggerRef = useRef(null); const [visible, setVisible] = useState(false); @@ -204,7 +206,10 @@ export default function ActionMenu({ {currentSubmenu && ( ‹ {currentSubmenu.title} @@ -258,7 +263,6 @@ const styles = StyleSheet.create({ paddingVertical: 10, paddingHorizontal: 14, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "rgba(128,128,128,0.2)", marginBottom: 4, }, back: { From 6f2648783ae470e7f0815543bd41e74da763680c Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 16:04:18 +0100 Subject: [PATCH 14/34] refactor(ui/action-menu): simplify menu positioning --- ui/components/ActionMenu.tsx | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index f61789bd..adb2f069 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -158,24 +158,20 @@ export default function ActionMenu({ if (!position) { return { alignSelf: "center" as const }; } - const window = Dimensions.get("window"); - const margin = 16; - const menuWidth = 320; - const estimatedMenuHeight = 260; - const left = Math.max(margin, Math.min(position.x, window.width - margin - menuWidth)); - const belowTop = position.y + position.height + 8; - const spaceBelow = window.height - margin - belowTop; - const canShowBelow = spaceBelow >= estimatedMenuHeight; - const top = canShowBelow - ? belowTop - : Math.max(margin, position.y - estimatedMenuHeight - 8); + const { width, height } = Dimensions.get("window"); + const MARGIN = 16; - return { - position: "absolute" as const, - top, - left, - }; + //A remplacer + const MENU_WIDTH = 320; + const MENU_HEIGHT = 260; + + const left = Math.min(Math.max(position.x, MARGIN), width - MARGIN - MENU_WIDTH); + const below = position.y + position.height + 8; + const fitsBelow = below + MENU_HEIGHT <= height - MARGIN; + const top = fitsBelow ? below : Math.max(MARGIN, position.y - MENU_HEIGHT - 8); + + return { position: "absolute" as const, top, left }; } const currentSubmenu = submenuStack[submenuStack.length - 1]; From c1a75cf94f9613de7971fee404be8e5ae4bcb946 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 16:08:48 +0100 Subject: [PATCH 15/34] refactor(ui/action-menu): use textColor for subtitle color --- ui/components/ActionMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index adb2f069..a0a899d2 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -101,9 +101,9 @@ export default function ActionMenu({ title, }: ActionMenuProps) { const handleActionPress = onPressAction ?? (() => {}); - const { colors, dark } = useTheme(); + const { colors } = useTheme(); const textColor = colors.text; - const subtitleColor = dark ? `${colors.text}80` : `${colors.text}80`; + const subtitleColor = `${textColor}80`; const primaryColor = colors.primary; const cardColor = colors.card; const borderColor = colors.border; From bed2b324fa939758bdd67edfe64e4d8ea66ec453 Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 16:16:25 +0100 Subject: [PATCH 16/34] refactor(ui/action-menu): replace Unicode chevron/check with Papicons --- ui/components/ActionMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index a0a899d2..cb72da0b 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -14,6 +14,7 @@ import { } from "react-native"; import { useTheme } from "@react-navigation/native"; import Stack from "@/ui/components/Stack"; +import { Papicons } from "@getpapillon/papicons"; import { runsIOS26 } from "@/ui/utils/IsLiquidGlass"; let NativeMenuView: ComponentType> | null = null; @@ -84,10 +85,10 @@ function MenuItem({ )} {hasSubactions && ( - + )} {isOn && !hasSubactions && ( - + )} From 3bbe1d3a9e86e6db2fea2c993bb15bf135b0bb1b Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 16:25:06 +0100 Subject: [PATCH 17/34] refactor(ui/action-menu): replace Unicode chevron/check with Papicons and fixes the errors in this first commit. --- ui/components/ActionMenu.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index cb72da0b..111bd4f9 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -208,9 +208,10 @@ export default function ActionMenu({ { borderBottomColor: Platform.OS === "ios" && !runsIOS26 ? borderColor : undefined }, ]} > - - ‹ {currentSubmenu.title} - + + + {currentSubmenu.title} + )} {currentActions.map((action, index) => ( @@ -265,6 +266,7 @@ const styles = StyleSheet.create({ back: { fontSize: 16, fontWeight: "600", + lineHeight: 20, }, item: { minHeight: 48, @@ -287,16 +289,17 @@ const styles = StyleSheet.create({ marginTop: 2, }, arrow: { - fontSize: 22, - fontWeight: "400", + alignSelf: "center", opacity: 0.7, marginLeft: 4, }, check: { - fontSize: 18, - fontWeight: "600", + alignSelf: "center", marginLeft: 6, }, + headerIcon: { + alignSelf: "center" + }, disabled: { opacity: 0.4, }, From 9de87a8dba4b775ef4524f2ce5c65766508b16cf Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 16:36:03 +0100 Subject: [PATCH 18/34] refactor(ui/action-menu): use theme destructive color for destructive items --- ui/components/ActionMenu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 111bd4f9..272d5110 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -42,12 +42,14 @@ function MenuItem({ textColor, subtitleColor, primaryColor, + destructiveColor, onPress, }: { action: MenuAction; textColor: string; subtitleColor: string; primaryColor: string; + destructiveColor: string; onPress: () => void; }) { // Reduced flags: compute isOn from action.state (attributes does not expose state in the types). @@ -64,7 +66,7 @@ function MenuItem({ (null); @@ -221,6 +224,7 @@ export default function ActionMenu({ textColor={textColor} subtitleColor={subtitleColor} primaryColor={primaryColor} + destructiveColor={destructiveColor} onPress={() => handlePress(action, `action-${submenuStack.length}-${index}`)} /> ))} From 790cc2ab739a7cd611d6ba1d747d97336ce7bbfc Mon Sep 17 00:00:00 2001 From: Enzo Date: Sat, 31 Jan 2026 20:52:47 +0100 Subject: [PATCH 19/34] refactor(ui/action-menu): remove const textColor --- ui/components/ActionMenu.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 272d5110..d978c8aa 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -105,8 +105,7 @@ export default function ActionMenu({ }: ActionMenuProps) { const handleActionPress = onPressAction ?? (() => {}); const { colors } = useTheme(); - const textColor = colors.text; - const subtitleColor = `${textColor}80`; + const subtitleColor = `${colors.text}80`; const primaryColor = colors.primary; const cardColor = colors.card; const destructiveColor = (colors as any).danger; @@ -212,8 +211,8 @@ export default function ActionMenu({ ]} > - - {currentSubmenu.title} + + {currentSubmenu.title} )} @@ -221,7 +220,7 @@ export default function ActionMenu({ Date: Mon, 2 Feb 2026 22:23:13 +0100 Subject: [PATCH 20/34] refactor(ui/actionmenu): remove hardcoded values and improve readability --- ui/components/ActionMenu.tsx | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index d978c8aa..47f64143 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -103,7 +103,7 @@ export default function ActionMenu({ onPressAction, title, }: ActionMenuProps) { - const handleActionPress = onPressAction ?? (() => {}); + const handleActionPress = onPressAction ?? (() => { }); const { colors } = useTheme(); const subtitleColor = `${colors.text}80`; const primaryColor = colors.primary; @@ -112,9 +112,11 @@ export default function ActionMenu({ const borderColor = colors.border; const triggerRef = useRef(null); + const menuRef = useRef(null); const [visible, setVisible] = useState(false); const [submenuStack, setSubmenuStack] = useState([]); const [position, setPosition] = useState(null); + const [menuSize, setMenuSize] = useState<{ width: number; height: number } | null>(null); // iOS if (Platform.OS === "ios" && NativeMenuView) { @@ -158,21 +160,24 @@ export default function ActionMenu({ } function getMenuPosition() { - if (!position) { + if (!position || !menuSize) { return { alignSelf: "center" as const }; } - const { width, height } = Dimensions.get("window"); + const screen = Dimensions.get("window"); const MARGIN = 16; + const SPACING = 8; - //A remplacer - const MENU_WIDTH = 320; - const MENU_HEIGHT = 260; + const left = Math.min( + Math.max(position.x, MARGIN), + screen.width - MARGIN - menuSize.width + ); - const left = Math.min(Math.max(position.x, MARGIN), width - MARGIN - MENU_WIDTH); - const below = position.y + position.height + 8; - const fitsBelow = below + MENU_HEIGHT <= height - MARGIN; - const top = fitsBelow ? below : Math.max(MARGIN, position.y - MENU_HEIGHT - 8); + const topIfBelow = position.y + position.height + SPACING; + const hasSpaceBelow = topIfBelow + menuSize.height <= screen.height - MARGIN; + const top = hasSpaceBelow + ? topIfBelow + : Math.max(MARGIN, position.y - menuSize.height - SPACING); return { position: "absolute" as const, top, left }; } @@ -196,10 +201,15 @@ export default function ActionMenu({ { + const { width, height } = e.nativeEvent.layout; + setMenuSize({ width, height }); + }} style={[ styles.menu, getMenuPosition(), - { backgroundColor: cardColor }, + { backgroundColor: cardColor, width: Math.min(Dimensions.get("window").width * 0.85, 320) }, ]} > {currentSubmenu && ( @@ -250,8 +260,6 @@ const styles = StyleSheet.create({ padding: 16, }, menu: { - minWidth: 260, - maxWidth: 320, borderRadius: 14, padding: 6, shadowColor: "#000", From b248f301dd25473103ef06e1eeb19c55929946a6 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 22:58:11 +0100 Subject: [PATCH 21/34] =?UTF-8?q?feat(ui/action-menu):=20support=20for=20t?= =?UTF-8?q?he=20=E2=80=9CimageColor=E2=80=9D=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/components/ActionMenu.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 47f64143..42cc4064 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -59,6 +59,12 @@ function MenuItem({ const destructive = Boolean(action.attributes?.destructive ?? legacy.destructive); const disabled = Boolean(action.attributes?.disabled ?? legacy.disabled); + const colorText = action.imageColor + ? action.imageColor + : destructive + ? destructiveColor + : textColor; + return ( @@ -66,7 +72,7 @@ function MenuItem({ Date: Mon, 2 Feb 2026 22:59:00 +0100 Subject: [PATCH 22/34] feat(app/(modals)/wallpaper): replace MenuView by component ActionMenu in wallpaper.tsx --- app/(modals)/wallpaper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/(modals)/wallpaper.tsx b/app/(modals)/wallpaper.tsx index 09ce819e..67ecefd4 100644 --- a/app/(modals)/wallpaper.tsx +++ b/app/(modals)/wallpaper.tsx @@ -16,6 +16,7 @@ import { Papicons } from "@getpapillon/papicons" import { MenuView } from "@react-native-menu/menu" import * as ImagePicker from 'expo-image-picker'; +import ActionMenu from "@/ui/components/ActionMenu"; const COLLECTIONS_SOURCE = "https://raw.githubusercontent.com/PapillonApp/datasets/refs/heads/main/wallpapers/index.json"; @@ -235,7 +236,7 @@ const WallpaperModal = () => { )} - { - + ) From 1d802f1cf8c04c2e90cc6bdd70148f9c52731975 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 23:01:21 +0100 Subject: [PATCH 23/34] refactor(app/(modals)/wallpaper): remove unused import in wallpaper.tsx --- app/(modals)/wallpaper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/(modals)/wallpaper.tsx b/app/(modals)/wallpaper.tsx index 67ecefd4..b73fad21 100644 --- a/app/(modals)/wallpaper.tsx +++ b/app/(modals)/wallpaper.tsx @@ -13,7 +13,6 @@ import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeH import Icon from "@/ui/components/Icon" import { router } from "expo-router"; import { Papicons } from "@getpapillon/papicons" -import { MenuView } from "@react-native-menu/menu" import * as ImagePicker from 'expo-image-picker'; import ActionMenu from "@/ui/components/ActionMenu"; From 48cf8d3d76e815a982c519a4d5a418d7d249da25 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 23:02:19 +0100 Subject: [PATCH 24/34] refactor(app/(modals)/wallpaper): remove unused import in wallpaper.tsx --- app/(modals)/wallpaper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/(modals)/wallpaper.tsx b/app/(modals)/wallpaper.tsx index b73fad21..d9280789 100644 --- a/app/(modals)/wallpaper.tsx +++ b/app/(modals)/wallpaper.tsx @@ -1,4 +1,3 @@ -import { useAccountStore } from "@/stores/account" import { useSettingsStore } from "@/stores/settings" import { Wallpaper } from "@/stores/settings/types" import AnimatedPressable from "@/ui/components/AnimatedPressable" From 482bbf9cc2a60878bfb971752c3f92f54e29d758 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 23:08:15 +0100 Subject: [PATCH 25/34] feat(app/(features)/attendance): replace MenuView by component ActionMenu in attendance.tsx --- app/(features)/attendance.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(features)/attendance.tsx b/app/(features)/attendance.tsx index 406ea847..729ef1cc 100644 --- a/app/(features)/attendance.tsx +++ b/app/(features)/attendance.tsx @@ -6,7 +6,6 @@ import { Platform, ScrollView, View } from "react-native"; import { Papicons } from "@getpapillon/papicons" import { useTheme } from "@react-navigation/native"; import { Dynamic } from "@/ui/components/Dynamic"; -import { MenuView } from "@react-native-menu/menu"; import { Period } from "@/services/shared/grade"; import { getPeriodName, getPeriodNumber, isPeriodWithNumber } from "@/utils/services/periods"; import { useMemo, useState } from "react"; @@ -21,6 +20,7 @@ import { error } from "@/utils/logger/logger"; import { getManager } from "@/services/shared"; import { t } from "i18next"; import i18n from "@/utils/i18n"; +import ActionMenu from "@/ui/components/ActionMenu"; export default function AttendanceView() { try { @@ -270,7 +270,7 @@ export default function AttendanceView() { - { const actionId = nativeEvent.event; @@ -328,7 +328,7 @@ export default function AttendanceView() { - + )} From 26daa166b44e26e4f6717f5ebe70058b7003fd30 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 23:12:07 +0100 Subject: [PATCH 26/34] feat(app/(tabs)/calendar/event/[id]): replace MenuView by component ActionMenu in [id].tsx --- app/(tabs)/calendar/event/[id].tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(tabs)/calendar/event/[id].tsx b/app/(tabs)/calendar/event/[id].tsx index b34b2bf7..4b0a72de 100644 --- a/app/(tabs)/calendar/event/[id].tsx +++ b/app/(tabs)/calendar/event/[id].tsx @@ -1,4 +1,3 @@ -import { MenuView } from '@react-native-menu/menu'; import { useTheme } from "@react-navigation/native"; import { useLocalSearchParams, useNavigation, useRouter } from "expo-router"; import { t } from 'i18next'; @@ -10,6 +9,7 @@ import UnderConstructionNotice from "@/components/UnderConstructionNotice"; import { useDatabase } from '@/database/DatabaseProvider'; import { useEventById } from '@/database/useEventsById'; import { NativeHeaderPressable, NativeHeaderSide } from '@/ui/components/NativeHeader'; +import ActionMenu from "@/ui/components/ActionMenu"; export default function TabOneScreen() { const { id, title } = useLocalSearchParams(); @@ -39,7 +39,7 @@ export default function TabOneScreen() { return ( <> - - + Date: Mon, 2 Feb 2026 23:14:07 +0100 Subject: [PATCH 27/34] feat(app/(modals)/profile): replace MenuView by component ActionMenu in profile.tsx --- app/(modals)/profile.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(modals)/profile.tsx b/app/(modals)/profile.tsx index 67d73520..36126237 100644 --- a/app/(modals)/profile.tsx +++ b/app/(modals)/profile.tsx @@ -1,5 +1,4 @@ import { Papicons } from "@getpapillon/papicons"; -import { MenuView, NativeActionEvent } from "@react-native-menu/menu"; import { useHeaderHeight } from "@react-navigation/elements"; import { useTheme } from "@react-navigation/native"; import * as ImagePicker from "expo-image-picker" @@ -23,6 +22,7 @@ import Icon from "@/ui/components/Icon"; import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader"; import Typography from "@/ui/components/Typography"; import { getInitials } from "@/utils/chats/initials"; +import ActionMenu from "@/ui/components/ActionMenu"; export default function CustomProfileScreen() { const { t } = useTranslation(); @@ -89,7 +89,7 @@ export default function CustomProfileScreen() { imageUrl={profilePictureUrl || undefined} /> - } title={t("Button_Change_ProfilePicture")} /> - + From 3745e254d43b2bb4286b57f9c577c90896d3f292 Mon Sep 17 00:00:00 2001 From: Enzo Date: Mon, 2 Feb 2026 23:15:26 +0100 Subject: [PATCH 28/34] feat(app/(features)/(news)/specific): replace MenuView by component ActionMenu in specific.tsx --- app/(features)/(news)/specific.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(features)/(news)/specific.tsx b/app/(features)/(news)/specific.tsx index 66211304..8aff83d6 100644 --- a/app/(features)/(news)/specific.tsx +++ b/app/(features)/(news)/specific.tsx @@ -16,11 +16,11 @@ import HTMLView from 'react-native-htmlview'; import * as WebBrowser from 'expo-web-browser'; import { useTheme } from "@react-navigation/native"; import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader"; -import { MenuView } from "@react-native-menu/menu"; import Icon from "@/ui/components/Icon"; import { t } from "i18next"; import List from "@/ui/components/List"; import Item from "@/ui/components/Item"; +import ActionMenu from "@/ui/components/ActionMenu"; export default function NewsPage() { const search = useLocalSearchParams(); @@ -180,7 +180,7 @@ export default function NewsPage() { - ({ id: theme.value, @@ -201,7 +201,7 @@ export default function NewsPage() { - + ) From 9ff542c147575ce97bc8f15aa2463dce166d57ae Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:18:13 +0100 Subject: [PATCH 29/34] chore(ui/action-menu): remove redundant NativeMenuView null assignment --- ui/components/ActionMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 42cc4064..81c56c48 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -24,7 +24,6 @@ if (Platform.OS === "ios") { NativeMenuView = mod?.MenuView ?? null; } catch (err: unknown) { console.warn("ActionMenu: impossible de charger @react-native-menu/menu MenuView:", err); - NativeMenuView = null; } } From 79746ea150873de006f8e833721461f26d3f00f8 Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:20:52 +0100 Subject: [PATCH 30/34] rfactor(ui/action-menu): use NativeMenuAction instead of MenuAction alias --- ui/components/ActionMenu.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 81c56c48..136829e6 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useState, useRef } from "react"; import type { ComponentType } from "react"; -import type { MenuAction as NativeMenuAction, NativeActionEvent } from "@react-native-menu/menu"; +import type { MenuAction as NativeMenuAction, MenuComponentProps as NativeMenuComponentProps } from "@react-native-menu/menu"; import { Modal, Platform, @@ -44,7 +44,7 @@ function MenuItem({ destructiveColor, onPress, }: { - action: MenuAction; + action: NativeMenuAction; textColor: string; subtitleColor: string; primaryColor: string; @@ -119,7 +119,7 @@ export default function ActionMenu({ const triggerRef = useRef(null); const menuRef = useRef(null); const [visible, setVisible] = useState(false); - const [submenuStack, setSubmenuStack] = useState([]); + const [submenuStack, setSubmenuStack] = useState([]); const [position, setPosition] = useState(null); const [menuSize, setMenuSize] = useState<{ width: number; height: number } | null>(null); @@ -151,7 +151,7 @@ export default function ActionMenu({ setSubmenuStack([]); } - function handlePress(action: MenuAction, fallbackId: string) { + function handlePress(action: NativeMenuAction, fallbackId: string) { if (action.subactions && action.subactions.length > 0) { setSubmenuStack((prev) => [...prev, action]); return; From 32bd437ef543f133826461f8eb0b12fede618df0 Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:22:00 +0100 Subject: [PATCH 31/34] rfactor(ui/action-menu): use NativeMenuComponentProps for component props --- ui/components/ActionMenu.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 136829e6..801ec2f5 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, useRef } from "react"; +import React, { useState, useRef } from "react"; import type { ComponentType } from "react"; import type { MenuAction as NativeMenuAction, MenuComponentProps as NativeMenuComponentProps } from "@react-native-menu/menu"; import { @@ -27,15 +27,6 @@ if (Platform.OS === "ios") { } } -type MenuAction = NativeMenuAction; - -interface ActionMenuProps { - actions: MenuAction[]; - children: ReactNode; - onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; - title?: string; -} - function MenuItem({ action, textColor, @@ -107,7 +98,7 @@ export default function ActionMenu({ children, onPressAction, title, -}: ActionMenuProps) { +}: NativeMenuComponentProps) { const handleActionPress = onPressAction ?? (() => { }); const { colors } = useTheme(); const subtitleColor = `${colors.text}80`; From 7f82b0f1f415079a14b5b2f6b538d894b78a45cc Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:26:01 +0100 Subject: [PATCH 32/34] rfactor(ui/action-menu): simplify colorText logic Corps --- ui/components/ActionMenu.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index 801ec2f5..c2daafe7 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -49,11 +49,14 @@ function MenuItem({ const destructive = Boolean(action.attributes?.destructive ?? legacy.destructive); const disabled = Boolean(action.attributes?.disabled ?? legacy.disabled); - const colorText = action.imageColor - ? action.imageColor - : destructive - ? destructiveColor - : textColor; + let colorText: string; + if (action.imageColor !== undefined && action.imageColor !== null) { + colorText = String(action.imageColor); + } else if (destructive) { + colorText = destructiveColor; + } else { + colorText = textColor; + } return ( From d383a637b1ee2cfddd1059cef80e14234212b4b6 Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:28:52 +0100 Subject: [PATCH 33/34] chore(ui/action-menu): use internal logger.warn instead of console.warn --- ui/components/ActionMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index c2daafe7..fdf9d9f3 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -16,6 +16,7 @@ import { useTheme } from "@react-navigation/native"; import Stack from "@/ui/components/Stack"; import { Papicons } from "@getpapillon/papicons"; import { runsIOS26 } from "@/ui/utils/IsLiquidGlass"; +import { warn } from "@/utils/logger/logger"; let NativeMenuView: ComponentType> | null = null; if (Platform.OS === "ios") { @@ -23,7 +24,7 @@ if (Platform.OS === "ios") { const mod = require("@react-native-menu/menu"); NativeMenuView = mod?.MenuView ?? null; } catch (err: unknown) { - console.warn("ActionMenu: impossible de charger @react-native-menu/menu MenuView:", err); + warn(`ActionMenu: impossible de charger @react-native-menu/menu MenuView: ${String(err)}`); } } From 417d7a56c2847e17ee4c97c95a837f24ef7b857b Mon Sep 17 00:00:00 2001 From: Enzo Date: Thu, 5 Feb 2026 22:34:04 +0100 Subject: [PATCH 34/34] feat(ui/action-menu): handle safe-areas in menu positioning --- ui/components/ActionMenu.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/components/ActionMenu.tsx b/ui/components/ActionMenu.tsx index fdf9d9f3..81922abd 100644 --- a/ui/components/ActionMenu.tsx +++ b/ui/components/ActionMenu.tsx @@ -13,6 +13,7 @@ import { Dimensions, } from "react-native"; import { useTheme } from "@react-navigation/native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import Stack from "@/ui/components/Stack"; import { Papicons } from "@getpapillon/papicons"; import { runsIOS26 } from "@/ui/utils/IsLiquidGlass"; @@ -105,6 +106,7 @@ export default function ActionMenu({ }: NativeMenuComponentProps) { const handleActionPress = onPressAction ?? (() => { }); const { colors } = useTheme(); + const insets = useSafeAreaInsets(); const subtitleColor = `${colors.text}80`; const primaryColor = colors.primary; const cardColor = colors.card; @@ -168,16 +170,21 @@ export default function ActionMenu({ const MARGIN = 16; const SPACING = 8; + const safeLeft = insets.left + MARGIN; + const safeRight = screen.width - insets.right - MARGIN; + const safeTop = insets.top + MARGIN; + const safeBottom = screen.height - insets.bottom - MARGIN; + const left = Math.min( - Math.max(position.x, MARGIN), - screen.width - MARGIN - menuSize.width + Math.max(position.x, safeLeft), + safeRight - menuSize.width ); const topIfBelow = position.y + position.height + SPACING; - const hasSpaceBelow = topIfBelow + menuSize.height <= screen.height - MARGIN; + const hasSpaceBelow = topIfBelow + menuSize.height <= safeBottom; const top = hasSpaceBelow ? topIfBelow - : Math.max(MARGIN, position.y - menuSize.height - SPACING); + : Math.max(safeTop, position.y - menuSize.height - SPACING); return { position: "absolute" as const, top, left }; }