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) ? (