| author | |
| committer | |
| log | 1f8ed54add4133ac39effceded8fc1fff12d8e03 |
| tree | 0d4a1445f2e01050a7ce6d53e0b114b0bc0ea6b4 |
| parent | 3d32797f6f4614205debf8a61f09e950f5f9a81c |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>31 files changed, 1800 insertions(+), 460 deletions(-)
.agents/skills/test-t3-mobile/SKILL.md+1-1| ... | ... | @@ -80,7 +80,6 @@ Run Metro from `apps/mobile`. |
| 80 | 80 | APP_VARIANT=development vp exec expo start \ |
| 81 | 81 | --dev-client \ |
| 82 | 82 | --scheme t3code-dev \ |
| 83 | --clear \ | |
| 84 | 83 | --lan \ |
| 85 | 84 | --port <metro-port> |
| 86 | 85 | ``` |
| ... | ... | @@ -179,6 +178,7 @@ Keep local verification focused. Do not turn this workflow into a full repositor |
| 179 | 178 | ## Troubleshoot predictable failures |
| 180 | 179 | |
| 181 | 180 | - **Old UI or an old error appears:** verify Metro's worktree, variant, URL, and port before diagnosing the app. |
| 181 | - **Metro serves stale or invalid transforms after those checks:** stop the owned Metro process and run `vp run dev:client:reset` once on the standard port. For a custom port, add `--clear` to the complete explicit `expo start` command above. | |
| 182 | 182 | - **The environment remains empty:** verify the platform-specific HTTP origin, use a fresh token, and confirm project seeding used the identical base directory. |
| 183 | 183 | - **A second client cannot pair:** pairing tokens are single-use; issue another token. |
| 184 | 184 | - **The pairing form opens but does not connect:** confirm the deep link uses the existing `connections/new` route, includes `autoConnect=1`, and carries a freshly minted encoded `pairingUrl`. |
AGENTS.md+1| ... | ... | @@ -104,6 +104,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real |
| 104 | 104 | ## Verifying |
| 105 | 105 | |
| 106 | 106 | - Smallest proof that the change works. `vp test run <files>` for the tests you touched, targeted lint and typecheck for the scope you changed. |
| 107 | - Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation. | |
| 107 | 108 | - **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite. |
| 108 | 109 | - Backend behavior changes ship with focused tests for that behavior. |
| 109 | 110 | - The server is event-sourced and its async flows emit typed receipts. Wait on receipts and worker drains, never on sleeps or polling. A test that needs a timeout to pass is wrong. |
apps/mobile/README.md+20-1| ... | ... | @@ -28,6 +28,23 @@ Start Metro for the dev client: |
| 28 | 28 | vp run dev:client |
| 29 | 29 | ``` |
| 30 | 30 | |
| 31 | Metro keeps its transform cache between ordinary starts. If the cache itself is causing stale or | |
| 32 | invalid output, clear it for one development-client start: | |
| 33 | ||
| 34 | ```bash | |
| 35 | vp run dev:client:reset | |
| 36 | ``` | |
| 37 | ||
| 38 | Run that reset once after installing or changing the Uniwind dependency patch. Cached transforms | |
| 39 | can otherwise reference its previous pnpm package path. Ordinary Metro starts still keep the cache. | |
| 40 | ||
| 41 | Component edits use Fast Refresh. Connection-runtime edits replace the active Effect layer through | |
| 42 | a stable atom runtime, preserving navigation and existing atom subscribers. Replaced registries | |
| 43 | and managed runtimes dispose their resources; the app does not force a JavaScript reload. The Uniwind patch | |
| 44 | skips global style invalidation when generated styles and themes are unchanged, while real style | |
| 45 | changes still refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md) | |
| 46 | for the lifetime boundaries. | |
| 47 | ||
| 31 | 48 | Build and run the local iOS dev client: |
| 32 | 49 | |
| 33 | 50 | ```bash |
| ... | ... | @@ -89,7 +106,9 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. |
| 89 | 106 | |
| 90 | 107 | ## EAS Builds |
| 91 | 108 | |
| 92 | CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy. | |
| 109 | Preview and production variants use Expo fingerprinting so OTA updates only reach binaries with matching native dependencies, config plugins, and patches. CI uses the `preview:dev` profile to reuse a compatible native build when possible. | |
| 110 | ||
| 111 | The development variant uses `appVersion` to avoid recalculating the native fingerprint for each Metro launch manifest. `MOBILE_VERSION_POLICY` can override either default. If you distribute a custom Release build with the development identity and publish OTA updates to it, set `MOBILE_VERSION_POLICY=fingerprint` for both its build and updates. Changing the runtime policy requires a native rebuild for OTA matching; an existing dev client can still load local Metro bundles. | |
| 93 | 112 | |
| 94 | 113 | For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`, |
| 95 | 114 | `T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL` |
apps/mobile/app.config.ts+7-5| ... | ... | @@ -10,6 +10,9 @@ Object.assign(process.env, repoEnv); |
| 10 | 10 | |
| 11 | 11 | const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); |
| 12 | 12 | const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1"; |
| 13 | const runtimeVersionPolicy = | |
| 14 | process.env.MOBILE_VERSION_POLICY ?? | |
| 15 | (APP_VARIANT === "development" ? "appVersion" : "fingerprint"); | |
| 13 | 16 | |
| 14 | 17 | const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); |
| 15 | 18 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; |
| ... | ... | @@ -166,11 +169,10 @@ const config: ExpoConfig = { |
| 166 | 169 | scheme: variant.scheme, |
| 167 | 170 | version: "1.0.4", |
| 168 | 171 | runtimeVersion: { |
| 169 | // Fingerprint (not appVersion) so an OTA only reaches binaries whose native | |
| 170 | // project — native deps, config plugins, AND patches/ — matches the update. | |
| 171 | // With appVersion, every 0.1.0 build shares a runtime version, so a JS update | |
| 172 | // could land on a binary missing the native changes it needs and crash. | |
| 173 | policy: process.env.MOBILE_VERSION_POLICY ?? "fingerprint", | |
| 172 | // Development manifests resolve on every launch, so avoid fingerprint's | |
| 173 | // expensive native-project calculation there. Preview and production stay | |
| 174 | // fingerprinted so OTAs only reach binaries with matching native projects. | |
| 175 | policy: runtimeVersionPolicy, | |
| 174 | 176 | }, |
| 175 | 177 | orientation: "portrait", |
| 176 | 178 | icon: variant.assets.appIcon, |
apps/mobile/package.json+6-5| ... | ... | @@ -4,14 +4,15 @@ |
| 4 | 4 | "private": true, |
| 5 | 5 | "main": "index.ts", |
| 6 | 6 | "scripts": { |
| 7 | "dev": "expo start --clear", | |
| 8 | "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan", | |
| 9 | "dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear --lan'", | |
| 7 | "dev": "expo start", | |
| 8 | "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --lan", | |
| 9 | "dev:client:reset": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan", | |
| 10 | "dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --lan'", | |
| 10 | 11 | "start": "expo start", |
| 11 | 12 | "start:dev": "APP_VARIANT=development expo start", |
| 12 | 13 | "start:preview": "APP_VARIANT=preview expo start", |
| 13 | 14 | "start:prod": "APP_VARIANT=production expo start", |
| 14 | "showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear", | |
| 15 | "showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code", | |
| 15 | 16 | "screenshots": "node ../../scripts/mobile-showcase.ts", |
| 16 | 17 | "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android", |
| 17 | 18 | "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android", |
| ... | ... | @@ -120,7 +121,7 @@ |
| 120 | 121 | "react-native-worklets": "0.10.1", |
| 121 | 122 | "shiki": "4.2.0", |
| 122 | 123 | "tailwind-merge": "^3.5.0", |
| 123 | "uniwind": "^1.6.2" | |
| 124 | "uniwind": "1.11.0" | |
| 124 | 125 | }, |
| 125 | 126 | "devDependencies": { |
| 126 | 127 | "@effect/vitest": "catalog:", |
apps/mobile/src/components/AppSymbol.ios.tsx created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | import { SymbolView as ExpoSymbolView, type SymbolViewProps } from "expo-symbols"; | |
| 2 | import { withUniwind } from "uniwind"; | |
| 3 | ||
| 4 | export type { SFSymbol } from "expo-symbols"; | |
| 5 | export type AppSymbolName = SymbolViewProps["name"]; | |
| 6 | ||
| 7 | /** | |
| 8 | * Keep the iOS implementation isolated from the Android Tabler fallback so | |
| 9 | * Metro does not initialize the icon package when iOS renders SF Symbols. | |
| 10 | */ | |
| 11 | function AppSymbolView(props: SymbolViewProps) { | |
| 12 | return <ExpoSymbolView {...props} />; | |
| 13 | } | |
| 14 | ||
| 15 | export const SymbolView = withUniwind(AppSymbolView); |
apps/mobile/src/components/AppSymbol.tabler.d.ts created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | // Tabler 3.44 exports per-icon runtime modules but points their declarations at missing files. | |
| 2 | declare module "@tabler/icons-react-native/Icon*" { | |
| 3 | import type { Icon } from "@tabler/icons-react-native"; | |
| 4 | ||
| 5 | const icon: Icon; | |
| 6 | export default icon; | |
| 7 | } |
apps/mobile/src/components/AppSymbol.tsx+84-87| ... | ... | @@ -1,86 +1,87 @@ |
| 1 | import { | |
| 2 | IconAdjustmentsHorizontal, | |
| 3 | IconAlertCircle, | |
| 4 | IconAlertTriangle, | |
| 5 | IconApps, | |
| 6 | IconArchive, | |
| 7 | IconArrowBackUp, | |
| 8 | IconArrowDownCircle, | |
| 9 | IconArrowRightCircle, | |
| 10 | IconArrowUp, | |
| 11 | IconArrowUpCircle, | |
| 12 | IconArrowUpRight, | |
| 13 | IconArrowUpRightCircle, | |
| 14 | IconArrowsMaximize, | |
| 15 | IconBellRinging, | |
| 16 | IconBolt, | |
| 17 | IconBox, | |
| 18 | IconCamera, | |
| 19 | IconChartBar, | |
| 20 | IconCheck, | |
| 21 | IconChevronDown, | |
| 22 | IconCode, | |
| 23 | IconChevronLeft, | |
| 24 | IconChevronRight, | |
| 25 | IconChevronUp, | |
| 26 | IconCircleCheck, | |
| 27 | IconCircleXFilled, | |
| 28 | IconClock, | |
| 29 | IconCopy, | |
| 30 | IconDeviceDesktop, | |
| 31 | IconDots, | |
| 32 | IconDotsCircleHorizontal, | |
| 33 | IconEdit, | |
| 34 | IconExternalLink, | |
| 35 | IconEye, | |
| 36 | IconFileText, | |
| 37 | IconFilter, | |
| 38 | IconFolder, | |
| 39 | IconFolderOpen, | |
| 40 | IconFolderPlus, | |
| 41 | IconGitBranch, | |
| 42 | IconHammer, | |
| 43 | IconGitMerge, | |
| 44 | IconGitPullRequest, | |
| 45 | IconInfoCircle, | |
| 46 | IconKeyboard, | |
| 47 | IconKeyboardHide, | |
| 48 | IconLayoutColumns, | |
| 49 | IconLayoutSidebar, | |
| 50 | IconLetterSpacing, | |
| 51 | IconLink, | |
| 52 | IconMessage, | |
| 53 | IconMinus, | |
| 54 | IconMoon, | |
| 55 | IconNetwork, | |
| 56 | IconPalette, | |
| 57 | IconPin, | |
| 58 | IconPinnedOff, | |
| 59 | IconPlayerPlay, | |
| 60 | IconPlayerStopFilled, | |
| 61 | IconPlus, | |
| 62 | IconQrcode, | |
| 63 | IconRefresh, | |
| 64 | IconSearch, | |
| 65 | IconServer, | |
| 66 | IconSettings, | |
| 67 | IconSparkles, | |
| 68 | IconSun, | |
| 69 | IconLayoutSidebarRight, | |
| 70 | IconTerminal2, | |
| 71 | IconTextDecrease, | |
| 72 | IconTextIncrease, | |
| 73 | IconTool, | |
| 74 | IconTrash, | |
| 75 | IconTypography, | |
| 76 | IconUserCircle, | |
| 77 | IconWifiOff, | |
| 78 | IconWorld, | |
| 79 | IconX, | |
| 80 | type Icon, | |
| 81 | } from "@tabler/icons-react-native"; | |
| 82 | import { Platform } from "react-native"; | |
| 83 | import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols"; | |
| 1 | import type { Icon } from "@tabler/icons-react-native/types"; | |
| 2 | /* | |
| 3 | * Keep these as per-icon exports. Importing the package root eagerly registers | |
| 4 | * the entire Tabler icon set in Metro. | |
| 5 | */ | |
| 6 | import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal"; | |
| 7 | import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle"; | |
| 8 | import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle"; | |
| 9 | import IconApps from "@tabler/icons-react-native/IconApps"; | |
| 10 | import IconArchive from "@tabler/icons-react-native/IconArchive"; | |
| 11 | import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp"; | |
| 12 | import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle"; | |
| 13 | import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle"; | |
| 14 | import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; | |
| 15 | import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; | |
| 16 | import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; | |
| 17 | import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; | |
| 18 | import IconArrowsMaximize from "@tabler/icons-react-native/IconArrowsMaximize"; | |
| 19 | import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; | |
| 20 | import IconBolt from "@tabler/icons-react-native/IconBolt"; | |
| 21 | import IconBox from "@tabler/icons-react-native/IconBox"; | |
| 22 | import IconCamera from "@tabler/icons-react-native/IconCamera"; | |
| 23 | import IconChartBar from "@tabler/icons-react-native/IconChartBar"; | |
| 24 | import IconCheck from "@tabler/icons-react-native/IconCheck"; | |
| 25 | import IconChevronDown from "@tabler/icons-react-native/IconChevronDown"; | |
| 26 | import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft"; | |
| 27 | import IconChevronRight from "@tabler/icons-react-native/IconChevronRight"; | |
| 28 | import IconChevronUp from "@tabler/icons-react-native/IconChevronUp"; | |
| 29 | import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck"; | |
| 30 | import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled"; | |
| 31 | import IconClock from "@tabler/icons-react-native/IconClock"; | |
| 32 | import IconCode from "@tabler/icons-react-native/IconCode"; | |
| 33 | import IconCopy from "@tabler/icons-react-native/IconCopy"; | |
| 34 | import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop"; | |
| 35 | import IconDots from "@tabler/icons-react-native/IconDots"; | |
| 36 | import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal"; | |
| 37 | import IconEdit from "@tabler/icons-react-native/IconEdit"; | |
| 38 | import IconExternalLink from "@tabler/icons-react-native/IconExternalLink"; | |
| 39 | import IconEye from "@tabler/icons-react-native/IconEye"; | |
| 40 | import IconFileText from "@tabler/icons-react-native/IconFileText"; | |
| 41 | import IconFilter from "@tabler/icons-react-native/IconFilter"; | |
| 42 | import IconFolder from "@tabler/icons-react-native/IconFolder"; | |
| 43 | import IconFolderOpen from "@tabler/icons-react-native/IconFolderOpen"; | |
| 44 | import IconFolderPlus from "@tabler/icons-react-native/IconFolderPlus"; | |
| 45 | import IconGitBranch from "@tabler/icons-react-native/IconGitBranch"; | |
| 46 | import IconGitMerge from "@tabler/icons-react-native/IconGitMerge"; | |
| 47 | import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest"; | |
| 48 | import IconHammer from "@tabler/icons-react-native/IconHammer"; | |
| 49 | import IconInfoCircle from "@tabler/icons-react-native/IconInfoCircle"; | |
| 50 | import IconKeyboard from "@tabler/icons-react-native/IconKeyboard"; | |
| 51 | import IconKeyboardHide from "@tabler/icons-react-native/IconKeyboardHide"; | |
| 52 | import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns"; | |
| 53 | import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar"; | |
| 54 | import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight"; | |
| 55 | import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing"; | |
| 56 | import IconLink from "@tabler/icons-react-native/IconLink"; | |
| 57 | import IconMessage from "@tabler/icons-react-native/IconMessage"; | |
| 58 | import IconMinus from "@tabler/icons-react-native/IconMinus"; | |
| 59 | import IconMoon from "@tabler/icons-react-native/IconMoon"; | |
| 60 | import IconNetwork from "@tabler/icons-react-native/IconNetwork"; | |
| 61 | import IconPalette from "@tabler/icons-react-native/IconPalette"; | |
| 62 | import IconPin from "@tabler/icons-react-native/IconPin"; | |
| 63 | import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff"; | |
| 64 | import IconPlayerPlay from "@tabler/icons-react-native/IconPlayerPlay"; | |
| 65 | import IconPlayerStopFilled from "@tabler/icons-react-native/IconPlayerStopFilled"; | |
| 66 | import IconPlus from "@tabler/icons-react-native/IconPlus"; | |
| 67 | import IconQrcode from "@tabler/icons-react-native/IconQrcode"; | |
| 68 | import IconRefresh from "@tabler/icons-react-native/IconRefresh"; | |
| 69 | import IconSearch from "@tabler/icons-react-native/IconSearch"; | |
| 70 | import IconServer from "@tabler/icons-react-native/IconServer"; | |
| 71 | import IconSettings from "@tabler/icons-react-native/IconSettings"; | |
| 72 | import IconSparkles from "@tabler/icons-react-native/IconSparkles"; | |
| 73 | import IconSun from "@tabler/icons-react-native/IconSun"; | |
| 74 | import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2"; | |
| 75 | import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease"; | |
| 76 | import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease"; | |
| 77 | import IconTool from "@tabler/icons-react-native/IconTool"; | |
| 78 | import IconTrash from "@tabler/icons-react-native/IconTrash"; | |
| 79 | import IconTypography from "@tabler/icons-react-native/IconTypography"; | |
| 80 | import IconUserCircle from "@tabler/icons-react-native/IconUserCircle"; | |
| 81 | import IconWifiOff from "@tabler/icons-react-native/IconWifiOff"; | |
| 82 | import IconWorld from "@tabler/icons-react-native/IconWorld"; | |
| 83 | import IconX from "@tabler/icons-react-native/IconX"; | |
| 84 | import type { SFSymbol, SymbolViewProps } from "expo-symbols"; | |
| 84 | 85 | import { withUniwind } from "uniwind"; |
| 85 | 86 | |
| 86 | 87 | const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = { |
| ... | ... | @@ -192,10 +193,6 @@ export type { SFSymbol } from "expo-symbols"; |
| 192 | 193 | export type AppSymbolName = SymbolViewProps["name"]; |
| 193 | 194 | |
| 194 | 195 | function AppSymbolView(props: SymbolViewProps) { |
| 195 | if (Platform.OS !== "android") { | |
| 196 | return <ExpoSymbolView {...props} />; | |
| 197 | } | |
| 198 | ||
| 199 | 196 | const materialName = typeof props.name === "string" ? undefined : props.name.android; |
| 200 | 197 | const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; |
| 201 | 198 | const AndroidIcon = |
apps/mobile/src/components/ControlPill.tsx+52-12| ... | ... | @@ -8,7 +8,7 @@ import { |
| 8 | 8 | type ReactNode, |
| 9 | 9 | useRef, |
| 10 | 10 | } from "react"; |
| 11 | import { Platform, Pressable, View } from "react-native"; | |
| 11 | import { Platform, Pressable, View, type PressableProps } from "react-native"; | |
| 12 | 12 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 13 | 13 | |
| 14 | 14 | import { cn } from "../lib/cn"; |
| ... | ... | @@ -120,6 +120,8 @@ export function ControlPillMenu( |
| 120 | 120 | ) { |
| 121 | 121 | const { themeAppearance } = useAppearancePreferences(); |
| 122 | 122 | const isDarkMode = themeAppearance === "dark"; |
| 123 | const menuPress = useRef({ isPreparing: false, isOpen: false, suppressPress: false }); | |
| 124 | const pendingPress = useRef<(() => void) | null>(null); | |
| 123 | 125 | |
| 124 | 126 | if (Platform.OS === "android") { |
| 125 | 127 | // Long-press menus keep their child interactive: the child element gets |
| ... | ... | @@ -161,20 +163,58 @@ export function ControlPillMenu( |
| 161 | 163 | |
| 162 | 164 | const { className: _className, ...menuProps } = props; |
| 163 | 165 | let children = menuProps.children; |
| 164 | // In long-press mode the wrapped pressable still receives the touch (the | |
| 165 | // patched MenuView button is touch-transparent) and RN's Fabric touch | |
| 166 | // handler is never cancelled by the in-tree UIContextMenuInteraction, so a | |
| 167 | // bare onPress would fire on finger-up even after the menu opened — and | |
| 168 | // also on a long press released just under the menu threshold. A dispatched | |
| 169 | // onLongPress makes Pressability swallow the release, so holds past 350ms | |
| 170 | // (below the ~500ms context-menu threshold) can only open the menu, never | |
| 171 | // tap through. | |
| 172 | 166 | if (props.shouldOpenOnLongPress && isValidElement(children)) { |
| 173 | const child = children as ReactElement<{ onLongPress?: () => void; delayLongPress?: number }>; | |
| 167 | const child = children as ReactElement<Pick<PressableProps, "onTouchStart" | "onPress">>; | |
| 174 | 168 | children = cloneElement(child, { |
| 175 | onLongPress: child.props.onLongPress ?? (() => undefined), | |
| 176 | delayLongPress: child.props.delayLongPress ?? 350, | |
| 169 | onTouchStart: (event) => { | |
| 170 | // Reset for a new touch, not onPressIn, which also fires when a | |
| 171 | // finger moves out of the row and back during the same gesture. | |
| 172 | menuPress.current.isPreparing = false; | |
| 173 | menuPress.current.suppressPress = menuPress.current.isOpen; | |
| 174 | pendingPress.current = null; | |
| 175 | child.props.onTouchStart?.(event); | |
| 176 | }, | |
| 177 | onPress: (event) => { | |
| 178 | // Accessibility clicks have no touch identifier and must not inherit | |
| 179 | // cancellation from a previous physical gesture. | |
| 180 | const isTouch = typeof event.nativeEvent.identifier === "number"; | |
| 181 | if (isTouch ? menuPress.current.suppressPress : menuPress.current.isOpen) { | |
| 182 | return; | |
| 183 | } | |
| 184 | if (isTouch && menuPress.current.isPreparing) { | |
| 185 | // A release can arrive between native menu preparation and display. | |
| 186 | // Let UIKit's display/cancel callback decide this press's outcome. | |
| 187 | event.persist(); | |
| 188 | pendingPress.current = () => child.props.onPress?.(event); | |
| 189 | return; | |
| 190 | } | |
| 191 | child.props.onPress?.(event); | |
| 192 | }, | |
| 177 | 193 | }); |
| 194 | menuProps.onMenuInteractionStart = () => { | |
| 195 | menuPress.current.isPreparing = true; | |
| 196 | props.onMenuInteractionStart?.(); | |
| 197 | }; | |
| 198 | menuProps.onOpenMenu = () => { | |
| 199 | menuPress.current.isPreparing = false; | |
| 200 | menuPress.current.isOpen = true; | |
| 201 | menuPress.current.suppressPress = true; | |
| 202 | pendingPress.current = null; | |
| 203 | props.onOpenMenu?.(); | |
| 204 | }; | |
| 205 | menuProps.onCloseMenu = () => { | |
| 206 | menuPress.current.isPreparing = false; | |
| 207 | menuPress.current.isOpen = false; | |
| 208 | // Keep this gesture cancelled even if dismissal precedes finger-up. | |
| 209 | // A separate JS long-press timer would also swallow holds that never | |
| 210 | // open the native menu. | |
| 211 | const press = pendingPress.current; | |
| 212 | pendingPress.current = null; | |
| 213 | props.onCloseMenu?.(); | |
| 214 | if (!menuPress.current.suppressPress) { | |
| 215 | press?.(); | |
| 216 | } | |
| 217 | }; | |
| 178 | 218 | } |
| 179 | 219 | return ( |
| 180 | 220 | <MenuView {...menuProps} themeVariant={isDarkMode ? "dark" : "light"}> |
apps/mobile/src/connection/runtime.ts+11-1| ... | ... | @@ -4,13 +4,18 @@ import { threadSnapshotLoaderLayer } from "@t3tools/client-runtime/state/threads |
| 4 | 4 | import * as Layer from "effect/Layer"; |
| 5 | 5 | import { Atom } from "effect/unstable/reactivity"; |
| 6 | 6 | |
| 7 | import type { FoundationHotModule } from "../lib/foundation-fast-refresh"; | |
| 8 | import { hotSwappableAtomRuntime } from "../lib/hot-swappable-atom-runtime"; | |
| 7 | 9 | import { runtimeContextLayer } from "../lib/runtime"; |
| 10 | import { appAtomRegistry } from "../state/atom-registry"; | |
| 8 | 11 | import { |
| 9 | 12 | mobileBackgroundActivityObserverLayer, |
| 10 | 13 | mobileBackgroundActivityReporterLayer, |
| 11 | 14 | } from "./background-activity"; |
| 12 | 15 | import { connectionPlatformLayer } from "./platform"; |
| 13 | 16 | |
| 17 | declare const module: { readonly hot?: FoundationHotModule } | undefined; | |
| 18 | ||
| 14 | 19 | const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( |
| 15 | 20 | Layer.provide(runtimeContextLayer), |
| 16 | 21 | ); |
| ... | ... | @@ -42,4 +47,9 @@ const connectionLayer = mobileBackgroundActivityReporterLayer.pipe( |
| 42 | 47 | export const connectionAtomRuntime: Atom.AtomRuntime< |
| 43 | 48 | Layer.Success<ConnectionLayerSource>, |
| 44 | 49 | Layer.Error<ConnectionLayerSource> |
| 45 | > = Atom.runtime(connectionLayer); | |
| 50 | > = hotSwappableAtomRuntime({ | |
| 51 | id: "t3.mobile.connection-runtime", | |
| 52 | hotModule: typeof module === "undefined" ? undefined : module.hot, | |
| 53 | registry: appAtomRegistry, | |
| 54 | layer: connectionLayer, | |
| 55 | }); |
apps/mobile/src/features/home/HomeRouteScreen.tsx+3-8| ... | ... | @@ -17,6 +17,7 @@ import { AndroidHomeFabLayout } from "./AndroidHomeFab"; |
| 17 | 17 | import { HomeScreen } from "./HomeScreen"; |
| 18 | 18 | import { HomeHeader } from "./HomeHeader"; |
| 19 | 19 | import { useHomeListOptions } from "./home-list-options"; |
| 20 | import { useHomeThreadSelection } from "./home-thread-navigation"; | |
| 20 | 21 | import { buildHomeProjectScopes } from "./homeThreadList"; |
| 21 | 22 | import { usePendingTaskListActions } from "./usePendingTaskListActions"; |
| 22 | 23 | import { useThreadListActions } from "./useThreadListActions"; |
| ... | ... | @@ -33,6 +34,7 @@ export function HomeRouteScreen() { |
| 33 | 34 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| 34 | 35 | const navigation = useNavigation(); |
| 35 | 36 | const [searchQuery, setSearchQuery] = useState(""); |
| 37 | const handleSelectThread = useHomeThreadSelection(); | |
| 36 | 38 | |
| 37 | 39 | useEffect(() => { |
| 38 | 40 | void checkForAppUpdateOnLaunch(); |
| ... | ... | @@ -209,14 +211,7 @@ export function HomeRouteScreen() { |
| 209 | 211 | } |
| 210 | 212 | onProjectSortOrderChange={setProjectSortOrder} |
| 211 | 213 | onSearchQueryChange={setSearchQuery} |
| 212 | onSelectThread={(thread) => { | |
| 213 | // Settled threads are live shells: opening one is plain | |
| 214 | // navigation, and sending a message un-settles server-side. | |
| 215 | navigation.navigate("Thread", { | |
| 216 | environmentId: thread.environmentId, | |
| 217 | threadId: thread.id, | |
| 218 | }); | |
| 219 | }} | |
| 214 | onSelectThread={handleSelectThread} | |
| 220 | 215 | onSelectPendingTask={openPendingTask} |
| 221 | 216 | onDeletePendingTask={confirmDeletePendingTask} |
| 222 | 217 | onNewThreadInProject={(project) => { |
apps/mobile/src/features/home/HomeScreen.tsx+11-9| ... | ... | @@ -19,6 +19,7 @@ import type { |
| 19 | 19 | } from "@t3tools/contracts"; |
| 20 | 20 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 21 | 21 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 22 | import { useFocusEffect } from "@react-navigation/native"; | |
| 22 | 23 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 23 | 24 | import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native"; |
| 24 | 25 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| ... | ... | @@ -587,15 +588,16 @@ export function HomeScreen(props: HomeScreenProps) { |
| 587 | 588 | // next wake boundary re-runs the partition with a fresh clock so a woken |
| 588 | 589 | // thread reappears immediately instead of on the next minute tick. |
| 589 | 590 | const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0); |
| 590 | useEffect(() => { | |
| 591 | if (!threadListV2Enabled) return; | |
| 592 | // Refresh immediately on enable: the mount-time value can be hours old | |
| 593 | // by the time the beta is switched on, which would misclassify the | |
| 594 | // inactivity auto-settle boundary until the first tick. | |
| 595 | setNowMinute(new Date().toISOString().slice(0, 16)); | |
| 596 | const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000); | |
| 597 | return () => clearInterval(id); | |
| 598 | }, [threadListV2Enabled]); | |
| 591 | useFocusEffect( | |
| 592 | useCallback(() => { | |
| 593 | if (!threadListV2Enabled) return; | |
| 594 | // Refresh immediately on enable or focus: the previous value can be hours | |
| 595 | // old and misclassify the inactivity auto-settle boundary until the first tick. | |
| 596 | setNowMinute(new Date().toISOString().slice(0, 16)); | |
| 597 | const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000); | |
| 598 | return () => clearInterval(id); | |
| 599 | }, [threadListV2Enabled]), | |
| 600 | ); | |
| 599 | 601 | // Threads on servers without the settlement capability never classify as |
| 600 | 602 | // settled (the user could neither un-settle nor pin them). |
| 601 | 603 | const serverConfigs = useAtomValue(environmentServerConfigsAtom); |
apps/mobile/src/features/home/home-thread-navigation.test.ts created+196| ... | ... | @@ -0,0 +1,196 @@ |
| 1 | import * as NodeModule from "node:module"; | |
| 2 | import type { | |
| 3 | StackNavigationState, | |
| 4 | StackRouter as StackRouterType, | |
| 5 | StackActions as StackActionsType, | |
| 6 | } from "@react-navigation/native"; | |
| 7 | import { describe, expect, it, vi } from "vite-plus/test"; | |
| 8 | ||
| 9 | function loadRouters() { | |
| 10 | const require = NodeModule.createRequire(import.meta.url); | |
| 11 | const nativePackage = require.resolve("@react-navigation/native/package.json"); | |
| 12 | const requireFromNative = NodeModule.createRequire(nativePackage); | |
| 13 | const corePackage = requireFromNative.resolve("@react-navigation/core/package.json"); | |
| 14 | const requireFromCore = NodeModule.createRequire(corePackage); | |
| 15 | return requireFromCore("@react-navigation/routers") as { | |
| 16 | readonly CommonActions: typeof import("@react-navigation/native").CommonActions; | |
| 17 | readonly StackActions: typeof StackActionsType; | |
| 18 | readonly StackRouter: typeof StackRouterType; | |
| 19 | }; | |
| 20 | } | |
| 21 | ||
| 22 | vi.mock("@react-navigation/native", () => { | |
| 23 | const { CommonActions, StackActions } = loadRouters(); | |
| 24 | return { CommonActions, StackActions }; | |
| 25 | }); | |
| 26 | ||
| 27 | import { createHomeThreadNavigationAction } from "./home-thread-navigation"; | |
| 28 | ||
| 29 | const { StackActions, StackRouter } = loadRouters(); | |
| 30 | const routeNames = ["Home", "Thread"]; | |
| 31 | const routeParamList = { | |
| 32 | Home: undefined, | |
| 33 | Thread: undefined, | |
| 34 | }; | |
| 35 | const router = StackRouter({}); | |
| 36 | const routerOptions = { | |
| 37 | routeNames, | |
| 38 | routeParamList, | |
| 39 | routeGetIdList: {}, | |
| 40 | }; | |
| 41 | ||
| 42 | type ThreadSelection = Parameters<typeof createHomeThreadNavigationAction>[0]["thread"]; | |
| 43 | ||
| 44 | function thread(id: string): ThreadSelection { | |
| 45 | return { | |
| 46 | environmentId: "environment-1", | |
| 47 | id, | |
| 48 | } as ThreadSelection; | |
| 49 | } | |
| 50 | ||
| 51 | function initialState() { | |
| 52 | return router.getInitialState(routerOptions); | |
| 53 | } | |
| 54 | ||
| 55 | function apply( | |
| 56 | state: StackNavigationState<Record<string, object | undefined>>, | |
| 57 | action: Parameters<typeof router.getStateForAction>[1], | |
| 58 | ) { | |
| 59 | const nextState = router.getStateForAction(state, action, routerOptions); | |
| 60 | expect(nextState).not.toBeNull(); | |
| 61 | return nextState as StackNavigationState<Record<string, object | undefined>>; | |
| 62 | } | |
| 63 | ||
| 64 | function selectThread( | |
| 65 | state: ReturnType<typeof initialState>, | |
| 66 | selectedThread: ThreadSelection, | |
| 67 | dismissingRouteKey: string | null = null, | |
| 68 | ) { | |
| 69 | return apply( | |
| 70 | state, | |
| 71 | createHomeThreadNavigationAction({ | |
| 72 | state, | |
| 73 | dismissingRouteKey, | |
| 74 | thread: selectedThread, | |
| 75 | }), | |
| 76 | ); | |
| 77 | } | |
| 78 | ||
| 79 | function dismissRoute(state: ReturnType<typeof initialState>, routeKey: string) { | |
| 80 | return apply(state, { | |
| 81 | ...StackActions.pop(), | |
| 82 | source: routeKey, | |
| 83 | target: state.key, | |
| 84 | }); | |
| 85 | } | |
| 86 | ||
| 87 | describe("createHomeThreadNavigationAction", () => { | |
| 88 | it("coalesces ordinary repeat selections onto the current thread route", () => { | |
| 89 | const firstState = selectThread(initialState(), thread("thread-a")); | |
| 90 | const threadRouteKey = firstState.routes[firstState.index]?.key; | |
| 91 | const secondAction = createHomeThreadNavigationAction({ | |
| 92 | state: firstState, | |
| 93 | dismissingRouteKey: null, | |
| 94 | thread: thread("thread-b"), | |
| 95 | }); | |
| 96 | ||
| 97 | const secondState = apply(firstState, secondAction); | |
| 98 | expect(secondState.routes).toHaveLength(2); | |
| 99 | expect(secondState.routes[secondState.index]).toMatchObject({ | |
| 100 | key: threadRouteKey, | |
| 101 | name: "Thread", | |
| 102 | params: { environmentId: "environment-1", threadId: "thread-b" }, | |
| 103 | }); | |
| 104 | }); | |
| 105 | ||
| 106 | it("keeps an overlap selection after native dismisses the outgoing route", () => { | |
| 107 | const outgoingState = selectThread(initialState(), thread("thread-a")); | |
| 108 | const outgoingRouteKey = outgoingState.routes[outgoingState.index]?.key; | |
| 109 | expect(outgoingRouteKey).toBeDefined(); | |
| 110 | ||
| 111 | const overlapAction = createHomeThreadNavigationAction({ | |
| 112 | state: outgoingState, | |
| 113 | dismissingRouteKey: outgoingRouteKey ?? null, | |
| 114 | thread: thread("thread-b"), | |
| 115 | }); | |
| 116 | const overlapState = apply(outgoingState, overlapAction); | |
| 117 | const incomingRoute = overlapState.routes[overlapState.index]; | |
| 118 | expect(incomingRoute?.key).not.toBe(outgoingRouteKey); | |
| 119 | ||
| 120 | const dismissedState = dismissRoute(overlapState, outgoingRouteKey!); | |
| 121 | expect(dismissedState.routes).toHaveLength(2); | |
| 122 | expect(dismissedState.routes[dismissedState.index]).toMatchObject({ | |
| 123 | key: incomingRoute?.key, | |
| 124 | name: "Thread", | |
| 125 | params: { environmentId: "environment-1", threadId: "thread-b" }, | |
| 126 | }); | |
| 127 | }); | |
| 128 | ||
| 129 | it("uses a fresh key for the same thread selected during dismissal", () => { | |
| 130 | const outgoingState = selectThread(initialState(), thread("thread-a")); | |
| 131 | const outgoingRouteKey = outgoingState.routes[outgoingState.index]?.key; | |
| 132 | expect(outgoingRouteKey).toBeDefined(); | |
| 133 | ||
| 134 | const overlapState = selectThread(outgoingState, thread("thread-a"), outgoingRouteKey ?? null); | |
| 135 | const incomingRouteKey = overlapState.routes[overlapState.index]?.key; | |
| 136 | expect(incomingRouteKey).not.toBe(outgoingRouteKey); | |
| 137 | ||
| 138 | const dismissedState = dismissRoute(overlapState, outgoingRouteKey!); | |
| 139 | expect(dismissedState.routes[dismissedState.index]).toMatchObject({ | |
| 140 | key: incomingRouteKey, | |
| 141 | params: { environmentId: "environment-1", threadId: "thread-a" }, | |
| 142 | }); | |
| 143 | }); | |
| 144 | ||
| 145 | it("coalesces a second overlap selection onto the fresh incoming route", () => { | |
| 146 | const outgoingState = selectThread(initialState(), thread("thread-a")); | |
| 147 | const outgoingRouteKey = outgoingState.routes[outgoingState.index]?.key; | |
| 148 | expect(outgoingRouteKey).toBeDefined(); | |
| 149 | ||
| 150 | const firstOverlapState = selectThread( | |
| 151 | outgoingState, | |
| 152 | thread("thread-b"), | |
| 153 | outgoingRouteKey ?? null, | |
| 154 | ); | |
| 155 | const incomingRouteKey = firstOverlapState.routes[firstOverlapState.index]?.key; | |
| 156 | const secondOverlapAction = createHomeThreadNavigationAction({ | |
| 157 | state: firstOverlapState, | |
| 158 | dismissingRouteKey: outgoingRouteKey ?? null, | |
| 159 | thread: thread("thread-c"), | |
| 160 | }); | |
| 161 | ||
| 162 | const secondOverlapState = apply(firstOverlapState, secondOverlapAction); | |
| 163 | expect(secondOverlapState.routes).toHaveLength(3); | |
| 164 | expect(secondOverlapState.routes[secondOverlapState.index]).toMatchObject({ | |
| 165 | key: incomingRouteKey, | |
| 166 | params: { environmentId: "environment-1", threadId: "thread-c" }, | |
| 167 | }); | |
| 168 | ||
| 169 | const dismissedState = dismissRoute(secondOverlapState, outgoingRouteKey!); | |
| 170 | expect(dismissedState.routes).toHaveLength(2); | |
| 171 | expect(dismissedState.routes[dismissedState.index]).toMatchObject({ | |
| 172 | key: incomingRouteKey, | |
| 173 | params: { environmentId: "environment-1", threadId: "thread-c" }, | |
| 174 | }); | |
| 175 | }); | |
| 176 | ||
| 177 | it("uses ordinary navigation when native pops before the selection", () => { | |
| 178 | const outgoingState = selectThread(initialState(), thread("thread-a")); | |
| 179 | const outgoingRouteKey = outgoingState.routes[outgoingState.index]?.key; | |
| 180 | expect(outgoingRouteKey).toBeDefined(); | |
| 181 | ||
| 182 | const poppedState = dismissRoute(outgoingState, outgoingRouteKey!); | |
| 183 | const action = createHomeThreadNavigationAction({ | |
| 184 | state: poppedState, | |
| 185 | dismissingRouteKey: outgoingRouteKey ?? null, | |
| 186 | thread: thread("thread-b"), | |
| 187 | }); | |
| 188 | ||
| 189 | const selectedState = apply(poppedState, action); | |
| 190 | expect(selectedState.routes).toHaveLength(2); | |
| 191 | expect(selectedState.routes[selectedState.index]).toMatchObject({ | |
| 192 | name: "Thread", | |
| 193 | params: { environmentId: "environment-1", threadId: "thread-b" }, | |
| 194 | }); | |
| 195 | }); | |
| 196 | }); |
apps/mobile/src/features/home/home-thread-navigation.ts created+71| ... | ... | @@ -0,0 +1,71 @@ |
| 1 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; | |
| 2 | import { | |
| 3 | CommonActions, | |
| 4 | StackActions, | |
| 5 | useNavigation, | |
| 6 | type NavigationState, | |
| 7 | } from "@react-navigation/native"; | |
| 8 | import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; | |
| 9 | import { useCallback, useEffect, useRef } from "react"; | |
| 10 | ||
| 11 | type ThreadSelection = Pick<EnvironmentThreadShell, "environmentId" | "id">; | |
| 12 | ||
| 13 | export function createHomeThreadNavigationAction(input: { | |
| 14 | readonly state: Pick<NavigationState, "index" | "routes">; | |
| 15 | readonly dismissingRouteKey: string | null; | |
| 16 | readonly thread: ThreadSelection; | |
| 17 | }) { | |
| 18 | const currentRoute = input.state.routes[input.state.index]; | |
| 19 | const params = { | |
| 20 | environmentId: input.thread.environmentId, | |
| 21 | threadId: input.thread.id, | |
| 22 | }; | |
| 23 | ||
| 24 | // Native swipe-back pops the outgoing route after its animation. Reusing | |
| 25 | // that key would also discard this selection when the dismissal arrives. | |
| 26 | if (input.dismissingRouteKey !== null && currentRoute?.key === input.dismissingRouteKey) { | |
| 27 | return StackActions.push("Thread", params); | |
| 28 | } | |
| 29 | ||
| 30 | return CommonActions.navigate("Thread", params); | |
| 31 | } | |
| 32 | ||
| 33 | export function useHomeThreadSelection() { | |
| 34 | const navigation = | |
| 35 | useNavigation<NativeStackNavigationProp<ReactNavigation.RootParamList, "Home">>(); | |
| 36 | const dismissingRouteKey = useRef<string | null>(null); | |
| 37 | ||
| 38 | useEffect(() => { | |
| 39 | const clear = () => { | |
| 40 | dismissingRouteKey.current = null; | |
| 41 | }; | |
| 42 | // This listener belongs to Home, so swipe-back is its opening transition. | |
| 43 | // Thread's closing event is targeted at the outgoing Thread route. | |
| 44 | const removeTransitionStart = navigation.addListener("transitionStart", ({ data }) => { | |
| 45 | const state = navigation.getState(); | |
| 46 | const currentRoute = state.routes[state.index]; | |
| 47 | dismissingRouteKey.current = | |
| 48 | !data.closing && currentRoute?.name === "Thread" ? currentRoute.key : null; | |
| 49 | }); | |
| 50 | const removeFocus = navigation.addListener("focus", clear); | |
| 51 | ||
| 52 | return () => { | |
| 53 | clear(); | |
| 54 | removeTransitionStart(); | |
| 55 | removeFocus(); | |
| 56 | }; | |
| 57 | }, [navigation]); | |
| 58 | ||
| 59 | return useCallback( | |
| 60 | (thread: ThreadSelection) => { | |
| 61 | navigation.dispatch((state) => | |
| 62 | createHomeThreadNavigationAction({ | |
| 63 | state, | |
| 64 | dismissingRouteKey: dismissingRouteKey.current, | |
| 65 | thread, | |
| 66 | }), | |
| 67 | ); | |
| 68 | }, | |
| 69 | [navigation], | |
| 70 | ); | |
| 71 | } |
apps/mobile/src/features/home/thread-swipe-actions.tsx+31-20| ... | ... | @@ -370,34 +370,45 @@ function SwipeActionButton(props: { |
| 370 | 370 | readonly stretchesOnFullSwipe: boolean; |
| 371 | 371 | readonly translation: SharedValue<number>; |
| 372 | 372 | }) { |
| 373 | const { | |
| 374 | actionsWidth, | |
| 375 | entryRange: [entryRangeStart, entryRangeEnd], | |
| 376 | fullSwipeThreshold, | |
| 377 | stretchesOnFullSwipe, | |
| 378 | translation, | |
| 379 | } = props; | |
| 373 | 380 | const circleSize = props.compact ? COMPACT_ACTION_CIRCLE_SIZE : ACTION_CIRCLE_SIZE; |
| 374 | 381 | const iconSize = props.compact ? COMPACT_ACTION_ICON_SIZE : ACTION_ICON_SIZE; |
| 375 | 382 | const actionStyle = useAnimatedStyle(() => { |
| 376 | const reveal = Math.max(-props.translation.value, 0); | |
| 377 | const entryProgress = interpolate(reveal, props.entryRange, [0, 1], Extrapolation.CLAMP); | |
| 378 | const stretch = Math.max(reveal - props.actionsWidth, 0); | |
| 383 | const reveal = Math.max(-translation.value, 0); | |
| 384 | const entryProgress = interpolate( | |
| 385 | reveal, | |
| 386 | [entryRangeStart, entryRangeEnd], | |
| 387 | [0, 1], | |
| 388 | Extrapolation.CLAMP, | |
| 389 | ); | |
| 390 | const stretch = Math.max(reveal - actionsWidth, 0); | |
| 379 | 391 | const fullSwipeProgress = interpolate( |
| 380 | 392 | reveal, |
| 381 | [props.actionsWidth, props.fullSwipeThreshold + 20], | |
| 393 | [actionsWidth, fullSwipeThreshold + 20], | |
| 382 | 394 | [0, 1], |
| 383 | 395 | Extrapolation.CLAMP, |
| 384 | 396 | ); |
| 385 | 397 | |
| 386 | 398 | return { |
| 387 | opacity: props.stretchesOnFullSwipe ? entryProgress : entryProgress * (1 - fullSwipeProgress), | |
| 399 | opacity: stretchesOnFullSwipe ? entryProgress : entryProgress * (1 - fullSwipeProgress), | |
| 388 | 400 | transform: [ |
| 389 | 401 | { |
| 390 | 402 | translateX: |
| 391 | interpolate(entryProgress, [0, 1], [22, 0]) - | |
| 392 | (props.stretchesOnFullSwipe ? 0 : stretch), | |
| 403 | interpolate(entryProgress, [0, 1], [22, 0]) - (stretchesOnFullSwipe ? 0 : stretch), | |
| 393 | 404 | }, |
| 394 | 405 | { scale: interpolate(entryProgress, [0, 1], [0.78, 1]) }, |
| 395 | 406 | ], |
| 396 | 407 | }; |
| 397 | 408 | }); |
| 398 | 409 | const circleStyle = useAnimatedStyle(() => { |
| 399 | const reveal = Math.max(-props.translation.value, 0); | |
| 400 | const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; | |
| 410 | const reveal = Math.max(-translation.value, 0); | |
| 411 | const stretch = stretchesOnFullSwipe ? Math.max(reveal - actionsWidth, 0) : 0; | |
| 401 | 412 | |
| 402 | 413 | return { |
| 403 | 414 | transform: [{ translateX: -stretch }], |
| ... | ... | @@ -405,11 +416,11 @@ function SwipeActionButton(props: { |
| 405 | 416 | }; |
| 406 | 417 | }); |
| 407 | 418 | const iconStyle = useAnimatedStyle(() => { |
| 408 | const reveal = Math.max(-props.translation.value, 0); | |
| 409 | const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; | |
| 419 | const reveal = Math.max(-translation.value, 0); | |
| 420 | const stretch = stretchesOnFullSwipe ? Math.max(reveal - actionsWidth, 0) : 0; | |
| 410 | 421 | const armedProgress = interpolate( |
| 411 | 422 | reveal, |
| 412 | [props.fullSwipeThreshold, props.fullSwipeThreshold + 20], | |
| 423 | [fullSwipeThreshold, fullSwipeThreshold + 20], | |
| 413 | 424 | [0, 1], |
| 414 | 425 | Extrapolation.CLAMP, |
| 415 | 426 | ); |
| ... | ... | @@ -419,16 +430,16 @@ function SwipeActionButton(props: { |
| 419 | 430 | }; |
| 420 | 431 | }); |
| 421 | 432 | const labelStyle = useAnimatedStyle(() => { |
| 422 | if (!props.stretchesOnFullSwipe) { | |
| 433 | if (!stretchesOnFullSwipe) { | |
| 423 | 434 | return { opacity: 1 }; |
| 424 | 435 | } |
| 425 | 436 | |
| 426 | const reveal = Math.max(-props.translation.value, 0); | |
| 427 | const stretch = Math.max(reveal - props.actionsWidth, 0); | |
| 437 | const reveal = Math.max(-translation.value, 0); | |
| 438 | const stretch = Math.max(reveal - actionsWidth, 0); | |
| 428 | 439 | return { |
| 429 | 440 | opacity: interpolate( |
| 430 | 441 | reveal, |
| 431 | [props.fullSwipeThreshold - 24, props.fullSwipeThreshold], | |
| 442 | [fullSwipeThreshold - 24, fullSwipeThreshold], | |
| 432 | 443 | [1, 0], |
| 433 | 444 | Extrapolation.CLAMP, |
| 434 | 445 | ), |
| ... | ... | @@ -532,17 +543,17 @@ export function ThreadSwipeActions(props: { |
| 532 | 543 | readonly secondaryAction: ThreadSwipeSecondaryAction | null; |
| 533 | 544 | readonly translation: SharedValue<number>; |
| 534 | 545 | }) { |
| 535 | const secondaryAction = props.secondaryAction; | |
| 546 | const { fullSwipeThreshold, onFullSwipeArmedChange, secondaryAction, translation } = props; | |
| 536 | 547 | const fullSwipeIsPrimary = props.fullSwipeAction === "primary" || secondaryAction === null; |
| 537 | 548 | const actionsWidth = swipeActionsWidth(secondaryAction !== null); |
| 538 | 549 | useAnimatedReaction( |
| 539 | () => -props.translation.value >= props.fullSwipeThreshold, | |
| 550 | () => -translation.value >= fullSwipeThreshold, | |
| 540 | 551 | (armed, previous) => { |
| 541 | 552 | if (armed !== previous) { |
| 542 | runOnJS(props.onFullSwipeArmedChange)(armed); | |
| 553 | runOnJS(onFullSwipeArmedChange)(armed); | |
| 543 | 554 | } |
| 544 | 555 | }, |
| 545 | [props.fullSwipeThreshold, props.onFullSwipeArmedChange], | |
| 556 | [fullSwipeThreshold, onFullSwipeArmedChange, translation], | |
| 546 | 557 | ); |
| 547 | 558 | |
| 548 | 559 | return ( |
apps/mobile/src/lib/foundation-fast-refresh.ts created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | export interface FoundationHotModule { | |
| 2 | readonly accept: (callback?: () => void) => void; | |
| 3 | readonly dispose: (callback: () => void) => void; | |
| 4 | } | |
| 5 | ||
| 6 | export function disposeOnFoundationReplace( | |
| 7 | hotModule: FoundationHotModule | undefined, | |
| 8 | dispose: () => void | Promise<void>, | |
| 9 | ): void { | |
| 10 | if (hotModule === undefined || typeof __DEV__ === "undefined" || !__DEV__) return; | |
| 11 | ||
| 12 | hotModule.dispose(() => { | |
| 13 | try { | |
| 14 | void Promise.resolve(dispose()).catch((error: unknown) => { | |
| 15 | console.error("[fast-refresh] could not dispose replaced mobile foundation", error); | |
| 16 | }); | |
| 17 | } catch (error) { | |
| 18 | console.error("[fast-refresh] could not dispose replaced mobile foundation", error); | |
| 19 | } | |
| 20 | }); | |
| 21 | } |
apps/mobile/src/lib/hot-swappable-atom-runtime.test.ts created+197| ... | ... | @@ -0,0 +1,197 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | import * as Context from "effect/Context"; | |
| 3 | import * as Effect from "effect/Effect"; | |
| 4 | import * as Layer from "effect/Layer"; | |
| 5 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; | |
| 6 | ||
| 7 | import { hotSwappableAtomRuntime } from "./hot-swappable-atom-runtime"; | |
| 8 | ||
| 9 | class RuntimeValue extends Context.Service<RuntimeValue, { readonly value: string }>()( | |
| 10 | "t3/mobile/test/RuntimeValue", | |
| 11 | ) {} | |
| 12 | ||
| 13 | function runtimeLayer(value: string, events: string[]) { | |
| 14 | return Layer.effect( | |
| 15 | RuntimeValue, | |
| 16 | Effect.acquireRelease( | |
| 17 | Effect.sync(() => { | |
| 18 | events.push(`acquire:${value}`); | |
| 19 | return RuntimeValue.of({ value }); | |
| 20 | }), | |
| 21 | () => | |
| 22 | Effect.sync(() => { | |
| 23 | events.push(`release:${value}`); | |
| 24 | }), | |
| 25 | ), | |
| 26 | ); | |
| 27 | } | |
| 28 | ||
| 29 | function runtimeLayerWithRelease( | |
| 30 | value: string, | |
| 31 | events: string[], | |
| 32 | release: Effect.Effect<void> = Effect.sync(() => { | |
| 33 | events.push(`release:${value}`); | |
| 34 | }), | |
| 35 | ) { | |
| 36 | return Layer.effect( | |
| 37 | RuntimeValue, | |
| 38 | Effect.acquireRelease( | |
| 39 | Effect.sync(() => { | |
| 40 | events.push(`acquire:${value}`); | |
| 41 | return RuntimeValue.of({ value }); | |
| 42 | }), | |
| 43 | () => release, | |
| 44 | ), | |
| 45 | ); | |
| 46 | } | |
| 47 | ||
| 48 | describe("hotSwappableAtomRuntime", () => { | |
| 49 | it("rebuilds a mounted runtime in place without disturbing unrelated subscribers", () => { | |
| 50 | vi.stubGlobal("__DEV__", true); | |
| 51 | const registry = AtomRegistry.make(); | |
| 52 | const accept = () => {}; | |
| 53 | const events: string[] = []; | |
| 54 | const id = `test-${crypto.randomUUID()}`; | |
| 55 | const runtime = hotSwappableAtomRuntime({ | |
| 56 | id, | |
| 57 | hotModule: { accept }, | |
| 58 | registry, | |
| 59 | layer: runtimeLayer("first", events), | |
| 60 | }); | |
| 61 | const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value))); | |
| 62 | const values: string[] = []; | |
| 63 | const unsubscribeRuntime = registry.subscribe( | |
| 64 | valueAtom, | |
| 65 | (result) => { | |
| 66 | if (AsyncResult.isSuccess(result)) values.push(result.value); | |
| 67 | }, | |
| 68 | { immediate: true }, | |
| 69 | ); | |
| 70 | const unrelatedAtom = Atom.make(0); | |
| 71 | const unrelatedValues: number[] = []; | |
| 72 | const unsubscribeUnrelated = registry.subscribe(unrelatedAtom, (value) => { | |
| 73 | unrelatedValues.push(value); | |
| 74 | }); | |
| 75 | registry.set(unrelatedAtom, 7); | |
| 76 | const unwatchedDraftAtom = Atom.make("saved"); | |
| 77 | registry.set(unwatchedDraftAtom, "edited"); | |
| 78 | const nodesBefore = registry.getNodes().size; | |
| 79 | ||
| 80 | const replacement = hotSwappableAtomRuntime({ | |
| 81 | id, | |
| 82 | hotModule: { accept }, | |
| 83 | registry, | |
| 84 | layer: runtimeLayer("second", events), | |
| 85 | }); | |
| 86 | hotSwappableAtomRuntime({ | |
| 87 | id, | |
| 88 | hotModule: { accept }, | |
| 89 | registry, | |
| 90 | layer: runtimeLayer("third", events), | |
| 91 | }); | |
| 92 | registry.set(unrelatedAtom, 8); | |
| 93 | ||
| 94 | expect(replacement).toBe(runtime); | |
| 95 | expect(events).toEqual([ | |
| 96 | "acquire:first", | |
| 97 | "release:first", | |
| 98 | "acquire:second", | |
| 99 | "release:second", | |
| 100 | "acquire:third", | |
| 101 | ]); | |
| 102 | expect(values).toEqual(["first", "second", "third"]); | |
| 103 | expect(registry.get(unwatchedDraftAtom)).toBe("edited"); | |
| 104 | expect(unrelatedValues).toEqual([7, 8]); | |
| 105 | expect(registry.getNodes().size).toBe(nodesBefore); | |
| 106 | unsubscribeRuntime(); | |
| 107 | unsubscribeUnrelated(); | |
| 108 | registry.dispose(); | |
| 109 | expect(events).toEqual([ | |
| 110 | "acquire:first", | |
| 111 | "release:first", | |
| 112 | "acquire:second", | |
| 113 | "release:second", | |
| 114 | "acquire:third", | |
| 115 | "release:third", | |
| 116 | ]); | |
| 117 | }); | |
| 118 | ||
| 119 | it("does not retain or accept a runtime outside development", () => { | |
| 120 | vi.stubGlobal("__DEV__", false); | |
| 121 | const registry = AtomRegistry.make(); | |
| 122 | const accept = vi.fn(); | |
| 123 | const layer = runtimeLayer("production", []); | |
| 124 | ||
| 125 | const first = hotSwappableAtomRuntime({ | |
| 126 | id: "production", | |
| 127 | hotModule: { accept }, | |
| 128 | registry, | |
| 129 | layer, | |
| 130 | }); | |
| 131 | const second = hotSwappableAtomRuntime({ | |
| 132 | id: "production", | |
| 133 | hotModule: { accept }, | |
| 134 | registry, | |
| 135 | layer, | |
| 136 | }); | |
| 137 | ||
| 138 | expect(second).not.toBe(first); | |
| 139 | expect(accept).not.toHaveBeenCalled(); | |
| 140 | registry.dispose(); | |
| 141 | }); | |
| 142 | ||
| 143 | it("starts an asynchronous old-layer release while exposing the fresh context", async () => { | |
| 144 | vi.stubGlobal("__DEV__", true); | |
| 145 | const registry = AtomRegistry.make(); | |
| 146 | const events: string[] = []; | |
| 147 | const id = `test-${crypto.randomUUID()}`; | |
| 148 | let finishRelease!: () => void; | |
| 149 | const releaseGate = new Promise<void>((resolve) => { | |
| 150 | finishRelease = resolve; | |
| 151 | }); | |
| 152 | let markReleaseComplete!: () => void; | |
| 153 | const releaseComplete = new Promise<void>((resolve) => { | |
| 154 | markReleaseComplete = resolve; | |
| 155 | }); | |
| 156 | const firstRelease = Effect.promise(async () => { | |
| 157 | events.push("release:start:first"); | |
| 158 | await releaseGate; | |
| 159 | events.push("release:end:first"); | |
| 160 | markReleaseComplete(); | |
| 161 | }); | |
| 162 | const runtime = hotSwappableAtomRuntime({ | |
| 163 | id, | |
| 164 | hotModule: { accept() {} }, | |
| 165 | registry, | |
| 166 | layer: runtimeLayerWithRelease("first", events, firstRelease), | |
| 167 | }); | |
| 168 | const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value))); | |
| 169 | const values: string[] = []; | |
| 170 | const unsubscribe = registry.subscribe(valueAtom, (result) => { | |
| 171 | if (AsyncResult.isSuccess(result)) values.push(result.value); | |
| 172 | }); | |
| 173 | registry.get(valueAtom); | |
| 174 | ||
| 175 | hotSwappableAtomRuntime({ | |
| 176 | id, | |
| 177 | hotModule: { accept() {} }, | |
| 178 | registry, | |
| 179 | layer: runtimeLayer("second", events), | |
| 180 | }); | |
| 181 | ||
| 182 | expect(events).toEqual(["acquire:first", "release:start:first", "acquire:second"]); | |
| 183 | expect(values).toEqual(["first", "second"]); | |
| 184 | ||
| 185 | finishRelease(); | |
| 186 | await releaseComplete; | |
| 187 | expect(events).toEqual([ | |
| 188 | "acquire:first", | |
| 189 | "release:start:first", | |
| 190 | "acquire:second", | |
| 191 | "release:end:first", | |
| 192 | ]); | |
| 193 | ||
| 194 | unsubscribe(); | |
| 195 | registry.dispose(); | |
| 196 | }); | |
| 197 | }); |
apps/mobile/src/lib/hot-swappable-atom-runtime.ts created+63| ... | ... | @@ -0,0 +1,63 @@ |
| 1 | import * as Layer from "effect/Layer"; | |
| 2 | import { Atom, AtomRegistry, Reactivity } from "effect/unstable/reactivity"; | |
| 3 | ||
| 4 | export interface AcceptingHotModule { | |
| 5 | readonly accept: (callback?: () => void) => void; | |
| 6 | } | |
| 7 | ||
| 8 | interface HotAtomRuntimeEntry { | |
| 9 | readonly layerAtom: Atom.Writable< | |
| 10 | Layer.Layer<unknown, unknown, AtomRegistry.AtomRegistry | Reactivity.Reactivity> | |
| 11 | >; | |
| 12 | readonly runtime: Atom.AtomRuntime<unknown, unknown>; | |
| 13 | } | |
| 14 | ||
| 15 | const hotAtomRuntimesKey = Symbol.for("t3.mobile.hot-atom-runtimes"); | |
| 16 | ||
| 17 | type HotAtomRuntimeGlobal = typeof globalThis & { | |
| 18 | [hotAtomRuntimesKey]?: Map<string, HotAtomRuntimeEntry>; | |
| 19 | }; | |
| 20 | ||
| 21 | function hotAtomRuntimes(): Map<string, HotAtomRuntimeEntry> { | |
| 22 | const runtimeGlobal = globalThis as HotAtomRuntimeGlobal; | |
| 23 | return (runtimeGlobal[hotAtomRuntimesKey] ??= new Map()); | |
| 24 | } | |
| 25 | ||
| 26 | export function hotSwappableAtomRuntime<R, E>(options: { | |
| 27 | readonly id: string; | |
| 28 | readonly hotModule: AcceptingHotModule | undefined; | |
| 29 | readonly registry: AtomRegistry.AtomRegistry; | |
| 30 | readonly layer: Layer.Layer<R, E, AtomRegistry.AtomRegistry | Reactivity.Reactivity>; | |
| 31 | }): Atom.AtomRuntime<R, E> { | |
| 32 | if (options.hotModule === undefined || typeof __DEV__ === "undefined" || !__DEV__) { | |
| 33 | return Atom.runtime(options.layer); | |
| 34 | } | |
| 35 | ||
| 36 | const runtimes = hotAtomRuntimes(); | |
| 37 | const existing = runtimes.get(options.id); | |
| 38 | let entry: HotAtomRuntimeEntry; | |
| 39 | ||
| 40 | if (existing === undefined) { | |
| 41 | const layerAtom = Atom.make(options.layer); | |
| 42 | entry = { | |
| 43 | layerAtom: layerAtom as HotAtomRuntimeEntry["layerAtom"], | |
| 44 | runtime: Atom.runtime((get) => get(layerAtom)) as HotAtomRuntimeEntry["runtime"], | |
| 45 | }; | |
| 46 | runtimes.set(options.id, entry); | |
| 47 | } else { | |
| 48 | entry = existing; | |
| 49 | options.registry.set( | |
| 50 | entry.layerAtom, | |
| 51 | options.layer as Layer.Layer< | |
| 52 | unknown, | |
| 53 | unknown, | |
| 54 | AtomRegistry.AtomRegistry | Reactivity.Reactivity | |
| 55 | >, | |
| 56 | ); | |
| 57 | } | |
| 58 | ||
| 59 | // This is a real HMR boundary: importers retain the stable AtomRuntime while | |
| 60 | // this module evaluation installs the freshly constructed Layer above. | |
| 61 | options.hotModule.accept(); | |
| 62 | return entry.runtime as Atom.AtomRuntime<R, E>; | |
| 63 | } |
apps/mobile/src/lib/runtime.ts+7| ... | ... | @@ -9,6 +9,9 @@ import { managedRelayClientLayer } from "../features/cloud/managedRelayLayer"; |
| 9 | 9 | import { resolveCloudPublicConfig } from "../features/cloud/publicConfig"; |
| 10 | 10 | import { tracingLayer } from "../features/observability/tracing"; |
| 11 | 11 | import * as Persistence from "../persistence/layer"; |
| 12 | import { disposeOnFoundationReplace, type FoundationHotModule } from "./foundation-fast-refresh"; | |
| 13 | ||
| 14 | declare const module: { readonly hot?: FoundationHotModule } | undefined; | |
| 12 | 15 | |
| 13 | 16 | function configuredRelayUrl(): string { |
| 14 | 17 | return resolveCloudPublicConfig().relay.url ?? "http://relay.invalid"; |
| ... | ... | @@ -43,3 +46,7 @@ export const runtimeContextLayer: Layer.Layer< |
| 43 | 46 | Layer.Success<RuntimeLayerSource>, |
| 44 | 47 | Layer.Error<RuntimeLayerSource> |
| 45 | 48 | > = Layer.effectContext(runtime.contextEffect); |
| 49 | ||
| 50 | disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => | |
| 51 | runtime.dispose(), | |
| 52 | ); |
apps/mobile/src/lib/uniwind-dev-refresh.test.ts created+97| ... | ... | @@ -0,0 +1,97 @@ |
| 1 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | import * as NodeURL from "node:url"; | |
| 3 | ||
| 4 | vi.mock("react-native", () => ({ | |
| 5 | Appearance: { | |
| 6 | addChangeListener: vi.fn(), | |
| 7 | getColorScheme: () => "light", | |
| 8 | setColorScheme: vi.fn(), | |
| 9 | }, | |
| 10 | Platform: { constants: {}, OS: "ios" }, | |
| 11 | })); | |
| 12 | ||
| 13 | vi.mock("../../node_modules/uniwind/src/core/listener", () => ({ | |
| 14 | UniwindListener: { notify() {}, notifyAll() {} }, | |
| 15 | })); | |
| 16 | ||
| 17 | vi.mock("../../node_modules/uniwind/src/core/native", () => ({ | |
| 18 | UniwindStore: { | |
| 19 | reinit: (generateStyleSheetCallback: () => unknown) => { | |
| 20 | generateStyleSheetCallback(); | |
| 21 | }, | |
| 22 | runtime: { currentThemeName: "light", insets: {} }, | |
| 23 | vars: {}, | |
| 24 | }, | |
| 25 | })); | |
| 26 | ||
| 27 | const loadUniwind = async () => { | |
| 28 | const modulePath = NodeURL.fileURLToPath( | |
| 29 | new URL("../../node_modules/uniwind/src/core/config/config.native.ts", import.meta.url), | |
| 30 | ); | |
| 31 | const { Uniwind } = (await import(/* @vite-ignore */ modulePath)) as { | |
| 32 | Uniwind: { readonly themes: Array<string> }; | |
| 33 | }; | |
| 34 | return Uniwind as typeof Uniwind & { | |
| 35 | __reinit: (initialize: () => unknown, themes: Array<string>, fingerprint?: string) => void; | |
| 36 | }; | |
| 37 | }; | |
| 38 | ||
| 39 | describe("Uniwind native stylesheet refresh", () => { | |
| 40 | beforeEach(() => { | |
| 41 | vi.resetModules(); | |
| 42 | vi.clearAllMocks(); | |
| 43 | vi.stubGlobal("__DEV__", true); | |
| 44 | }); | |
| 45 | ||
| 46 | it("initializes once for identical generated styles", async () => { | |
| 47 | const Uniwind = await loadUniwind(); | |
| 48 | const initialize = vi.fn(() => ({})); | |
| 49 | ||
| 50 | Uniwind.__reinit(initialize, ["light", "dark"], "same-output"); | |
| 51 | Uniwind.__reinit(initialize, ["light", "dark"], "same-output"); | |
| 52 | ||
| 53 | expect(initialize).toHaveBeenCalledTimes(1); | |
| 54 | }); | |
| 55 | ||
| 56 | it("reinitializes for changed generated styles and themes", async () => { | |
| 57 | const Uniwind = await loadUniwind(); | |
| 58 | const initialize = vi.fn(() => ({})); | |
| 59 | ||
| 60 | Uniwind.__reinit(initialize, ["light", "dark"], "before"); | |
| 61 | Uniwind.__reinit(initialize, ["light", "dark"], "after"); | |
| 62 | Uniwind.__reinit(initialize, ["light", "dark", "dim"], "themes-with-dim"); | |
| 63 | ||
| 64 | expect(initialize).toHaveBeenCalledTimes(3); | |
| 65 | expect(Uniwind.themes).toEqual(["light", "dark", "dim"]); | |
| 66 | }); | |
| 67 | ||
| 68 | it("retries the same output after initialization fails", async () => { | |
| 69 | const Uniwind = await loadUniwind(); | |
| 70 | const initialize = vi | |
| 71 | .fn() | |
| 72 | .mockImplementationOnce(() => { | |
| 73 | throw new Error("initialization failed"); | |
| 74 | }) | |
| 75 | .mockImplementationOnce(() => ({})); | |
| 76 | ||
| 77 | expect(() => Uniwind.__reinit(initialize, ["light", "dark"], "retry-output")).toThrow( | |
| 78 | "initialization failed", | |
| 79 | ); | |
| 80 | Uniwind.__reinit(initialize, ["light", "dark"], "retry-output"); | |
| 81 | ||
| 82 | expect(initialize).toHaveBeenCalledTimes(2); | |
| 83 | }); | |
| 84 | ||
| 85 | it("keeps no-fingerprint and production reinitialization semantics", async () => { | |
| 86 | const Uniwind = await loadUniwind(); | |
| 87 | const initialize = vi.fn(() => ({})); | |
| 88 | ||
| 89 | Uniwind.__reinit(initialize, ["light", "dark"]); | |
| 90 | Uniwind.__reinit(initialize, ["light", "dark"]); | |
| 91 | vi.stubGlobal("__DEV__", false); | |
| 92 | Uniwind.__reinit(initialize, ["light", "dark"], "same-output"); | |
| 93 | Uniwind.__reinit(initialize, ["light", "dark"], "same-output"); | |
| 94 | ||
| 95 | expect(initialize).toHaveBeenCalledTimes(4); | |
| 96 | }); | |
| 97 | }); |
apps/mobile/src/state/atom-registry.ts+11| ... | ... | @@ -1,3 +1,14 @@ |
| 1 | 1 | import { AtomRegistry } from "effect/unstable/reactivity"; |
| 2 | 2 | |
| 3 | import { | |
| 4 | disposeOnFoundationReplace, | |
| 5 | type FoundationHotModule, | |
| 6 | } from "../lib/foundation-fast-refresh"; | |
| 7 | ||
| 8 | declare const module: { readonly hot?: FoundationHotModule } | undefined; | |
| 9 | ||
| 3 | 10 | export const appAtomRegistry = AtomRegistry.make(); |
| 11 | ||
| 12 | disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => | |
| 13 | appAtomRegistry.dispose(), | |
| 14 | ); |
apps/mobile/src/state/remote-environment-projections.test.ts created+161| ... | ... | @@ -0,0 +1,161 @@ |
| 1 | import type { | |
| 2 | EnvironmentPresentation, | |
| 3 | PreparedConnection, | |
| 4 | } from "@t3tools/client-runtime/connection"; | |
| 5 | import { PrimaryConnectionTarget } from "@t3tools/client-runtime/connection"; | |
| 6 | import type { ServerConfig } from "@t3tools/contracts"; | |
| 7 | import { EnvironmentId } from "@t3tools/contracts"; | |
| 8 | import { describe, expect, it } from "@effect/vitest"; | |
| 9 | import * as Option from "effect/Option"; | |
| 10 | import { Atom, AtomRegistry } from "effect/unstable/reactivity"; | |
| 11 | ||
| 12 | import { createRemoteEnvironmentProjectionAtoms } from "./remote-environment-projections"; | |
| 13 | ||
| 14 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); | |
| 15 | const OTHER_ENVIRONMENT_ID = EnvironmentId.make("environment-2"); | |
| 16 | ||
| 17 | function target(environmentId: EnvironmentId, endpoint: string = environmentId) { | |
| 18 | return new PrimaryConnectionTarget({ | |
| 19 | environmentId, | |
| 20 | label: `Environment ${environmentId}`, | |
| 21 | httpBaseUrl: `https://${endpoint}.example.test`, | |
| 22 | wsBaseUrl: `wss://${endpoint}.example.test`, | |
| 23 | }); | |
| 24 | } | |
| 25 | ||
| 26 | function presentation( | |
| 27 | environmentId: EnvironmentId, | |
| 28 | endpoint: string = environmentId, | |
| 29 | serverConfig: ServerConfig | null = null, | |
| 30 | ): EnvironmentPresentation { | |
| 31 | return { | |
| 32 | entry: { target: target(environmentId, endpoint), profile: Option.none() }, | |
| 33 | connection: { phase: "connected", error: null, traceId: null }, | |
| 34 | serverConfig, | |
| 35 | }; | |
| 36 | } | |
| 37 | ||
| 38 | function prepared( | |
| 39 | environmentId: EnvironmentId, | |
| 40 | endpoint: string, | |
| 41 | token: string, | |
| 42 | ): PreparedConnection { | |
| 43 | return { | |
| 44 | environmentId, | |
| 45 | label: `Environment ${environmentId}`, | |
| 46 | httpBaseUrl: `https://${endpoint}.example.test`, | |
| 47 | socketUrl: `wss://${endpoint}.example.test/ws?token=redacted`, | |
| 48 | httpAuthorization: { _tag: "Bearer", token }, | |
| 49 | target: target(environmentId, endpoint), | |
| 50 | }; | |
| 51 | } | |
| 52 | ||
| 53 | function makeHarness() { | |
| 54 | const presentationAtoms = Atom.family((environmentId: EnvironmentId) => | |
| 55 | Atom.make<EnvironmentPresentation | null>(presentation(environmentId)), | |
| 56 | ); | |
| 57 | const preparedConnectionAtoms = Atom.family((_environmentId: EnvironmentId) => | |
| 58 | Atom.make<Option.Option<PreparedConnection>>(Option.none()), | |
| 59 | ); | |
| 60 | const serverConfigAtoms = Atom.family((_environmentId: EnvironmentId) => | |
| 61 | Atom.make<ServerConfig | null>(null), | |
| 62 | ); | |
| 63 | const projections = createRemoteEnvironmentProjectionAtoms({ | |
| 64 | presentationAtom: presentationAtoms, | |
| 65 | preparedConnectionAtom: preparedConnectionAtoms, | |
| 66 | serverConfigAtom: serverConfigAtoms, | |
| 67 | }); | |
| 68 | ||
| 69 | return { | |
| 70 | registry: AtomRegistry.make(), | |
| 71 | presentationAtom: presentationAtoms, | |
| 72 | preparedConnectionAtom: preparedConnectionAtoms, | |
| 73 | serverConfigAtom: serverConfigAtoms, | |
| 74 | projections, | |
| 75 | }; | |
| 76 | } | |
| 77 | ||
| 78 | describe("remote environment projections", () => { | |
| 79 | it("shares each environment projection and invalidates only changed inputs", () => { | |
| 80 | const harness = makeHarness(); | |
| 81 | const firstConsumer = Atom.make((get) => | |
| 82 | get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)), | |
| 83 | ); | |
| 84 | const secondConsumer = Atom.make((get) => | |
| 85 | get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)), | |
| 86 | ); | |
| 87 | const otherConsumer = Atom.make((get) => | |
| 88 | get(harness.projections.savedConnectionAtom(OTHER_ENVIRONMENT_ID)), | |
| 89 | ); | |
| 90 | const initial = harness.registry.get(firstConsumer); | |
| 91 | const otherInitial = harness.registry.get(otherConsumer); | |
| 92 | ||
| 93 | expect(harness.registry.get(secondConsumer)).toBe(initial); | |
| 94 | expect(initial).toMatchObject({ | |
| 95 | environmentLabel: "Environment environment-1", | |
| 96 | pairingUrl: "https://environment-1.example.test", | |
| 97 | displayUrl: "https://environment-1.example.test", | |
| 98 | httpBaseUrl: "https://environment-1.example.test", | |
| 99 | wsBaseUrl: "wss://environment-1.example.test", | |
| 100 | bearerToken: null, | |
| 101 | }); | |
| 102 | ||
| 103 | harness.registry.set( | |
| 104 | harness.preparedConnectionAtom(ENVIRONMENT_ID), | |
| 105 | Option.some(prepared(ENVIRONMENT_ID, "rotated", "rotated-token")), | |
| 106 | ); | |
| 107 | const rotated = harness.registry.get(firstConsumer); | |
| 108 | ||
| 109 | expect(rotated).not.toBe(initial); | |
| 110 | expect(rotated).toMatchObject({ | |
| 111 | httpBaseUrl: "https://rotated.example.test", | |
| 112 | wsBaseUrl: "wss://rotated.example.test", | |
| 113 | bearerToken: "rotated-token", | |
| 114 | }); | |
| 115 | expect(harness.registry.get(secondConsumer)).toBe(rotated); | |
| 116 | expect(harness.registry.get(otherConsumer)).toBe(otherInitial); | |
| 117 | ||
| 118 | harness.registry.set(harness.preparedConnectionAtom(ENVIRONMENT_ID), Option.none()); | |
| 119 | harness.registry.set( | |
| 120 | harness.presentationAtom(ENVIRONMENT_ID), | |
| 121 | presentation(ENVIRONMENT_ID, "catalog-updated"), | |
| 122 | ); | |
| 123 | ||
| 124 | expect(harness.registry.get(firstConsumer)).toMatchObject({ | |
| 125 | displayUrl: "https://catalog-updated.example.test", | |
| 126 | httpBaseUrl: "https://catalog-updated.example.test", | |
| 127 | wsBaseUrl: "wss://catalog-updated.example.test", | |
| 128 | bearerToken: null, | |
| 129 | }); | |
| 130 | }); | |
| 131 | ||
| 132 | it("preserves saved identity across config-only updates and refreshes runtime state", () => { | |
| 133 | const harness = makeHarness(); | |
| 134 | const savedAtom = harness.projections.savedConnectionAtom(ENVIRONMENT_ID); | |
| 135 | const runtimeAtom = harness.projections.runtimeStateAtom(ENVIRONMENT_ID); | |
| 136 | const savedInitial = harness.registry.get(savedAtom); | |
| 137 | const runtimeInitial = harness.registry.get(runtimeAtom); | |
| 138 | const config = { cwd: "/repo" } as ServerConfig; | |
| 139 | const initialPresentation = harness.registry.get(harness.presentationAtom(ENVIRONMENT_ID)); | |
| 140 | ||
| 141 | harness.registry.set( | |
| 142 | harness.presentationAtom(ENVIRONMENT_ID), | |
| 143 | initialPresentation === null ? null : { ...initialPresentation, serverConfig: config }, | |
| 144 | ); | |
| 145 | harness.registry.set(harness.serverConfigAtom(ENVIRONMENT_ID), config); | |
| 146 | ||
| 147 | expect(harness.registry.get(savedAtom)).toBe(savedInitial); | |
| 148 | expect(harness.registry.get(runtimeAtom)).not.toBe(runtimeInitial); | |
| 149 | expect(harness.registry.get(runtimeAtom)?.serverConfig).toBe(config); | |
| 150 | }); | |
| 151 | ||
| 152 | it("keeps missing environments null", () => { | |
| 153 | const harness = makeHarness(); | |
| 154 | harness.registry.set(harness.presentationAtom(ENVIRONMENT_ID), null); | |
| 155 | ||
| 156 | expect( | |
| 157 | harness.registry.get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)), | |
| 158 | ).toBeNull(); | |
| 159 | expect(harness.registry.get(harness.projections.runtimeStateAtom(ENVIRONMENT_ID))).toBeNull(); | |
| 160 | }); | |
| 161 | }); |
apps/mobile/src/state/remote-environment-projections.ts created+120| ... | ... | @@ -0,0 +1,120 @@ |
| 1 | import type { | |
| 2 | EnvironmentPresentation, | |
| 3 | PreparedConnection, | |
| 4 | } from "@t3tools/client-runtime/connection"; | |
| 5 | import { connectionCatalogDisplayUrl } from "@t3tools/client-runtime/connection"; | |
| 6 | import type { EnvironmentId, ServerConfig } from "@t3tools/contracts"; | |
| 7 | import * as Option from "effect/Option"; | |
| 8 | import { Atom } from "effect/unstable/reactivity"; | |
| 9 | ||
| 10 | import type { SavedRemoteConnection } from "../lib/connection"; | |
| 11 | import type { EnvironmentRuntimeState } from "./remote-runtime-types"; | |
| 12 | ||
| 13 | export function createRemoteEnvironmentProjectionAtoms(input: { | |
| 14 | readonly presentationAtom: ( | |
| 15 | environmentId: EnvironmentId, | |
| 16 | ) => Atom.Atom<EnvironmentPresentation | null>; | |
| 17 | readonly preparedConnectionAtom: ( | |
| 18 | environmentId: EnvironmentId, | |
| 19 | ) => Atom.Atom<Option.Option<PreparedConnection>>; | |
| 20 | readonly serverConfigAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>; | |
| 21 | }) { | |
| 22 | const savedConnectionAtom = Atom.family((environmentId: EnvironmentId) => { | |
| 23 | let previousEntry: EnvironmentPresentation["entry"] | null = null; | |
| 24 | let previousPrepared: PreparedConnection | null = null; | |
| 25 | let previous: SavedRemoteConnection | null = null; | |
| 26 | ||
| 27 | return Atom.make((get) => { | |
| 28 | const presentation = get(input.presentationAtom(environmentId)); | |
| 29 | if (presentation === null) { | |
| 30 | previousEntry = null; | |
| 31 | previousPrepared = null; | |
| 32 | previous = null; | |
| 33 | return null; | |
| 34 | } | |
| 35 | ||
| 36 | const prepared = Option.getOrNull(get(input.preparedConnectionAtom(environmentId))); | |
| 37 | if ( | |
| 38 | previous !== null && | |
| 39 | presentation.entry === previousEntry && | |
| 40 | prepared === previousPrepared | |
| 41 | ) { | |
| 42 | return previous; | |
| 43 | } | |
| 44 | ||
| 45 | const displayUrl = connectionCatalogDisplayUrl(presentation.entry) ?? ""; | |
| 46 | const httpBaseUrl = prepared?.httpBaseUrl ?? displayUrl; | |
| 47 | const socketUrl = prepared?.socketUrl ?? ""; | |
| 48 | const wsBaseUrl = | |
| 49 | socketUrl === "" | |
| 50 | ? displayUrl.startsWith("https://") | |
| 51 | ? displayUrl.replace(/^https:/, "wss:") | |
| 52 | : displayUrl.replace(/^http:/, "ws:") | |
| 53 | : new URL(socketUrl).origin; | |
| 54 | const authorization = prepared?.httpAuthorization ?? null; | |
| 55 | const relayManaged = presentation.entry.target._tag === "RelayConnectionTarget"; | |
| 56 | ||
| 57 | previousEntry = presentation.entry; | |
| 58 | previousPrepared = prepared; | |
| 59 | previous = { | |
| 60 | environmentId, | |
| 61 | environmentLabel: presentation.entry.target.label, | |
| 62 | pairingUrl: displayUrl, | |
| 63 | displayUrl, | |
| 64 | httpBaseUrl, | |
| 65 | wsBaseUrl, | |
| 66 | bearerToken: authorization?._tag === "Bearer" ? authorization.token : null, | |
| 67 | ...(relayManaged | |
| 68 | ? { | |
| 69 | authenticationMethod: "dpop" as const, | |
| 70 | relayManaged: true as const, | |
| 71 | ...(authorization?._tag === "Dpop" | |
| 72 | ? { dpopAccessToken: authorization.accessToken } | |
| 73 | : {}), | |
| 74 | } | |
| 75 | : { authenticationMethod: "bearer" as const }), | |
| 76 | }; | |
| 77 | return previous; | |
| 78 | }).pipe(Atom.withLabel(`mobile:saved-connection:${environmentId}`)); | |
| 79 | }); | |
| 80 | ||
| 81 | const runtimeStateAtom = Atom.family((environmentId: EnvironmentId) => { | |
| 82 | let previousConnection: EnvironmentPresentation["connection"] | null = null; | |
| 83 | let previousServerConfig: ServerConfig | null = null; | |
| 84 | let previous: EnvironmentRuntimeState | null = null; | |
| 85 | ||
| 86 | return Atom.make((get) => { | |
| 87 | const presentation = get(input.presentationAtom(environmentId)); | |
| 88 | if (presentation === null) { | |
| 89 | previousConnection = null; | |
| 90 | previousServerConfig = null; | |
| 91 | previous = null; | |
| 92 | return null; | |
| 93 | } | |
| 94 | ||
| 95 | const connection = presentation.connection; | |
| 96 | const serverConfig = get(input.serverConfigAtom(environmentId)); | |
| 97 | if ( | |
| 98 | previous !== null && | |
| 99 | connection.phase === previousConnection?.phase && | |
| 100 | connection.error === previousConnection?.error && | |
| 101 | connection.traceId === previousConnection?.traceId && | |
| 102 | serverConfig === previousServerConfig | |
| 103 | ) { | |
| 104 | return previous; | |
| 105 | } | |
| 106 | ||
| 107 | previousConnection = connection; | |
| 108 | previousServerConfig = serverConfig; | |
| 109 | previous = { | |
| 110 | connectionState: connection.phase, | |
| 111 | connectionError: connection.error, | |
| 112 | connectionErrorTraceId: connection.traceId, | |
| 113 | serverConfig, | |
| 114 | }; | |
| 115 | return previous; | |
| 116 | }).pipe(Atom.withLabel(`mobile:environment-runtime-state:${environmentId}`)); | |
| 117 | }); | |
| 118 | ||
| 119 | return { savedConnectionAtom, runtimeStateAtom }; | |
| 120 | } |
apps/mobile/src/state/use-remote-environment-registry.ts+30-73| ... | ... | @@ -1,26 +1,20 @@ |
| 1 | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import type { PreparedConnection } from "@t3tools/client-runtime/connection"; | |
| 3 | 2 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 4 | import type { ServerConfig } from "@t3tools/contracts"; | |
| 5 | 3 | import * as Cause from "effect/Cause"; |
| 6 | import * as Option from "effect/Option"; | |
| 7 | 4 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 8 | 5 | import { useCallback, useMemo } from "react"; |
| 9 | 6 | import { Alert } from "react-native"; |
| 10 | 7 | |
| 11 | import { useEnvironmentServerConfig } from "../state/entities"; | |
| 12 | 8 | import { useConnectionController } from "../features/connection/useConnectionController"; |
| 13 | import { environmentPresentations, useEnvironmentPresentation } from "./presentation"; | |
| 14 | import { | |
| 15 | projectEnvironmentPresentation, | |
| 16 | type EnvironmentPresentation, | |
| 17 | } from "../state/environments"; | |
| 9 | import { environmentPresentations } from "./presentation"; | |
| 18 | 10 | import { useWorkspaceState } from "../state/workspace"; |
| 19 | 11 | import type { SavedRemoteConnection } from "../lib/connection"; |
| 20 | 12 | import { appAtomRegistry } from "./atom-registry"; |
| 21 | 13 | import type { ConnectedEnvironmentSummary, EnvironmentRuntimeState } from "./remote-runtime-types"; |
| 22 | import { environmentSession, usePreparedConnection } from "./session"; | |
| 14 | import { environmentSession } from "./session"; | |
| 23 | 15 | import { environmentCatalog } from "../connection/catalog"; |
| 16 | import { createRemoteEnvironmentProjectionAtoms } from "./remote-environment-projections"; | |
| 17 | import { serverEnvironment } from "./server"; | |
| 24 | 18 | |
| 25 | 19 | const connectionPairingUrlAtom = Atom.make("").pipe( |
| 26 | 20 | Atom.keepAlive, |
| ... | ... | @@ -36,65 +30,30 @@ export function setPendingConnectionError(message: string | null): void { |
| 36 | 30 | appAtomRegistry.set(pendingConnectionErrorAtom, message); |
| 37 | 31 | } |
| 38 | 32 | |
| 39 | function toSavedConnection( | |
| 40 | environment: EnvironmentPresentation, | |
| 41 | prepared: Option.Option<PreparedConnection>, | |
| 42 | ): SavedRemoteConnection { | |
| 43 | const displayUrl = environment.displayUrl ?? ""; | |
| 44 | const active = Option.getOrNull(prepared); | |
| 45 | const httpBaseUrl = active?.httpBaseUrl ?? displayUrl; | |
| 46 | const socketUrl = active?.socketUrl ?? ""; | |
| 47 | const wsBaseUrl = | |
| 48 | socketUrl === "" | |
| 49 | ? displayUrl.startsWith("https://") | |
| 50 | ? displayUrl.replace(/^https:/, "wss:") | |
| 51 | : displayUrl.replace(/^http:/, "ws:") | |
| 52 | : new URL(socketUrl).origin; | |
| 53 | const authorization = active?.httpAuthorization ?? null; | |
| 33 | const remoteEnvironmentProjections = createRemoteEnvironmentProjectionAtoms({ | |
| 34 | presentationAtom: environmentPresentations.presentationAtom, | |
| 35 | preparedConnectionAtom: environmentSession.preparedConnectionValueAtom, | |
| 36 | serverConfigAtom: serverEnvironment.configValueAtom, | |
| 37 | }); | |
| 54 | 38 | |
| 55 | return { | |
| 56 | environmentId: environment.environmentId, | |
| 57 | environmentLabel: environment.label, | |
| 58 | pairingUrl: displayUrl, | |
| 59 | displayUrl, | |
| 60 | httpBaseUrl, | |
| 61 | wsBaseUrl, | |
| 62 | bearerToken: authorization?._tag === "Bearer" ? authorization.token : null, | |
| 63 | ...(environment.relayManaged | |
| 64 | ? { | |
| 65 | authenticationMethod: "dpop" as const, | |
| 66 | relayManaged: true as const, | |
| 67 | ...(authorization?._tag === "Dpop" ? { dpopAccessToken: authorization.accessToken } : {}), | |
| 68 | } | |
| 69 | : { authenticationMethod: "bearer" as const }), | |
| 70 | }; | |
| 71 | } | |
| 39 | const EMPTY_SAVED_CONNECTION_ATOM = Atom.make<SavedRemoteConnection | null>(null).pipe( | |
| 40 | Atom.withLabel("mobile:saved-connection:empty"), | |
| 41 | ); | |
| 42 | ||
| 43 | const EMPTY_RUNTIME_STATE_ATOM = Atom.make<EnvironmentRuntimeState | null>(null).pipe( | |
| 44 | Atom.withLabel("mobile:environment-runtime-state:empty"), | |
| 45 | ); | |
| 72 | 46 | |
| 73 | 47 | const savedConnectionsByIdAtom = Atom.make((get) => { |
| 74 | 48 | const presentationById = get(environmentPresentations.presentationsAtom); |
| 75 | 49 | return Object.fromEntries( |
| 76 | [...presentationById.entries()].map(([environmentId, presentation]) => [ | |
| 77 | environmentId, | |
| 78 | toSavedConnection( | |
| 79 | projectEnvironmentPresentation(environmentId, presentation), | |
| 80 | get(environmentSession.preparedConnectionValueAtom(environmentId)), | |
| 81 | ), | |
| 82 | ]), | |
| 50 | [...presentationById.keys()].flatMap((environmentId) => { | |
| 51 | const connection = get(remoteEnvironmentProjections.savedConnectionAtom(environmentId)); | |
| 52 | return connection === null ? [] : [[environmentId, connection]]; | |
| 53 | }), | |
| 83 | 54 | ) as Record<EnvironmentId, SavedRemoteConnection>; |
| 84 | 55 | }).pipe(Atom.withLabel("mobile:saved-connections-by-id")); |
| 85 | 56 | |
| 86 | function toRuntimeState( | |
| 87 | environment: EnvironmentPresentation, | |
| 88 | serverConfig: ServerConfig | null, | |
| 89 | ): EnvironmentRuntimeState { | |
| 90 | return { | |
| 91 | connectionState: environment.connection.phase, | |
| 92 | connectionError: environment.connection.error, | |
| 93 | connectionErrorTraceId: environment.connection.traceId, | |
| 94 | serverConfig, | |
| 95 | }; | |
| 96 | } | |
| 97 | ||
| 98 | 57 | export function useSavedRemoteConnections() { |
| 99 | 58 | const catalog = useAtomValue(environmentCatalog.catalogValueAtom); |
| 100 | 59 | const savedConnectionsById = useAtomValue(savedConnectionsByIdAtom); |
| ... | ... | @@ -108,23 +67,21 @@ export function useSavedRemoteConnections() { |
| 108 | 67 | export function useSavedRemoteConnection( |
| 109 | 68 | environmentId: EnvironmentId | null, |
| 110 | 69 | ): SavedRemoteConnection | null { |
| 111 | const { presentation } = useEnvironmentPresentation(environmentId); | |
| 112 | const prepared = usePreparedConnection(environmentId); | |
| 113 | if (environmentId === null || presentation === null) { | |
| 114 | return null; | |
| 115 | } | |
| 116 | return toSavedConnection(projectEnvironmentPresentation(environmentId, presentation), prepared); | |
| 70 | return useAtomValue( | |
| 71 | environmentId === null | |
| 72 | ? EMPTY_SAVED_CONNECTION_ATOM | |
| 73 | : remoteEnvironmentProjections.savedConnectionAtom(environmentId), | |
| 74 | ); | |
| 117 | 75 | } |
| 118 | 76 | |
| 119 | 77 | export function useRemoteEnvironmentRuntime( |
| 120 | 78 | environmentId: EnvironmentId | null, |
| 121 | 79 | ): EnvironmentRuntimeState | null { |
| 122 | const { presentation } = useEnvironmentPresentation(environmentId); | |
| 123 | const serverConfig = useEnvironmentServerConfig(environmentId); | |
| 124 | if (environmentId === null || presentation === null) { | |
| 125 | return null; | |
| 126 | } | |
| 127 | return toRuntimeState(projectEnvironmentPresentation(environmentId, presentation), serverConfig); | |
| 80 | return useAtomValue( | |
| 81 | environmentId === null | |
| 82 | ? EMPTY_RUNTIME_STATE_ATOM | |
| 83 | : remoteEnvironmentProjections.runtimeStateAtom(environmentId), | |
| 84 | ); | |
| 128 | 85 | } |
| 129 | 86 | |
| 130 | 87 | export function useRemoteConnectionStatus() { |
docs/internals/mobile-development.md created+50| ... | ... | @@ -0,0 +1,50 @@ |
| 1 | # Mobile development lifecycle | |
| 2 | ||
| 3 | Ordinary component changes use Metro Fast Refresh. The connection runtime uses | |
| 4 | a stable development-only atom runtime whose writable layer atom receives the | |
| 5 | newly evaluated Effect layer. Its module accepts the update only after installing | |
| 6 | that layer. Existing subscribers observe the new context, and Effect releases the | |
| 7 | previous runtime scope without restarting React Navigation. Production uses an | |
| 8 | ordinary `Atom.runtime` without this hot-update boundary. | |
| 9 | ||
| 10 | Replacing the atom registry itself disposes its old nodes; replacing the managed | |
| 11 | runtime starts its asynchronous disposal. These modules use normal Metro update | |
| 12 | propagation. The app does not call `DevSettings.reload` during their replacement. | |
| 13 | React can still reset state when its normal Fast Refresh rules require it. | |
| 14 | ||
| 15 | Do not reset the shared registry to clean up a connection-runtime edit: registry | |
| 16 | reset removes listeners from unedited mounted consumers. Do not self-accept a | |
| 17 | runtime module while leaving importers attached to its old implementation. A | |
| 18 | hot-update boundary must install fresh behavior through the existing reactive | |
| 19 | dependencies. These boundaries do not make every module-level atom family in the | |
| 20 | app hot-swappable; new runtime singletons still need explicit ownership. Editing | |
| 21 | the registry or managed runtime can still rebuild a much larger dependency graph | |
| 22 | than an ordinary component or connection-runtime edit. | |
| 23 | ||
| 24 | Environment supervisor scopes are children of the connection registry scope. | |
| 25 | The registry's per-environment map supports targeted shutdown, but it cannot be | |
| 26 | the sole owner: a supervisor created after that map's finalizer has run must | |
| 27 | still inherit the closed parent scope. Otherwise interrupted startup or runtime | |
| 28 | replacement can leave a session and WebSocket alive outside the current registry. | |
| 29 | ||
| 30 | The compact Home list owns its minute-based presentation clock in a focus effect. | |
| 31 | Blur clears the interval without a render-driving focus subscription; focus | |
| 32 | refreshes the clock immediately. Other prop or state changes can still render | |
| 33 | the hidden list. Exact snooze-expiry timers remain active, and the visible iPad | |
| 34 | sidebar keeps its own minute updates. | |
| 35 | ||
| 36 | Connection and runtime projections are shared per environment. Thread selection | |
| 37 | consumers should read those atoms instead of repeatedly parsing the same socket | |
| 38 | URL or constructing new connection objects during each render. | |
| 39 | ||
| 40 | The Uniwind dependency patch still recompiles CSS on Metro updates so newly used | |
| 41 | classes are discovered. It fingerprints the generated native stylesheet and | |
| 42 | theme list, then skips development-only global invalidation when that output is | |
| 43 | unchanged. A changed stylesheet or theme list still resets the style caches and | |
| 44 | notifies subscribers. The digest is committed only after initialization succeeds. | |
| 45 | Web and production retain their existing initialization behavior. | |
| 46 | ||
| 47 | After installing or changing the Uniwind patch, restart Metro once with | |
| 48 | `vp run dev:client:reset` from `apps/mobile`. pnpm gives patched packages new | |
| 49 | filesystem paths, and cached transforms can otherwise retain references to the | |
| 50 | previous package. Ordinary development starts should retain the transform cache. |
packages/client-runtime/src/connection/registry.test.ts+26| ... | ... | @@ -4,13 +4,17 @@ import { |
| 4 | 4 | type OrchestrationShellSnapshot, |
| 5 | 5 | } from "@t3tools/contracts"; |
| 6 | 6 | import { describe, expect, it } from "@effect/vitest"; |
| 7 | import * as Context from "effect/Context"; | |
| 7 | 8 | import * as Deferred from "effect/Deferred"; |
| 8 | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Exit from "effect/Exit"; | |
| 9 | 11 | import * as Fiber from "effect/Fiber"; |
| 10 | 12 | import * as Layer from "effect/Layer"; |
| 11 | 13 | import * as Option from "effect/Option"; |
| 12 | 14 | import * as Ref from "effect/Ref"; |
| 13 | 15 | import * as Result from "effect/Result"; |
| 16 | import * as Scheduler from "effect/Scheduler"; | |
| 17 | import * as Scope from "effect/Scope"; | |
| 14 | 18 | import * as Stream from "effect/Stream"; |
| 15 | 19 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 16 | 20 | |
| ... | ... | @@ -420,6 +424,28 @@ function awaitConnectionState( |
| 420 | 424 | } |
| 421 | 425 | |
| 422 | 426 | describe("EnvironmentRegistry", () => { |
| 427 | it.effect("does not acquire a session after the registry scope has already closed", () => | |
| 428 | Effect.gen(function* () { | |
| 429 | const harness = yield* makeHarness([TARGET]); | |
| 430 | const registryScope = yield* Scope.make(); | |
| 431 | const context = yield* Layer.build(harness.layer).pipe(Scope.provide(registryScope)); | |
| 432 | const registry = Context.get(context, EnvironmentRegistry.EnvironmentRegistry); | |
| 433 | const dispatcher = new Scheduler.MixedScheduler("sync", () => () => {}).makeDispatcher(); | |
| 434 | const scheduler: Scheduler.Scheduler = { | |
| 435 | executionMode: "sync", | |
| 436 | shouldYield: () => false, | |
| 437 | makeDispatcher: () => dispatcher, | |
| 438 | }; | |
| 439 | ||
| 440 | yield* Scope.close(registryScope, Exit.void); | |
| 441 | yield* registry.start.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)); | |
| 442 | dispatcher.flush(); | |
| 443 | ||
| 444 | expect(yield* Ref.get(harness.sessions)).toHaveLength(0); | |
| 445 | expect(yield* Ref.get(harness.releasedSessions)).toBe(0); | |
| 446 | }), | |
| 447 | ); | |
| 448 | ||
| 423 | 449 | it.effect("hydrates connection profiles into catalog entries", () => |
| 424 | 450 | Effect.gen(function* () { |
| 425 | 451 | const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]); |
packages/client-runtime/src/connection/registry.ts+2-1| ... | ... | @@ -126,6 +126,7 @@ interface EnvironmentServiceScope { |
| 126 | 126 | } |
| 127 | 127 | |
| 128 | 128 | export const make = Effect.gen(function* () { |
| 129 | const registryScope = yield* Scope.Scope; | |
| 129 | 130 | const storage = yield* Persistence.ConnectionTargetStore; |
| 130 | 131 | const registrations = yield* Persistence.ConnectionRegistrationStore; |
| 131 | 132 | const cache = yield* Persistence.EnvironmentCacheStore; |
| ... | ... | @@ -249,7 +250,7 @@ export const make = Effect.gen(function* () { |
| 249 | 250 | Effect.uninterruptible( |
| 250 | 251 | Effect.gen(function* () { |
| 251 | 252 | const environmentId = entry.target.environmentId; |
| 252 | const scope = yield* Scope.make(); | |
| 253 | const scope = yield* Scope.fork(registryScope); | |
| 253 | 254 | const supervisor = yield* EnvironmentSupervisor.make(entry, { |
| 254 | 255 | initiallyDesired: false, |
| 255 | 256 | }).pipe( |
patches/@react-native-menu__menu@2.0.0.patch+257-15| ... | ... | @@ -1,8 +1,94 @@ |
| 1 | diff --git a/ios/MenuViewManager.mm b/ios/MenuViewManager.mm | |
| 2 | index 3d149b3e7426b8f7c999603ddf1e15e12331c33a..e80fe2caae86e339fcb03408419a7d64b3e5b04f 100644 | |
| 3 | --- a/ios/MenuViewManager.mm | |
| 4 | +++ b/ios/MenuViewManager.mm | |
| 5 | @@ -65,6 +65,10 @@ - (UIView *)view | |
| 6 | * onOpenMenu: callback to be called when the menu is opened | |
| 7 | */ | |
| 8 | RCT_EXPORT_VIEW_PROPERTY(onOpenMenu, RCTDirectEventBlock); | |
| 9 | +/** | |
| 10 | + * onMenuInteractionStart: callback to be called when UIKit starts preparing a menu interaction | |
| 11 | + */ | |
| 12 | +RCT_EXPORT_VIEW_PROPERTY(onMenuInteractionStart, RCTDirectEventBlock); | |
| 13 | /** | |
| 14 | * shouldOpenOnLongPress: determines whether menu should be opened after long press or normal press | |
| 15 | */ | |
| 16 | diff --git a/ios/NewArch/FabricActionSheetView.swift b/ios/NewArch/FabricActionSheetView.swift | |
| 17 | index 484ff8a1d73a6d445273883c5b28b1c8e80901ec..bc92310d00369b853a80f4d2080578f3903191d2 100644 | |
| 18 | --- a/ios/NewArch/FabricActionSheetView.swift | |
| 19 | +++ b/ios/NewArch/FabricActionSheetView.swift | |
| 20 | @@ -3,6 +3,7 @@ public class FabricActionSheetView: ActionSheetView, FabricViewImplementationPro | |
| 21 | public var onPressAction: ((String) -> Void)? | |
| 22 | public var onCloseMenu: (() -> Void)? | |
| 23 | public var onOpenMenu: (() -> Void)? | |
| 24 | + public var onMenuInteractionStart: (() -> Void)? | |
| 25 | ||
| 26 | @objc override func sendButtonAction(_ action: String) { | |
| 27 | if let onPress = onPressAction { | |
| 28 | diff --git a/ios/NewArch/FabricMenuViewImplementation.swift b/ios/NewArch/FabricMenuViewImplementation.swift | |
| 29 | index 33d27c5a66593ae1a9b143804413578eb79f1a7d..359cd19fa53d10c0374b2b537517ea8629e79d0c 100644 | |
| 30 | --- a/ios/NewArch/FabricMenuViewImplementation.swift | |
| 31 | +++ b/ios/NewArch/FabricMenuViewImplementation.swift | |
| 32 | @@ -12,6 +12,7 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp | |
| 33 | public var onPressAction: ((String) -> Void)? | |
| 34 | public var onCloseMenu: (() -> Void)? | |
| 35 | public var onOpenMenu: (() -> Void)? | |
| 36 | + public var onMenuInteractionStart: (() -> Void)? | |
| 37 | ||
| 38 | @objc override func sendButtonAction(_ action: UIAction) { | |
| 39 | if let onPress = onPressAction { | |
| 40 | @@ -31,4 +32,10 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | + @objc override func sendMenuInteractionStart() { | |
| 45 | + if let onMenuInteractionStart = onMenuInteractionStart { | |
| 46 | + onMenuInteractionStart() | |
| 47 | + } | |
| 48 | + } | |
| 49 | + | |
| 50 | } | |
| 51 | diff --git a/ios/NewArch/FabricViewImplementationProtocol.swift b/ios/NewArch/FabricViewImplementationProtocol.swift | |
| 52 | index 2cef2fb8c74497da46775a490edc58cf8cbf29cc..54429c756eeecf6e96b55d62cc1e70213096f62a 100644 | |
| 53 | --- a/ios/NewArch/FabricViewImplementationProtocol.swift | |
| 54 | +++ b/ios/NewArch/FabricViewImplementationProtocol.swift | |
| 55 | @@ -8,4 +8,5 @@ import Foundation | |
| 56 | var onPressAction: ((String) -> Void)? { get set } | |
| 57 | var onCloseMenu: (() -> Void)? { get set } | |
| 58 | var onOpenMenu: (() -> Void)? { get set } | |
| 59 | + var onMenuInteractionStart: (() -> Void)? { get set } | |
| 60 | } | |
| 1 | 61 | diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mm |
| 2 | index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a94a8f7d4 100644 | |
| 62 | index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..773f6f1f5d4e0663fd3211fe51085a7f3935e445 100644 | |
| 3 | 63 | --- a/ios/NewArch/MenuView.mm |
| 4 | 64 | +++ b/ios/NewArch/MenuView.mm |
| 5 | @@ -105,6 +105,27 @@ - (void)onOpenMenu { | |
| 65 | @@ -46,6 +46,10 @@ - (instancetype)initWithFrame:(CGRect)frame | |
| 66 | [self onOpenMenu]; | |
| 67 | }; | |
| 68 | ||
| 69 | + _view.onMenuInteractionStart = ^{ | |
| 70 | + [self onMenuInteractionStart]; | |
| 71 | + }; | |
| 72 | + | |
| 73 | _view.onCloseMenu = ^{ | |
| 74 | [self onCloseMenu]; | |
| 75 | }; | |
| 76 | @@ -92,6 +96,14 @@ - (void)onOpenMenu { | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | +- (void)onMenuInteractionStart { | |
| 81 | + // If screen is already unmounted then there will be no event emitter | |
| 82 | + const auto eventEmitter = [self getEventEmitter]; | |
| 83 | + if (eventEmitter != nullptr) { | |
| 84 | + eventEmitter->onMenuInteractionStart({}); | |
| 85 | + } | |
| 86 | +} | |
| 87 | + | |
| 88 | /** | |
| 89 | Responsible for iterating through the C++ vector<struct> and convert each struct element to NSDictionary, then return it all in an NSArray | |
| 90 | */ | |
| 91 | @@ -105,6 +117,27 @@ - (void)onOpenMenu { | |
| 6 | 92 | NSMutableArray<NSDictionary *> *subactionsArray = [NSMutableArray arrayWithCapacity:actions.size()]; |
| 7 | 93 | if (action.subactions.size() > 0) { |
| 8 | 94 | for (const MenuViewActionsSubactionsStruct &subaction : action.subactions) { |
| ... | ... | @@ -30,7 +116,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 30 | 116 | NSDictionary *subactionDict = @{ |
| 31 | 117 | @"id": [NSString stringWithUTF8String:subaction.id.c_str()], |
| 32 | 118 | @"title": [NSString stringWithUTF8String:subaction.title.c_str()], |
| 33 | @@ -118,7 +139,9 @@ - (void)onOpenMenu { | |
| 119 | @@ -118,7 +151,9 @@ - (void)onOpenMenu { | |
| 34 | 120 | @"destructive": @(subaction.attributes.destructive), |
| 35 | 121 | @"disabled": @(subaction.attributes.disabled), |
| 36 | 122 | @"hidden": @(subaction.attributes.hidden), |
| ... | ... | @@ -40,7 +126,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 40 | 126 | }; |
| 41 | 127 | [subactionsArray addObject:subactionDict]; |
| 42 | 128 | } |
| 43 | @@ -138,6 +161,7 @@ - (void)onOpenMenu { | |
| 129 | @@ -138,6 +173,7 @@ - (void)onOpenMenu { | |
| 44 | 130 | @"destructive": @(action.attributes.destructive), |
| 45 | 131 | @"disabled": @(action.attributes.disabled), |
| 46 | 132 | @"hidden": @(action.attributes.hidden), |
| ... | ... | @@ -48,20 +134,50 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 48 | 134 | }, |
| 49 | 135 | @"subactions": subactionsArray, |
| 50 | 136 | }; |
| 137 | diff --git a/ios/OldArch/LegacyActionSheetView.swift b/ios/OldArch/LegacyActionSheetView.swift | |
| 138 | index 3cd1a2517624d99b51331c564fba9145d7c8d799..e458504bea6ef167d9b0db0c889f0f76ea965287 100644 | |
| 139 | --- a/ios/OldArch/LegacyActionSheetView.swift | |
| 140 | +++ b/ios/OldArch/LegacyActionSheetView.swift | |
| 141 | @@ -3,6 +3,7 @@ public class LegacyActionSheetView: ActionSheetView { | |
| 142 | @objc var onPressAction: RCTDirectEventBlock? | |
| 143 | @objc var onCloseMenu: RCTDirectEventBlock? | |
| 144 | @objc var onOpenMenu: RCTDirectEventBlock? | |
| 145 | + @objc var onMenuInteractionStart: RCTDirectEventBlock? | |
| 146 | ||
| 147 | ||
| 148 | ||
| 149 | diff --git a/ios/OldArch/LegacyMenuViewImplementation.swift b/ios/OldArch/LegacyMenuViewImplementation.swift | |
| 150 | index 6a2a205f95c1001f3fdf6fb835815fa7c4c02c16..4f42b64349966fb1438f72439cec8256a76aab9c 100644 | |
| 151 | --- a/ios/OldArch/LegacyMenuViewImplementation.swift | |
| 152 | +++ b/ios/OldArch/LegacyMenuViewImplementation.swift | |
| 153 | @@ -5,6 +5,7 @@ public class LegacyMenuViewImplementation: MenuViewImplementation { | |
| 154 | @objc var onPressAction: RCTDirectEventBlock? | |
| 155 | @objc var onCloseMenu: RCTDirectEventBlock? | |
| 156 | @objc var onOpenMenu: RCTDirectEventBlock? | |
| 157 | + @objc var onMenuInteractionStart: RCTDirectEventBlock? | |
| 158 | ||
| 159 | @objc override func sendButtonAction(_ action: UIAction) { | |
| 160 | if let onPress = onPressAction { | |
| 161 | @@ -24,4 +25,10 @@ public class LegacyMenuViewImplementation: MenuViewImplementation { | |
| 162 | } | |
| 163 | } | |
| 164 | ||
| 165 | + @objc override func sendMenuInteractionStart() { | |
| 166 | + if let onMenuInteractionStart = onMenuInteractionStart { | |
| 167 | + onMenuInteractionStart([:]) | |
| 168 | + } | |
| 169 | + } | |
| 170 | + | |
| 171 | } | |
| 51 | 172 | diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift |
| 52 | index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e8037a005 100644 | |
| 173 | index 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c77f2cc47 100644 | |
| 53 | 174 | --- a/ios/Shared/MenuViewImplementation.swift |
| 54 | 175 | +++ b/ios/Shared/MenuViewImplementation.swift |
| 55 | @@ -59,18 +59,43 @@ public class MenuViewImplementation: UIButton { | |
| 56 | self.setup() | |
| 176 | @@ -60,17 +60,46 @@ public class MenuViewImplementation: UIButton { | |
| 57 | 177 | } |
| 58 | 178 | |
| 59 | + // Presentation is tracked from the two delegate methods the class already | |
| 60 | + // overrode. Overriding willDisplayMenuFor as well (even for bookkeeping) | |
| 61 | + // shadows UIButton's own implementation and degrades the button-anchored | |
| 62 | + // presentation into generic context-menu chrome — an empty header row with | |
| 63 | + // a dismiss chevron appears above the actions. | |
| 64 | 179 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { |
| 180 | - sendMenuOpen() | |
| 65 | 181 | + // Flush updates deferred by the presented-guard before the action |
| 66 | 182 | + // provider snapshots self.menu (covers a stuck flag from an |
| 67 | 183 | + // interaction that never ended cleanly). |
| ... | ... | @@ -71,15 +187,24 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 71 | 187 | + self.setup() |
| 72 | 188 | + } |
| 73 | 189 | + isMenuPresented = true |
| 74 | sendMenuOpen() | |
| 190 | + sendMenuInteractionStart() | |
| 75 | 191 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in |
| 76 | 192 | guard let self = self else { return nil } |
| 77 | 193 | return self.menu |
| 78 | 194 | } |
| 79 | 195 | } |
| 80 | 196 | - |
| 197 | + | |
| 198 | + public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { | |
| 199 | + // UIKit can request a configuration for a press that never opens a | |
| 200 | + // menu. Notify React only once presentation actually begins, and | |
| 201 | + // preserve UIButton's presentation behavior by calling super. | |
| 202 | + super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator) | |
| 203 | + sendMenuOpen() | |
| 204 | + } | |
| 81 | 205 | + |
| 82 | 206 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { |
| 207 | + super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator) | |
| 83 | 208 | sendMenuClose() |
| 84 | 209 | + isMenuPresented = false |
| 85 | 210 | + if pendingMenu != nil { |
| ... | ... | @@ -97,7 +222,7 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 97 | 222 | func setup () { |
| 98 | 223 | let menu = UIMenu(title: _title, |
| 99 | 224 | identifier: nil, |
| 100 | @@ -86,8 +111,98 @@ public class MenuViewImplementation: UIButton { | |
| 225 | @@ -86,8 +115,98 @@ public class MenuViewImplementation: UIButton { | |
| 101 | 226 | } |
| 102 | 227 | } |
| 103 | 228 | |
| ... | ... | @@ -196,6 +321,15 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 196 | 321 | } |
| 197 | 322 | |
| 198 | 323 | public override func reactSetFrame(_ frame: CGRect) { |
| 324 | @@ -124,4 +243,8 @@ public class MenuViewImplementation: UIButton { | |
| 325 | @objc func sendMenuOpen() { | |
| 326 | // NO-OP (should be overriden by parent) | |
| 327 | } | |
| 328 | + | |
| 329 | + @objc func sendMenuInteractionStart() { | |
| 330 | + // NO-OP (should be overriden by parent) | |
| 331 | + } | |
| 332 | } | |
| 199 | 333 | diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swift |
| 200 | 334 | index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f619443d5a 100644 |
| 201 | 335 | --- a/ios/Shared/RCTMenuItem.swift |
| ... | ... | @@ -220,8 +354,83 @@ index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f6 |
| 220 | 354 | } |
| 221 | 355 | |
| 222 | 356 | if #available(iOS 16.0, *) { |
| 357 | diff --git a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 358 | index e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644 | |
| 359 | --- a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 360 | +++ b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 361 | @@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps { | |
| 362 | 	onPressAction?: DirectEventHandler<{ event: string }>; | |
| 363 | 	onCloseMenu?: DirectEventHandler<{ event: string }>; | |
| 364 | 	onOpenMenu?: DirectEventHandler<{ event: string }>; | |
| 365 | +	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; | |
| 366 | 	actions: Array<MenuAction>; | |
| 367 | 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) | |
| 368 | 	title?: string; | |
| 369 | diff --git a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 370 | index e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644 | |
| 371 | --- a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 372 | +++ b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts | |
| 373 | @@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps { | |
| 374 | 	onPressAction?: DirectEventHandler<{ event: string }>; | |
| 375 | 	onCloseMenu?: DirectEventHandler<{ event: string }>; | |
| 376 | 	onOpenMenu?: DirectEventHandler<{ event: string }>; | |
| 377 | +	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; | |
| 378 | 	actions: Array<MenuAction>; | |
| 379 | 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) | |
| 380 | 	title?: string; | |
| 381 | diff --git a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts | |
| 382 | index 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..5ee7b45bafea8547d68949aaba70584055f9fb53 100644 | |
| 383 | --- a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts | |
| 384 | +++ b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts | |
| 385 | @@ -41,6 +41,9 @@ export interface NativeProps extends ViewProps { | |
| 386 | onOpenMenu?: DirectEventHandler<{ | |
| 387 | event: string; | |
| 388 | }>; | |
| 389 | + onMenuInteractionStart?: DirectEventHandler<{ | |
| 390 | + event: string; | |
| 391 | + }>; | |
| 392 | actions: Array<MenuAction>; | |
| 393 | actionsHash: string; | |
| 394 | title?: string; | |
| 395 | diff --git a/lib/typescript/src/index.d.ts b/lib/typescript/src/index.d.ts | |
| 396 | index 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..84ac2bc8b4edac045ed59896d5eb4e6eaef4725d 100644 | |
| 397 | --- a/lib/typescript/src/index.d.ts | |
| 398 | +++ b/lib/typescript/src/index.d.ts | |
| 399 | @@ -4,6 +4,7 @@ declare const MenuView: import("react").ForwardRefExoticComponent<{ | |
| 400 | onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; | |
| 401 | onCloseMenu?: () => void; | |
| 402 | onOpenMenu?: () => void; | |
| 403 | + onMenuInteractionStart?: () => void; | |
| 404 | actions: MenuAction[]; | |
| 405 | title?: string; | |
| 406 | isAnchoredToRight?: boolean; | |
| 407 | diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts | |
| 408 | index eac7940ab7eb6a69ac329848c9f3eeffdcbaf43d..271cd27f777be06bc02f05d4b4dac5bb940f5f0f 100644 | |
| 409 | --- a/lib/typescript/src/types.d.ts | |
| 410 | +++ b/lib/typescript/src/types.d.ts | |
| 411 | @@ -113,6 +113,12 @@ type MenuComponentPropsBase = { | |
| 412 | * Callback function that will be called when the menu opens. | |
| 413 | */ | |
| 414 | onOpenMenu?: () => void; | |
| 415 | + /** | |
| 416 | + * Callback function that will be called when UIKit starts preparing a menu interaction. | |
| 417 | + * Preparation can end without the menu opening. | |
| 418 | + * @platform iOS | |
| 419 | + */ | |
| 420 | + onMenuInteractionStart?: () => void; | |
| 421 | /** | |
| 422 | * Actions to be displayed in the menu. | |
| 423 | */ | |
| 424 | @@ -170,6 +176,7 @@ export type NativeMenuComponentProps = { | |
| 425 | onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; | |
| 426 | onCloseMenu?: () => void; | |
| 427 | onOpenMenu?: () => void; | |
| 428 | + onMenuInteractionStart?: () => void; | |
| 429 | actions: ProcessedMenuAction[]; | |
| 430 | actionsHash: string; | |
| 431 | title?: string; | |
| 223 | 432 | diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 224 | index e6509355275596c451f9d082223cc16bfe504e1a..c783cdaf73f5635cf9835824ca85fba3b46453b4 100644 | |
| 433 | index e6509355275596c451f9d082223cc16bfe504e1a..604dba0d90a2545910f8724a80205565de2391d4 100644 | |
| 225 | 434 | --- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 226 | 435 | +++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 227 | 436 | @@ -13,6 +13,22 @@ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNati |
| ... | ... | @@ -267,3 +476,36 @@ index e6509355275596c451f9d082223cc16bfe504e1a..c783cdaf73f5635cf9835824ca85fba3 |
| 267 | 476 | 	}; |
| 268 | 477 | 	subactions?: Array<SubAction>; |
| 269 | 478 | }; |
| 479 | @@ -48,6 +69,7 @@ export interface NativeProps extends ViewProps { | |
| 480 | 	onPressAction?: DirectEventHandler<{ event: string }>; | |
| 481 | 	onCloseMenu?: DirectEventHandler<{ event: string }>; | |
| 482 | 	onOpenMenu?: DirectEventHandler<{ event: string }>; | |
| 483 | +	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; | |
| 484 | 	actions: Array<MenuAction>; | |
| 485 | 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) | |
| 486 | 	title?: string; | |
| 487 | diff --git a/src/types.ts b/src/types.ts | |
| 488 | index 9c323d9c510e7359c83d4d6633affde673570632..5d5fdfb8b8235fcaa8295b85630443eaf5991a17 100644 | |
| 489 | --- a/src/types.ts | |
| 490 | +++ b/src/types.ts | |
| 491 | @@ -123,6 +123,12 @@ type MenuComponentPropsBase = { | |
| 492 | 	 * Callback function that will be called when the menu opens. | |
| 493 | 	 */ | |
| 494 | 	onOpenMenu?: () => void; | |
| 495 | +	/** | |
| 496 | +	 * Callback function that will be called when UIKit starts preparing a menu interaction. | |
| 497 | +	 * Preparation can end without the menu opening. | |
| 498 | +	 * @platform iOS | |
| 499 | +	 */ | |
| 500 | +	onMenuInteractionStart?: () => void; | |
| 501 | 	/** | |
| 502 | 	 * Actions to be displayed in the menu. | |
| 503 | 	 */ | |
| 504 | @@ -189,6 +195,7 @@ export type NativeMenuComponentProps = { | |
| 505 | 	onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; | |
| 506 | 	onCloseMenu?: () => void; | |
| 507 | 	onOpenMenu?: () => void; | |
| 508 | +	onMenuInteractionStart?: () => void; | |
| 509 | 	actions: ProcessedMenuAction[]; | |
| 510 | 	actionsHash: string; | |
| 511 | 	title?: string; |
patches/uniwind@1.11.0.patch created+117| ... | ... | @@ -0,0 +1,117 @@ |
| 1 | diff --git a/dist/metro/transformer.cjs b/dist/metro/transformer.cjs | |
| 2 | index d57ca81..fd8693c 100644 | |
| 3 | --- a/dist/metro/transformer.cjs | |
| 4 | +++ b/dist/metro/transformer.cjs | |
| 5 | @@ -7,6 +7,7 @@ const culori = require('culori'); | |
| 6 | const node = require('@tailwindcss/node'); | |
| 7 | const oxide = require('@tailwindcss/oxide'); | |
| 8 | const fs = require('fs'); | |
| 9 | +const node_crypto = require('node:crypto'); | |
| 10 | ||
| 11 | function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } | |
| 12 | ||
| 13 | @@ -1753,10 +1754,11 @@ const transform = async (config$1, projectRoot, filePath, data, options) => { | |
| 14 | await bundlerConfig.generateArtifacts(cssArtifactPath); | |
| 15 | const virtualCode = await compileCSS(bundlerConfig); | |
| 16 | const isWeb = bundlerConfig.platform === config.Platform.Web; | |
| 17 | + const nativeStylesFingerprint = isWeb ? void 0 : node_crypto.createHash("sha256").update(virtualCode).update("\0").update(bundlerConfig.stringifiedThemes).digest("hex"); | |
| 18 | data = Buffer.from( | |
| 19 | isWeb ? virtualCode : [ | |
| 20 | `const { Uniwind } = require('uniwind');`, | |
| 21 | - `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});` | |
| 22 | + `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');` | |
| 23 | ].join(""), | |
| 24 | "utf-8" | |
| 25 | ); | |
| 26 | diff --git a/dist/metro/transformer.mjs b/dist/metro/transformer.mjs | |
| 27 | index d3e7475..bf63587 100644 | |
| 28 | --- a/dist/metro/transformer.mjs | |
| 29 | +++ b/dist/metro/transformer.mjs | |
| 30 | @@ -5,6 +5,7 @@ import { converter, parse, formatHex, formatHex8 } from 'culori'; | |
| 31 | import { compile } from '@tailwindcss/node'; | |
| 32 | import { Scanner } from '@tailwindcss/oxide'; | |
| 33 | import fs from 'fs'; | |
| 34 | +import { createHash } from 'node:crypto'; | |
| 35 | ||
| 36 | const toCamelCase = (str) => str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); | |
| 37 | const pipe = (data) => ((...fns) => fns.reduce((acc, fn) => fn(acc), data)); | |
| 38 | @@ -1746,10 +1747,11 @@ const transform = async (config, projectRoot, filePath, data, options) => { | |
| 39 | await bundlerConfig.generateArtifacts(cssArtifactPath); | |
| 40 | const virtualCode = await compileCSS(bundlerConfig); | |
| 41 | const isWeb = bundlerConfig.platform === Platform.Web; | |
| 42 | + const nativeStylesFingerprint = isWeb ? void 0 : createHash("sha256").update(virtualCode).update("\0").update(bundlerConfig.stringifiedThemes).digest("hex"); | |
| 43 | data = Buffer.from( | |
| 44 | isWeb ? virtualCode : [ | |
| 45 | `const { Uniwind } = require('uniwind');`, | |
| 46 | - `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});` | |
| 47 | + `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');` | |
| 48 | ].join(""), | |
| 49 | "utf-8" | |
| 50 | ); | |
| 51 | diff --git a/src/bundler/adapters/metro/transformer.ts b/src/bundler/adapters/metro/transformer.ts | |
| 52 | index 08059b0..05c0515 100644 | |
| 53 | --- a/src/bundler/adapters/metro/transformer.ts | |
| 54 | +++ b/src/bundler/adapters/metro/transformer.ts | |
| 55 | @@ -5,6 +5,7 @@ import { Platform } from '@/common/consts' | |
| 56 | import type * as ExpoMetroConfig from '@expo/metro-config' | |
| 57 | import type * as MetroTransformWorker from 'metro-transform-worker' | |
| 58 | import type { JsTransformerConfig, JsTransformOptions } from 'metro-transform-worker' | |
| 59 | +import { createHash } from 'node:crypto' | |
| 60 | import path from 'path' | |
| 61 | ||
| 62 | const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css') | |
| 63 | @@ -69,13 +70,20 @@ export const transform = async ( | |
| 64 | await bundlerConfig.generateArtifacts(cssArtifactPath) | |
| 65 | const virtualCode = await compileCSS(bundlerConfig) | |
| 66 | const isWeb = bundlerConfig.platform === Platform.Web | |
| 67 | + const nativeStylesFingerprint = isWeb | |
| 68 | + ? undefined | |
| 69 | + : createHash('sha256') | |
| 70 | + .update(virtualCode) | |
| 71 | + .update('\0') | |
| 72 | + .update(bundlerConfig.stringifiedThemes) | |
| 73 | + .digest('hex') | |
| 74 | ||
| 75 | data = Buffer.from( | |
| 76 | isWeb | |
| 77 | ? virtualCode | |
| 78 | : [ | |
| 79 | `const { Uniwind } = require('uniwind');`, | |
| 80 | - `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});`, | |
| 81 | + `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');`, | |
| 82 | ].join(''), | |
| 83 | 'utf-8', | |
| 84 | ) | |
| 85 | diff --git a/src/core/config/config.native.ts b/src/core/config/config.native.ts | |
| 86 | index 5e0832b..01aed1b 100644 | |
| 87 | --- a/src/core/config/config.native.ts | |
| 88 | +++ b/src/core/config/config.native.ts | |
| 89 | @@ -8,6 +8,8 @@ import type { CSSVariables, GenerateStyleSheetsCallback, ThemeName } from '../ty | |
| 90 | import { UniwindConfigBuilder as UniwindConfigBuilderBase } from './config.common' | |
| 91 | ||
| 92 | class UniwindConfigBuilder extends UniwindConfigBuilderBase { | |
| 93 | + private stylesFingerprint: string | undefined | |
| 94 | + | |
| 95 | constructor() { | |
| 96 | super() | |
| 97 | } | |
| 98 | @@ -35,9 +37,18 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase { | |
| 99 | UniwindListener.notify([StyleDependency.Insets]) | |
| 100 | } | |
| 101 | ||
| 102 | - protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array<string>) { | |
| 103 | + protected __reinit( | |
| 104 | + generateStyleSheetCallback: GenerateStyleSheetsCallback, | |
| 105 | + themes: Array<string>, | |
| 106 | + stylesFingerprint?: string, | |
| 107 | + ) { | |
| 108 | + if (__DEV__ && stylesFingerprint !== undefined && stylesFingerprint === this.stylesFingerprint) { | |
| 109 | + return | |
| 110 | + } | |
| 111 | + | |
| 112 | super.__reinit(generateStyleSheetCallback, themes) | |
| 113 | UniwindStore.reinit(generateStyleSheetCallback, themes) | |
| 114 | + this.stylesFingerprint = stylesFingerprint | |
| 115 | } | |
| 116 | ||
| 117 | protected onThemeChange() { |
pnpm-lock.yaml+124-222| ... | ... | @@ -88,13 +88,14 @@ patchedDependencies: |
| 88 | 88 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 |
| 89 | 89 | '@legendapp/list@3.3.5': 064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706 |
| 90 | 90 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa |
| 91 | '@react-native-menu/menu@2.0.0': c7f66d121c726ade4f5c4e1aed11a691e5711d244c544084e289ac26132a0045 | |
| 91 | '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd | |
| 92 | 92 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 |
| 93 | 93 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 |
| 94 | 94 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 |
| 95 | 95 | react-native-keyboard-controller@1.21.13: 20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008 |
| 96 | 96 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 |
| 97 | 97 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 |
| 98 | uniwind@1.11.0: 329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196 | |
| 98 | 99 | |
| 99 | 100 | importers: |
| 100 | 101 | |
| ... | ... | @@ -239,7 +240,7 @@ importers: |
| 239 | 240 | version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) |
| 240 | 241 | '@react-native-menu/menu': |
| 241 | 242 | specifier: ^2.0.0 |
| 242 | version: 2.0.0(patch_hash=c7f66d121c726ade4f5c4e1aed11a691e5711d244c544084e289ac26132a0045)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | |
| 243 | version: 2.0.0(patch_hash=f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | |
| 243 | 244 | '@react-navigation/elements': |
| 244 | 245 | specifier: 2.9.26 |
| 245 | 246 | version: 2.9.26(c10301b6e0c42fc6434d2b643197a81e) |
| ... | ... | @@ -436,8 +437,8 @@ importers: |
| 436 | 437 | specifier: ^3.5.0 |
| 437 | 438 | version: 3.6.0 |
| 438 | 439 | uniwind: |
| 439 | specifier: ^1.6.2 | |
| 440 | version: 1.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) | |
| 440 | specifier: 1.11.0 | |
| 441 | version: 1.11.0(patch_hash=329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196)(@expo/metro-config@57.0.12(patch_hash=96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2)(bufferutil@4.1.0)(expo@57.0.18)(typescript@6.0.3)(utf-8-validate@6.0.6))(metro-cache@0.84.5)(metro-transform-worker@0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) | |
| 441 | 442 | devDependencies: |
| 442 | 443 | '@effect/vitest': |
| 443 | 444 | specifier: 4.0.0-beta.103 |
| ... | ... | @@ -4412,17 +4413,11 @@ packages: |
| 4412 | 4413 | '@tabler/icons@3.44.0': |
| 4413 | 4414 | resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} |
| 4414 | 4415 | |
| 4415 | '@tailwindcss/node@4.2.1': | |
| 4416 | resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} | |
| 4417 | ||
| 4418 | 4416 | '@tailwindcss/node@4.3.0': |
| 4419 | 4417 | resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} |
| 4420 | 4418 | |
| 4421 | '@tailwindcss/oxide-android-arm64@4.2.1': | |
| 4422 | resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} | |
| 4423 | engines: {node: '>= 20'} | |
| 4424 | cpu: [arm64] | |
| 4425 | os: [android] | |
| 4419 | '@tailwindcss/node@4.3.2': | |
| 4420 | resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} | |
| 4426 | 4421 | |
| 4427 | 4422 | '@tailwindcss/oxide-android-arm64@4.3.0': |
| 4428 | 4423 | resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} |
| ... | ... | @@ -4430,11 +4425,11 @@ packages: |
| 4430 | 4425 | cpu: [arm64] |
| 4431 | 4426 | os: [android] |
| 4432 | 4427 | |
| 4433 | '@tailwindcss/oxide-darwin-arm64@4.2.1': | |
| 4434 | resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} | |
| 4428 | '@tailwindcss/oxide-android-arm64@4.3.2': | |
| 4429 | resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} | |
| 4435 | 4430 | engines: {node: '>= 20'} |
| 4436 | 4431 | cpu: [arm64] |
| 4437 | os: [darwin] | |
| 4432 | os: [android] | |
| 4438 | 4433 | |
| 4439 | 4434 | '@tailwindcss/oxide-darwin-arm64@4.3.0': |
| 4440 | 4435 | resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} |
| ... | ... | @@ -4442,10 +4437,10 @@ packages: |
| 4442 | 4437 | cpu: [arm64] |
| 4443 | 4438 | os: [darwin] |
| 4444 | 4439 | |
| 4445 | '@tailwindcss/oxide-darwin-x64@4.2.1': | |
| 4446 | resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} | |
| 4440 | '@tailwindcss/oxide-darwin-arm64@4.3.2': | |
| 4441 | resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} | |
| 4447 | 4442 | engines: {node: '>= 20'} |
| 4448 | cpu: [x64] | |
| 4443 | cpu: [arm64] | |
| 4449 | 4444 | os: [darwin] |
| 4450 | 4445 | |
| 4451 | 4446 | '@tailwindcss/oxide-darwin-x64@4.3.0': |
| ... | ... | @@ -4454,11 +4449,11 @@ packages: |
| 4454 | 4449 | cpu: [x64] |
| 4455 | 4450 | os: [darwin] |
| 4456 | 4451 | |
| 4457 | '@tailwindcss/oxide-freebsd-x64@4.2.1': | |
| 4458 | resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} | |
| 4452 | '@tailwindcss/oxide-darwin-x64@4.3.2': | |
| 4453 | resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} | |
| 4459 | 4454 | engines: {node: '>= 20'} |
| 4460 | 4455 | cpu: [x64] |
| 4461 | os: [freebsd] | |
| 4456 | os: [darwin] | |
| 4462 | 4457 | |
| 4463 | 4458 | '@tailwindcss/oxide-freebsd-x64@4.3.0': |
| 4464 | 4459 | resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} |
| ... | ... | @@ -4466,11 +4461,11 @@ packages: |
| 4466 | 4461 | cpu: [x64] |
| 4467 | 4462 | os: [freebsd] |
| 4468 | 4463 | |
| 4469 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': | |
| 4470 | resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} | |
| 4464 | '@tailwindcss/oxide-freebsd-x64@4.3.2': | |
| 4465 | resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} | |
| 4471 | 4466 | engines: {node: '>= 20'} |
| 4472 | cpu: [arm] | |
| 4473 | os: [linux] | |
| 4467 | cpu: [x64] | |
| 4468 | os: [freebsd] | |
| 4474 | 4469 | |
| 4475 | 4470 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': |
| 4476 | 4471 | resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} |
| ... | ... | @@ -4478,12 +4473,11 @@ packages: |
| 4478 | 4473 | cpu: [arm] |
| 4479 | 4474 | os: [linux] |
| 4480 | 4475 | |
| 4481 | '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': | |
| 4482 | resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} | |
| 4476 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': | |
| 4477 | resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} | |
| 4483 | 4478 | engines: {node: '>= 20'} |
| 4484 | cpu: [arm64] | |
| 4479 | cpu: [arm] | |
| 4485 | 4480 | os: [linux] |
| 4486 | libc: [glibc] | |
| 4487 | 4481 | |
| 4488 | 4482 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': |
| 4489 | 4483 | resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} |
| ... | ... | @@ -4492,12 +4486,12 @@ packages: |
| 4492 | 4486 | os: [linux] |
| 4493 | 4487 | libc: [glibc] |
| 4494 | 4488 | |
| 4495 | '@tailwindcss/oxide-linux-arm64-musl@4.2.1': | |
| 4496 | resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} | |
| 4489 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': | |
| 4490 | resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} | |
| 4497 | 4491 | engines: {node: '>= 20'} |
| 4498 | 4492 | cpu: [arm64] |
| 4499 | 4493 | os: [linux] |
| 4500 | libc: [musl] | |
| 4494 | libc: [glibc] | |
| 4501 | 4495 | |
| 4502 | 4496 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': |
| 4503 | 4497 | resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} |
| ... | ... | @@ -4506,12 +4500,12 @@ packages: |
| 4506 | 4500 | os: [linux] |
| 4507 | 4501 | libc: [musl] |
| 4508 | 4502 | |
| 4509 | '@tailwindcss/oxide-linux-x64-gnu@4.2.1': | |
| 4510 | resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} | |
| 4503 | '@tailwindcss/oxide-linux-arm64-musl@4.3.2': | |
| 4504 | resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} | |
| 4511 | 4505 | engines: {node: '>= 20'} |
| 4512 | cpu: [x64] | |
| 4506 | cpu: [arm64] | |
| 4513 | 4507 | os: [linux] |
| 4514 | libc: [glibc] | |
| 4508 | libc: [musl] | |
| 4515 | 4509 | |
| 4516 | 4510 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': |
| 4517 | 4511 | resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} |
| ... | ... | @@ -4520,12 +4514,12 @@ packages: |
| 4520 | 4514 | os: [linux] |
| 4521 | 4515 | libc: [glibc] |
| 4522 | 4516 | |
| 4523 | '@tailwindcss/oxide-linux-x64-musl@4.2.1': | |
| 4524 | resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} | |
| 4517 | '@tailwindcss/oxide-linux-x64-gnu@4.3.2': | |
| 4518 | resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} | |
| 4525 | 4519 | engines: {node: '>= 20'} |
| 4526 | 4520 | cpu: [x64] |
| 4527 | 4521 | os: [linux] |
| 4528 | libc: [musl] | |
| 4522 | libc: [glibc] | |
| 4529 | 4523 | |
| 4530 | 4524 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': |
| 4531 | 4525 | resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} |
| ... | ... | @@ -4534,8 +4528,15 @@ packages: |
| 4534 | 4528 | os: [linux] |
| 4535 | 4529 | libc: [musl] |
| 4536 | 4530 | |
| 4537 | '@tailwindcss/oxide-wasm32-wasi@4.2.1': | |
| 4538 | resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} | |
| 4531 | '@tailwindcss/oxide-linux-x64-musl@4.3.2': | |
| 4532 | resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} | |
| 4533 | engines: {node: '>= 20'} | |
| 4534 | cpu: [x64] | |
| 4535 | os: [linux] | |
| 4536 | libc: [musl] | |
| 4537 | ||
| 4538 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': | |
| 4539 | resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} | |
| 4539 | 4540 | engines: {node: '>=14.0.0'} |
| 4540 | 4541 | cpu: [wasm32] |
| 4541 | 4542 | bundledDependencies: |
| ... | ... | @@ -4546,8 +4547,8 @@ packages: |
| 4546 | 4547 | - '@emnapi/wasi-threads' |
| 4547 | 4548 | - tslib |
| 4548 | 4549 | |
| 4549 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': | |
| 4550 | resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} | |
| 4550 | '@tailwindcss/oxide-wasm32-wasi@4.3.2': | |
| 4551 | resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} | |
| 4551 | 4552 | engines: {node: '>=14.0.0'} |
| 4552 | 4553 | cpu: [wasm32] |
| 4553 | 4554 | bundledDependencies: |
| ... | ... | @@ -4558,22 +4559,16 @@ packages: |
| 4558 | 4559 | - '@emnapi/wasi-threads' |
| 4559 | 4560 | - tslib |
| 4560 | 4561 | |
| 4561 | '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': | |
| 4562 | resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} | |
| 4563 | engines: {node: '>= 20'} | |
| 4564 | cpu: [arm64] | |
| 4565 | os: [win32] | |
| 4566 | ||
| 4567 | 4562 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': |
| 4568 | 4563 | resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} |
| 4569 | 4564 | engines: {node: '>= 20'} |
| 4570 | 4565 | cpu: [arm64] |
| 4571 | 4566 | os: [win32] |
| 4572 | 4567 | |
| 4573 | '@tailwindcss/oxide-win32-x64-msvc@4.2.1': | |
| 4574 | resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} | |
| 4568 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': | |
| 4569 | resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} | |
| 4575 | 4570 | engines: {node: '>= 20'} |
| 4576 | cpu: [x64] | |
| 4571 | cpu: [arm64] | |
| 4577 | 4572 | os: [win32] |
| 4578 | 4573 | |
| 4579 | 4574 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': |
| ... | ... | @@ -4582,14 +4577,20 @@ packages: |
| 4582 | 4577 | cpu: [x64] |
| 4583 | 4578 | os: [win32] |
| 4584 | 4579 | |
| 4585 | '@tailwindcss/oxide@4.2.1': | |
| 4586 | resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} | |
| 4580 | '@tailwindcss/oxide-win32-x64-msvc@4.3.2': | |
| 4581 | resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} | |
| 4587 | 4582 | engines: {node: '>= 20'} |
| 4583 | cpu: [x64] | |
| 4584 | os: [win32] | |
| 4588 | 4585 | |
| 4589 | 4586 | '@tailwindcss/oxide@4.3.0': |
| 4590 | 4587 | resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} |
| 4591 | 4588 | engines: {node: '>= 20'} |
| 4592 | 4589 | |
| 4590 | '@tailwindcss/oxide@4.3.2': | |
| 4591 | resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} | |
| 4592 | engines: {node: '>= 20'} | |
| 4593 | ||
| 4593 | 4594 | '@tailwindcss/vite@4.3.0': |
| 4594 | 4595 | resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} |
| 4595 | 4596 | peerDependencies: |
| ... | ... | @@ -6309,6 +6310,10 @@ packages: |
| 6309 | 6310 | end-of-stream@1.4.5: |
| 6310 | 6311 | resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} |
| 6311 | 6312 | |
| 6313 | enhanced-resolve@5.21.6: | |
| 6314 | resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} | |
| 6315 | engines: {node: '>=10.13.0'} | |
| 6316 | ||
| 6312 | 6317 | enhanced-resolve@5.22.1: |
| 6313 | 6318 | resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} |
| 6314 | 6319 | engines: {node: '>=10.13.0'} |
| ... | ... | @@ -7480,12 +7485,6 @@ packages: |
| 7480 | 7485 | lighthouse-logger@1.4.2: |
| 7481 | 7486 | resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} |
| 7482 | 7487 | |
| 7483 | lightningcss-android-arm64@1.31.1: | |
| 7484 | resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} | |
| 7485 | engines: {node: '>= 12.0.0'} | |
| 7486 | cpu: [arm64] | |
| 7487 | os: [android] | |
| 7488 | ||
| 7489 | 7488 | lightningcss-android-arm64@1.32.0: |
| 7490 | 7489 | resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} |
| 7491 | 7490 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7498,12 +7497,6 @@ packages: |
| 7498 | 7497 | cpu: [arm64] |
| 7499 | 7498 | os: [darwin] |
| 7500 | 7499 | |
| 7501 | lightningcss-darwin-arm64@1.31.1: | |
| 7502 | resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} | |
| 7503 | engines: {node: '>= 12.0.0'} | |
| 7504 | cpu: [arm64] | |
| 7505 | os: [darwin] | |
| 7506 | ||
| 7507 | 7500 | lightningcss-darwin-arm64@1.32.0: |
| 7508 | 7501 | resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} |
| 7509 | 7502 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7516,12 +7509,6 @@ packages: |
| 7516 | 7509 | cpu: [x64] |
| 7517 | 7510 | os: [darwin] |
| 7518 | 7511 | |
| 7519 | lightningcss-darwin-x64@1.31.1: | |
| 7520 | resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} | |
| 7521 | engines: {node: '>= 12.0.0'} | |
| 7522 | cpu: [x64] | |
| 7523 | os: [darwin] | |
| 7524 | ||
| 7525 | 7512 | lightningcss-darwin-x64@1.32.0: |
| 7526 | 7513 | resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} |
| 7527 | 7514 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7534,12 +7521,6 @@ packages: |
| 7534 | 7521 | cpu: [x64] |
| 7535 | 7522 | os: [freebsd] |
| 7536 | 7523 | |
| 7537 | lightningcss-freebsd-x64@1.31.1: | |
| 7538 | resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} | |
| 7539 | engines: {node: '>= 12.0.0'} | |
| 7540 | cpu: [x64] | |
| 7541 | os: [freebsd] | |
| 7542 | ||
| 7543 | 7524 | lightningcss-freebsd-x64@1.32.0: |
| 7544 | 7525 | resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} |
| 7545 | 7526 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7552,12 +7533,6 @@ packages: |
| 7552 | 7533 | cpu: [arm] |
| 7553 | 7534 | os: [linux] |
| 7554 | 7535 | |
| 7555 | lightningcss-linux-arm-gnueabihf@1.31.1: | |
| 7556 | resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} | |
| 7557 | engines: {node: '>= 12.0.0'} | |
| 7558 | cpu: [arm] | |
| 7559 | os: [linux] | |
| 7560 | ||
| 7561 | 7536 | lightningcss-linux-arm-gnueabihf@1.32.0: |
| 7562 | 7537 | resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} |
| 7563 | 7538 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7571,13 +7546,6 @@ packages: |
| 7571 | 7546 | os: [linux] |
| 7572 | 7547 | libc: [glibc] |
| 7573 | 7548 | |
| 7574 | lightningcss-linux-arm64-gnu@1.31.1: | |
| 7575 | resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} | |
| 7576 | engines: {node: '>= 12.0.0'} | |
| 7577 | cpu: [arm64] | |
| 7578 | os: [linux] | |
| 7579 | libc: [glibc] | |
| 7580 | ||
| 7581 | 7549 | lightningcss-linux-arm64-gnu@1.32.0: |
| 7582 | 7550 | resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} |
| 7583 | 7551 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7592,13 +7560,6 @@ packages: |
| 7592 | 7560 | os: [linux] |
| 7593 | 7561 | libc: [musl] |
| 7594 | 7562 | |
| 7595 | lightningcss-linux-arm64-musl@1.31.1: | |
| 7596 | resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} | |
| 7597 | engines: {node: '>= 12.0.0'} | |
| 7598 | cpu: [arm64] | |
| 7599 | os: [linux] | |
| 7600 | libc: [musl] | |
| 7601 | ||
| 7602 | 7563 | lightningcss-linux-arm64-musl@1.32.0: |
| 7603 | 7564 | resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} |
| 7604 | 7565 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7613,13 +7574,6 @@ packages: |
| 7613 | 7574 | os: [linux] |
| 7614 | 7575 | libc: [glibc] |
| 7615 | 7576 | |
| 7616 | lightningcss-linux-x64-gnu@1.31.1: | |
| 7617 | resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} | |
| 7618 | engines: {node: '>= 12.0.0'} | |
| 7619 | cpu: [x64] | |
| 7620 | os: [linux] | |
| 7621 | libc: [glibc] | |
| 7622 | ||
| 7623 | 7577 | lightningcss-linux-x64-gnu@1.32.0: |
| 7624 | 7578 | resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} |
| 7625 | 7579 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7634,13 +7588,6 @@ packages: |
| 7634 | 7588 | os: [linux] |
| 7635 | 7589 | libc: [musl] |
| 7636 | 7590 | |
| 7637 | lightningcss-linux-x64-musl@1.31.1: | |
| 7638 | resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} | |
| 7639 | engines: {node: '>= 12.0.0'} | |
| 7640 | cpu: [x64] | |
| 7641 | os: [linux] | |
| 7642 | libc: [musl] | |
| 7643 | ||
| 7644 | 7591 | lightningcss-linux-x64-musl@1.32.0: |
| 7645 | 7592 | resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} |
| 7646 | 7593 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7654,12 +7601,6 @@ packages: |
| 7654 | 7601 | cpu: [arm64] |
| 7655 | 7602 | os: [win32] |
| 7656 | 7603 | |
| 7657 | lightningcss-win32-arm64-msvc@1.31.1: | |
| 7658 | resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} | |
| 7659 | engines: {node: '>= 12.0.0'} | |
| 7660 | cpu: [arm64] | |
| 7661 | os: [win32] | |
| 7662 | ||
| 7663 | 7604 | lightningcss-win32-arm64-msvc@1.32.0: |
| 7664 | 7605 | resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} |
| 7665 | 7606 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7672,12 +7613,6 @@ packages: |
| 7672 | 7613 | cpu: [x64] |
| 7673 | 7614 | os: [win32] |
| 7674 | 7615 | |
| 7675 | lightningcss-win32-x64-msvc@1.31.1: | |
| 7676 | resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} | |
| 7677 | engines: {node: '>= 12.0.0'} | |
| 7678 | cpu: [x64] | |
| 7679 | os: [win32] | |
| 7680 | ||
| 7681 | 7616 | lightningcss-win32-x64-msvc@1.32.0: |
| 7682 | 7617 | resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} |
| 7683 | 7618 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -7688,10 +7623,6 @@ packages: |
| 7688 | 7623 | resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} |
| 7689 | 7624 | engines: {node: '>= 12.0.0'} |
| 7690 | 7625 | |
| 7691 | lightningcss@1.31.1: | |
| 7692 | resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} | |
| 7693 | engines: {node: '>= 12.0.0'} | |
| 7694 | ||
| 7695 | 7626 | lightningcss@1.32.0: |
| 7696 | 7627 | resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} |
| 7697 | 7628 | engines: {node: '>= 12.0.0'} |
| ... | ... | @@ -9620,12 +9551,12 @@ packages: |
| 9620 | 9551 | tailwind-merge@3.6.0: |
| 9621 | 9552 | resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} |
| 9622 | 9553 | |
| 9623 | tailwindcss@4.2.1: | |
| 9624 | resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} | |
| 9625 | ||
| 9626 | 9554 | tailwindcss@4.3.0: |
| 9627 | 9555 | resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} |
| 9628 | 9556 | |
| 9557 | tailwindcss@4.3.2: | |
| 9558 | resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} | |
| 9559 | ||
| 9629 | 9560 | tapable@2.3.3: |
| 9630 | 9561 | resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} |
| 9631 | 9562 | engines: {node: '>=6'} |
| ... | ... | @@ -9881,12 +9812,22 @@ packages: |
| 9881 | 9812 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} |
| 9882 | 9813 | engines: {node: '>= 10.0.0'} |
| 9883 | 9814 | |
| 9884 | uniwind@1.7.0: | |
| 9885 | resolution: {integrity: sha512-kIixWI3OprrWf/ypjglV2xubYvzCjtTKk110rQmXn8A3ZA/7z/nK6P/8EMxbfPCxjkdvhLcMniafa4qJcYIhZA==} | |
| 9815 | uniwind@1.11.0: | |
| 9816 | resolution: {integrity: sha512-e2mYlHrZzAQtZUf3STrfVDTetspv7p5vm/cu6p8kjLRQaL9r6QFG+MlwZ3hrI2LHnzgzjbSo+MwvoE380zz2JQ==} | |
| 9817 | hasBin: true | |
| 9886 | 9818 | peerDependencies: |
| 9819 | '@expo/metro-config': 57.0.12 | |
| 9820 | metro: '*' | |
| 9821 | metro-cache: '*' | |
| 9822 | metro-transform-worker: '*' | |
| 9887 | 9823 | react: '>=19.0.0' |
| 9888 | 9824 | react-native: '>=0.81.0' |
| 9889 | 9825 | tailwindcss: '>=4' |
| 9826 | peerDependenciesMeta: | |
| 9827 | '@expo/metro-config': | |
| 9828 | optional: true | |
| 9829 | metro-transform-worker: | |
| 9830 | optional: true | |
| 9890 | 9831 | |
| 9891 | 9832 | unpipe@1.0.0: |
| 9892 | 9833 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} |
| ... | ... | @@ -13702,7 +13643,7 @@ snapshots: |
| 13702 | 13643 | react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| 13703 | 13644 | optional: true |
| 13704 | 13645 | |
| 13705 | '@react-native-menu/menu@2.0.0(patch_hash=c7f66d121c726ade4f5c4e1aed11a691e5711d244c544084e289ac26132a0045)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': | |
| 13646 | '@react-native-menu/menu@2.0.0(patch_hash=f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': | |
| 13706 | 13647 | dependencies: |
| 13707 | 13648 | react: 19.2.3 |
| 13708 | 13649 | react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| ... | ... | @@ -14263,112 +14204,97 @@ snapshots: |
| 14263 | 14204 | |
| 14264 | 14205 | '@tabler/icons@3.44.0': {} |
| 14265 | 14206 | |
| 14266 | '@tailwindcss/node@4.2.1': | |
| 14207 | '@tailwindcss/node@4.3.0': | |
| 14267 | 14208 | dependencies: |
| 14268 | 14209 | '@jridgewell/remapping': 2.3.5 |
| 14269 | 14210 | enhanced-resolve: 5.22.1 |
| 14270 | 14211 | jiti: 2.7.0 |
| 14271 | lightningcss: 1.31.1 | |
| 14212 | lightningcss: 1.32.0 | |
| 14272 | 14213 | magic-string: 0.30.21 |
| 14273 | 14214 | source-map-js: 1.2.1 |
| 14274 | tailwindcss: 4.2.1 | |
| 14215 | tailwindcss: 4.3.0 | |
| 14275 | 14216 | |
| 14276 | '@tailwindcss/node@4.3.0': | |
| 14217 | '@tailwindcss/node@4.3.2': | |
| 14277 | 14218 | dependencies: |
| 14278 | 14219 | '@jridgewell/remapping': 2.3.5 |
| 14279 | enhanced-resolve: 5.22.1 | |
| 14220 | enhanced-resolve: 5.21.6 | |
| 14280 | 14221 | jiti: 2.7.0 |
| 14281 | 14222 | lightningcss: 1.32.0 |
| 14282 | 14223 | magic-string: 0.30.21 |
| 14283 | 14224 | source-map-js: 1.2.1 |
| 14284 | tailwindcss: 4.3.0 | |
| 14285 | ||
| 14286 | '@tailwindcss/oxide-android-arm64@4.2.1': | |
| 14287 | optional: true | |
| 14225 | tailwindcss: 4.3.2 | |
| 14288 | 14226 | |
| 14289 | 14227 | '@tailwindcss/oxide-android-arm64@4.3.0': |
| 14290 | 14228 | optional: true |
| 14291 | 14229 | |
| 14292 | '@tailwindcss/oxide-darwin-arm64@4.2.1': | |
| 14230 | '@tailwindcss/oxide-android-arm64@4.3.2': | |
| 14293 | 14231 | optional: true |
| 14294 | 14232 | |
| 14295 | 14233 | '@tailwindcss/oxide-darwin-arm64@4.3.0': |
| 14296 | 14234 | optional: true |
| 14297 | 14235 | |
| 14298 | '@tailwindcss/oxide-darwin-x64@4.2.1': | |
| 14236 | '@tailwindcss/oxide-darwin-arm64@4.3.2': | |
| 14299 | 14237 | optional: true |
| 14300 | 14238 | |
| 14301 | 14239 | '@tailwindcss/oxide-darwin-x64@4.3.0': |
| 14302 | 14240 | optional: true |
| 14303 | 14241 | |
| 14304 | '@tailwindcss/oxide-freebsd-x64@4.2.1': | |
| 14242 | '@tailwindcss/oxide-darwin-x64@4.3.2': | |
| 14305 | 14243 | optional: true |
| 14306 | 14244 | |
| 14307 | 14245 | '@tailwindcss/oxide-freebsd-x64@4.3.0': |
| 14308 | 14246 | optional: true |
| 14309 | 14247 | |
| 14310 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': | |
| 14248 | '@tailwindcss/oxide-freebsd-x64@4.3.2': | |
| 14311 | 14249 | optional: true |
| 14312 | 14250 | |
| 14313 | 14251 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': |
| 14314 | 14252 | optional: true |
| 14315 | 14253 | |
| 14316 | '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': | |
| 14254 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': | |
| 14317 | 14255 | optional: true |
| 14318 | 14256 | |
| 14319 | 14257 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': |
| 14320 | 14258 | optional: true |
| 14321 | 14259 | |
| 14322 | '@tailwindcss/oxide-linux-arm64-musl@4.2.1': | |
| 14260 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': | |
| 14323 | 14261 | optional: true |
| 14324 | 14262 | |
| 14325 | 14263 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': |
| 14326 | 14264 | optional: true |
| 14327 | 14265 | |
| 14328 | '@tailwindcss/oxide-linux-x64-gnu@4.2.1': | |
| 14266 | '@tailwindcss/oxide-linux-arm64-musl@4.3.2': | |
| 14329 | 14267 | optional: true |
| 14330 | 14268 | |
| 14331 | 14269 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': |
| 14332 | 14270 | optional: true |
| 14333 | 14271 | |
| 14334 | '@tailwindcss/oxide-linux-x64-musl@4.2.1': | |
| 14272 | '@tailwindcss/oxide-linux-x64-gnu@4.3.2': | |
| 14335 | 14273 | optional: true |
| 14336 | 14274 | |
| 14337 | 14275 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': |
| 14338 | 14276 | optional: true |
| 14339 | 14277 | |
| 14340 | '@tailwindcss/oxide-wasm32-wasi@4.2.1': | |
| 14278 | '@tailwindcss/oxide-linux-x64-musl@4.3.2': | |
| 14341 | 14279 | optional: true |
| 14342 | 14280 | |
| 14343 | 14281 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': |
| 14344 | 14282 | optional: true |
| 14345 | 14283 | |
| 14346 | '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': | |
| 14284 | '@tailwindcss/oxide-wasm32-wasi@4.3.2': | |
| 14347 | 14285 | optional: true |
| 14348 | 14286 | |
| 14349 | 14287 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': |
| 14350 | 14288 | optional: true |
| 14351 | 14289 | |
| 14352 | '@tailwindcss/oxide-win32-x64-msvc@4.2.1': | |
| 14290 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': | |
| 14353 | 14291 | optional: true |
| 14354 | 14292 | |
| 14355 | 14293 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': |
| 14356 | 14294 | optional: true |
| 14357 | 14295 | |
| 14358 | '@tailwindcss/oxide@4.2.1': | |
| 14359 | optionalDependencies: | |
| 14360 | '@tailwindcss/oxide-android-arm64': 4.2.1 | |
| 14361 | '@tailwindcss/oxide-darwin-arm64': 4.2.1 | |
| 14362 | '@tailwindcss/oxide-darwin-x64': 4.2.1 | |
| 14363 | '@tailwindcss/oxide-freebsd-x64': 4.2.1 | |
| 14364 | '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 | |
| 14365 | '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 | |
| 14366 | '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 | |
| 14367 | '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 | |
| 14368 | '@tailwindcss/oxide-linux-x64-musl': 4.2.1 | |
| 14369 | '@tailwindcss/oxide-wasm32-wasi': 4.2.1 | |
| 14370 | '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 | |
| 14371 | '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 | |
| 14296 | '@tailwindcss/oxide-win32-x64-msvc@4.3.2': | |
| 14297 | optional: true | |
| 14372 | 14298 | |
| 14373 | 14299 | '@tailwindcss/oxide@4.3.0': |
| 14374 | 14300 | optionalDependencies: |
| ... | ... | @@ -14385,6 +14311,21 @@ snapshots: |
| 14385 | 14311 | '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 |
| 14386 | 14312 | '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 |
| 14387 | 14313 | |
| 14314 | '@tailwindcss/oxide@4.3.2': | |
| 14315 | optionalDependencies: | |
| 14316 | '@tailwindcss/oxide-android-arm64': 4.3.2 | |
| 14317 | '@tailwindcss/oxide-darwin-arm64': 4.3.2 | |
| 14318 | '@tailwindcss/oxide-darwin-x64': 4.3.2 | |
| 14319 | '@tailwindcss/oxide-freebsd-x64': 4.3.2 | |
| 14320 | '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 | |
| 14321 | '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 | |
| 14322 | '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 | |
| 14323 | '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 | |
| 14324 | '@tailwindcss/oxide-linux-x64-musl': 4.3.2 | |
| 14325 | '@tailwindcss/oxide-wasm32-wasi': 4.3.2 | |
| 14326 | '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 | |
| 14327 | '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 | |
| 14328 | ||
| 14388 | 14329 | '@tailwindcss/vite@4.3.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))': |
| 14389 | 14330 | dependencies: |
| 14390 | 14331 | '@tailwindcss/node': 4.3.0 |
| ... | ... | @@ -16174,6 +16115,11 @@ snapshots: |
| 16174 | 16115 | dependencies: |
| 16175 | 16116 | once: 1.4.0 |
| 16176 | 16117 | |
| 16118 | enhanced-resolve@5.21.6: | |
| 16119 | dependencies: | |
| 16120 | graceful-fs: 4.2.11 | |
| 16121 | tapable: 2.3.3 | |
| 16122 | ||
| 16177 | 16123 | enhanced-resolve@5.22.1: |
| 16178 | 16124 | dependencies: |
| 16179 | 16125 | graceful-fs: 4.2.11 |
| ... | ... | @@ -17676,99 +17622,66 @@ snapshots: |
| 17676 | 17622 | transitivePeerDependencies: |
| 17677 | 17623 | - supports-color |
| 17678 | 17624 | |
| 17679 | lightningcss-android-arm64@1.31.1: | |
| 17680 | optional: true | |
| 17681 | ||
| 17682 | 17625 | lightningcss-android-arm64@1.32.0: |
| 17683 | 17626 | optional: true |
| 17684 | 17627 | |
| 17685 | 17628 | lightningcss-darwin-arm64@1.30.1: |
| 17686 | 17629 | optional: true |
| 17687 | 17630 | |
| 17688 | lightningcss-darwin-arm64@1.31.1: | |
| 17689 | optional: true | |
| 17690 | ||
| 17691 | 17631 | lightningcss-darwin-arm64@1.32.0: |
| 17692 | 17632 | optional: true |
| 17693 | 17633 | |
| 17694 | 17634 | lightningcss-darwin-x64@1.30.1: |
| 17695 | 17635 | optional: true |
| 17696 | 17636 | |
| 17697 | lightningcss-darwin-x64@1.31.1: | |
| 17698 | optional: true | |
| 17699 | ||
| 17700 | 17637 | lightningcss-darwin-x64@1.32.0: |
| 17701 | 17638 | optional: true |
| 17702 | 17639 | |
| 17703 | 17640 | lightningcss-freebsd-x64@1.30.1: |
| 17704 | 17641 | optional: true |
| 17705 | 17642 | |
| 17706 | lightningcss-freebsd-x64@1.31.1: | |
| 17707 | optional: true | |
| 17708 | ||
| 17709 | 17643 | lightningcss-freebsd-x64@1.32.0: |
| 17710 | 17644 | optional: true |
| 17711 | 17645 | |
| 17712 | 17646 | lightningcss-linux-arm-gnueabihf@1.30.1: |
| 17713 | 17647 | optional: true |
| 17714 | 17648 | |
| 17715 | lightningcss-linux-arm-gnueabihf@1.31.1: | |
| 17716 | optional: true | |
| 17717 | ||
| 17718 | 17649 | lightningcss-linux-arm-gnueabihf@1.32.0: |
| 17719 | 17650 | optional: true |
| 17720 | 17651 | |
| 17721 | 17652 | lightningcss-linux-arm64-gnu@1.30.1: |
| 17722 | 17653 | optional: true |
| 17723 | 17654 | |
| 17724 | lightningcss-linux-arm64-gnu@1.31.1: | |
| 17725 | optional: true | |
| 17726 | ||
| 17727 | 17655 | lightningcss-linux-arm64-gnu@1.32.0: |
| 17728 | 17656 | optional: true |
| 17729 | 17657 | |
| 17730 | 17658 | lightningcss-linux-arm64-musl@1.30.1: |
| 17731 | 17659 | optional: true |
| 17732 | 17660 | |
| 17733 | lightningcss-linux-arm64-musl@1.31.1: | |
| 17734 | optional: true | |
| 17735 | ||
| 17736 | 17661 | lightningcss-linux-arm64-musl@1.32.0: |
| 17737 | 17662 | optional: true |
| 17738 | 17663 | |
| 17739 | 17664 | lightningcss-linux-x64-gnu@1.30.1: |
| 17740 | 17665 | optional: true |
| 17741 | 17666 | |
| 17742 | lightningcss-linux-x64-gnu@1.31.1: | |
| 17743 | optional: true | |
| 17744 | ||
| 17745 | 17667 | lightningcss-linux-x64-gnu@1.32.0: |
| 17746 | 17668 | optional: true |
| 17747 | 17669 | |
| 17748 | 17670 | lightningcss-linux-x64-musl@1.30.1: |
| 17749 | 17671 | optional: true |
| 17750 | 17672 | |
| 17751 | lightningcss-linux-x64-musl@1.31.1: | |
| 17752 | optional: true | |
| 17753 | ||
| 17754 | 17673 | lightningcss-linux-x64-musl@1.32.0: |
| 17755 | 17674 | optional: true |
| 17756 | 17675 | |
| 17757 | 17676 | lightningcss-win32-arm64-msvc@1.30.1: |
| 17758 | 17677 | optional: true |
| 17759 | 17678 | |
| 17760 | lightningcss-win32-arm64-msvc@1.31.1: | |
| 17761 | optional: true | |
| 17762 | ||
| 17763 | 17679 | lightningcss-win32-arm64-msvc@1.32.0: |
| 17764 | 17680 | optional: true |
| 17765 | 17681 | |
| 17766 | 17682 | lightningcss-win32-x64-msvc@1.30.1: |
| 17767 | 17683 | optional: true |
| 17768 | 17684 | |
| 17769 | lightningcss-win32-x64-msvc@1.31.1: | |
| 17770 | optional: true | |
| 17771 | ||
| 17772 | 17685 | lightningcss-win32-x64-msvc@1.32.0: |
| 17773 | 17686 | optional: true |
| 17774 | 17687 | |
| ... | ... | @@ -17787,22 +17700,6 @@ snapshots: |
| 17787 | 17700 | lightningcss-win32-arm64-msvc: 1.30.1 |
| 17788 | 17701 | lightningcss-win32-x64-msvc: 1.30.1 |
| 17789 | 17702 | |
| 17790 | lightningcss@1.31.1: | |
| 17791 | dependencies: | |
| 17792 | detect-libc: 2.1.2 | |
| 17793 | optionalDependencies: | |
| 17794 | lightningcss-android-arm64: 1.31.1 | |
| 17795 | lightningcss-darwin-arm64: 1.31.1 | |
| 17796 | lightningcss-darwin-x64: 1.31.1 | |
| 17797 | lightningcss-freebsd-x64: 1.31.1 | |
| 17798 | lightningcss-linux-arm-gnueabihf: 1.31.1 | |
| 17799 | lightningcss-linux-arm64-gnu: 1.31.1 | |
| 17800 | lightningcss-linux-arm64-musl: 1.31.1 | |
| 17801 | lightningcss-linux-x64-gnu: 1.31.1 | |
| 17802 | lightningcss-linux-x64-musl: 1.31.1 | |
| 17803 | lightningcss-win32-arm64-msvc: 1.31.1 | |
| 17804 | lightningcss-win32-x64-msvc: 1.31.1 | |
| 17805 | ||
| 17806 | 17703 | lightningcss@1.32.0: |
| 17807 | 17704 | dependencies: |
| 17808 | 17705 | detect-libc: 2.1.2 |
| ... | ... | @@ -20463,10 +20360,10 @@ snapshots: |
| 20463 | 20360 | |
| 20464 | 20361 | tailwind-merge@3.6.0: {} |
| 20465 | 20362 | |
| 20466 | tailwindcss@4.2.1: {} | |
| 20467 | ||
| 20468 | 20363 | tailwindcss@4.3.0: {} |
| 20469 | 20364 | |
| 20365 | tailwindcss@4.3.2: {} | |
| 20366 | ||
| 20470 | 20367 | tapable@2.3.3: {} |
| 20471 | 20368 | |
| 20472 | 20369 | tar@7.5.16: |
| ... | ... | @@ -20702,15 +20599,20 @@ snapshots: |
| 20702 | 20599 | |
| 20703 | 20600 | universalify@2.0.1: {} |
| 20704 | 20601 | |
| 20705 | uniwind@1.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): | |
| 20602 | uniwind@1.11.0(patch_hash=329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196)(@expo/metro-config@57.0.12(patch_hash=96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2)(bufferutil@4.1.0)(expo@57.0.18)(typescript@6.0.3)(utf-8-validate@6.0.6))(metro-cache@0.84.5)(metro-transform-worker@0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): | |
| 20706 | 20603 | dependencies: |
| 20707 | '@tailwindcss/node': 4.2.1 | |
| 20708 | '@tailwindcss/oxide': 4.2.1 | |
| 20604 | '@tailwindcss/node': 4.3.2 | |
| 20605 | '@tailwindcss/oxide': 4.3.2 | |
| 20709 | 20606 | culori: 4.0.2 |
| 20710 | 20607 | lightningcss: 1.30.1 |
| 20608 | metro: 0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) | |
| 20609 | metro-cache: 0.84.5 | |
| 20711 | 20610 | react: 19.2.3 |
| 20712 | 20611 | react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| 20713 | 20612 | tailwindcss: 4.3.0 |
| 20613 | optionalDependencies: | |
| 20614 | '@expo/metro-config': 57.0.12(patch_hash=96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2)(bufferutil@4.1.0)(expo@57.0.18)(typescript@6.0.3)(utf-8-validate@6.0.6) | |
| 20615 | metro-transform-worker: 0.84.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) | |
| 20714 | 20616 | |
| 20715 | 20617 | unpipe@1.0.0: {} |
| 20716 | 20618 |
pnpm-workspace.yaml+1| ... | ... | @@ -157,6 +157,7 @@ patchedDependencies: |
| 157 | 157 | react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch |
| 158 | 158 | react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch |
| 159 | 159 | react-native-screens@4.26.2: patches/react-native-screens@4.26.2.patch |
| 160 | uniwind@1.11.0: patches/uniwind@1.11.0.patch | |
| 160 | 161 | |
| 161 | 162 | peerDependencyRules: |
| 162 | 163 | allowAny: |