diff --git a/apps/mobile/src/components/AppSymbol.ios.tsx b/apps/mobile/src/components/AppSymbol.ios.tsx
index f1a28ed3f338..8f3f5f8abf75 100644
--- a/apps/mobile/src/components/AppSymbol.ios.tsx
+++ b/apps/mobile/src/components/AppSymbol.ios.tsx
@@ -1,14 +1,15 @@
-import { SymbolView as ExpoSymbolView, type SymbolViewProps } from "expo-symbols";
+import { SymbolView as ExpoSymbolView } from "expo-symbols";
import { withUniwind } from "uniwind";
+import type { AppSymbolViewProps } from "./AppSymbol";
export type { SFSymbol } from "expo-symbols";
-export type AppSymbolName = SymbolViewProps["name"];
+export type { AppSymbolName } from "./AppSymbol";
/**
* Keep the iOS implementation isolated from the Android Tabler fallback so
* Metro does not initialize the icon package when iOS renders SF Symbols.
*/
-function AppSymbolView(props: SymbolViewProps) {
+function AppSymbolView(props: AppSymbolViewProps) {
return ;
}
diff --git a/apps/mobile/src/components/AppSymbol.tsx b/apps/mobile/src/components/AppSymbol.tsx
index b65fb769e75e..6002dc48cd91 100644
--- a/apps/mobile/src/components/AppSymbol.tsx
+++ b/apps/mobile/src/components/AppSymbol.tsx
@@ -4,11 +4,14 @@ import type { Icon } from "@tabler/icons-react-native/types";
* the entire Tabler icon set in Metro.
*/
import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal";
+import IconAlignLeft from "@tabler/icons-react-native/IconAlignLeft";
import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle";
import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle";
import IconApps from "@tabler/icons-react-native/IconApps";
import IconArchive from "@tabler/icons-react-native/IconArchive";
import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp";
+import IconArrowDown from "@tabler/icons-react-native/IconArrowDown";
+import IconArrowForwardUp from "@tabler/icons-react-native/IconArrowForwardUp";
import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft";
import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle";
import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle";
@@ -30,15 +33,17 @@ import IconChevronDown from "@tabler/icons-react-native/IconChevronDown";
import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft";
import IconChevronRight from "@tabler/icons-react-native/IconChevronRight";
import IconChevronUp from "@tabler/icons-react-native/IconChevronUp";
+import IconCircle from "@tabler/icons-react-native/IconCircle";
import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck";
import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled";
import IconTicket from "@tabler/icons-react-native/IconTicket";
import IconClock from "@tabler/icons-react-native/IconClock";
import IconCode from "@tabler/icons-react-native/IconCode";
import IconCopy from "@tabler/icons-react-native/IconCopy";
-import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
import IconDatabase from "@tabler/icons-react-native/IconDatabase";
+import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop";
+import IconDeviceMobile from "@tabler/icons-react-native/IconDeviceMobile";
import IconDots from "@tabler/icons-react-native/IconDots";
import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical";
import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal";
@@ -62,13 +67,15 @@ import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns";
import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar";
import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight";
import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing";
-import IconMenu2 from "@tabler/icons-react-native/IconMenu2";
+import IconMicrophone from "@tabler/icons-react-native/IconMicrophone";
import IconLink from "@tabler/icons-react-native/IconLink";
+import IconMenu2 from "@tabler/icons-react-native/IconMenu2";
import IconMessage from "@tabler/icons-react-native/IconMessage";
import IconMinus from "@tabler/icons-react-native/IconMinus";
import IconMoon from "@tabler/icons-react-native/IconMoon";
import IconNetwork from "@tabler/icons-react-native/IconNetwork";
import IconPalette from "@tabler/icons-react-native/IconPalette";
+import IconPencil from "@tabler/icons-react-native/IconPencil";
import IconPhoto from "@tabler/icons-react-native/IconPhoto";
import IconPin from "@tabler/icons-react-native/IconPin";
import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff";
@@ -97,13 +104,14 @@ import IconUserCircle from "@tabler/icons-react-native/IconUserCircle";
import IconWifiOff from "@tabler/icons-react-native/IconWifiOff";
import IconWorld from "@tabler/icons-react-native/IconWorld";
import IconX from "@tabler/icons-react-native/IconX";
-import type { SFSymbol, SymbolViewProps } from "expo-symbols";
+import type { AndroidSymbol, SFSymbol, SymbolViewProps } from "expo-symbols";
import { withUniwind } from "uniwind";
-const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
+const ANDROID_ICON_BY_SF_SYMBOL = {
"arrow.branch": IconGitBranch,
"arrow.left": IconArrowLeft,
"arrow.clockwise": IconRefresh,
+ "arrow.down": IconArrowDown,
"arrow.down.circle": IconArrowDownCircle,
"arrow.right.circle": IconArrowRightCircle,
"arrow.triangle.branch": IconGitBranch,
@@ -117,6 +125,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
"arrow.up.right": IconArrowUpRight,
"arrow.up.right.circle": IconArrowUpRightCircle,
"arrow.uturn.backward": IconArrowBackUp,
+ "arrow.uturn.forward": IconArrowForwardUp,
archivebox: IconArchive,
"archivebox.fill": IconArchive,
"bell.badge": IconBellRinging,
@@ -127,6 +136,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
"chart.bar.xaxis": IconChartBar,
checkmark: IconCheck,
"checkmark.circle": IconCircleCheck,
+ circle: IconCircle,
clock: IconClock,
ticket: IconTicket,
cloud: IconCloud,
@@ -143,11 +153,13 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
moon: IconMoon,
"ellipsis.circle": IconDotsCircleHorizontal,
"exclamationmark.triangle": IconAlertTriangle,
+ "exclamationmark.circle": IconAlertCircle,
eye: IconEye,
folder: IconFolder,
"folder.badge.plus": IconFolderPlus,
"folder.fill": IconFolder,
gearshape: IconSettings,
+ globe: IconWorld,
hammer: IconHammer,
"info.circle": IconInfoCircle,
internaldrive: IconDatabase,
@@ -162,7 +174,9 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
macmini: IconServer,
macstudio: IconDeviceDesktop,
magnifyingglass: IconSearch,
+ mic: IconMicrophone,
paintbrush: IconPalette,
+ pencil: IconPencil,
"person.crop.circle": IconUserCircle,
photo: IconPhoto,
pin: IconPin,
@@ -187,6 +201,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
"sun.max": IconSun,
"stop.fill": IconPlayerStopFilled,
terminal: IconTerminal2,
+ "text.alignleft": IconAlignLeft,
"text.bubble": IconMessage,
"text.word.spacing": IconLetterSpacing,
"textformat.size": IconTypography,
@@ -197,13 +212,14 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = {
"wifi.slash": IconWifiOff,
xmark: IconX,
"xmark.circle.fill": IconCircleXFilled,
-};
+} satisfies Partial>;
+const SF_ICON_LOOKUP: Partial> = ANDROID_ICON_BY_SF_SYMBOL;
// Callers can pass `{ ios, android }` names where `android` is a Material
// icon name (the raw expo-symbols contract). Resolve those here too so the
// android key keeps working through this wrapper — it wins over the SF map
// when both match (e.g. folder vs folder_open for expanded project groups).
-const ANDROID_ICON_BY_MATERIAL_NAME: Record = {
+const ANDROID_ICON_BY_MATERIAL_NAME = {
auto_awesome: IconSparkles,
bolt: IconBolt,
build: IconTool,
@@ -222,21 +238,34 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record = {
keyboard_arrow_up: IconChevronUp,
keyboard_hide: IconKeyboardHide,
more_vert: IconDotsVertical,
+ merge: IconGitMerge,
public: IconWorld,
remove: IconMinus,
+ smartphone: IconDeviceMobile,
terminal: IconTerminal2,
visibility: IconEye,
-};
+} satisfies Partial>;
export type { SFSymbol } from "expo-symbols";
-export type AppSymbolName = SymbolViewProps["name"];
+export type AppSymbolName =
+ | keyof typeof ANDROID_ICON_BY_SF_SYMBOL
+ | {
+ ios: SFSymbol;
+ android: keyof typeof ANDROID_ICON_BY_MATERIAL_NAME;
+ };
+
+export type AppSymbolViewProps = Omit & { name: AppSymbolName };
+
+export function isAppSymbolName(name: string): name is Extract {
+ return Object.prototype.hasOwnProperty.call(ANDROID_ICON_BY_SF_SYMBOL, name);
+}
-function AppSymbolView(props: SymbolViewProps) {
+function AppSymbolView(props: AppSymbolViewProps) {
const materialName = typeof props.name === "string" ? undefined : props.name.android;
const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios;
const AndroidIcon =
(materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ??
- (sfSymbol ? ANDROID_ICON_BY_SF_SYMBOL[sfSymbol] : undefined);
+ (sfSymbol ? SF_ICON_LOOKUP[sfSymbol] : undefined);
if (!AndroidIcon) {
return props.fallback ?? null;
diff --git a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx
index 968040fd4113..5586f3a22130 100644
--- a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx
+++ b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx
@@ -1,9 +1,7 @@
import type { EnvironmentMachineKind } from "@t3tools/contracts";
-import type { SFSymbol } from "expo-symbols";
+import { SymbolView, type AppSymbolName } from "./AppSymbol";
-import { SymbolView } from "./AppSymbol";
-
-const SYMBOL_BY_KIND: Record = {
+const SYMBOL_BY_KIND: Record = {
server: "server.rack",
cloud: "cloud",
linux: "terminal",
diff --git a/apps/mobile/src/components/MaterialMenuPopup.android.tsx b/apps/mobile/src/components/MaterialMenuPopup.android.tsx
index 87b467af9675..c49db1815039 100644
--- a/apps/mobile/src/components/MaterialMenuPopup.android.tsx
+++ b/apps/mobile/src/components/MaterialMenuPopup.android.tsx
@@ -12,7 +12,7 @@ import { View } from "react-native";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import type { MaterialMenuPopupProps } from "./MaterialMenuPopup";
-import { SymbolView, type AppSymbolName } from "./AppSymbol";
+import { isAppSymbolName, SymbolView, type AppSymbolName } from "./AppSymbol";
function MenuIcon(props: {
readonly name: AppSymbolName;
@@ -96,10 +96,10 @@ export function MaterialMenuPopup(props: MaterialMenuPopupProps) {
) : null}
- {action.image ? (
+ {action.image && isAppSymbolName(action.image) ? (