| 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,7 +80,6 @@ Run Metro from `apps/mobile`. |
| 80 | APP_VARIANT=development vp exec expo start \ | 80 | APP_VARIANT=development vp exec expo start \ |
| 81 | --dev-client \ | 81 | --dev-client \ |
| 82 | --scheme t3code-dev \ | 82 | --scheme t3code-dev \ |
| 83 | --clear \ | ||
| 84 | --lan \ | 83 | --lan \ |
| 85 | --port <metro-port> | 84 | --port <metro-port> |
| 86 | ``` | 85 | ``` |
| ... | @@ -179,6 +178,7 @@ Keep local verification focused. Do not turn this workflow into a full repositor | ... | @@ -179,6 +178,7 @@ Keep local verification focused. Do not turn this workflow into a full repositor |
| 179 | ## Troubleshoot predictable failures | 178 | ## Troubleshoot predictable failures |
| 180 | 179 | ||
| 181 | - **Old UI or an old error appears:** verify Metro's worktree, variant, URL, and port before diagnosing the app. | 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 | - **The environment remains empty:** verify the platform-specific HTTP origin, use a fresh token, and confirm project seeding used the identical base directory. | 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 | - **A second client cannot pair:** pairing tokens are single-use; issue another token. | 183 | - **A second client cannot pair:** pairing tokens are single-use; issue another token. |
| 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`. | 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,6 +104,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real |
| 104 | ## Verifying | 104 | ## Verifying |
| 105 | 105 | ||
| 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. | 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 | - **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 | - **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 | - Backend behavior changes ship with focused tests for that behavior. | 109 | - Backend behavior changes ship with focused tests for that behavior. |
| 109 | - 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. | 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,6 +28,23 @@ Start Metro for the dev client: |
| 28 | vp run dev:client | 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 | Build and run the local iOS dev client: | 48 | Build and run the local iOS dev client: |
| 32 | 49 | ||
| 33 | ```bash | 50 | ```bash |
| ... | @@ -89,7 +106,9 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. | ... | @@ -89,7 +106,9 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. |
| 89 | 106 | ||
| 90 | ## EAS Builds | 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 | For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`, | 113 | For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`, |
| 95 | `T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL` | 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,6 +10,9 @@ Object.assign(process.env, repoEnv); |
| 10 | 10 | ||
| 11 | const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); | 11 | const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); |
| 12 | const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1"; | 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 | const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); | 17 | const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); |
| 15 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; | 18 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; |
| ... | @@ -166,11 +169,10 @@ const config: ExpoConfig = { | ... | @@ -166,11 +169,10 @@ const config: ExpoConfig = { |
| 166 | scheme: variant.scheme, | 169 | scheme: variant.scheme, |
| 167 | version: "1.0.4", | 170 | version: "1.0.4", |
| 168 | runtimeVersion: { | 171 | runtimeVersion: { |
| 169 | // Fingerprint (not appVersion) so an OTA only reaches binaries whose native | 172 | // Development manifests resolve on every launch, so avoid fingerprint's |
| 170 | // project — native deps, config plugins, AND patches/ — matches the update. | 173 | // expensive native-project calculation there. Preview and production stay |
| 171 | // With appVersion, every 0.1.0 build shares a runtime version, so a JS update | 174 | // fingerprinted so OTAs only reach binaries with matching native projects. |
| 172 | // could land on a binary missing the native changes it needs and crash. | 175 | policy: runtimeVersionPolicy, |
| 173 | policy: process.env.MOBILE_VERSION_POLICY ?? "fingerprint", | ||
| 174 | }, | 176 | }, |
| 175 | orientation: "portrait", | 177 | orientation: "portrait", |
| 176 | icon: variant.assets.appIcon, | 178 | icon: variant.assets.appIcon, |
apps/mobile/package.json+6-5| ... | @@ -4,14 +4,15 @@ | ... | @@ -4,14 +4,15 @@ |
| 4 | "private": true, | 4 | "private": true, |
| 5 | "main": "index.ts", | 5 | "main": "index.ts", |
| 6 | "scripts": { | 6 | "scripts": { |
| 7 | "dev": "expo start --clear", | 7 | "dev": "expo start", |
| 8 | "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan", | 8 | "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --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'", | 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 | "start": "expo start", | 11 | "start": "expo start", |
| 11 | "start:dev": "APP_VARIANT=development expo start", | 12 | "start:dev": "APP_VARIANT=development expo start", |
| 12 | "start:preview": "APP_VARIANT=preview expo start", | 13 | "start:preview": "APP_VARIANT=preview expo start", |
| 13 | "start:prod": "APP_VARIANT=production expo start", | 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 | "screenshots": "node ../../scripts/mobile-showcase.ts", | 16 | "screenshots": "node ../../scripts/mobile-showcase.ts", |
| 16 | "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android", | 17 | "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android", |
| 17 | "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android", | 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,7 +121,7 @@ |
| 120 | "react-native-worklets": "0.10.1", | 121 | "react-native-worklets": "0.10.1", |
| 121 | "shiki": "4.2.0", | 122 | "shiki": "4.2.0", |
| 122 | "tailwind-merge": "^3.5.0", | 123 | "tailwind-merge": "^3.5.0", |
| 123 | "uniwind": "^1.6.2" | 124 | "uniwind": "1.11.0" |
| 124 | }, | 125 | }, |
| 125 | "devDependencies": { | 126 | "devDependencies": { |
| 126 | "@effect/vitest": "catalog:", | 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,86 +1,87 @@ |
| 1 | import { | 1 | import type { Icon } from "@tabler/icons-react-native/types"; |
| 2 | IconAdjustmentsHorizontal, | 2 | /* |
| 3 | IconAlertCircle, | 3 | * Keep these as per-icon exports. Importing the package root eagerly registers |
| 4 | IconAlertTriangle, | 4 | * the entire Tabler icon set in Metro. |
| 5 | IconApps, | 5 | */ |
| 6 | IconArchive, | 6 | import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal"; |
| 7 | IconArrowBackUp, | 7 | import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle"; |
| 8 | IconArrowDownCircle, | 8 | import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle"; |
| 9 | IconArrowRightCircle, | 9 | import IconApps from "@tabler/icons-react-native/IconApps"; |
| 10 | IconArrowUp, | 10 | import IconArchive from "@tabler/icons-react-native/IconArchive"; |
| 11 | IconArrowUpCircle, | 11 | import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp"; |
| 12 | IconArrowUpRight, | 12 | import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle"; |
| 13 | IconArrowUpRightCircle, | 13 | import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle"; |
| 14 | IconArrowsMaximize, | 14 | import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; |
| 15 | IconBellRinging, | 15 | import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; |
| 16 | IconBolt, | 16 | import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; |
| 17 | IconBox, | 17 | import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; |
| 18 | IconCamera, | 18 | import IconArrowsMaximize from "@tabler/icons-react-native/IconArrowsMaximize"; |
| 19 | IconChartBar, | 19 | import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; |
| 20 | IconCheck, | 20 | import IconBolt from "@tabler/icons-react-native/IconBolt"; |
| 21 | IconChevronDown, | 21 | import IconBox from "@tabler/icons-react-native/IconBox"; |
| 22 | IconCode, | 22 | import IconCamera from "@tabler/icons-react-native/IconCamera"; |
| 23 | IconChevronLeft, | 23 | import IconChartBar from "@tabler/icons-react-native/IconChartBar"; |
| 24 | IconChevronRight, | 24 | import IconCheck from "@tabler/icons-react-native/IconCheck"; |
| 25 | IconChevronUp, | 25 | import IconChevronDown from "@tabler/icons-react-native/IconChevronDown"; |
| 26 | IconCircleCheck, | 26 | import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft"; |
| 27 | IconCircleXFilled, | 27 | import IconChevronRight from "@tabler/icons-react-native/IconChevronRight"; |
| 28 | IconClock, | 28 | import IconChevronUp from "@tabler/icons-react-native/IconChevronUp"; |
| 29 | IconCopy, | 29 | import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck"; |
| 30 | IconDeviceDesktop, | 30 | import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled"; |
| 31 | IconDots, | 31 | import IconClock from "@tabler/icons-react-native/IconClock"; |
| 32 | IconDotsCircleHorizontal, | 32 | import IconCode from "@tabler/icons-react-native/IconCode"; |
| 33 | IconEdit, | 33 | import IconCopy from "@tabler/icons-react-native/IconCopy"; |
| 34 | IconExternalLink, | 34 | import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop"; |
| 35 | IconEye, | 35 | import IconDots from "@tabler/icons-react-native/IconDots"; |
| 36 | IconFileText, | 36 | import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal"; |
| 37 | IconFilter, | 37 | import IconEdit from "@tabler/icons-react-native/IconEdit"; |
| 38 | IconFolder, | 38 | import IconExternalLink from "@tabler/icons-react-native/IconExternalLink"; |
| 39 | IconFolderOpen, | 39 | import IconEye from "@tabler/icons-react-native/IconEye"; |
| 40 | IconFolderPlus, | 40 | import IconFileText from "@tabler/icons-react-native/IconFileText"; |
| 41 | IconGitBranch, | 41 | import IconFilter from "@tabler/icons-react-native/IconFilter"; |
| 42 | IconHammer, | 42 | import IconFolder from "@tabler/icons-react-native/IconFolder"; |
| 43 | IconGitMerge, | 43 | import IconFolderOpen from "@tabler/icons-react-native/IconFolderOpen"; |
| 44 | IconGitPullRequest, | 44 | import IconFolderPlus from "@tabler/icons-react-native/IconFolderPlus"; |
| 45 | IconInfoCircle, | 45 | import IconGitBranch from "@tabler/icons-react-native/IconGitBranch"; |
| 46 | IconKeyboard, | 46 | import IconGitMerge from "@tabler/icons-react-native/IconGitMerge"; |
| 47 | IconKeyboardHide, | 47 | import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest"; |
| 48 | IconLayoutColumns, | 48 | import IconHammer from "@tabler/icons-react-native/IconHammer"; |
| 49 | IconLayoutSidebar, | 49 | import IconInfoCircle from "@tabler/icons-react-native/IconInfoCircle"; |
| 50 | IconLetterSpacing, | 50 | import IconKeyboard from "@tabler/icons-react-native/IconKeyboard"; |
| 51 | IconLink, | 51 | import IconKeyboardHide from "@tabler/icons-react-native/IconKeyboardHide"; |
| 52 | IconMessage, | 52 | import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns"; |
| 53 | IconMinus, | 53 | import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar"; |
| 54 | IconMoon, | 54 | import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight"; |
| 55 | IconNetwork, | 55 | import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing"; |
| 56 | IconPalette, | 56 | import IconLink from "@tabler/icons-react-native/IconLink"; |
| 57 | IconPin, | 57 | import IconMessage from "@tabler/icons-react-native/IconMessage"; |
| 58 | IconPinnedOff, | 58 | import IconMinus from "@tabler/icons-react-native/IconMinus"; |
| 59 | IconPlayerPlay, | 59 | import IconMoon from "@tabler/icons-react-native/IconMoon"; |
| 60 | IconPlayerStopFilled, | 60 | import IconNetwork from "@tabler/icons-react-native/IconNetwork"; |
| 61 | IconPlus, | 61 | import IconPalette from "@tabler/icons-react-native/IconPalette"; |
| 62 | IconQrcode, | 62 | import IconPin from "@tabler/icons-react-native/IconPin"; |
| 63 | IconRefresh, | 63 | import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff"; |
| 64 | IconSearch, | 64 | import IconPlayerPlay from "@tabler/icons-react-native/IconPlayerPlay"; |
| 65 | IconServer, | 65 | import IconPlayerStopFilled from "@tabler/icons-react-native/IconPlayerStopFilled"; |
| 66 | IconSettings, | 66 | import IconPlus from "@tabler/icons-react-native/IconPlus"; |
| 67 | IconSparkles, | 67 | import IconQrcode from "@tabler/icons-react-native/IconQrcode"; |
| 68 | IconSun, | 68 | import IconRefresh from "@tabler/icons-react-native/IconRefresh"; |
| 69 | IconLayoutSidebarRight, | 69 | import IconSearch from "@tabler/icons-react-native/IconSearch"; |
| 70 | IconTerminal2, | 70 | import IconServer from "@tabler/icons-react-native/IconServer"; |
| 71 | IconTextDecrease, | 71 | import IconSettings from "@tabler/icons-react-native/IconSettings"; |
| 72 | IconTextIncrease, | 72 | import IconSparkles from "@tabler/icons-react-native/IconSparkles"; |
| 73 | IconTool, | 73 | import IconSun from "@tabler/icons-react-native/IconSun"; |
| 74 | IconTrash, | 74 | import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2"; |
| 75 | IconTypography, | 75 | import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease"; |
| 76 | IconUserCircle, | 76 | import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease"; |
| 77 | IconWifiOff, | 77 | import IconTool from "@tabler/icons-react-native/IconTool"; |
| 78 | IconWorld, | 78 | import IconTrash from "@tabler/icons-react-native/IconTrash"; |
| 79 | IconX, | 79 | import IconTypography from "@tabler/icons-react-native/IconTypography"; |
| 80 | type Icon, | 80 | import IconUserCircle from "@tabler/icons-react-native/IconUserCircle"; |
| 81 | } from "@tabler/icons-react-native"; | 81 | import IconWifiOff from "@tabler/icons-react-native/IconWifiOff"; |
| 82 | import { Platform } from "react-native"; | 82 | import IconWorld from "@tabler/icons-react-native/IconWorld"; |
| 83 | import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols"; | 83 | import IconX from "@tabler/icons-react-native/IconX"; |
| 84 | import type { SFSymbol, SymbolViewProps } from "expo-symbols"; | ||
| 84 | import { withUniwind } from "uniwind"; | 85 | import { withUniwind } from "uniwind"; |
| 85 | 86 | ||
| 86 | const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = { | 87 | const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = { |
| ... | @@ -192,10 +193,6 @@ export type { SFSymbol } from "expo-symbols"; | ... | @@ -192,10 +193,6 @@ export type { SFSymbol } from "expo-symbols"; |
| 192 | export type AppSymbolName = SymbolViewProps["name"]; | 193 | export type AppSymbolName = SymbolViewProps["name"]; |
| 193 | 194 | ||
| 194 | function AppSymbolView(props: SymbolViewProps) { | 195 | function AppSymbolView(props: SymbolViewProps) { |
| 195 | if (Platform.OS !== "android") { | ||
| 196 | return <ExpoSymbolView {...props} />; | ||
| 197 | } | ||
| 198 | |||
| 199 | const materialName = typeof props.name === "string" ? undefined : props.name.android; | 196 | const materialName = typeof props.name === "string" ? undefined : props.name.android; |
| 200 | const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; | 197 | const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; |
| 201 | const AndroidIcon = | 198 | const AndroidIcon = |
apps/mobile/src/components/ControlPill.tsx+52-12| ... | @@ -8,7 +8,7 @@ import { | ... | @@ -8,7 +8,7 @@ import { |
| 8 | type ReactNode, | 8 | type ReactNode, |
| 9 | useRef, | 9 | useRef, |
| 10 | } from "react"; | 10 | } from "react"; |
| 11 | import { Platform, Pressable, View } from "react-native"; | 11 | import { Platform, Pressable, View, type PressableProps } from "react-native"; |
| 12 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | 12 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 13 | 13 | ||
| 14 | import { cn } from "../lib/cn"; | 14 | import { cn } from "../lib/cn"; |
| ... | @@ -120,6 +120,8 @@ export function ControlPillMenu( | ... | @@ -120,6 +120,8 @@ export function ControlPillMenu( |
| 120 | ) { | 120 | ) { |
| 121 | const { themeAppearance } = useAppearancePreferences(); | 121 | const { themeAppearance } = useAppearancePreferences(); |
| 122 | const isDarkMode = themeAppearance === "dark"; | 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 | if (Platform.OS === "android") { | 126 | if (Platform.OS === "android") { |
| 125 | // Long-press menus keep their child interactive: the child element gets | 127 | // Long-press menus keep their child interactive: the child element gets |
| ... | @@ -161,20 +163,58 @@ export function ControlPillMenu( | ... | @@ -161,20 +163,58 @@ export function ControlPillMenu( |
| 161 | 163 | ||
| 162 | const { className: _className, ...menuProps } = props; | 164 | const { className: _className, ...menuProps } = props; |
| 163 | let children = menuProps.children; | 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 | if (props.shouldOpenOnLongPress && isValidElement(children)) { | 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 | children = cloneElement(child, { | 168 | children = cloneElement(child, { |
| 175 | onLongPress: child.props.onLongPress ?? (() => undefined), | 169 | onTouchStart: (event) => { |
| 176 | delayLongPress: child.props.delayLongPress ?? 350, | 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 | return ( | 219 | return ( |
| 180 | <MenuView {...menuProps} themeVariant={isDarkMode ? "dark" : "light"}> | 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,13 +4,18 @@ import { threadSnapshotLoaderLayer } from "@t3tools/client-runtime/state/threads |
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import { Atom } from "effect/unstable/reactivity"; | 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 | import { runtimeContextLayer } from "../lib/runtime"; | 9 | import { runtimeContextLayer } from "../lib/runtime"; |
| 10 | import { appAtomRegistry } from "../state/atom-registry"; | ||
| 8 | import { | 11 | import { |
| 9 | mobileBackgroundActivityObserverLayer, | 12 | mobileBackgroundActivityObserverLayer, |
| 10 | mobileBackgroundActivityReporterLayer, | 13 | mobileBackgroundActivityReporterLayer, |
| 11 | } from "./background-activity"; | 14 | } from "./background-activity"; |
| 12 | import { connectionPlatformLayer } from "./platform"; | 15 | import { connectionPlatformLayer } from "./platform"; |
| 13 | 16 | ||
| 17 | declare const module: { readonly hot?: FoundationHotModule } | undefined; | ||
| 18 | |||
| 14 | const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( | 19 | const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( |
| 15 | Layer.provide(runtimeContextLayer), | 20 | Layer.provide(runtimeContextLayer), |
| 16 | ); | 21 | ); |
| ... | @@ -42,4 +47,9 @@ const connectionLayer = mobileBackgroundActivityReporterLayer.pipe( | ... | @@ -42,4 +47,9 @@ const connectionLayer = mobileBackgroundActivityReporterLayer.pipe( |
| 42 | export const connectionAtomRuntime: Atom.AtomRuntime< | 47 | export const connectionAtomRuntime: Atom.AtomRuntime< |
| 43 | Layer.Success<ConnectionLayerSource>, | 48 | Layer.Success<ConnectionLayerSource>, |
| 44 | Layer.Error<ConnectionLayerSource> | 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,6 +17,7 @@ import { AndroidHomeFabLayout } from "./AndroidHomeFab"; |
| 17 | import { HomeScreen } from "./HomeScreen"; | 17 | import { HomeScreen } from "./HomeScreen"; |
| 18 | import { HomeHeader } from "./HomeHeader"; | 18 | import { HomeHeader } from "./HomeHeader"; |
| 19 | import { useHomeListOptions } from "./home-list-options"; | 19 | import { useHomeListOptions } from "./home-list-options"; |
| 20 | import { useHomeThreadSelection } from "./home-thread-navigation"; | ||
| 20 | import { buildHomeProjectScopes } from "./homeThreadList"; | 21 | import { buildHomeProjectScopes } from "./homeThreadList"; |
| 21 | import { usePendingTaskListActions } from "./usePendingTaskListActions"; | 22 | import { usePendingTaskListActions } from "./usePendingTaskListActions"; |
| 22 | import { useThreadListActions } from "./useThreadListActions"; | 23 | import { useThreadListActions } from "./useThreadListActions"; |
| ... | @@ -33,6 +34,7 @@ export function HomeRouteScreen() { | ... | @@ -33,6 +34,7 @@ export function HomeRouteScreen() { |
| 33 | const { savedConnectionsById } = useSavedRemoteConnections(); | 34 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| 34 | const navigation = useNavigation(); | 35 | const navigation = useNavigation(); |
| 35 | const [searchQuery, setSearchQuery] = useState(""); | 36 | const [searchQuery, setSearchQuery] = useState(""); |
| 37 | const handleSelectThread = useHomeThreadSelection(); | ||
| 36 | 38 | ||
| 37 | useEffect(() => { | 39 | useEffect(() => { |
| 38 | void checkForAppUpdateOnLaunch(); | 40 | void checkForAppUpdateOnLaunch(); |
| ... | @@ -209,14 +211,7 @@ export function HomeRouteScreen() { | ... | @@ -209,14 +211,7 @@ export function HomeRouteScreen() { |
| 209 | } | 211 | } |
| 210 | onProjectSortOrderChange={setProjectSortOrder} | 212 | onProjectSortOrderChange={setProjectSortOrder} |
| 211 | onSearchQueryChange={setSearchQuery} | 213 | onSearchQueryChange={setSearchQuery} |
| 212 | onSelectThread={(thread) => { | 214 | onSelectThread={handleSelectThread} |
| 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 | }} | ||
| 220 | onSelectPendingTask={openPendingTask} | 215 | onSelectPendingTask={openPendingTask} |
| 221 | onDeletePendingTask={confirmDeletePendingTask} | 216 | onDeletePendingTask={confirmDeletePendingTask} |
| 222 | onNewThreadInProject={(project) => { | 217 | onNewThreadInProject={(project) => { |
apps/mobile/src/features/home/HomeScreen.tsx+11-9| ... | @@ -19,6 +19,7 @@ import type { | ... | @@ -19,6 +19,7 @@ import type { |
| 19 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 20 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | 20 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 21 | import { AsyncResult } from "effect/unstable/reactivity"; | 21 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 22 | import { useFocusEffect } from "@react-navigation/native"; | ||
| 22 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 23 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 23 | import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native"; | 24 | import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native"; |
| 24 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; | 25 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| ... | @@ -587,15 +588,16 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -587,15 +588,16 @@ export function HomeScreen(props: HomeScreenProps) { |
| 587 | // next wake boundary re-runs the partition with a fresh clock so a woken | 588 | // next wake boundary re-runs the partition with a fresh clock so a woken |
| 588 | // thread reappears immediately instead of on the next minute tick. | 589 | // thread reappears immediately instead of on the next minute tick. |
| 589 | const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0); | 590 | const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0); |
| 590 | useEffect(() => { | 591 | useFocusEffect( |
| 591 | if (!threadListV2Enabled) return; | 592 | useCallback(() => { |
| 592 | // Refresh immediately on enable: the mount-time value can be hours old | 593 | if (!threadListV2Enabled) return; |
| 593 | // by the time the beta is switched on, which would misclassify the | 594 | // Refresh immediately on enable or focus: the previous value can be hours |
| 594 | // inactivity auto-settle boundary until the first tick. | 595 | // old and misclassify the inactivity auto-settle boundary until the first tick. |
| 595 | setNowMinute(new Date().toISOString().slice(0, 16)); | 596 | setNowMinute(new Date().toISOString().slice(0, 16)); |
| 596 | const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000); | 597 | const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000); |
| 597 | return () => clearInterval(id); | 598 | return () => clearInterval(id); |
| 598 | }, [threadListV2Enabled]); | 599 | }, [threadListV2Enabled]), |
| 600 | ); | ||
| 599 | // Threads on servers without the settlement capability never classify as | 601 | // Threads on servers without the settlement capability never classify as |
| 600 | // settled (the user could neither un-settle nor pin them). | 602 | // settled (the user could neither un-settle nor pin them). |
| 601 | const serverConfigs = useAtomValue(environmentServerConfigsAtom); | 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,34 +370,45 @@ function SwipeActionButton(props: { |
| 370 | readonly stretchesOnFullSwipe: boolean; | 370 | readonly stretchesOnFullSwipe: boolean; |
| 371 | readonly translation: SharedValue<number>; | 371 | readonly translation: SharedValue<number>; |
| 372 | }) { | 372 | }) { |
| 373 | const { | ||
| 374 | actionsWidth, | ||
| 375 | entryRange: [entryRangeStart, entryRangeEnd], | ||
| 376 | fullSwipeThreshold, | ||
| 377 | stretchesOnFullSwipe, | ||
| 378 | translation, | ||
| 379 | } = props; | ||
| 373 | const circleSize = props.compact ? COMPACT_ACTION_CIRCLE_SIZE : ACTION_CIRCLE_SIZE; | 380 | const circleSize = props.compact ? COMPACT_ACTION_CIRCLE_SIZE : ACTION_CIRCLE_SIZE; |
| 374 | const iconSize = props.compact ? COMPACT_ACTION_ICON_SIZE : ACTION_ICON_SIZE; | 381 | const iconSize = props.compact ? COMPACT_ACTION_ICON_SIZE : ACTION_ICON_SIZE; |
| 375 | const actionStyle = useAnimatedStyle(() => { | 382 | const actionStyle = useAnimatedStyle(() => { |
| 376 | const reveal = Math.max(-props.translation.value, 0); | 383 | const reveal = Math.max(-translation.value, 0); |
| 377 | const entryProgress = interpolate(reveal, props.entryRange, [0, 1], Extrapolation.CLAMP); | 384 | const entryProgress = interpolate( |
| 378 | const stretch = Math.max(reveal - props.actionsWidth, 0); | 385 | reveal, |
| 386 | [entryRangeStart, entryRangeEnd], | ||
| 387 | [0, 1], | ||
| 388 | Extrapolation.CLAMP, | ||
| 389 | ); | ||
| 390 | const stretch = Math.max(reveal - actionsWidth, 0); | ||
| 379 | const fullSwipeProgress = interpolate( | 391 | const fullSwipeProgress = interpolate( |
| 380 | reveal, | 392 | reveal, |
| 381 | [props.actionsWidth, props.fullSwipeThreshold + 20], | 393 | [actionsWidth, fullSwipeThreshold + 20], |
| 382 | [0, 1], | 394 | [0, 1], |
| 383 | Extrapolation.CLAMP, | 395 | Extrapolation.CLAMP, |
| 384 | ); | 396 | ); |
| 385 | 397 | ||
| 386 | return { | 398 | return { |
| 387 | opacity: props.stretchesOnFullSwipe ? entryProgress : entryProgress * (1 - fullSwipeProgress), | 399 | opacity: stretchesOnFullSwipe ? entryProgress : entryProgress * (1 - fullSwipeProgress), |
| 388 | transform: [ | 400 | transform: [ |
| 389 | { | 401 | { |
| 390 | translateX: | 402 | translateX: |
| 391 | interpolate(entryProgress, [0, 1], [22, 0]) - | 403 | interpolate(entryProgress, [0, 1], [22, 0]) - (stretchesOnFullSwipe ? 0 : stretch), |
| 392 | (props.stretchesOnFullSwipe ? 0 : stretch), | ||
| 393 | }, | 404 | }, |
| 394 | { scale: interpolate(entryProgress, [0, 1], [0.78, 1]) }, | 405 | { scale: interpolate(entryProgress, [0, 1], [0.78, 1]) }, |
| 395 | ], | 406 | ], |
| 396 | }; | 407 | }; |
| 397 | }); | 408 | }); |
| 398 | const circleStyle = useAnimatedStyle(() => { | 409 | const circleStyle = useAnimatedStyle(() => { |
| 399 | const reveal = Math.max(-props.translation.value, 0); | 410 | const reveal = Math.max(-translation.value, 0); |
| 400 | const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; | 411 | const stretch = stretchesOnFullSwipe ? Math.max(reveal - actionsWidth, 0) : 0; |
| 401 | 412 | ||
| 402 | return { | 413 | return { |
| 403 | transform: [{ translateX: -stretch }], | 414 | transform: [{ translateX: -stretch }], |
| ... | @@ -405,11 +416,11 @@ function SwipeActionButton(props: { | ... | @@ -405,11 +416,11 @@ function SwipeActionButton(props: { |
| 405 | }; | 416 | }; |
| 406 | }); | 417 | }); |
| 407 | const iconStyle = useAnimatedStyle(() => { | 418 | const iconStyle = useAnimatedStyle(() => { |
| 408 | const reveal = Math.max(-props.translation.value, 0); | 419 | const reveal = Math.max(-translation.value, 0); |
| 409 | const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; | 420 | const stretch = stretchesOnFullSwipe ? Math.max(reveal - actionsWidth, 0) : 0; |
| 410 | const armedProgress = interpolate( | 421 | const armedProgress = interpolate( |
| 411 | reveal, | 422 | reveal, |
| 412 | [props.fullSwipeThreshold, props.fullSwipeThreshold + 20], | 423 | [fullSwipeThreshold, fullSwipeThreshold + 20], |
| 413 | [0, 1], | 424 | [0, 1], |
| 414 | Extrapolation.CLAMP, | 425 | Extrapolation.CLAMP, |
| 415 | ); | 426 | ); |
| ... | @@ -419,16 +430,16 @@ function SwipeActionButton(props: { | ... | @@ -419,16 +430,16 @@ function SwipeActionButton(props: { |
| 419 | }; | 430 | }; |
| 420 | }); | 431 | }); |
| 421 | const labelStyle = useAnimatedStyle(() => { | 432 | const labelStyle = useAnimatedStyle(() => { |
| 422 | if (!props.stretchesOnFullSwipe) { | 433 | if (!stretchesOnFullSwipe) { |
| 423 | return { opacity: 1 }; | 434 | return { opacity: 1 }; |
| 424 | } | 435 | } |
| 425 | 436 | ||
| 426 | const reveal = Math.max(-props.translation.value, 0); | 437 | const reveal = Math.max(-translation.value, 0); |
| 427 | const stretch = Math.max(reveal - props.actionsWidth, 0); | 438 | const stretch = Math.max(reveal - actionsWidth, 0); |
| 428 | return { | 439 | return { |
| 429 | opacity: interpolate( | 440 | opacity: interpolate( |
| 430 | reveal, | 441 | reveal, |
| 431 | [props.fullSwipeThreshold - 24, props.fullSwipeThreshold], | 442 | [fullSwipeThreshold - 24, fullSwipeThreshold], |
| 432 | [1, 0], | 443 | [1, 0], |
| 433 | Extrapolation.CLAMP, | 444 | Extrapolation.CLAMP, |
| 434 | ), | 445 | ), |
| ... | @@ -532,17 +543,17 @@ export function ThreadSwipeActions(props: { | ... | @@ -532,17 +543,17 @@ export function ThreadSwipeActions(props: { |
| 532 | readonly secondaryAction: ThreadSwipeSecondaryAction | null; | 543 | readonly secondaryAction: ThreadSwipeSecondaryAction | null; |
| 533 | readonly translation: SharedValue<number>; | 544 | readonly translation: SharedValue<number>; |
| 534 | }) { | 545 | }) { |
| 535 | const secondaryAction = props.secondaryAction; | 546 | const { fullSwipeThreshold, onFullSwipeArmedChange, secondaryAction, translation } = props; |
| 536 | const fullSwipeIsPrimary = props.fullSwipeAction === "primary" || secondaryAction === null; | 547 | const fullSwipeIsPrimary = props.fullSwipeAction === "primary" || secondaryAction === null; |
| 537 | const actionsWidth = swipeActionsWidth(secondaryAction !== null); | 548 | const actionsWidth = swipeActionsWidth(secondaryAction !== null); |
| 538 | useAnimatedReaction( | 549 | useAnimatedReaction( |
| 539 | () => -props.translation.value >= props.fullSwipeThreshold, | 550 | () => -translation.value >= fullSwipeThreshold, |
| 540 | (armed, previous) => { | 551 | (armed, previous) => { |
| 541 | if (armed !== previous) { | 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 | return ( | 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,6 +9,9 @@ import { managedRelayClientLayer } from "../features/cloud/managedRelayLayer"; |
| 9 | import { resolveCloudPublicConfig } from "../features/cloud/publicConfig"; | 9 | import { resolveCloudPublicConfig } from "../features/cloud/publicConfig"; |
| 10 | import { tracingLayer } from "../features/observability/tracing"; | 10 | import { tracingLayer } from "../features/observability/tracing"; |
| 11 | import * as Persistence from "../persistence/layer"; | 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 | function configuredRelayUrl(): string { | 16 | function configuredRelayUrl(): string { |
| 14 | return resolveCloudPublicConfig().relay.url ?? "http://relay.invalid"; | 17 | return resolveCloudPublicConfig().relay.url ?? "http://relay.invalid"; |
| ... | @@ -43,3 +46,7 @@ export const runtimeContextLayer: Layer.Layer< | ... | @@ -43,3 +46,7 @@ export const runtimeContextLayer: Layer.Layer< |
| 43 | Layer.Success<RuntimeLayerSource>, | 46 | Layer.Success<RuntimeLayerSource>, |
| 44 | Layer.Error<RuntimeLayerSource> | 47 | Layer.Error<RuntimeLayerSource> |
| 45 | > = Layer.effectContext(runtime.contextEffect); | 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,3 +1,14 @@ |
| 1 | import { AtomRegistry } from "effect/unstable/reactivity"; | 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 | export const appAtomRegistry = AtomRegistry.make(); | 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,26 +1,20 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import type { PreparedConnection } from "@t3tools/client-runtime/connection"; | ||
| 3 | import type { EnvironmentId } from "@t3tools/contracts"; | 2 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 4 | import type { ServerConfig } from "@t3tools/contracts"; | ||
| 5 | import * as Cause from "effect/Cause"; | 3 | import * as Cause from "effect/Cause"; |
| 6 | import * as Option from "effect/Option"; | ||
| 7 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | 4 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 8 | import { useCallback, useMemo } from "react"; | 5 | import { useCallback, useMemo } from "react"; |
| 9 | import { Alert } from "react-native"; | 6 | import { Alert } from "react-native"; |
| 10 | 7 | ||
| 11 | import { useEnvironmentServerConfig } from "../state/entities"; | ||
| 12 | import { useConnectionController } from "../features/connection/useConnectionController"; | 8 | import { useConnectionController } from "../features/connection/useConnectionController"; |
| 13 | import { environmentPresentations, useEnvironmentPresentation } from "./presentation"; | 9 | import { environmentPresentations } from "./presentation"; |
| 14 | import { | ||
| 15 | projectEnvironmentPresentation, | ||
| 16 | type EnvironmentPresentation, | ||
| 17 | } from "../state/environments"; | ||
| 18 | import { useWorkspaceState } from "../state/workspace"; | 10 | import { useWorkspaceState } from "../state/workspace"; |
| 19 | import type { SavedRemoteConnection } from "../lib/connection"; | 11 | import type { SavedRemoteConnection } from "../lib/connection"; |
| 20 | import { appAtomRegistry } from "./atom-registry"; | 12 | import { appAtomRegistry } from "./atom-registry"; |
| 21 | import type { ConnectedEnvironmentSummary, EnvironmentRuntimeState } from "./remote-runtime-types"; | 13 | import type { ConnectedEnvironmentSummary, EnvironmentRuntimeState } from "./remote-runtime-types"; |
| 22 | import { environmentSession, usePreparedConnection } from "./session"; | 14 | import { environmentSession } from "./session"; |
| 23 | import { environmentCatalog } from "../connection/catalog"; | 15 | import { environmentCatalog } from "../connection/catalog"; |
| 16 | import { createRemoteEnvironmentProjectionAtoms } from "./remote-environment-projections"; | ||
| 17 | import { serverEnvironment } from "./server"; | ||
| 24 | 18 | ||
| 25 | const connectionPairingUrlAtom = Atom.make("").pipe( | 19 | const connectionPairingUrlAtom = Atom.make("").pipe( |
| 26 | Atom.keepAlive, | 20 | Atom.keepAlive, |
| ... | @@ -36,65 +30,30 @@ export function setPendingConnectionError(message: string | null): void { | ... | @@ -36,65 +30,30 @@ export function setPendingConnectionError(message: string | null): void { |
| 36 | appAtomRegistry.set(pendingConnectionErrorAtom, message); | 30 | appAtomRegistry.set(pendingConnectionErrorAtom, message); |
| 37 | } | 31 | } |
| 38 | 32 | ||
| 39 | function toSavedConnection( | 33 | const remoteEnvironmentProjections = createRemoteEnvironmentProjectionAtoms({ |
| 40 | environment: EnvironmentPresentation, | 34 | presentationAtom: environmentPresentations.presentationAtom, |
| 41 | prepared: Option.Option<PreparedConnection>, | 35 | preparedConnectionAtom: environmentSession.preparedConnectionValueAtom, |
| 42 | ): SavedRemoteConnection { | 36 | serverConfigAtom: serverEnvironment.configValueAtom, |
| 43 | const displayUrl = environment.displayUrl ?? ""; | 37 | }); |
| 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; | ||
| 54 | 38 | ||
| 55 | return { | 39 | const EMPTY_SAVED_CONNECTION_ATOM = Atom.make<SavedRemoteConnection | null>(null).pipe( |
| 56 | environmentId: environment.environmentId, | 40 | Atom.withLabel("mobile:saved-connection:empty"), |
| 57 | environmentLabel: environment.label, | 41 | ); |
| 58 | pairingUrl: displayUrl, | 42 | |
| 59 | displayUrl, | 43 | const EMPTY_RUNTIME_STATE_ATOM = Atom.make<EnvironmentRuntimeState | null>(null).pipe( |
| 60 | httpBaseUrl, | 44 | Atom.withLabel("mobile:environment-runtime-state:empty"), |
| 61 | wsBaseUrl, | 45 | ); |
| 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 | } | ||
| 72 | 46 | ||
| 73 | const savedConnectionsByIdAtom = Atom.make((get) => { | 47 | const savedConnectionsByIdAtom = Atom.make((get) => { |
| 74 | const presentationById = get(environmentPresentations.presentationsAtom); | 48 | const presentationById = get(environmentPresentations.presentationsAtom); |
| 75 | return Object.fromEntries( | 49 | return Object.fromEntries( |
| 76 | [...presentationById.entries()].map(([environmentId, presentation]) => [ | 50 | [...presentationById.keys()].flatMap((environmentId) => { |
| 77 | environmentId, | 51 | const connection = get(remoteEnvironmentProjections.savedConnectionAtom(environmentId)); |
| 78 | toSavedConnection( | 52 | return connection === null ? [] : [[environmentId, connection]]; |
| 79 | projectEnvironmentPresentation(environmentId, presentation), | 53 | }), |
| 80 | get(environmentSession.preparedConnectionValueAtom(environmentId)), | ||
| 81 | ), | ||
| 82 | ]), | ||
| 83 | ) as Record<EnvironmentId, SavedRemoteConnection>; | 54 | ) as Record<EnvironmentId, SavedRemoteConnection>; |
| 84 | }).pipe(Atom.withLabel("mobile:saved-connections-by-id")); | 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 | export function useSavedRemoteConnections() { | 57 | export function useSavedRemoteConnections() { |
| 99 | const catalog = useAtomValue(environmentCatalog.catalogValueAtom); | 58 | const catalog = useAtomValue(environmentCatalog.catalogValueAtom); |
| 100 | const savedConnectionsById = useAtomValue(savedConnectionsByIdAtom); | 59 | const savedConnectionsById = useAtomValue(savedConnectionsByIdAtom); |
| ... | @@ -108,23 +67,21 @@ export function useSavedRemoteConnections() { | ... | @@ -108,23 +67,21 @@ export function useSavedRemoteConnections() { |
| 108 | export function useSavedRemoteConnection( | 67 | export function useSavedRemoteConnection( |
| 109 | environmentId: EnvironmentId | null, | 68 | environmentId: EnvironmentId | null, |
| 110 | ): SavedRemoteConnection | null { | 69 | ): SavedRemoteConnection | null { |
| 111 | const { presentation } = useEnvironmentPresentation(environmentId); | 70 | return useAtomValue( |
| 112 | const prepared = usePreparedConnection(environmentId); | 71 | environmentId === null |
| 113 | if (environmentId === null || presentation === null) { | 72 | ? EMPTY_SAVED_CONNECTION_ATOM |
| 114 | return null; | 73 | : remoteEnvironmentProjections.savedConnectionAtom(environmentId), |
| 115 | } | 74 | ); |
| 116 | return toSavedConnection(projectEnvironmentPresentation(environmentId, presentation), prepared); | ||
| 117 | } | 75 | } |
| 118 | 76 | ||
| 119 | export function useRemoteEnvironmentRuntime( | 77 | export function useRemoteEnvironmentRuntime( |
| 120 | environmentId: EnvironmentId | null, | 78 | environmentId: EnvironmentId | null, |
| 121 | ): EnvironmentRuntimeState | null { | 79 | ): EnvironmentRuntimeState | null { |
| 122 | const { presentation } = useEnvironmentPresentation(environmentId); | 80 | return useAtomValue( |
| 123 | const serverConfig = useEnvironmentServerConfig(environmentId); | 81 | environmentId === null |
| 124 | if (environmentId === null || presentation === null) { | 82 | ? EMPTY_RUNTIME_STATE_ATOM |
| 125 | return null; | 83 | : remoteEnvironmentProjections.runtimeStateAtom(environmentId), |
| 126 | } | 84 | ); |
| 127 | return toRuntimeState(projectEnvironmentPresentation(environmentId, presentation), serverConfig); | ||
| 128 | } | 85 | } |
| 129 | 86 | ||
| 130 | export function useRemoteConnectionStatus() { | 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,13 +4,17 @@ import { |
| 4 | type OrchestrationShellSnapshot, | 4 | type OrchestrationShellSnapshot, |
| 5 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 6 | import { describe, expect, it } from "@effect/vitest"; | 6 | import { describe, expect, it } from "@effect/vitest"; |
| 7 | import * as Context from "effect/Context"; | ||
| 7 | import * as Deferred from "effect/Deferred"; | 8 | import * as Deferred from "effect/Deferred"; |
| 8 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Exit from "effect/Exit"; | ||
| 9 | import * as Fiber from "effect/Fiber"; | 11 | import * as Fiber from "effect/Fiber"; |
| 10 | import * as Layer from "effect/Layer"; | 12 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; | 13 | import * as Option from "effect/Option"; |
| 12 | import * as Ref from "effect/Ref"; | 14 | import * as Ref from "effect/Ref"; |
| 13 | import * as Result from "effect/Result"; | 15 | import * as Result from "effect/Result"; |
| 16 | import * as Scheduler from "effect/Scheduler"; | ||
| 17 | import * as Scope from "effect/Scope"; | ||
| 14 | import * as Stream from "effect/Stream"; | 18 | import * as Stream from "effect/Stream"; |
| 15 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 19 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 16 | 20 | ||
| ... | @@ -420,6 +424,28 @@ function awaitConnectionState( | ... | @@ -420,6 +424,28 @@ function awaitConnectionState( |
| 420 | } | 424 | } |
| 421 | 425 | ||
| 422 | describe("EnvironmentRegistry", () => { | 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 | it.effect("hydrates connection profiles into catalog entries", () => | 449 | it.effect("hydrates connection profiles into catalog entries", () => |
| 424 | Effect.gen(function* () { | 450 | Effect.gen(function* () { |
| 425 | const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]); | 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,6 +126,7 @@ interface EnvironmentServiceScope { |
| 126 | } | 126 | } |
| 127 | 127 | ||
| 128 | export const make = Effect.gen(function* () { | 128 | export const make = Effect.gen(function* () { |
| 129 | const registryScope = yield* Scope.Scope; | ||
| 129 | const storage = yield* Persistence.ConnectionTargetStore; | 130 | const storage = yield* Persistence.ConnectionTargetStore; |
| 130 | const registrations = yield* Persistence.ConnectionRegistrationStore; | 131 | const registrations = yield* Persistence.ConnectionRegistrationStore; |
| 131 | const cache = yield* Persistence.EnvironmentCacheStore; | 132 | const cache = yield* Persistence.EnvironmentCacheStore; |
| ... | @@ -249,7 +250,7 @@ export const make = Effect.gen(function* () { | ... | @@ -249,7 +250,7 @@ export const make = Effect.gen(function* () { |
| 249 | Effect.uninterruptible( | 250 | Effect.uninterruptible( |
| 250 | Effect.gen(function* () { | 251 | Effect.gen(function* () { |
| 251 | const environmentId = entry.target.environmentId; | 252 | const environmentId = entry.target.environmentId; |
| 252 | const scope = yield* Scope.make(); | 253 | const scope = yield* Scope.fork(registryScope); |
| 253 | const supervisor = yield* EnvironmentSupervisor.make(entry, { | 254 | const supervisor = yield* EnvironmentSupervisor.make(entry, { |
| 254 | initiallyDesired: false, | 255 | initiallyDesired: false, |
| 255 | }).pipe( | 256 | }).pipe( |
patches/@react-native-menu__menu@2.0.0.patch+257-15| ... | @@ -1,8 +1,94 @@ | ... | @@ -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 | diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mm | 61 | diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mm |
| 2 | index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a94a8f7d4 100644 | 62 | index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..773f6f1f5d4e0663fd3211fe51085a7f3935e445 100644 |
| 3 | --- a/ios/NewArch/MenuView.mm | 63 | --- a/ios/NewArch/MenuView.mm |
| 4 | +++ b/ios/NewArch/MenuView.mm | 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 | NSMutableArray<NSDictionary *> *subactionsArray = [NSMutableArray arrayWithCapacity:actions.size()]; | 92 | NSMutableArray<NSDictionary *> *subactionsArray = [NSMutableArray arrayWithCapacity:actions.size()]; |
| 7 | if (action.subactions.size() > 0) { | 93 | if (action.subactions.size() > 0) { |
| 8 | for (const MenuViewActionsSubactionsStruct &subaction : action.subactions) { | 94 | for (const MenuViewActionsSubactionsStruct &subaction : action.subactions) { |
| ... | @@ -30,7 +116,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a | ... | @@ -30,7 +116,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 30 | NSDictionary *subactionDict = @{ | 116 | NSDictionary *subactionDict = @{ |
| 31 | @"id": [NSString stringWithUTF8String:subaction.id.c_str()], | 117 | @"id": [NSString stringWithUTF8String:subaction.id.c_str()], |
| 32 | @"title": [NSString stringWithUTF8String:subaction.title.c_str()], | 118 | @"title": [NSString stringWithUTF8String:subaction.title.c_str()], |
| 33 | @@ -118,7 +139,9 @@ - (void)onOpenMenu { | 119 | @@ -118,7 +151,9 @@ - (void)onOpenMenu { |
| 34 | @"destructive": @(subaction.attributes.destructive), | 120 | @"destructive": @(subaction.attributes.destructive), |
| 35 | @"disabled": @(subaction.attributes.disabled), | 121 | @"disabled": @(subaction.attributes.disabled), |
| 36 | @"hidden": @(subaction.attributes.hidden), | 122 | @"hidden": @(subaction.attributes.hidden), |
| ... | @@ -40,7 +126,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a | ... | @@ -40,7 +126,7 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 40 | }; | 126 | }; |
| 41 | [subactionsArray addObject:subactionDict]; | 127 | [subactionsArray addObject:subactionDict]; |
| 42 | } | 128 | } |
| 43 | @@ -138,6 +161,7 @@ - (void)onOpenMenu { | 129 | @@ -138,6 +173,7 @@ - (void)onOpenMenu { |
| 44 | @"destructive": @(action.attributes.destructive), | 130 | @"destructive": @(action.attributes.destructive), |
| 45 | @"disabled": @(action.attributes.disabled), | 131 | @"disabled": @(action.attributes.disabled), |
| 46 | @"hidden": @(action.attributes.hidden), | 132 | @"hidden": @(action.attributes.hidden), |
| ... | @@ -48,20 +134,50 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a | ... | @@ -48,20 +134,50 @@ index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..066b01259a3c8ad9532688606be3d29a |
| 48 | }, | 134 | }, |
| 49 | @"subactions": subactionsArray, | 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 | diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift | 172 | diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift |
| 52 | index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e8037a005 100644 | 173 | index 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c77f2cc47 100644 |
| 53 | --- a/ios/Shared/MenuViewImplementation.swift | 174 | --- a/ios/Shared/MenuViewImplementation.swift |
| 54 | +++ b/ios/Shared/MenuViewImplementation.swift | 175 | +++ b/ios/Shared/MenuViewImplementation.swift |
| 55 | @@ -59,18 +59,43 @@ public class MenuViewImplementation: UIButton { | 176 | @@ -60,17 +60,46 @@ public class MenuViewImplementation: UIButton { |
| 56 | self.setup() | ||
| 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 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { | 179 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { |
| 180 | - sendMenuOpen() | ||
| 65 | + // Flush updates deferred by the presented-guard before the action | 181 | + // Flush updates deferred by the presented-guard before the action |
| 66 | + // provider snapshots self.menu (covers a stuck flag from an | 182 | + // provider snapshots self.menu (covers a stuck flag from an |
| 67 | + // interaction that never ended cleanly). | 183 | + // interaction that never ended cleanly). |
| ... | @@ -71,15 +187,24 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e | ... | @@ -71,15 +187,24 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 71 | + self.setup() | 187 | + self.setup() |
| 72 | + } | 188 | + } |
| 73 | + isMenuPresented = true | 189 | + isMenuPresented = true |
| 74 | sendMenuOpen() | 190 | + sendMenuInteractionStart() |
| 75 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in | 191 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in |
| 76 | guard let self = self else { return nil } | 192 | guard let self = self else { return nil } |
| 77 | return self.menu | 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 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { | 206 | public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { |
| 207 | + super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator) | ||
| 83 | sendMenuClose() | 208 | sendMenuClose() |
| 84 | + isMenuPresented = false | 209 | + isMenuPresented = false |
| 85 | + if pendingMenu != nil { | 210 | + if pendingMenu != nil { |
| ... | @@ -97,7 +222,7 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e | ... | @@ -97,7 +222,7 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 97 | func setup () { | 222 | func setup () { |
| 98 | let menu = UIMenu(title: _title, | 223 | let menu = UIMenu(title: _title, |
| 99 | identifier: nil, | 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,6 +321,15 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..db134864676ed83dbcd895d7a1bde38e |
| 196 | } | 321 | } |
| 197 | 322 | ||
| 198 | public override func reactSetFrame(_ frame: CGRect) { | 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 | diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swift | 333 | diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swift |
| 200 | index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f619443d5a 100644 | 334 | index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f619443d5a 100644 |
| 201 | --- a/ios/Shared/RCTMenuItem.swift | 335 | --- a/ios/Shared/RCTMenuItem.swift |
| ... | @@ -220,8 +354,83 @@ index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f6 | ... | @@ -220,8 +354,83 @@ index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f6 |
| 220 | } | 354 | } |
| 221 | 355 | ||
| 222 | if #available(iOS 16.0, *) { | 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 | diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts | 432 | diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 224 | index e6509355275596c451f9d082223cc16bfe504e1a..c783cdaf73f5635cf9835824ca85fba3b46453b4 100644 | 433 | index e6509355275596c451f9d082223cc16bfe504e1a..604dba0d90a2545910f8724a80205565de2391d4 100644 |
| 225 | --- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts | 434 | --- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 226 | +++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts | 435 | +++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 227 | @@ -13,6 +13,22 @@ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNati | 436 | @@ -13,6 +13,22 @@ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNati |
| ... | @@ -267,3 +476,36 @@ index e6509355275596c451f9d082223cc16bfe504e1a..c783cdaf73f5635cf9835824ca85fba3 | ... | @@ -267,3 +476,36 @@ index e6509355275596c451f9d082223cc16bfe504e1a..c783cdaf73f5635cf9835824ca85fba3 |
| 267 | 	}; | 476 | 	}; |
| 268 | 	subactions?: Array<SubAction>; | 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,13 +88,14 @@ patchedDependencies: |
| 88 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 | 88 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 |
| 89 | '@legendapp/list@3.3.5': 064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706 | 89 | '@legendapp/list@3.3.5': 064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706 |
| 90 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa | 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 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 | 92 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 |
| 93 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 | 93 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 |
| 94 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 | 94 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 |
| 95 | react-native-keyboard-controller@1.21.13: 20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008 | 95 | react-native-keyboard-controller@1.21.13: 20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008 |
| 96 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 | 96 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 |
| 97 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 | 97 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 |
| 98 | uniwind@1.11.0: 329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196 | ||
| 98 | 99 | ||
| 99 | importers: | 100 | importers: |
| 100 | 101 | ||
| ... | @@ -239,7 +240,7 @@ importers: | ... | @@ -239,7 +240,7 @@ importers: |
| 239 | 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 | 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 | '@react-native-menu/menu': | 241 | '@react-native-menu/menu': |
| 241 | specifier: ^2.0.0 | 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 | '@react-navigation/elements': | 244 | '@react-navigation/elements': |
| 244 | specifier: 2.9.26 | 245 | specifier: 2.9.26 |
| 245 | version: 2.9.26(c10301b6e0c42fc6434d2b643197a81e) | 246 | version: 2.9.26(c10301b6e0c42fc6434d2b643197a81e) |
| ... | @@ -436,8 +437,8 @@ importers: | ... | @@ -436,8 +437,8 @@ importers: |
| 436 | specifier: ^3.5.0 | 437 | specifier: ^3.5.0 |
| 437 | version: 3.6.0 | 438 | version: 3.6.0 |
| 438 | uniwind: | 439 | uniwind: |
| 439 | specifier: ^1.6.2 | 440 | specifier: 1.11.0 |
| 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) | 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 | devDependencies: | 442 | devDependencies: |
| 442 | '@effect/vitest': | 443 | '@effect/vitest': |
| 443 | specifier: 4.0.0-beta.103 | 444 | specifier: 4.0.0-beta.103 |
| ... | @@ -4412,17 +4413,11 @@ packages: | ... | @@ -4412,17 +4413,11 @@ packages: |
| 4412 | '@tabler/icons@3.44.0': | 4413 | '@tabler/icons@3.44.0': |
| 4413 | resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} | 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 | '@tailwindcss/node@4.3.0': | 4416 | '@tailwindcss/node@4.3.0': |
| 4419 | resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} | 4417 | resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} |
| 4420 | 4418 | ||
| 4421 | '@tailwindcss/oxide-android-arm64@4.2.1': | 4419 | '@tailwindcss/node@4.3.2': |
| 4422 | resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} | 4420 | resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} |
| 4423 | engines: {node: '>= 20'} | ||
| 4424 | cpu: [arm64] | ||
| 4425 | os: [android] | ||
| 4426 | 4421 | ||
| 4427 | '@tailwindcss/oxide-android-arm64@4.3.0': | 4422 | '@tailwindcss/oxide-android-arm64@4.3.0': |
| 4428 | resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} | 4423 | resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} |
| ... | @@ -4430,11 +4425,11 @@ packages: | ... | @@ -4430,11 +4425,11 @@ packages: |
| 4430 | cpu: [arm64] | 4425 | cpu: [arm64] |
| 4431 | os: [android] | 4426 | os: [android] |
| 4432 | 4427 | ||
| 4433 | '@tailwindcss/oxide-darwin-arm64@4.2.1': | 4428 | '@tailwindcss/oxide-android-arm64@4.3.2': |
| 4434 | resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} | 4429 | resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} |
| 4435 | engines: {node: '>= 20'} | 4430 | engines: {node: '>= 20'} |
| 4436 | cpu: [arm64] | 4431 | cpu: [arm64] |
| 4437 | os: [darwin] | 4432 | os: [android] |
| 4438 | 4433 | ||
| 4439 | '@tailwindcss/oxide-darwin-arm64@4.3.0': | 4434 | '@tailwindcss/oxide-darwin-arm64@4.3.0': |
| 4440 | resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} | 4435 | resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} |
| ... | @@ -4442,10 +4437,10 @@ packages: | ... | @@ -4442,10 +4437,10 @@ packages: |
| 4442 | cpu: [arm64] | 4437 | cpu: [arm64] |
| 4443 | os: [darwin] | 4438 | os: [darwin] |
| 4444 | 4439 | ||
| 4445 | '@tailwindcss/oxide-darwin-x64@4.2.1': | 4440 | '@tailwindcss/oxide-darwin-arm64@4.3.2': |
| 4446 | resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} | 4441 | resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} |
| 4447 | engines: {node: '>= 20'} | 4442 | engines: {node: '>= 20'} |
| 4448 | cpu: [x64] | 4443 | cpu: [arm64] |
| 4449 | os: [darwin] | 4444 | os: [darwin] |
| 4450 | 4445 | ||
| 4451 | '@tailwindcss/oxide-darwin-x64@4.3.0': | 4446 | '@tailwindcss/oxide-darwin-x64@4.3.0': |
| ... | @@ -4454,11 +4449,11 @@ packages: | ... | @@ -4454,11 +4449,11 @@ packages: |
| 4454 | cpu: [x64] | 4449 | cpu: [x64] |
| 4455 | os: [darwin] | 4450 | os: [darwin] |
| 4456 | 4451 | ||
| 4457 | '@tailwindcss/oxide-freebsd-x64@4.2.1': | 4452 | '@tailwindcss/oxide-darwin-x64@4.3.2': |
| 4458 | resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} | 4453 | resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} |
| 4459 | engines: {node: '>= 20'} | 4454 | engines: {node: '>= 20'} |
| 4460 | cpu: [x64] | 4455 | cpu: [x64] |
| 4461 | os: [freebsd] | 4456 | os: [darwin] |
| 4462 | 4457 | ||
| 4463 | '@tailwindcss/oxide-freebsd-x64@4.3.0': | 4458 | '@tailwindcss/oxide-freebsd-x64@4.3.0': |
| 4464 | resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} | 4459 | resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} |
| ... | @@ -4466,11 +4461,11 @@ packages: | ... | @@ -4466,11 +4461,11 @@ packages: |
| 4466 | cpu: [x64] | 4461 | cpu: [x64] |
| 4467 | os: [freebsd] | 4462 | os: [freebsd] |
| 4468 | 4463 | ||
| 4469 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': | 4464 | '@tailwindcss/oxide-freebsd-x64@4.3.2': |
| 4470 | resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} | 4465 | resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} |
| 4471 | engines: {node: '>= 20'} | 4466 | engines: {node: '>= 20'} |
| 4472 | cpu: [arm] | 4467 | cpu: [x64] |
| 4473 | os: [linux] | 4468 | os: [freebsd] |
| 4474 | 4469 | ||
| 4475 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': | 4470 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': |
| 4476 | resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} | 4471 | resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} |
| ... | @@ -4478,12 +4473,11 @@ packages: | ... | @@ -4478,12 +4473,11 @@ packages: |
| 4478 | cpu: [arm] | 4473 | cpu: [arm] |
| 4479 | os: [linux] | 4474 | os: [linux] |
| 4480 | 4475 | ||
| 4481 | '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': | 4476 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': |
| 4482 | resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} | 4477 | resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} |
| 4483 | engines: {node: '>= 20'} | 4478 | engines: {node: '>= 20'} |
| 4484 | cpu: [arm64] | 4479 | cpu: [arm] |
| 4485 | os: [linux] | 4480 | os: [linux] |
| 4486 | libc: [glibc] | ||
| 4487 | 4481 | ||
| 4488 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': | 4482 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': |
| 4489 | resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} | 4483 | resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} |
| ... | @@ -4492,12 +4486,12 @@ packages: | ... | @@ -4492,12 +4486,12 @@ packages: |
| 4492 | os: [linux] | 4486 | os: [linux] |
| 4493 | libc: [glibc] | 4487 | libc: [glibc] |
| 4494 | 4488 | ||
| 4495 | '@tailwindcss/oxide-linux-arm64-musl@4.2.1': | 4489 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': |
| 4496 | resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} | 4490 | resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} |
| 4497 | engines: {node: '>= 20'} | 4491 | engines: {node: '>= 20'} |
| 4498 | cpu: [arm64] | 4492 | cpu: [arm64] |
| 4499 | os: [linux] | 4493 | os: [linux] |
| 4500 | libc: [musl] | 4494 | libc: [glibc] |
| 4501 | 4495 | ||
| 4502 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': | 4496 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': |
| 4503 | resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} | 4497 | resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} |
| ... | @@ -4506,12 +4500,12 @@ packages: | ... | @@ -4506,12 +4500,12 @@ packages: |
| 4506 | os: [linux] | 4500 | os: [linux] |
| 4507 | libc: [musl] | 4501 | libc: [musl] |
| 4508 | 4502 | ||
| 4509 | '@tailwindcss/oxide-linux-x64-gnu@4.2.1': | 4503 | '@tailwindcss/oxide-linux-arm64-musl@4.3.2': |
| 4510 | resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} | 4504 | resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} |
| 4511 | engines: {node: '>= 20'} | 4505 | engines: {node: '>= 20'} |
| 4512 | cpu: [x64] | 4506 | cpu: [arm64] |
| 4513 | os: [linux] | 4507 | os: [linux] |
| 4514 | libc: [glibc] | 4508 | libc: [musl] |
| 4515 | 4509 | ||
| 4516 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': | 4510 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': |
| 4517 | resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} | 4511 | resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} |
| ... | @@ -4520,12 +4514,12 @@ packages: | ... | @@ -4520,12 +4514,12 @@ packages: |
| 4520 | os: [linux] | 4514 | os: [linux] |
| 4521 | libc: [glibc] | 4515 | libc: [glibc] |
| 4522 | 4516 | ||
| 4523 | '@tailwindcss/oxide-linux-x64-musl@4.2.1': | 4517 | '@tailwindcss/oxide-linux-x64-gnu@4.3.2': |
| 4524 | resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} | 4518 | resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} |
| 4525 | engines: {node: '>= 20'} | 4519 | engines: {node: '>= 20'} |
| 4526 | cpu: [x64] | 4520 | cpu: [x64] |
| 4527 | os: [linux] | 4521 | os: [linux] |
| 4528 | libc: [musl] | 4522 | libc: [glibc] |
| 4529 | 4523 | ||
| 4530 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': | 4524 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': |
| 4531 | resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} | 4525 | resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} |
| ... | @@ -4534,8 +4528,15 @@ packages: | ... | @@ -4534,8 +4528,15 @@ packages: |
| 4534 | os: [linux] | 4528 | os: [linux] |
| 4535 | libc: [musl] | 4529 | libc: [musl] |
| 4536 | 4530 | ||
| 4537 | '@tailwindcss/oxide-wasm32-wasi@4.2.1': | 4531 | '@tailwindcss/oxide-linux-x64-musl@4.3.2': |
| 4538 | resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} | 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 | engines: {node: '>=14.0.0'} | 4540 | engines: {node: '>=14.0.0'} |
| 4540 | cpu: [wasm32] | 4541 | cpu: [wasm32] |
| 4541 | bundledDependencies: | 4542 | bundledDependencies: |
| ... | @@ -4546,8 +4547,8 @@ packages: | ... | @@ -4546,8 +4547,8 @@ packages: |
| 4546 | - '@emnapi/wasi-threads' | 4547 | - '@emnapi/wasi-threads' |
| 4547 | - tslib | 4548 | - tslib |
| 4548 | 4549 | ||
| 4549 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': | 4550 | '@tailwindcss/oxide-wasm32-wasi@4.3.2': |
| 4550 | resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} | 4551 | resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} |
| 4551 | engines: {node: '>=14.0.0'} | 4552 | engines: {node: '>=14.0.0'} |
| 4552 | cpu: [wasm32] | 4553 | cpu: [wasm32] |
| 4553 | bundledDependencies: | 4554 | bundledDependencies: |
| ... | @@ -4558,22 +4559,16 @@ packages: | ... | @@ -4558,22 +4559,16 @@ packages: |
| 4558 | - '@emnapi/wasi-threads' | 4559 | - '@emnapi/wasi-threads' |
| 4559 | - tslib | 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 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': | 4562 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': |
| 4568 | resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} | 4563 | resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} |
| 4569 | engines: {node: '>= 20'} | 4564 | engines: {node: '>= 20'} |
| 4570 | cpu: [arm64] | 4565 | cpu: [arm64] |
| 4571 | os: [win32] | 4566 | os: [win32] |
| 4572 | 4567 | ||
| 4573 | '@tailwindcss/oxide-win32-x64-msvc@4.2.1': | 4568 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': |
| 4574 | resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} | 4569 | resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} |
| 4575 | engines: {node: '>= 20'} | 4570 | engines: {node: '>= 20'} |
| 4576 | cpu: [x64] | 4571 | cpu: [arm64] |
| 4577 | os: [win32] | 4572 | os: [win32] |
| 4578 | 4573 | ||
| 4579 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': | 4574 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': |
| ... | @@ -4582,14 +4577,20 @@ packages: | ... | @@ -4582,14 +4577,20 @@ packages: |
| 4582 | cpu: [x64] | 4577 | cpu: [x64] |
| 4583 | os: [win32] | 4578 | os: [win32] |
| 4584 | 4579 | ||
| 4585 | '@tailwindcss/oxide@4.2.1': | 4580 | '@tailwindcss/oxide-win32-x64-msvc@4.3.2': |
| 4586 | resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} | 4581 | resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} |
| 4587 | engines: {node: '>= 20'} | 4582 | engines: {node: '>= 20'} |
| 4583 | cpu: [x64] | ||
| 4584 | os: [win32] | ||
| 4588 | 4585 | ||
| 4589 | '@tailwindcss/oxide@4.3.0': | 4586 | '@tailwindcss/oxide@4.3.0': |
| 4590 | resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} | 4587 | resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} |
| 4591 | engines: {node: '>= 20'} | 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 | '@tailwindcss/vite@4.3.0': | 4594 | '@tailwindcss/vite@4.3.0': |
| 4594 | resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} | 4595 | resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} |
| 4595 | peerDependencies: | 4596 | peerDependencies: |
| ... | @@ -6309,6 +6310,10 @@ packages: | ... | @@ -6309,6 +6310,10 @@ packages: |
| 6309 | end-of-stream@1.4.5: | 6310 | end-of-stream@1.4.5: |
| 6310 | resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} | 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 | enhanced-resolve@5.22.1: | 6317 | enhanced-resolve@5.22.1: |
| 6313 | resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} | 6318 | resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} |
| 6314 | engines: {node: '>=10.13.0'} | 6319 | engines: {node: '>=10.13.0'} |
| ... | @@ -7480,12 +7485,6 @@ packages: | ... | @@ -7480,12 +7485,6 @@ packages: |
| 7480 | lighthouse-logger@1.4.2: | 7485 | lighthouse-logger@1.4.2: |
| 7481 | resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} | 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 | lightningcss-android-arm64@1.32.0: | 7488 | lightningcss-android-arm64@1.32.0: |
| 7490 | resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} | 7489 | resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} |
| 7491 | engines: {node: '>= 12.0.0'} | 7490 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7498,12 +7497,6 @@ packages: | ... | @@ -7498,12 +7497,6 @@ packages: |
| 7498 | cpu: [arm64] | 7497 | cpu: [arm64] |
| 7499 | os: [darwin] | 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 | lightningcss-darwin-arm64@1.32.0: | 7500 | lightningcss-darwin-arm64@1.32.0: |
| 7508 | resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} | 7501 | resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} |
| 7509 | engines: {node: '>= 12.0.0'} | 7502 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7516,12 +7509,6 @@ packages: | ... | @@ -7516,12 +7509,6 @@ packages: |
| 7516 | cpu: [x64] | 7509 | cpu: [x64] |
| 7517 | os: [darwin] | 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 | lightningcss-darwin-x64@1.32.0: | 7512 | lightningcss-darwin-x64@1.32.0: |
| 7526 | resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} | 7513 | resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} |
| 7527 | engines: {node: '>= 12.0.0'} | 7514 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7534,12 +7521,6 @@ packages: | ... | @@ -7534,12 +7521,6 @@ packages: |
| 7534 | cpu: [x64] | 7521 | cpu: [x64] |
| 7535 | os: [freebsd] | 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 | lightningcss-freebsd-x64@1.32.0: | 7524 | lightningcss-freebsd-x64@1.32.0: |
| 7544 | resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} | 7525 | resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} |
| 7545 | engines: {node: '>= 12.0.0'} | 7526 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7552,12 +7533,6 @@ packages: | ... | @@ -7552,12 +7533,6 @@ packages: |
| 7552 | cpu: [arm] | 7533 | cpu: [arm] |
| 7553 | os: [linux] | 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 | lightningcss-linux-arm-gnueabihf@1.32.0: | 7536 | lightningcss-linux-arm-gnueabihf@1.32.0: |
| 7562 | resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} | 7537 | resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} |
| 7563 | engines: {node: '>= 12.0.0'} | 7538 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7571,13 +7546,6 @@ packages: | ... | @@ -7571,13 +7546,6 @@ packages: |
| 7571 | os: [linux] | 7546 | os: [linux] |
| 7572 | libc: [glibc] | 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 | lightningcss-linux-arm64-gnu@1.32.0: | 7549 | lightningcss-linux-arm64-gnu@1.32.0: |
| 7582 | resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} | 7550 | resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} |
| 7583 | engines: {node: '>= 12.0.0'} | 7551 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7592,13 +7560,6 @@ packages: | ... | @@ -7592,13 +7560,6 @@ packages: |
| 7592 | os: [linux] | 7560 | os: [linux] |
| 7593 | libc: [musl] | 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 | lightningcss-linux-arm64-musl@1.32.0: | 7563 | lightningcss-linux-arm64-musl@1.32.0: |
| 7603 | resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} | 7564 | resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} |
| 7604 | engines: {node: '>= 12.0.0'} | 7565 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7613,13 +7574,6 @@ packages: | ... | @@ -7613,13 +7574,6 @@ packages: |
| 7613 | os: [linux] | 7574 | os: [linux] |
| 7614 | libc: [glibc] | 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 | lightningcss-linux-x64-gnu@1.32.0: | 7577 | lightningcss-linux-x64-gnu@1.32.0: |
| 7624 | resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} | 7578 | resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} |
| 7625 | engines: {node: '>= 12.0.0'} | 7579 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7634,13 +7588,6 @@ packages: | ... | @@ -7634,13 +7588,6 @@ packages: |
| 7634 | os: [linux] | 7588 | os: [linux] |
| 7635 | libc: [musl] | 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 | lightningcss-linux-x64-musl@1.32.0: | 7591 | lightningcss-linux-x64-musl@1.32.0: |
| 7645 | resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} | 7592 | resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} |
| 7646 | engines: {node: '>= 12.0.0'} | 7593 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7654,12 +7601,6 @@ packages: | ... | @@ -7654,12 +7601,6 @@ packages: |
| 7654 | cpu: [arm64] | 7601 | cpu: [arm64] |
| 7655 | os: [win32] | 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 | lightningcss-win32-arm64-msvc@1.32.0: | 7604 | lightningcss-win32-arm64-msvc@1.32.0: |
| 7664 | resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} | 7605 | resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} |
| 7665 | engines: {node: '>= 12.0.0'} | 7606 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7672,12 +7613,6 @@ packages: | ... | @@ -7672,12 +7613,6 @@ packages: |
| 7672 | cpu: [x64] | 7613 | cpu: [x64] |
| 7673 | os: [win32] | 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 | lightningcss-win32-x64-msvc@1.32.0: | 7616 | lightningcss-win32-x64-msvc@1.32.0: |
| 7682 | resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} | 7617 | resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} |
| 7683 | engines: {node: '>= 12.0.0'} | 7618 | engines: {node: '>= 12.0.0'} |
| ... | @@ -7688,10 +7623,6 @@ packages: | ... | @@ -7688,10 +7623,6 @@ packages: |
| 7688 | resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} | 7623 | resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} |
| 7689 | engines: {node: '>= 12.0.0'} | 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 | lightningcss@1.32.0: | 7626 | lightningcss@1.32.0: |
| 7696 | resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} | 7627 | resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} |
| 7697 | engines: {node: '>= 12.0.0'} | 7628 | engines: {node: '>= 12.0.0'} |
| ... | @@ -9620,12 +9551,12 @@ packages: | ... | @@ -9620,12 +9551,12 @@ packages: |
| 9620 | tailwind-merge@3.6.0: | 9551 | tailwind-merge@3.6.0: |
| 9621 | resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} | 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 | tailwindcss@4.3.0: | 9554 | tailwindcss@4.3.0: |
| 9627 | resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} | 9555 | resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} |
| 9628 | 9556 | ||
| 9557 | tailwindcss@4.3.2: | ||
| 9558 | resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} | ||
| 9559 | |||
| 9629 | tapable@2.3.3: | 9560 | tapable@2.3.3: |
| 9630 | resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} | 9561 | resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} |
| 9631 | engines: {node: '>=6'} | 9562 | engines: {node: '>=6'} |
| ... | @@ -9881,12 +9812,22 @@ packages: | ... | @@ -9881,12 +9812,22 @@ packages: |
| 9881 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} | 9812 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} |
| 9882 | engines: {node: '>= 10.0.0'} | 9813 | engines: {node: '>= 10.0.0'} |
| 9883 | 9814 | ||
| 9884 | uniwind@1.7.0: | 9815 | uniwind@1.11.0: |
| 9885 | resolution: {integrity: sha512-kIixWI3OprrWf/ypjglV2xubYvzCjtTKk110rQmXn8A3ZA/7z/nK6P/8EMxbfPCxjkdvhLcMniafa4qJcYIhZA==} | 9816 | resolution: {integrity: sha512-e2mYlHrZzAQtZUf3STrfVDTetspv7p5vm/cu6p8kjLRQaL9r6QFG+MlwZ3hrI2LHnzgzjbSo+MwvoE380zz2JQ==} |
| 9817 | hasBin: true | ||
| 9886 | peerDependencies: | 9818 | peerDependencies: |
| 9819 | '@expo/metro-config': 57.0.12 | ||
| 9820 | metro: '*' | ||
| 9821 | metro-cache: '*' | ||
| 9822 | metro-transform-worker: '*' | ||
| 9887 | react: '>=19.0.0' | 9823 | react: '>=19.0.0' |
| 9888 | react-native: '>=0.81.0' | 9824 | react-native: '>=0.81.0' |
| 9889 | tailwindcss: '>=4' | 9825 | tailwindcss: '>=4' |
| 9826 | peerDependenciesMeta: | ||
| 9827 | '@expo/metro-config': | ||
| 9828 | optional: true | ||
| 9829 | metro-transform-worker: | ||
| 9830 | optional: true | ||
| 9890 | 9831 | ||
| 9891 | unpipe@1.0.0: | 9832 | unpipe@1.0.0: |
| 9892 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} | 9833 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} |
| ... | @@ -13702,7 +13643,7 @@ snapshots: | ... | @@ -13702,7 +13643,7 @@ snapshots: |
| 13702 | 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) | 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 | optional: true | 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 | dependencies: | 13647 | dependencies: |
| 13707 | react: 19.2.3 | 13648 | react: 19.2.3 |
| 13708 | 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) | 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,112 +14204,97 @@ snapshots: |
| 14263 | 14204 | ||
| 14264 | '@tabler/icons@3.44.0': {} | 14205 | '@tabler/icons@3.44.0': {} |
| 14265 | 14206 | ||
| 14266 | '@tailwindcss/node@4.2.1': | 14207 | '@tailwindcss/node@4.3.0': |
| 14267 | dependencies: | 14208 | dependencies: |
| 14268 | '@jridgewell/remapping': 2.3.5 | 14209 | '@jridgewell/remapping': 2.3.5 |
| 14269 | enhanced-resolve: 5.22.1 | 14210 | enhanced-resolve: 5.22.1 |
| 14270 | jiti: 2.7.0 | 14211 | jiti: 2.7.0 |
| 14271 | lightningcss: 1.31.1 | 14212 | lightningcss: 1.32.0 |
| 14272 | magic-string: 0.30.21 | 14213 | magic-string: 0.30.21 |
| 14273 | source-map-js: 1.2.1 | 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 | dependencies: | 14218 | dependencies: |
| 14278 | '@jridgewell/remapping': 2.3.5 | 14219 | '@jridgewell/remapping': 2.3.5 |
| 14279 | enhanced-resolve: 5.22.1 | 14220 | enhanced-resolve: 5.21.6 |
| 14280 | jiti: 2.7.0 | 14221 | jiti: 2.7.0 |
| 14281 | lightningcss: 1.32.0 | 14222 | lightningcss: 1.32.0 |
| 14282 | magic-string: 0.30.21 | 14223 | magic-string: 0.30.21 |
| 14283 | source-map-js: 1.2.1 | 14224 | source-map-js: 1.2.1 |
| 14284 | tailwindcss: 4.3.0 | 14225 | tailwindcss: 4.3.2 |
| 14285 | |||
| 14286 | '@tailwindcss/oxide-android-arm64@4.2.1': | ||
| 14287 | optional: true | ||
| 14288 | 14226 | ||
| 14289 | '@tailwindcss/oxide-android-arm64@4.3.0': | 14227 | '@tailwindcss/oxide-android-arm64@4.3.0': |
| 14290 | optional: true | 14228 | optional: true |
| 14291 | 14229 | ||
| 14292 | '@tailwindcss/oxide-darwin-arm64@4.2.1': | 14230 | '@tailwindcss/oxide-android-arm64@4.3.2': |
| 14293 | optional: true | 14231 | optional: true |
| 14294 | 14232 | ||
| 14295 | '@tailwindcss/oxide-darwin-arm64@4.3.0': | 14233 | '@tailwindcss/oxide-darwin-arm64@4.3.0': |
| 14296 | optional: true | 14234 | optional: true |
| 14297 | 14235 | ||
| 14298 | '@tailwindcss/oxide-darwin-x64@4.2.1': | 14236 | '@tailwindcss/oxide-darwin-arm64@4.3.2': |
| 14299 | optional: true | 14237 | optional: true |
| 14300 | 14238 | ||
| 14301 | '@tailwindcss/oxide-darwin-x64@4.3.0': | 14239 | '@tailwindcss/oxide-darwin-x64@4.3.0': |
| 14302 | optional: true | 14240 | optional: true |
| 14303 | 14241 | ||
| 14304 | '@tailwindcss/oxide-freebsd-x64@4.2.1': | 14242 | '@tailwindcss/oxide-darwin-x64@4.3.2': |
| 14305 | optional: true | 14243 | optional: true |
| 14306 | 14244 | ||
| 14307 | '@tailwindcss/oxide-freebsd-x64@4.3.0': | 14245 | '@tailwindcss/oxide-freebsd-x64@4.3.0': |
| 14308 | optional: true | 14246 | optional: true |
| 14309 | 14247 | ||
| 14310 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': | 14248 | '@tailwindcss/oxide-freebsd-x64@4.3.2': |
| 14311 | optional: true | 14249 | optional: true |
| 14312 | 14250 | ||
| 14313 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': | 14251 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': |
| 14314 | optional: true | 14252 | optional: true |
| 14315 | 14253 | ||
| 14316 | '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': | 14254 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': |
| 14317 | optional: true | 14255 | optional: true |
| 14318 | 14256 | ||
| 14319 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': | 14257 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': |
| 14320 | optional: true | 14258 | optional: true |
| 14321 | 14259 | ||
| 14322 | '@tailwindcss/oxide-linux-arm64-musl@4.2.1': | 14260 | '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': |
| 14323 | optional: true | 14261 | optional: true |
| 14324 | 14262 | ||
| 14325 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': | 14263 | '@tailwindcss/oxide-linux-arm64-musl@4.3.0': |
| 14326 | optional: true | 14264 | optional: true |
| 14327 | 14265 | ||
| 14328 | '@tailwindcss/oxide-linux-x64-gnu@4.2.1': | 14266 | '@tailwindcss/oxide-linux-arm64-musl@4.3.2': |
| 14329 | optional: true | 14267 | optional: true |
| 14330 | 14268 | ||
| 14331 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': | 14269 | '@tailwindcss/oxide-linux-x64-gnu@4.3.0': |
| 14332 | optional: true | 14270 | optional: true |
| 14333 | 14271 | ||
| 14334 | '@tailwindcss/oxide-linux-x64-musl@4.2.1': | 14272 | '@tailwindcss/oxide-linux-x64-gnu@4.3.2': |
| 14335 | optional: true | 14273 | optional: true |
| 14336 | 14274 | ||
| 14337 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': | 14275 | '@tailwindcss/oxide-linux-x64-musl@4.3.0': |
| 14338 | optional: true | 14276 | optional: true |
| 14339 | 14277 | ||
| 14340 | '@tailwindcss/oxide-wasm32-wasi@4.2.1': | 14278 | '@tailwindcss/oxide-linux-x64-musl@4.3.2': |
| 14341 | optional: true | 14279 | optional: true |
| 14342 | 14280 | ||
| 14343 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': | 14281 | '@tailwindcss/oxide-wasm32-wasi@4.3.0': |
| 14344 | optional: true | 14282 | optional: true |
| 14345 | 14283 | ||
| 14346 | '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': | 14284 | '@tailwindcss/oxide-wasm32-wasi@4.3.2': |
| 14347 | optional: true | 14285 | optional: true |
| 14348 | 14286 | ||
| 14349 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': | 14287 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': |
| 14350 | optional: true | 14288 | optional: true |
| 14351 | 14289 | ||
| 14352 | '@tailwindcss/oxide-win32-x64-msvc@4.2.1': | 14290 | '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': |
| 14353 | optional: true | 14291 | optional: true |
| 14354 | 14292 | ||
| 14355 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': | 14293 | '@tailwindcss/oxide-win32-x64-msvc@4.3.0': |
| 14356 | optional: true | 14294 | optional: true |
| 14357 | 14295 | ||
| 14358 | '@tailwindcss/oxide@4.2.1': | 14296 | '@tailwindcss/oxide-win32-x64-msvc@4.3.2': |
| 14359 | optionalDependencies: | 14297 | optional: true |
| 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 | ||
| 14372 | 14298 | ||
| 14373 | '@tailwindcss/oxide@4.3.0': | 14299 | '@tailwindcss/oxide@4.3.0': |
| 14374 | optionalDependencies: | 14300 | optionalDependencies: |
| ... | @@ -14385,6 +14311,21 @@ snapshots: | ... | @@ -14385,6 +14311,21 @@ snapshots: |
| 14385 | '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 | 14311 | '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 |
| 14386 | '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 | 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 | '@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))': | 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 | dependencies: | 14330 | dependencies: |
| 14390 | '@tailwindcss/node': 4.3.0 | 14331 | '@tailwindcss/node': 4.3.0 |
| ... | @@ -16174,6 +16115,11 @@ snapshots: | ... | @@ -16174,6 +16115,11 @@ snapshots: |
| 16174 | dependencies: | 16115 | dependencies: |
| 16175 | once: 1.4.0 | 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 | enhanced-resolve@5.22.1: | 16123 | enhanced-resolve@5.22.1: |
| 16178 | dependencies: | 16124 | dependencies: |
| 16179 | graceful-fs: 4.2.11 | 16125 | graceful-fs: 4.2.11 |
| ... | @@ -17676,99 +17622,66 @@ snapshots: | ... | @@ -17676,99 +17622,66 @@ snapshots: |
| 17676 | transitivePeerDependencies: | 17622 | transitivePeerDependencies: |
| 17677 | - supports-color | 17623 | - supports-color |
| 17678 | 17624 | ||
| 17679 | lightningcss-android-arm64@1.31.1: | ||
| 17680 | optional: true | ||
| 17681 | |||
| 17682 | lightningcss-android-arm64@1.32.0: | 17625 | lightningcss-android-arm64@1.32.0: |
| 17683 | optional: true | 17626 | optional: true |
| 17684 | 17627 | ||
| 17685 | lightningcss-darwin-arm64@1.30.1: | 17628 | lightningcss-darwin-arm64@1.30.1: |
| 17686 | optional: true | 17629 | optional: true |
| 17687 | 17630 | ||
| 17688 | lightningcss-darwin-arm64@1.31.1: | ||
| 17689 | optional: true | ||
| 17690 | |||
| 17691 | lightningcss-darwin-arm64@1.32.0: | 17631 | lightningcss-darwin-arm64@1.32.0: |
| 17692 | optional: true | 17632 | optional: true |
| 17693 | 17633 | ||
| 17694 | lightningcss-darwin-x64@1.30.1: | 17634 | lightningcss-darwin-x64@1.30.1: |
| 17695 | optional: true | 17635 | optional: true |
| 17696 | 17636 | ||
| 17697 | lightningcss-darwin-x64@1.31.1: | ||
| 17698 | optional: true | ||
| 17699 | |||
| 17700 | lightningcss-darwin-x64@1.32.0: | 17637 | lightningcss-darwin-x64@1.32.0: |
| 17701 | optional: true | 17638 | optional: true |
| 17702 | 17639 | ||
| 17703 | lightningcss-freebsd-x64@1.30.1: | 17640 | lightningcss-freebsd-x64@1.30.1: |
| 17704 | optional: true | 17641 | optional: true |
| 17705 | 17642 | ||
| 17706 | lightningcss-freebsd-x64@1.31.1: | ||
| 17707 | optional: true | ||
| 17708 | |||
| 17709 | lightningcss-freebsd-x64@1.32.0: | 17643 | lightningcss-freebsd-x64@1.32.0: |
| 17710 | optional: true | 17644 | optional: true |
| 17711 | 17645 | ||
| 17712 | lightningcss-linux-arm-gnueabihf@1.30.1: | 17646 | lightningcss-linux-arm-gnueabihf@1.30.1: |
| 17713 | optional: true | 17647 | optional: true |
| 17714 | 17648 | ||
| 17715 | lightningcss-linux-arm-gnueabihf@1.31.1: | ||
| 17716 | optional: true | ||
| 17717 | |||
| 17718 | lightningcss-linux-arm-gnueabihf@1.32.0: | 17649 | lightningcss-linux-arm-gnueabihf@1.32.0: |
| 17719 | optional: true | 17650 | optional: true |
| 17720 | 17651 | ||
| 17721 | lightningcss-linux-arm64-gnu@1.30.1: | 17652 | lightningcss-linux-arm64-gnu@1.30.1: |
| 17722 | optional: true | 17653 | optional: true |
| 17723 | 17654 | ||
| 17724 | lightningcss-linux-arm64-gnu@1.31.1: | ||
| 17725 | optional: true | ||
| 17726 | |||
| 17727 | lightningcss-linux-arm64-gnu@1.32.0: | 17655 | lightningcss-linux-arm64-gnu@1.32.0: |
| 17728 | optional: true | 17656 | optional: true |
| 17729 | 17657 | ||
| 17730 | lightningcss-linux-arm64-musl@1.30.1: | 17658 | lightningcss-linux-arm64-musl@1.30.1: |
| 17731 | optional: true | 17659 | optional: true |
| 17732 | 17660 | ||
| 17733 | lightningcss-linux-arm64-musl@1.31.1: | ||
| 17734 | optional: true | ||
| 17735 | |||
| 17736 | lightningcss-linux-arm64-musl@1.32.0: | 17661 | lightningcss-linux-arm64-musl@1.32.0: |
| 17737 | optional: true | 17662 | optional: true |
| 17738 | 17663 | ||
| 17739 | lightningcss-linux-x64-gnu@1.30.1: | 17664 | lightningcss-linux-x64-gnu@1.30.1: |
| 17740 | optional: true | 17665 | optional: true |
| 17741 | 17666 | ||
| 17742 | lightningcss-linux-x64-gnu@1.31.1: | ||
| 17743 | optional: true | ||
| 17744 | |||
| 17745 | lightningcss-linux-x64-gnu@1.32.0: | 17667 | lightningcss-linux-x64-gnu@1.32.0: |
| 17746 | optional: true | 17668 | optional: true |
| 17747 | 17669 | ||
| 17748 | lightningcss-linux-x64-musl@1.30.1: | 17670 | lightningcss-linux-x64-musl@1.30.1: |
| 17749 | optional: true | 17671 | optional: true |
| 17750 | 17672 | ||
| 17751 | lightningcss-linux-x64-musl@1.31.1: | ||
| 17752 | optional: true | ||
| 17753 | |||
| 17754 | lightningcss-linux-x64-musl@1.32.0: | 17673 | lightningcss-linux-x64-musl@1.32.0: |
| 17755 | optional: true | 17674 | optional: true |
| 17756 | 17675 | ||
| 17757 | lightningcss-win32-arm64-msvc@1.30.1: | 17676 | lightningcss-win32-arm64-msvc@1.30.1: |
| 17758 | optional: true | 17677 | optional: true |
| 17759 | 17678 | ||
| 17760 | lightningcss-win32-arm64-msvc@1.31.1: | ||
| 17761 | optional: true | ||
| 17762 | |||
| 17763 | lightningcss-win32-arm64-msvc@1.32.0: | 17679 | lightningcss-win32-arm64-msvc@1.32.0: |
| 17764 | optional: true | 17680 | optional: true |
| 17765 | 17681 | ||
| 17766 | lightningcss-win32-x64-msvc@1.30.1: | 17682 | lightningcss-win32-x64-msvc@1.30.1: |
| 17767 | optional: true | 17683 | optional: true |
| 17768 | 17684 | ||
| 17769 | lightningcss-win32-x64-msvc@1.31.1: | ||
| 17770 | optional: true | ||
| 17771 | |||
| 17772 | lightningcss-win32-x64-msvc@1.32.0: | 17685 | lightningcss-win32-x64-msvc@1.32.0: |
| 17773 | optional: true | 17686 | optional: true |
| 17774 | 17687 | ||
| ... | @@ -17787,22 +17700,6 @@ snapshots: | ... | @@ -17787,22 +17700,6 @@ snapshots: |
| 17787 | lightningcss-win32-arm64-msvc: 1.30.1 | 17700 | lightningcss-win32-arm64-msvc: 1.30.1 |
| 17788 | lightningcss-win32-x64-msvc: 1.30.1 | 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 | lightningcss@1.32.0: | 17703 | lightningcss@1.32.0: |
| 17807 | dependencies: | 17704 | dependencies: |
| 17808 | detect-libc: 2.1.2 | 17705 | detect-libc: 2.1.2 |
| ... | @@ -20463,10 +20360,10 @@ snapshots: | ... | @@ -20463,10 +20360,10 @@ snapshots: |
| 20463 | 20360 | ||
| 20464 | tailwind-merge@3.6.0: {} | 20361 | tailwind-merge@3.6.0: {} |
| 20465 | 20362 | ||
| 20466 | tailwindcss@4.2.1: {} | ||
| 20467 | |||
| 20468 | tailwindcss@4.3.0: {} | 20363 | tailwindcss@4.3.0: {} |
| 20469 | 20364 | ||
| 20365 | tailwindcss@4.3.2: {} | ||
| 20366 | |||
| 20470 | tapable@2.3.3: {} | 20367 | tapable@2.3.3: {} |
| 20471 | 20368 | ||
| 20472 | tar@7.5.16: | 20369 | tar@7.5.16: |
| ... | @@ -20702,15 +20599,20 @@ snapshots: | ... | @@ -20702,15 +20599,20 @@ snapshots: |
| 20702 | 20599 | ||
| 20703 | universalify@2.0.1: {} | 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 | dependencies: | 20603 | dependencies: |
| 20707 | '@tailwindcss/node': 4.2.1 | 20604 | '@tailwindcss/node': 4.3.2 |
| 20708 | '@tailwindcss/oxide': 4.2.1 | 20605 | '@tailwindcss/oxide': 4.3.2 |
| 20709 | culori: 4.0.2 | 20606 | culori: 4.0.2 |
| 20710 | lightningcss: 1.30.1 | 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 | react: 19.2.3 | 20610 | react: 19.2.3 |
| 20712 | 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) | 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 | tailwindcss: 4.3.0 | 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 | unpipe@1.0.0: {} | 20617 | unpipe@1.0.0: {} |
| 20716 | 20618 |
pnpm-workspace.yaml+1| ... | @@ -157,6 +157,7 @@ patchedDependencies: | ... | @@ -157,6 +157,7 @@ patchedDependencies: |
| 157 | react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch | 157 | react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch |
| 158 | react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch | 158 | react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch |
| 159 | react-native-screens@4.26.2: patches/react-native-screens@4.26.2.patch | 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 | peerDependencyRules: | 162 | peerDependencyRules: |
| 162 | allowAny: | 163 | allowAny: |