From e1fda9d6601628007b480f5d7417aec72bdce48d Mon Sep 17 00:00:00 2001 From: jackjieyyy <53422254+jackjieYYY@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:18:39 +1000 Subject: [PATCH 1/3] Refactor layout size handling and remove deprecated query dependencies - Replaced the use of `useLayoutSize` with `useMedia` from Tamagui for responsive design. - Updated components to use the new media queries for layout size adjustments. - Removed the `layout-size-provider` and associated tests as they are no longer needed. - Simplified API calls by directly using the relevant APIs instead of through a dependency injection mechanism. - Adjusted tests to reflect the removal of layout size provider and updated mocking strategies. --- AGENTS.md | 9 +- package-lock.json | 422 +++++++++--------- package.json | 20 +- scripts/generate-dashboard-assets.mjs | 12 +- src/app/(app)/dashboard/_layout.tsx | 7 +- src/app/(app)/settings/(tabs)/_layout.tsx | 6 +- src/components/layout/section-page-header.tsx | 6 +- src/components/ui/item-artwork-config.ts | 6 - src/components/ui/item-artwork.test.tsx | 18 +- src/components/ui/item-artwork.tsx | 36 +- .../ui/terminal/terminal-marquee.tsx | 2 +- src/features/auth/api/index.ts | 4 + src/features/auth/components/access-orbit.tsx | 15 +- src/features/auth/components/login-form.tsx | 4 +- .../auth/components/terminal-brand.tsx | 21 +- src/features/auth/queries.ts | 9 +- src/features/auth/screens/auth-screen.tsx | 18 +- src/features/dashboard/api/index.ts | 6 + .../components/dashboard-secondary-header.tsx | 20 +- .../components/dashboard-shell.test.tsx | 79 +--- .../dashboard/components/dashboard-shell.tsx | 2 +- .../components/game-account-overview-view.tsx | 20 +- .../components/game-hosting-config-view.tsx | 14 +- .../inventory/components/inventory-cell.tsx | 49 +- .../components/inventory-view.test.tsx | 12 +- .../inventory/components/inventory-view.tsx | 60 ++- .../components/operator-card.test.tsx | 12 +- .../components/operator-card.tsx | 8 +- .../components/operator-roster-view.test.tsx | 25 +- .../components/operator-roster-view.tsx | 9 +- src/features/dashboard/queries.ts | 9 +- src/features/dashboard/resources.test.tsx | 30 +- src/features/dashboard/resources.ts | 9 +- .../large-screen-navigation-sidebar.tsx | 3 +- .../components/navigation-frame.tsx | 8 +- .../components/navigation-header.tsx | 6 +- .../components/settings-pager-tabs.tsx | 2 - .../navigation/navigation-frames.test.tsx | 10 +- .../navigation/screens/settings-frame.tsx | 8 +- .../session/components/session-shell.tsx | 2 +- src/features/settings/api-node/api/index.ts | 4 + .../settings/api-node/queries.test.tsx | 11 +- src/features/settings/api-node/queries.ts | 4 +- .../screens/network-settings-screen.test.tsx | 4 - .../screens/network-settings-screen.tsx | 33 +- .../settings/components/settings-page.tsx | 10 +- .../contributors-operations-roster.tsx | 10 +- .../components/contributors-tribute.tsx | 18 +- .../screens/contributors-screen.tsx | 2 +- .../screens/account-settings-screen.test.tsx | 4 - .../screens/account-settings-screen.tsx | 26 +- src/providers/layout-size-provider.test.tsx | 28 -- src/providers/layout-size-provider.tsx | 8 - src/schemas/layout-size/index.ts | 2 - .../layout-size/layout-size.schema.test.ts | 13 - src/schemas/layout-size/layout-size.schema.ts | 5 - src/services/query-dependencies.ts | 39 -- tamagui.config.ts | 3 + tests/route-layouts.test.tsx | 4 - 59 files changed, 499 insertions(+), 747 deletions(-) delete mode 100644 src/components/ui/item-artwork-config.ts delete mode 100644 src/providers/layout-size-provider.test.tsx delete mode 100644 src/providers/layout-size-provider.tsx delete mode 100644 src/schemas/layout-size/index.ts delete mode 100644 src/schemas/layout-size/layout-size.schema.test.ts delete mode 100644 src/schemas/layout-size/layout-size.schema.ts delete mode 100644 src/services/query-dependencies.ts diff --git a/AGENTS.md b/AGENTS.md index 0cfc315..6bf974b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Screen / Route - The App Store is the single owner of client state and client workflows; screens and routes read it through selectors and call its actions. - The Feature Query Layer (TanStack Query) is the single owner of server state: cache, mutations, and SSE updates. - Store actions own client workflows; query hooks, options, and mutations own server operations. -- Screens never call the API service or MMKV directly; presentational UI receives values and callbacks through props and never imports the Store, query dependencies, services, or MMKV. +- Screens never call the API service or MMKV directly; presentational UI receives values and callbacks through props and never imports the Store, query layer, services, or MMKV. - Combine Store and Query state in the owning feature hook, not in every Route. - Never mirror state across the boundary: no Query -> Store and no Store -> Query copies. - Validate server responses, SSE payloads, and persisted storage once at their ingress boundary with the owning Valibot schema. Never keep synchronized copies of the same data in UI, networking, and persistence. @@ -39,6 +39,13 @@ Use Tamagui as the default UI and styling layer. Before writing Tamagui code, ru - Keep `tamagui` and `@tamagui/*` versions aligned. After UI or config changes run `npm run tamagui:check`, `npm run tamagui:generate`, and `npx tsc --noEmit`. - Put user-visible prose in i18n resources, not hardcoded strings. +Closure has two width layouts. Small is the default; large starts at 768px. + +- Use small styles as component defaults and `$large` for structural overrides. Do not introduce additional width breakpoints. +- Use flex, wrap, or grid for continuous resizing instead of adding breakpoints. +- Call `useMedia().large` only when JavaScript values or different render trees are required. Read it at the lowest component that consumes it; never pass layout size through props. +- Do not create layout schemas, layout providers, or wrapper hooks around Tamagui media APIs. + # Valibot Schema-First Domain Models Valibot schemas in `src/schemas//` are the source of truth for domain data and cross-boundary input. The full contract lives in `src/schemas/README.md`. diff --git a/package-lock.json b/package-lock.json index 628f663..fdf1e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,33 +11,33 @@ "dependencies": { "@expo-google-fonts/geist-mono": "^0.4.3", "@expo-google-fonts/oxanium": "^0.4.2", - "@expo/ui": "~57.0.13", + "@expo/ui": "~57.0.14", "@shopify/flash-list": "2.0.2", "@shopify/react-native-skia": "2.6.2", "@tamagui/config": "^2.5.1", "@tamagui/portal": "^2.5.1", "@tanstack/react-query": "^5.101.4", - "expo": "~57.0.16", + "expo": "~57.0.17", "expo-blur": "~57.0.2", "expo-constants": "~57.0.8", - "expo-dev-client": "~57.0.15", + "expo-dev-client": "~57.0.16", "expo-device": "~57.0.1", "expo-font": "~57.0.1", "expo-image": "~57.0.3", - "expo-linking": "~57.0.7", + "expo-linking": "~57.0.8", "expo-localization": "~57.0.1", - "expo-router": "~57.0.16", + "expo-router": "~57.0.17", "expo-splash-screen": "~57.0.8", "expo-status-bar": "~57.0.1", "expo-symbols": "~57.0.2", - "expo-system-ui": "~57.0.2", + "expo-system-ui": "~57.0.3", "expo-web-browser": "~57.0.2", "i18next": "^26.3.6", "lucide-react-native": "^1.26.0", "react": "19.2.3", "react-dom": "19.2.3", "react-i18next": "^17.0.11", - "react-native": "0.86.2", + "react-native": "0.86.3", "react-native-gesture-handler": "~2.32.0", "react-native-mmkv": "4.3.2", "react-native-nitro-modules": "0.36.5", @@ -54,7 +54,7 @@ "zustand": "^5.0.14" }, "devDependencies": { - "@react-native/jest-preset": "0.86.2", + "@react-native/jest-preset": "^0.86.3", "@tamagui/cli": "^2.5.1", "@tamagui/metro-plugin": "^2.5.1", "@testing-library/react-native": "^14.0.1", @@ -62,10 +62,10 @@ "@types/node": "~26.1.1", "@types/react": "~19.2.2", "eslint": "^9.0.0", - "eslint-config-expo": "~57.0.1", + "eslint-config-expo": "~57.0.2", "expo-doctor": "^1.20.1", "jest": "~29.7.0", - "jest-expo": "~57.0.4", + "jest-expo": "~57.0.5", "sharp": "^0.35.3", "typescript": "~6.0.3", "typescript-eslint": "^8.65.0" @@ -2135,9 +2135,9 @@ "license": "MIT AND OFL-1.1" }, "node_modules/@expo-google-fonts/material-symbols": { - "version": "0.4.43", - "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.43.tgz", - "integrity": "sha512-jJzgVcbkJnl48X5iBFNaL5CQHwoBElJapl8unc2Ri8DnyuBR+gRiuAD4gu3ESZjjo3sknIoYIwloOsWzKbLknQ==", + "version": "0.4.45", + "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.45.tgz", + "integrity": "sha512-D5gd/uFa7KfMJX9S11hvej4GpQnBl27oKiUgyIq3cLTPtyDOWBwRgmeePIdV6+PGcedlDkaCND2BnRgAkxzNaA==", "license": "MIT AND Apache-2.0" }, "node_modules/@expo-google-fonts/oxanium": { @@ -2309,12 +2309,12 @@ } }, "node_modules/@expo/inline-modules": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.1.6.tgz", - "integrity": "sha512-5f6EiOIKsFj9zlrCBet4ZIQRPEa9dBUdQgTzpjYwdZ8Z/M8W5lqMVL9dEs4HYKvEmDnqv0dQuDkFLyRSwu/DAQ==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.1.7.tgz", + "integrity": "sha512-Bz/khd1gIJqDkje7t5ejD5e9jFbm4xEJzWSwRKadRo6gruepbw1xJ3Eb+e58OS8fY1ZNQYjzZbspnuph67sN0g==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~57.0.8" + "@expo/config-plugins": "~57.0.9" } }, "node_modules/@expo/json-file": { @@ -2328,19 +2328,19 @@ } }, "node_modules/@expo/local-build-cache-provider": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-57.0.7.tgz", - "integrity": "sha512-Hq5xWhXJWuyH3CWh3uqDWoHtxM0XYqLs9h2OzRWImBJahCPfePOo4kmw2uTEB6qHT5T47eqO4jtDG3MRGGZCpw==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-57.0.8.tgz", + "integrity": "sha512-SEdE0pAQrr90bRh3MNR0ZuwoIBw390cYdFgbn7Vk0Mtm9EHaBfP7kYj+2hXnXZJgOEm3/JMtfoD3rV7rWA9FGg==", "license": "MIT", "dependencies": { - "@expo/config": "~57.0.8", + "@expo/config": "~57.0.9", "chalk": "^4.1.2" } }, "node_modules/@expo/log-box": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-57.0.3.tgz", - "integrity": "sha512-qv/cMliNax6es07Un/4IGJIcs4PUuhTKKTrJZX/0X2YRcOT5cqm/QicibZwTIbiZWi1i8P4YBRQTfFT2B17giw==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-57.0.4.tgz", + "integrity": "sha512-IxwS9s1L2muj8mj8AQSuiy7u8OFJdc02NRFo2me/Tj6DiaeG5SREqmpBE4rQpR2cadqSg5jl8Qab8Cjie616dg==", "license": "MIT", "dependencies": { "@expo/dom-webview": "^57.0.1", @@ -2377,9 +2377,9 @@ } }, "node_modules/@expo/metro-config": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.10.tgz", - "integrity": "sha512-YHcocILXfcBrwPlX5CtMKy0MitssjyHQg7ndFq+P26GzMnk8uXMpw9PFo/aNaAwh7d27/cOhbZON/TJdoHfVdQ==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.11.tgz", + "integrity": "sha512-aIKcCAQALafeIf+HOoHgjj7hOb771VQRFZARDjm+GJRT74Lb3bYHjkATtS4Wv2zqVx/aFYkNqYNqPF/Py3kSlw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -2430,19 +2430,19 @@ } }, "node_modules/@expo/metro-runtime": { - "version": "57.0.13", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.13.tgz", - "integrity": "sha512-o5VKd/vGti4z8uEJRAvKgzUZf+b5UE6eXx9R9rjT5UFmcnjCGXjv5LNbzvcb8wiSSakDDXE/z4wpOhJEPqV5JA==", + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.14.tgz", + "integrity": "sha512-LVpsfF0QOzlLSKlHYpxgW1cP1NKcYeaoalLO18jYHgqseSveEkb9VOffFpicTQ7AN03odZ4fgkUF+p5YpoY4yw==", "license": "MIT", "dependencies": { - "@expo/log-box": "^57.0.3", + "@expo/log-box": "^57.0.4", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { - "@expo/log-box": "^57.0.3", + "@expo/log-box": "^57.0.4", "expo": "*", "react": "*", "react-dom": "*", @@ -2786,9 +2786,9 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "57.0.14", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.14.tgz", - "integrity": "sha512-Lopsj0O/gqjVf94ZL8OH8Q7OV7YsREzxC+ScUQjgmYP5DTLlB193C6WO4hq/Mtia52NBLVAjRVMbW6TUJQtiWA==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.15.tgz", + "integrity": "sha512-xTbWHroj0PDmlbqvmU+zF9ZZxveJkiuyiPoeRJYRGruFHebRAWnoTdw5S7d/UCzDBI8ropGGu9g2eb2nMxtvAw==", "license": "MIT", "dependencies": { "@expo/config": "~57.0.9", @@ -2796,17 +2796,17 @@ "@expo/config-types": "^57.0.2", "@expo/image-utils": "^0.11.5", "@expo/json-file": "^11.0.1", - "@react-native/normalize-colors": "0.86.2", + "@react-native/normalize-colors": "0.86.3", "debug": "^4.3.1", - "expo-modules-autolinking": "~57.0.11", + "expo-modules-autolinking": "~57.0.12", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.3.tgz", + "integrity": "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==", "license": "MIT" }, "node_modules/@expo/require-utils": { @@ -2859,9 +2859,9 @@ "license": "MIT" }, "node_modules/@expo/ui": { - "version": "57.0.13", - "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.13.tgz", - "integrity": "sha512-vGM2iVFKFg4P+XESrPPfCkHws9HuE5bzzw3BuxdJjIcHiVqq3lQlIyDTMTMg66sjYVRtqR6wAeadj1uDFYcRcg==", + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.14.tgz", + "integrity": "sha512-m8Z5dCplkOOQ70lu/xfB67qhk00Muw4U1T9frHBIW3LxxJeJZQ+jOmV6vCk6O6zVgoG/ib1et7PeX6etxI4Rsw==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.1.0", @@ -5031,31 +5031,31 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.2.tgz", - "integrity": "sha512-vcX/mBjWAVnWofu7KecotquI2unZ/tITwA7OGdq/mdY/zmGXIEvYhfEYyOQij/LRqi9WAL+iizInTBWnxDhK/Q==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.3.tgz", + "integrity": "sha512-TDhgCZA4wjJg84d5A9swiOQYPIWSKEEVdg9IwMFZDupQzW/F3QoLUrfAJOcalgqTDA9/buTB8awhE3Whwg6u9Q==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.2.tgz", - "integrity": "sha512-NNDZqOlNbH5SzgPks1jFDYH3234Rpa5e/nhZymxhIiBH3NcE3uD+rGj/HWXhH7nHF2ToGK6XbUpqy7nmJPeh+g==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.3.tgz", + "integrity": "sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.86.2" + "@react-native/codegen": "0.86.3" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/babel-preset": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.2.tgz", - "integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.3.tgz", + "integrity": "sha512-/eqs/Hy9RZRcjdcs4wj3Cqmxvtb3NM5g+Uuh1RIvsjynMO8PRsrVWWLWgBcZL/jYUo+ogXd2NFB0W8L5Bg89xw==", "license": "MIT", "peer": true, "dependencies": { @@ -5088,7 +5088,7 @@ "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.86.2", + "@react-native/babel-plugin-codegen": "0.86.3", "babel-plugin-syntax-hermes-parser": "0.36.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -5101,9 +5101,9 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.2.tgz", - "integrity": "sha512-xKkudsahUJ1n//55g4fXk5BStVqqmZlz8HQveL45ZxcfDnwvhuYe2GymksQANFsSN+slvrarjrfq8kIxJzbceA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.3.tgz", + "integrity": "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -5122,12 +5122,12 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.2.tgz", - "integrity": "sha512-YHXNKoM6Y/HjREySZ5arET2xgiHgg67r1MdwJB//MPJAJ0Xc5g0u6UHxY9VzsHO3Y07dre6s0BinYwjt1SEWvQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.3.tgz", + "integrity": "sha512-qSDL9LQc5mZSZPNczT95WU9YQuPzxBklgON9vLhhqfI0yWIwKInqFx88dQ/uiEXBtf0yossthaQIqA4Ml6bF6g==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.86.2", + "@react-native/dev-middleware": "0.86.3", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.84.3", @@ -5140,7 +5140,7 @@ }, "peerDependencies": { "@react-native-community/cli": "*", - "@react-native/metro-config": "0.86.2" + "@react-native/metro-config": "0.86.3" }, "peerDependenciesMeta": { "@react-native-community/cli": { @@ -5152,18 +5152,18 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.2.tgz", - "integrity": "sha512-KGS1aV5F6cIqpnoIUhLBXyVzy1oAj8jBFGau6vX4Vy0HXRJN7p+68RU7x6NuyraHvQcR14ccMGT5TkFuNjQ4gA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.3.tgz", + "integrity": "sha512-TQmeofQ0PcuylhhlleOeuzHYZfbrgm3gayXzowqUEzgRisTm1D40/J3ggqs7XkQi5HP5ZA3n8dHmKL9vIzPcsw==", "license": "BSD-3-Clause", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.2.tgz", - "integrity": "sha512-/TaVJ2+gGajZPJGrFaObUQmHmlaxAlfmOPZicl6pNKDUjzSgFMpcLkdTOExvb+USYTVdGX1XwxXyvjQdUO2bvg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.3.tgz", + "integrity": "sha512-O4ds+J7xZfxkbih9T+cAGegBdvKSPKYJm/lDgC9CpEjFMkmzWTpVLU3Qsv9sqZuo58z+sGhIcJfPsCFFWHpqbQ==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6", @@ -5175,14 +5175,14 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.2.tgz", - "integrity": "sha512-B7L0vKvg+IcEElT7Vpqh1xj5yJAqWUegjbP+bQRaorJMAYnv11GkliTnZV2AdTDfZQJWgOEx8i8LGkHkUg7bnA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.3.tgz", + "integrity": "sha512-LiEPTqTg/63bYUnrPyHLfjTDCNhA/+CUqI1+DsA9tYyewtSbULd5awsva6SgE10I+2iMhgKXS3ymkhU/kSCrGA==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.86.2", - "@react-native/debugger-shell": "0.86.2", + "@react-native/debugger-frontend": "0.86.3", + "@react-native/debugger-shell": "0.86.3", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", @@ -5198,23 +5198,23 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.2.tgz", - "integrity": "sha512-2F6x14NcHMpVmfTTFKfMkpV5dZedZrLiv6PE+c3vgnesV2bjleUBydr4U+NI8VkI7OwW71L0A5qQ76I9LCrfoQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.3.tgz", + "integrity": "sha512-lxmx0GqLEWRIpZfpYFXlYVIs3ENQwaW6Vmp6oi29l2GoQJ1wZfFZRdMimDWlGEk8LKfHar3QH3iaPMkTcK9lEQ==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/jest-preset": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/jest-preset/-/jest-preset-0.86.2.tgz", - "integrity": "sha512-wneoqwKWdv6wIcWotVgp6NMlMWcJDjxDnp7fVYym2Pn6JLgAKgkbmuHGmxW0cLCGoa9wtcO680uciv+Kzx0G7w==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/jest-preset/-/jest-preset-0.86.3.tgz", + "integrity": "sha512-oMmBwXUpTgJvGlcXQQPuiQLykoM8jsWYp6RwdJ2NWyIWzJcGp1rmSr3XV7istoIcUt0wdOVLtQs0A0nDm9ND+A==", "devOptional": true, "license": "MIT", "dependencies": { "@jest/create-cache-key-function": "^29.7.0", - "@react-native/js-polyfills": "0.86.2", + "@react-native/js-polyfills": "0.86.3", "babel-jest": "^29.7.0", "jest-environment-node": "^29.7.0", "regenerator-runtime": "^0.13.2" @@ -5227,23 +5227,23 @@ } }, "node_modules/@react-native/js-polyfills": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.2.tgz", - "integrity": "sha512-bIwNcGBaQ74shB5z1mRkxOpjikimuwsnOCEkZSzL67Z1FTyK1ObpENfyd2QvcvVW9Cjl+tHuw9ynpBnb2jPoJQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.3.tgz", + "integrity": "sha512-eYIJ0es967+tePBFQDnl/gidVFxLns3fnbiK6rxscQrGodvuUO6hwxpQnfNynJ8MjbbndImXihXjcnJdc7SzJg==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/metro-babel-transformer": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz", - "integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.3.tgz", + "integrity": "sha512-0nlwVkG0uT9o72nrGTKUz/IqDnCkM4zE0ihe4r+OJcxH93yBdDm58AXJ3EXZeSjv3osHEFXU7ceajdZiS34mTw==", "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.86.2", + "@react-native/babel-preset": "0.86.3", "hermes-parser": "0.36.0", "nullthrows": "^1.1.1" }, @@ -5255,14 +5255,14 @@ } }, "node_modules/@react-native/metro-config": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.2.tgz", - "integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.3.tgz", + "integrity": "sha512-qdzDMepV2xdUhsO4XC7idnnbt8K6+Hd59AjeK9y4KS86fxSQq5oL0TAbqUuajDGR7beNQoYTuS6AowN/zP0oaQ==", "license": "MIT", "peer": true, "dependencies": { - "@react-native/js-polyfills": "0.86.2", - "@react-native/metro-babel-transformer": "0.86.2", + "@react-native/js-polyfills": "0.86.3", + "@react-native/metro-babel-transformer": "0.86.3", "metro-config": "^0.84.3", "metro-runtime": "^0.84.3" }, @@ -5283,9 +5283,9 @@ "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.2.tgz", - "integrity": "sha512-uO0J72gh3EvE+1/GHRk18QRyBDTRHRB0AraAfojsRjbT7VMuJwKrZYaKGshavoaEud6aw00ZB9/8mTMIKjjcAw==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.3.tgz", + "integrity": "sha512-1j44NEyNn05Ut40vHAmoSWbsIcybFkMAOBTwQt1PrESyfSS+qBoyU1LGIogNva0VIa0rQyEC5PzbA7R4/7Nhyw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -5297,7 +5297,7 @@ "peerDependencies": { "@types/react": "^19.2.0", "react": "*", - "react-native": "0.86.2" + "react-native": "0.86.3" }, "peerDependenciesMeta": { "@types/react": { @@ -9212,9 +9212,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.8.tgz", - "integrity": "sha512-i4XOFUayGudQ845hbzKYYDAr2CL9jF1CQh8ehZTlNu3SKsWEIM+1KyY91P/3aqiEDsQOu5Pa/dVs937HvQTXig==", + "version": "57.0.9", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.9.tgz", + "integrity": "sha512-T19biGOBTnMp161PyBqWOoSIEeZug8/EjtuUdVob8JPQKZMTalHUQaQt+qtQQE7XsEkZJ6erb3k0CohneTFzQg==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -9253,7 +9253,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-plugin-codegen": "0.86.2", + "@react-native/babel-plugin-codegen": "0.86.3", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.36.0", @@ -9263,7 +9263,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^57.0.12", + "expo-widgets": "^57.0.13", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -11212,9 +11212,9 @@ } }, "node_modules/eslint-config-expo": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.1.tgz", - "integrity": "sha512-OsLAReXOF3uXBwyxxw4f55sORYC+lOH4pSaGdU9MnYhRpjqPziK2uCnRxjLTGKeAKER7GSX5Wz9kNMGJT1MZEw==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.2.tgz", + "integrity": "sha512-dOWkx+MWclLVnYpPPBzas4sfnPaAjk+fe/dbvJSzbWtHUR4fGGaTT/lTcOpxqhwRIlTKKBUYGKude0tcTBsZHg==", "dev": true, "license": "MIT", "dependencies": { @@ -11966,31 +11966,31 @@ } }, "node_modules/expo": { - "version": "57.0.16", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.16.tgz", - "integrity": "sha512-+RyhqrVXDJDJGlhV24DM8pLmofrR63J5q9xNcbWJlRefR7ggVgHmrl2DNs7qDYSlbFOAbIM0ijN7wanMi2PAMw==", + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.17.tgz", + "integrity": "sha512-RG9BItgEObzSafkJKHPUh5iCnaPCFXJTDwfEJe3t7uhmqipVhcsf6gAJEtME0xaGaMp4Z3C+HLbPW9DMea8G0w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.18", + "@expo/cli": "^57.0.19", "@expo/config": "~57.0.9", "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", "@expo/dom-webview": "~57.0.1", "@expo/fingerprint": "^0.20.10", - "@expo/local-build-cache-provider": "^57.0.7", - "@expo/log-box": "^57.0.3", + "@expo/local-build-cache-provider": "^57.0.8", + "@expo/log-box": "^57.0.4", "@expo/metro": "~56.0.2", - "@expo/metro-config": "~57.0.10", + "@expo/metro-config": "~57.0.11", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.8", - "expo-asset": "~57.0.14", - "expo-constants": "~57.0.14", - "expo-file-system": "~57.0.5", + "babel-preset-expo": "~57.0.9", + "expo-asset": "~57.0.15", + "expo-constants": "~57.0.15", + "expo-file-system": "~57.0.6", "expo-font": "~57.0.1", "expo-keep-awake": "~57.0.1", - "expo-modules-autolinking": "~57.0.11", - "expo-modules-core": "~57.0.13", + "expo-modules-autolinking": "~57.0.12", + "expo-modules-core": "~57.0.14", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -12028,13 +12028,13 @@ } }, "node_modules/expo-asset": { - "version": "57.0.14", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.14.tgz", - "integrity": "sha512-OR1wVSRcF6g8clB0tk3rY/uv3k8MUHf5PN6cHulnNpjkl5IYWpCvkcTZT3RFiJA9ThkumvWaWIJWtLAIeesTJw==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.15.tgz", + "integrity": "sha512-ZoiUftQb1Nn1jvL6l1kZoUXvgRCx57tMQYxSsp+g4ZvY+jgVWp/THruXOtPtZVdC/WWZxPUTct53bfAm2EPDAQ==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.11.5", - "expo-constants": "~57.0.14" + "expo-constants": "~57.0.15" }, "peerDependencies": { "expo": "*", @@ -12054,9 +12054,9 @@ } }, "node_modules/expo-constants": { - "version": "57.0.14", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.14.tgz", - "integrity": "sha512-c9WlP18GqXRm/Kaa7rALRbpFvWiuijrXu3C/38iYdHt9lscA8deJhfK7TURRApWLacdzre5mnNmHdEthNeCEjg==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.15.tgz", + "integrity": "sha512-rLME1H/K+/uFMR/jEpe/p1nribvCEZZmQegzqxu/R1HTyvYweq4E+B38zB0baie+SEwmeIPLaM4KLVMnejhm5A==", "license": "MIT", "dependencies": { "@expo/env": "~2.4.2" @@ -12067,13 +12067,13 @@ } }, "node_modules/expo-dev-client": { - "version": "57.0.15", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.15.tgz", - "integrity": "sha512-3ryaF/bVBmq7v0jzg07zkBvPmfW6B34xZbEU0j18p6nedtJjAMfa2wNmyB7Eq1I/vhhxs5uI21nh08eRaZgF2g==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.16.tgz", + "integrity": "sha512-Txss9sgzEFI0E+4I34E76vPiukeVDHnqCj0ZXy8SDijVM31/P6MCNniVbEZk7MBPQclD0aopfuaCFtkV8+r1sw==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~57.0.15", - "expo-dev-menu": "~57.0.15", + "expo-dev-launcher": "~57.0.16", + "expo-dev-menu": "~57.0.16", "expo-dev-menu-interface": "~57.0.0", "expo-manifests": "~57.0.1", "expo-updates-interface": "~57.0.1" @@ -12083,13 +12083,13 @@ } }, "node_modules/expo-dev-launcher": { - "version": "57.0.15", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.15.tgz", - "integrity": "sha512-lTRHfPcDKog/lK4RgQdN/252lplGm3MZ5DrVjVsUcmgjx6YfIohMRtkW3DTXXdRfTRcfpGUIFKg9JZvc1RO5kA==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.16.tgz", + "integrity": "sha512-jaBr6q4A5js/r465m2mRxN8A87YOi8I6PiIt8XIt4qfQWDgw6hqGovaF1AVFGT1Z5jxkjtgK8WNz/rAeypjqPQ==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^57.0.2", - "expo-dev-menu": "~57.0.15", + "expo-dev-menu": "~57.0.16", "expo-manifests": "~57.0.1" }, "peerDependencies": { @@ -12098,9 +12098,9 @@ } }, "node_modules/expo-dev-menu": { - "version": "57.0.15", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.15.tgz", - "integrity": "sha512-T6kJ/G9P+Ef5MS3TpH/JBtZIt+v4kQKUED1gzUV9aqKfCKhI6jC3rVV3vTGqBMOA2/RaP2VlkmTrg9/aSXtlRA==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.16.tgz", + "integrity": "sha512-KzMHtAmnr0GqyU6MCrKLzIo/AfOjcToGhzK+pFeXlcqSPOrPJbTGlZqQ3E1o2G2jZMs+M1XvXh/VgnRyJUaRGw==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "~57.0.0" @@ -12145,9 +12145,9 @@ } }, "node_modules/expo-file-system": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.5.tgz", - "integrity": "sha512-XjGrCClF0935y5wLB9qNQQUVptNEy+0OloBstXlCd+IeNXLo3uNOod6cX4N0hofIhNIrN1Al8fwfay7R0Z1a2A==", + "version": "57.0.6", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz", + "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -12216,12 +12216,12 @@ } }, "node_modules/expo-linking": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.7.tgz", - "integrity": "sha512-Ujn4kY0bd1MoJFLYuvIUWamlZH61pkahrUpug3hBtOeS89LDbPyKNjjmWef6+CPTGQfed2PlfuMZuPO1BCrg5w==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.8.tgz", + "integrity": "sha512-gv0ZNaF7tILbcvX8xinNbQr6GIGQStxeUAJseweP+LjC9ped7Wl6EeyIMBlDR83U6Da+8drVeqBQSMvwAXgytA==", "license": "MIT", "dependencies": { - "expo-constants": "~57.0.13", + "expo-constants": "~57.0.15", "invariant": "^2.2.4" }, "peerDependencies": { @@ -12255,9 +12255,9 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "57.0.11", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.11.tgz", - "integrity": "sha512-cvbEwYDKGhvMWoStZVceCdFRvrizm82uNMIDKIeo6NO4X/QC7PN4Eqm7Vn57DmgBdZNBJIk/647/NpNowPqAxQ==", + "version": "57.0.12", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.12.tgz", + "integrity": "sha512-Q8KAlq37nLKsQ+HsS9NpQVpd5jCgqtu694TDUNHBUBpV9ViD82mRBh8Uug/h68RG9xnLS+kuL4nYaCuFRghHjg==", "license": "MIT", "dependencies": { "@expo/require-utils": "^57.0.5", @@ -12270,13 +12270,13 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.13", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.13.tgz", - "integrity": "sha512-RQRlhb5ETd5zgS4RV02ktZRLGXFAe8EgK6gywQTq1lBZkthUrVsmg9j5xQ9nijQmTZqdsJtHNifaQdNoKf7ugw==", + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.14.tgz", + "integrity": "sha512-Cg3aQnVsQhZcMOF/RFgPGzMuhIak5W9eR9ecwmFSeQ813r4/49ut6RMA6PqUuUKuPCUIRqnBAHfSyTm1W9nfzg==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.0.5", + "expo-modules-jsi": "~57.0.6", "invariant": "^2.2.4" }, "peerDependencies": { @@ -12291,24 +12291,24 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.5.tgz", - "integrity": "sha512-NQF7MUF0j3rQHV8KJqETYA1SrJr3USvQ/AWEhODo+unMRFBDP9BKpn8+aNGGmx6fa2XLnqC2qulVw4DT6Gp13Q==", + "version": "57.0.6", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.6.tgz", + "integrity": "sha512-WK0xFEe0FdZTCLCdfXK+HVNYfAmEbA6goecRxqjr0gh3XYRKAr9tikxqcM0ItLQoEPKntscPZhWcPLGvr97Tfw==", "license": "MIT", "peerDependencies": { "react-native": "*" } }, "node_modules/expo-router": { - "version": "57.0.16", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.16.tgz", - "integrity": "sha512-k2ykpt91f2tVuaK+NTlyZPpy7ZL+VigGdgcrnTg+3KMDLBItXFAZezubeOe93CiirjJpXt5ktgQz8QMSse22HA==", + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.17.tgz", + "integrity": "sha512-zKAfGLxrbdkUFzzW1xHezLXPSa0V4DlYKEWweDUUFGgnPWPNy3OY0EKYAqOhlBGSDebAvtnJXn+2zyyJjYKwyg==", "license": "MIT", "dependencies": { - "@expo/log-box": "^57.0.3", - "@expo/metro-runtime": "^57.0.13", + "@expo/log-box": "^57.0.4", + "@expo/metro-runtime": "^57.0.14", "@expo/schema-utils": "^57.0.2", - "@expo/ui": "^57.0.13", + "@expo/ui": "^57.0.14", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -12334,12 +12334,12 @@ "vaul": "^1.1.2" }, "peerDependencies": { - "@expo/log-box": "^57.0.3", - "@expo/metro-runtime": "^57.0.13", + "@expo/log-box": "^57.0.4", + "@expo/metro-runtime": "^57.0.14", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^57.0.14", - "expo-linking": "^57.0.7", + "expo-constants": "^57.0.15", + "expo-linking": "^57.0.8", "react": "*", "react-dom": "*", "react-native": "*", @@ -12422,12 +12422,12 @@ } }, "node_modules/expo-system-ui": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.2.tgz", - "integrity": "sha512-zABCRqFSioDBAo/RtmS0dQiGgDtDPZUVk01Y3Ti4ducobNM4HTM2sNAtq+YPpEUhaVW3hccPVsEUH4LK/ADVhA==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.3.tgz", + "integrity": "sha512-3N3BoagGojEvQw0h7kL0Lvv2LrVjqEvODSNS+Nc9lj/kzaTaVx4K1HI8qqxeALMf5jui3rLP2b3Ai2pXotoNuA==", "license": "MIT", "dependencies": { - "@react-native/normalize-colors": "0.86.2", + "@react-native/normalize-colors": "0.86.3", "debug": "^4.3.2" }, "peerDependencies": { @@ -12442,9 +12442,9 @@ } }, "node_modules/expo-system-ui/node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.3.tgz", + "integrity": "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==", "license": "MIT" }, "node_modules/expo-updates-interface": { @@ -12467,9 +12467,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.18.tgz", - "integrity": "sha512-ZLHe7/a76nno3ZvC8czmUhbv/tmdzKa27fqW2J/Ybhx78u71C7qpeRk8UgWGYWeIhonT8XILVMxECjlYsj6hfQ==", + "version": "57.0.19", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.19.tgz", + "integrity": "sha512-HcvVmm0FNL1P3Lf23z1L/sLZtdcWfKW8P5L/doQbICWVG/PL7uI5+G2Ttxnj9QGNmpYgJ89E7+LJefS5jJMUlQ==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -12478,23 +12478,23 @@ "@expo/devcert": "^1.2.1", "@expo/env": "~2.4.2", "@expo/image-utils": "^0.11.5", - "@expo/inline-modules": "^0.1.6", + "@expo/inline-modules": "^0.1.7", "@expo/json-file": "^11.0.1", - "@expo/log-box": "^57.0.3", + "@expo/log-box": "^57.0.4", "@expo/metro": "~56.0.2", - "@expo/metro-config": "~57.0.10", + "@expo/metro-config": "~57.0.11", "@expo/metro-file-map": "^57.0.2", "@expo/osascript": "^2.7.1", "@expo/package-manager": "^1.13.1", "@expo/plist": "^0.8.1", - "@expo/prebuild-config": "^57.0.14", + "@expo/prebuild-config": "^57.0.15", "@expo/require-utils": "^57.0.5", - "@expo/router-server": "^57.0.7", + "@expo/router-server": "^57.0.8", "@expo/schema-utils": "^57.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", "@expo/xcpretty": "^4.4.4", - "@react-native/dev-middleware": "0.86.2", + "@react-native/dev-middleware": "0.86.3", "accepts": "^1.3.8", "agent-cli-detector": "0.1.6", "arg": "^5.0.2", @@ -12550,17 +12550,17 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.7.tgz", - "integrity": "sha512-QFHwt6V7UovmYA7+8BoMeKj3fh6WtkM9zksKjNAlZdcEI/hUhcW4uged6So5yc4tq+eA60kA7gIVZPKgBIZdrQ==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.8.tgz", + "integrity": "sha512-lEPGYoDmh97yyEcY/MFWQsfE7yIgMRnGDcnsBN7B6AiWBFSJIMEcGnO0aYPgY0ZjNWyJDi7Ee8QZRGqB+LhI6w==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^57.0.12", + "@expo/metro-runtime": "^57.0.14", "expo": "*", - "expo-constants": "^57.0.13", + "expo-constants": "^57.0.15", "expo-font": "^57.0.1", "expo-router": "*", "expo-server": "^57.0.3", @@ -13659,9 +13659,9 @@ } }, "node_modules/hermes-compiler": { - "version": "250829098.0.16", - "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.16.tgz", - "integrity": "sha512-xsgzk+mUyvt9t1nUbF8USBlYxajTUtPJhVZ86q85s/SEoMKCF+52YZcudb0ENSnV3T3lV9mgB3s6R7+pH90zgw==", + "version": "250829098.0.17", + "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.17.tgz", + "integrity": "sha512-qG1PXzTEtriF6oQLZF3vyHhSMxOdW5h2TqqLri0rdpstPustd2fSvRZQMVAPdlhgFwBfYnj3OUZtiO6LjYsEFw==", "license": "MIT" }, "node_modules/hermes-estree": { @@ -15148,9 +15148,9 @@ } }, "node_modules/jest-expo": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-57.0.4.tgz", - "integrity": "sha512-EpTc8zizYWepKXHdHYuYBdEP0mZLjZfB1ldb2dyLKHug0HYFGuBnQ3Qp3gmBWCj3rSRaJYP90Lq/qPozzx2Khg==", + "version": "57.0.5", + "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-57.0.5.tgz", + "integrity": "sha512-U08fwjaDBwnnbKnZcOA0WQbHfKqOWTCGku+IdCaekrLZmfbbZr3P1WqEsXHo6ntidQ3DmW+TT6G1aOp+ChYfKA==", "dev": true, "license": "MIT", "dependencies": { @@ -15171,7 +15171,7 @@ "jest": "bin/jest.js" }, "peerDependencies": { - "@react-native/jest-preset": "^0.86.2", + "@react-native/jest-preset": "^0.86.3", "expo": "*", "react-native": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" @@ -18600,18 +18600,18 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.2.tgz", - "integrity": "sha512-zbJXGZpwfZGA79Z9ob6Atvfx4nAQL8yJBa35s58E4Oo+khPykfQP2sTeumkKbjwajFYfVayg8pj7Il9nIfTk7A==", - "license": "MIT", - "dependencies": { - "@react-native/assets-registry": "0.86.2", - "@react-native/codegen": "0.86.2", - "@react-native/community-cli-plugin": "0.86.2", - "@react-native/gradle-plugin": "0.86.2", - "@react-native/js-polyfills": "0.86.2", - "@react-native/normalize-colors": "0.86.2", - "@react-native/virtualized-lists": "0.86.2", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.3.tgz", + "integrity": "sha512-JR5s3bM9ezud+Mw24GlNXNfthqPIKwrQgPPJcam+L97t2sKjjEavhCzBn+fyqZZRcM5+XlhYxpTxVkK7e1n38Q==", + "license": "MIT", + "dependencies": { + "@react-native/assets-registry": "0.86.3", + "@react-native/codegen": "0.86.3", + "@react-native/community-cli-plugin": "0.86.3", + "@react-native/gradle-plugin": "0.86.3", + "@react-native/js-polyfills": "0.86.3", + "@react-native/normalize-colors": "0.86.3", + "@react-native/virtualized-lists": "0.86.3", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -18619,7 +18619,7 @@ "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", - "hermes-compiler": "250829098.0.16", + "hermes-compiler": "250829098.0.17", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", @@ -18645,7 +18645,7 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { - "@react-native/jest-preset": "0.86.2", + "@react-native/jest-preset": "0.86.3", "@types/react": "^19.1.1", "react": "^19.2.3" }, @@ -18659,9 +18659,9 @@ } }, "node_modules/react-native-drawer-layout": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.9.tgz", - "integrity": "sha512-ETOxvlhhb4LmuuG3RN7A3qwt9jr9AZ2it+1G2kNE4g2fTyxxay7QQkPm1HfKi/JzmMSWC5+YTepGSgZNpJIDGg==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.10.tgz", + "integrity": "sha512-O6TQdZ5LSm3dqnuR4rX9KPtE+9dVg7jsezEYz1l01rkQTe4fQvYZIoPu2sZFl5X2N70uhRdjnPULySVR3sBUwA==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -18882,9 +18882,9 @@ } }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.3.tgz", + "integrity": "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==", "license": "MIT" }, "node_modules/react-native/node_modules/commander": { diff --git a/package.json b/package.json index 186eafe..8adb463 100644 --- a/package.json +++ b/package.json @@ -5,33 +5,33 @@ "dependencies": { "@expo-google-fonts/geist-mono": "^0.4.3", "@expo-google-fonts/oxanium": "^0.4.2", - "@expo/ui": "~57.0.13", + "@expo/ui": "~57.0.14", "@shopify/flash-list": "2.0.2", "@shopify/react-native-skia": "2.6.2", "@tamagui/config": "^2.5.1", "@tamagui/portal": "^2.5.1", "@tanstack/react-query": "^5.101.4", - "expo": "~57.0.16", + "expo": "~57.0.17", "expo-blur": "~57.0.2", "expo-constants": "~57.0.8", - "expo-dev-client": "~57.0.15", + "expo-dev-client": "~57.0.16", "expo-device": "~57.0.1", "expo-font": "~57.0.1", "expo-image": "~57.0.3", - "expo-linking": "~57.0.7", + "expo-linking": "~57.0.8", "expo-localization": "~57.0.1", - "expo-router": "~57.0.16", + "expo-router": "~57.0.17", "expo-splash-screen": "~57.0.8", "expo-status-bar": "~57.0.1", "expo-symbols": "~57.0.2", - "expo-system-ui": "~57.0.2", + "expo-system-ui": "~57.0.3", "expo-web-browser": "~57.0.2", "i18next": "^26.3.6", "lucide-react-native": "^1.26.0", "react": "19.2.3", "react-dom": "19.2.3", "react-i18next": "^17.0.11", - "react-native": "0.86.2", + "react-native": "0.86.3", "react-native-gesture-handler": "~2.32.0", "react-native-mmkv": "4.3.2", "react-native-nitro-modules": "0.36.5", @@ -48,7 +48,7 @@ "zustand": "^5.0.14" }, "devDependencies": { - "@react-native/jest-preset": "0.86.2", + "@react-native/jest-preset": "^0.86.3", "@tamagui/cli": "^2.5.1", "@tamagui/metro-plugin": "^2.5.1", "@testing-library/react-native": "^14.0.1", @@ -56,10 +56,10 @@ "@types/node": "~26.1.1", "@types/react": "~19.2.2", "eslint": "^9.0.0", - "eslint-config-expo": "~57.0.1", + "eslint-config-expo": "~57.0.2", "expo-doctor": "^1.20.1", "jest": "~29.7.0", - "jest-expo": "~57.0.4", + "jest-expo": "~57.0.5", "sharp": "^0.35.3", "typescript": "~6.0.3", "typescript-eslint": "^8.65.0" diff --git a/scripts/generate-dashboard-assets.mjs b/scripts/generate-dashboard-assets.mjs index 13ee4c7..7442fb6 100644 --- a/scripts/generate-dashboard-assets.mjs +++ b/scripts/generate-dashboard-assets.mjs @@ -11,7 +11,6 @@ import { AVATAR_FILTER_SCANLINE_OPACITY, AVATAR_FILTER_WASH_STOPS, } from '../src/components/ui/avatar-filter-config.ts'; -import { ITEM_ARTWORK_SIZE } from '../src/components/ui/item-artwork-config.ts'; import { OPERATOR_PORTRAIT_FILTER, OPERATOR_PORTRAIT_GEOMETRY, @@ -78,9 +77,10 @@ const ICONS = [ ...Array.from({ length: 6 }, (_, index) => ({ category: 'potential', name: `potential_${index}` })), ]; -const INVENTORY_FILTERS = Object.entries(ITEM_ARTWORK_SIZE).map( - ([layoutSize, size]) => ({ layoutSize, size }), -); +const INVENTORY_FILTERS = [ + { name: 'small', size: 48 }, + { name: 'large', size: 104 }, +]; function parseColor(value) { const hexMatch = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(value); @@ -505,13 +505,13 @@ async function generateCellBottomTransition(outputRoot) { return outputPath; } -async function generateInventoryFilter(outputRoot, { layoutSize, size }) { +async function generateInventoryFilter(outputRoot, { name, size }) { const outputPath = path.join( outputRoot, 'assets', 'images', 'inventory', - `grid-filter-${layoutSize}.webp`, + `grid-filter-${name}.webp`, ); const data = Buffer.alloc(size * size * 4); diff --git a/src/app/(app)/dashboard/_layout.tsx b/src/app/(app)/dashboard/_layout.tsx index d032dc0..d61dfdb 100644 --- a/src/app/(app)/dashboard/_layout.tsx +++ b/src/app/(app)/dashboard/_layout.tsx @@ -1,6 +1,6 @@ import { Tabs as DashboardTabs } from 'expo-router/tabs'; import { useEffect } from 'react'; -import { Spinner, YStack } from 'tamagui'; +import { Spinner, YStack, useMedia } from 'tamagui'; import { MonoText } from '@/components'; import { @@ -12,7 +12,6 @@ import { DashboardSmallScreenTabBar, dashboardPagesList, } from '@/features/navigation'; -import { useLayoutSize } from '@/providers/layout-size-provider'; function DashboardState({ label }: { label: string }) { return ( @@ -24,7 +23,7 @@ function DashboardState({ label }: { label: string }) { } function DashboardContent() { - const layoutSize = useLayoutSize(); + const { large } = useMedia(); const { gameAccountsQuery, selectedGameAccount, @@ -55,7 +54,7 @@ function DashboardContent() { lazy: true, sceneStyle: { backgroundColor: 'transparent' }, }} - tabBar={layoutSize === 'small' + tabBar={!large ? (props) => ( @@ -20,7 +20,7 @@ export default function SettingsTabsLayout() { sceneStyle: { backgroundColor: 'transparent' }, swipeEnabled: true, }} - tabBar={layoutSize === 'small' ? SettingsTabBar : () => null} + tabBar={!large ? SettingsTabBar : () => null} > {settingsPagesList.map((page) => ( diff --git a/src/components/layout/section-page-header.tsx b/src/components/layout/section-page-header.tsx index 5bd9f63..554c8f3 100644 --- a/src/components/layout/section-page-header.tsx +++ b/src/components/layout/section-page-header.tsx @@ -16,7 +16,7 @@ export function SectionPageHeader({ title: string; }) { return ( - + @@ -24,8 +24,8 @@ export function SectionPageHeader({ {code} - - + + {title} diff --git a/src/components/ui/item-artwork-config.ts b/src/components/ui/item-artwork-config.ts deleted file mode 100644 index 41f3046..0000000 --- a/src/components/ui/item-artwork-config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { LayoutSize } from '@/schemas/layout-size'; - -export const ITEM_ARTWORK_SIZE = { - small: 48, - large: 104, -} as const satisfies Record; diff --git a/src/components/ui/item-artwork.test.tsx b/src/components/ui/item-artwork.test.tsx index 6a66d41..be23c3c 100644 --- a/src/components/ui/item-artwork.test.tsx +++ b/src/components/ui/item-artwork.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react-native'; +import { setMediaState } from '@tamagui/web'; import { TamaguiProvider } from 'tamagui'; import itemArtworkFilterLarge from '@/assets/images/inventory/grid-filter-large.webp'; @@ -18,12 +19,12 @@ jest.mock('expo-image', () => { }; }); -async function renderArtwork(layoutSize: 'small' | 'large', source: string | number) { +async function renderArtwork(large: boolean, source: string | number) { + setMediaState({ large }); return await render( { + afterEach(() => { + setMediaState({ large: false }); + }); it.each([ - { layoutSize: 'small' as const, size: 48, filter: itemArtworkFilterSmall }, - { layoutSize: 'large' as const, size: 104, filter: itemArtworkFilterLarge }, - ])('renders the $layoutSize artwork geometry and filter', async ({ filter, layoutSize, size }) => { - const view = await renderArtwork(layoutSize, 'https://example.test/item.webp'); + { large: false, size: 48, filter: itemArtworkFilterSmall }, + { large: true, size: 104, filter: itemArtworkFilterLarge }, + ])('renders the responsive artwork geometry and filter', async ({ filter, large, size }) => { + const view = await renderArtwork(large, 'https://example.test/item.webp'); expect(view.getByTestId('test-item-artwork')).toHaveStyle({ borderBottomLeftRadius: 999, @@ -66,7 +70,7 @@ describe('ItemArtwork', () => { }); it('passes local asset module sources through the same image path', async () => { - const view = await renderArtwork('small', 42); + const view = await renderArtwork(false, 42); expect(view.getByTestId('test-item-artwork-image').props.src).toBe(42); }); diff --git a/src/components/ui/item-artwork.tsx b/src/components/ui/item-artwork.tsx index 5ec1432..9a22974 100644 --- a/src/components/ui/item-artwork.tsx +++ b/src/components/ui/item-artwork.tsx @@ -1,17 +1,12 @@ import { Image, type ImageProps } from 'expo-image'; import { memo } from 'react'; import { StyleSheet } from 'react-native'; -import { XStack, styled } from 'tamagui'; +import { XStack, styled, useMedia } from 'tamagui'; import itemArtworkFilterLarge from '@/assets/images/inventory/grid-filter-large.webp'; import itemArtworkFilterSmall from '@/assets/images/inventory/grid-filter-small.webp'; -import type { LayoutSize } from '@/schemas/layout-size'; -import { ITEM_ARTWORK_SIZE } from './item-artwork-config'; - -const ITEM_ARTWORK_FILTER_IMAGES = { - small: itemArtworkFilterSmall, - large: itemArtworkFilterLarge, -} as const satisfies Record; +export const ITEM_ARTWORK_SIZE = 48; +export const ITEM_ARTWORK_LARGE_SIZE = 104; const ItemArtworkFrame = styled(XStack, { name: 'ItemArtwork', @@ -21,23 +16,16 @@ const ItemArtworkFrame = styled(XStack, { justify: 'center', overflow: 'hidden', rounded: 999, - variants: { - layoutSize: { - small: { - width: ITEM_ARTWORK_SIZE.small, - height: ITEM_ARTWORK_SIZE.small, - }, - large: { - width: ITEM_ARTWORK_SIZE.large, - height: ITEM_ARTWORK_SIZE.large, - }, - }, - } as const, + width: ITEM_ARTWORK_SIZE, + height: ITEM_ARTWORK_SIZE, + $large: { + width: ITEM_ARTWORK_LARGE_SIZE, + height: ITEM_ARTWORK_LARGE_SIZE, + }, }); export type ItemArtworkProps = { accessibilityLabel: string; - layoutSize: LayoutSize; recyclingKey: string; source: Exclude; testID?: string; @@ -45,16 +33,16 @@ export type ItemArtworkProps = { export const ItemArtwork = memo(function ItemArtwork({ accessibilityLabel, - layoutSize, recyclingKey, source, testID, }: ItemArtworkProps) { + const { large } = useMedia(); const imageTestID = testID ? `${testID}-image` : undefined; const filterTestID = testID ? `${testID}-filter` : undefined; return ( - + diff --git a/src/features/auth/api/index.ts b/src/features/auth/api/index.ts index 4a70ea8..ef4b8c1 100644 --- a/src/features/auth/api/index.ts +++ b/src/features/auth/api/index.ts @@ -1,3 +1,5 @@ +import { MockAuthAdapter } from './auth-adapter.mock'; + export type { AuthAdapter, AuthBusinessFailureCode, @@ -5,3 +7,5 @@ export type { AuthResult, } from './auth-adapter'; export { MockAuthAdapter } from './auth-adapter.mock'; + +export const authApi = new MockAuthAdapter(); diff --git a/src/features/auth/components/access-orbit.tsx b/src/features/auth/components/access-orbit.tsx index e5b5de6..a82a9d8 100644 --- a/src/features/auth/components/access-orbit.tsx +++ b/src/features/auth/components/access-orbit.tsx @@ -9,19 +9,18 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import Svg, { Circle, G, Line, Path } from 'react-native-svg'; -import { YStack, getTokens } from 'tamagui'; +import { YStack, getTokens, useMedia } from 'tamagui'; import { MonoText, TerminalText } from '@/components'; -import { useLayoutSize } from '@/providers/layout-size-provider'; const AMBIENT_ORBIT_DURATION_MS = 18_000; export function AccessOrbit({ label, nodeId }: { label: string; nodeId: string }) { const colors = getTokens().color; - const layoutSize = useLayoutSize(); + const { large } = useMedia(); const reducedMotion = useReducedMotion(); const rotation = useSharedValue(0); - const size = layoutSize === 'large' ? 430 : 144; + const size = large ? 430 : 144; const center = size / 2; useEffect(() => { @@ -49,8 +48,8 @@ export function AccessOrbit({ label, nodeId }: { label: string; nodeId: string } height={size} items="center" justify="center" - r={layoutSize === 'large' ? -20 : -40} - t={layoutSize === 'large' ? -30 : -80} + r={large ? -20 : -40} + t={large ? -30 : -80} opacity={0.58} style={{ pointerEvents: 'none' }} > @@ -74,10 +73,10 @@ export function AccessOrbit({ label, nodeId }: { label: string; nodeId: string } - + {nodeId} - + {label} diff --git a/src/features/auth/components/login-form.tsx b/src/features/auth/components/login-form.tsx index d55da0e..494c7d6 100644 --- a/src/features/auth/components/login-form.tsx +++ b/src/features/auth/components/login-form.tsx @@ -122,7 +122,7 @@ export function LoginForm({
- + - + {recoveryStatus === 'success' ? ( diff --git a/src/features/auth/components/terminal-brand.tsx b/src/features/auth/components/terminal-brand.tsx index 064b9fc..636d479 100644 --- a/src/features/auth/components/terminal-brand.tsx +++ b/src/features/auth/components/terminal-brand.tsx @@ -2,34 +2,30 @@ import type { ReactNode } from 'react'; import { XStack, YStack, getTokens } from 'tamagui'; import { MonoText, NotchedSurface, TerminalText } from '@/components'; -import { useLayoutSize } from '@/providers/layout-size-provider'; -type TerminalBrandProps = Omit, 'children' | 'scale'> & { +type TerminalBrandProps = Omit, 'children'> & { mark?: ReactNode; title?: ReactNode; subtitle?: ReactNode; - scale?: 'compact' | 'large' | 'responsive'; }; export function TerminalBrand({ mark = 'C', title = '可露希尔工作室', subtitle = 'Closure · Studio', - scale = 'responsive', ...props }: TerminalBrandProps) { const colors = getTokens().color; - const layoutSize = useLayoutSize(); - const large = scale === 'large' || (scale === 'responsive' && layoutSize === 'large'); return ( {mark} - + {title} - + {subtitle} diff --git a/src/features/auth/queries.ts b/src/features/auth/queries.ts index a22fe0b..885d5f3 100644 --- a/src/features/auth/queries.ts +++ b/src/features/auth/queries.ts @@ -1,7 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import * as v from 'valibot'; -import { getQueryDependencies } from '@/services/query-dependencies'; import { loginSubmissionSchema, passwordRecoveryRequestInputSchema, @@ -14,7 +13,7 @@ import type { import type { PasswordChangeInput } from '@/schemas/user-account'; import { appStore } from '@/store'; import { FailureError, unwrapResult } from '@/utils/failure-error'; -import type { AuthFailure } from './api'; +import { authApi, type AuthFailure } from './api'; function invalidInputFailure(): AuthFailure { return { code: 'invalid-input', kind: 'business' }; @@ -26,7 +25,7 @@ export function useLogin() { const parsedSubmission = v.safeParse(loginSubmissionSchema, submission); if (!parsedSubmission.success) throw new FailureError(invalidInputFailure()); return unwrapResult( - await getQueryDependencies().authAdapter.login(parsedSubmission.output.credentials), + await authApi.login(parsedSubmission.output.credentials), ); }, onSuccess: (session) => { @@ -41,7 +40,7 @@ export function usePasswordRecovery() { const parsedInput = v.safeParse(passwordRecoveryRequestInputSchema, input); if (!parsedInput.success) throw new FailureError(invalidInputFailure()); unwrapResult( - await getQueryDependencies().authAdapter.requestPasswordRecovery(parsedInput.output), + await authApi.requestPasswordRecovery(parsedInput.output), ); return undefined; }, @@ -55,7 +54,7 @@ export function useUpdatePassword() { if (!session) { throw new FailureError({ code: 'session-expired', kind: 'business' }); } - const result = await getQueryDependencies().authAdapter.updatePassword({ + const result = await authApi.updatePassword({ accessToken: session.accessToken, currentPassword: input.currentPassword, email: session.principal.email, diff --git a/src/features/auth/screens/auth-screen.tsx b/src/features/auth/screens/auth-screen.tsx index e4a340b..c3d6cfb 100644 --- a/src/features/auth/screens/auth-screen.tsx +++ b/src/features/auth/screens/auth-screen.tsx @@ -43,20 +43,20 @@ export function AuthScreen({ - - - + + + - {t('meta.description')} + {t('meta.description')} - + {t('meta.secureChannel')} {t('meta.syncReady')} - + - - {t('meta.footer')} - {t('meta.syncReady')} + + {t('meta.footer')} + {t('meta.syncReady')} diff --git a/src/features/dashboard/api/index.ts b/src/features/dashboard/api/index.ts index 598aee2..2666e7c 100644 --- a/src/features/dashboard/api/index.ts +++ b/src/features/dashboard/api/index.ts @@ -1,3 +1,6 @@ +import { MockArkHostApi } from './arkhost-api.mock'; +import { RemoteGameResourcesApi } from './game-resources-api'; + export type { ArkHostApi, ArkHostFailure, @@ -14,3 +17,6 @@ export type { GameResourceResult, GameResourcesApi, } from './game-resources-api'; + +export const arkHostApi = new MockArkHostApi(); +export const gameResourcesApi = new RemoteGameResourcesApi(); diff --git a/src/features/dashboard/components/dashboard-secondary-header.tsx b/src/features/dashboard/components/dashboard-secondary-header.tsx index 6e9a087..4348d1e 100644 --- a/src/features/dashboard/components/dashboard-secondary-header.tsx +++ b/src/features/dashboard/components/dashboard-secondary-header.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { YStack } from 'tamagui'; import { TerminalMarquee } from '@/components'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import type { GameAccount } from '@/schemas/game-account'; import { GameAccountSwitcher } from './dashboard-navigation'; @@ -26,7 +25,6 @@ export function DashboardSecondaryHeader({ onSelectGameAccount, }: DashboardSecondaryHeaderProps) { const { t } = useTranslation('navigation'); - const layoutSize = useLayoutSize(); const marqueeItems = useMemo( () => dashboardMarqueeMessages.map((message) => ({ id: message.id, @@ -38,17 +36,15 @@ export function DashboardSecondaryHeader({ return ( - {layoutSize === 'large' && ( - - - - + + + - )} + ); } diff --git a/src/features/dashboard/components/dashboard-shell.test.tsx b/src/features/dashboard/components/dashboard-shell.test.tsx index b8346ee..089219f 100644 --- a/src/features/dashboard/components/dashboard-shell.test.tsx +++ b/src/features/dashboard/components/dashboard-shell.test.tsx @@ -4,7 +4,6 @@ import { TamaguiProvider, YStack } from 'tamagui'; import { i18n } from '@/i18n'; import { mockArkHostGameListResponse } from '@/mocks/arkhost'; -import type { LayoutSize } from '@/schemas/layout-size'; import { tamaguiConfig } from '../../../../tamagui.config'; import { DashboardShell } from './dashboard-shell'; @@ -12,32 +11,17 @@ const initialGameAccounts = mockArkHostGameListResponse.code === 1 ? mockArkHostGameListResponse.data.map((entry) => ({ account: entry.status.account, ap: entry.status.ap, avatar: entry.status.avatar, captchaInfo: entry.captcha_info, color: 'primary' as const, config: entry.game_config, createdAt: entry.status.created_at, isVerified: entry.status.is_verify, level: entry.status.level, nickname: entry.status.nick_name, platform: entry.status.platform, statusCode: entry.status.code, statusText: entry.status.text, userId: entry.status.uuid })) : []; -let mockLayoutSize: LayoutSize = 'large'; - -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => mockLayoutSize, -})); - jest.mock('react-native-reanimated', () => { const reanimated = jest.requireActual('react-native-reanimated'); const reanimatedMock = jest.requireActual('react-native-reanimated/mock'); - - return { - ...reanimated, - ...reanimatedMock, - useReducedMotion: () => true, - }; + return { ...reanimated, ...reanimatedMock, useReducedMotion: () => true }; }); function DashboardShellTestTree({ pageId, onSelectGameAccount }: { pageId: string; onSelectGameAccount: (gameAccountId: string) => void }) { return ( - + @@ -47,72 +31,21 @@ function DashboardShellTestTree({ pageId, onSelectGameAccount }: { pageId: strin describe('DashboardShell', () => { it('keeps the secondary header mounted while dashboard content changes', async () => { - mockLayoutSize = 'large'; const screen = await render(); - - expect(screen.getAllByTestId('dashboard-secondary-header')).toHaveLength(1); + expect(screen.getByTestId('dashboard-secondary-header')).toBeTruthy(); expect(screen.getByTestId('dashboard-page-overview')).toBeTruthy(); - expect(screen.getByTestId('game-account-option-G18928069156')).toBeTruthy(); await screen.rerender(); - - expect(screen.getAllByTestId('dashboard-secondary-header')).toHaveLength(1); expect(screen.getByTestId('dashboard-page-inventory')).toBeTruthy(); - expect(screen.getByTestId('game-account-option-G18928069156')).toBeTruthy(); expect(screen.queryByTestId('dashboard-page-overview')).toBeNull(); }); - it('hides the account switcher subtree on small layouts', async () => { - mockLayoutSize = 'small'; - const screen = await render(); - - expect(screen.getAllByTestId('dashboard-secondary-header')).toHaveLength(1); - expect(screen.getAllByText('// SECURE FRAME LINK · 18MS LATENCY').length).toBeGreaterThan(0); - expect(screen.getByTestId('dashboard-page-overview')).toBeTruthy(); - expect(screen.queryAllByTestId(/game-account-option-/)).toHaveLength(0); - }); - - it('renders the account switcher with selection state and callback on large layouts', async () => { - mockLayoutSize = 'large'; + it('keeps account navigation under the large-only style owner', async () => { const onSelectGameAccount = jest.fn(); const screen = await render(); + expect(screen.getByTestId('dashboard-account-switcher', { includeHiddenElements: true })).toHaveStyle({ display: 'none' }); - expect(screen.getByTestId('game-account-option-G18928069156')).toHaveStyle({ - transform: [{ scale: 1 }, { scale: 1 }], - }); - - await fireEvent.press(screen.getByTestId('game-account-option-G16601716973')); - + await fireEvent.press(screen.getByTestId('game-account-option-G16601716973', { includeHiddenElements: true })); expect(onSelectGameAccount).toHaveBeenCalledWith('G16601716973'); }); - - it('mounts the switcher when the layout grows from small to large', async () => { - mockLayoutSize = 'small'; - const screen = await render(); - - expect(screen.queryAllByTestId(/game-account-option-/)).toHaveLength(0); - - mockLayoutSize = 'large'; - await screen.rerender(); - - expect(screen.getByTestId('game-account-option-G18928069156')).toBeTruthy(); - expect(screen.getByTestId('game-account-option-G18928069156')).toHaveStyle({ - transform: [{ scale: 1 }, { scale: 1 }], - }); - }); - - it('unmounts the switcher subtree when the layout shrinks to small', async () => { - mockLayoutSize = 'large'; - const screen = await render(); - - expect(screen.getByTestId('game-account-option-G18928069156')).toBeTruthy(); - - mockLayoutSize = 'small'; - await screen.rerender(); - - expect(screen.queryAllByTestId(/game-account-option-/)).toHaveLength(0); - expect(screen.getAllByTestId('dashboard-secondary-header')).toHaveLength(1); - expect(screen.getAllByText('// SECURE FRAME LINK · 18MS LATENCY').length).toBeGreaterThan(0); - expect(screen.getByTestId('dashboard-page-inventory')).toBeTruthy(); - }); }); diff --git a/src/features/dashboard/components/dashboard-shell.tsx b/src/features/dashboard/components/dashboard-shell.tsx index 27dc796..6996474 100644 --- a/src/features/dashboard/components/dashboard-shell.tsx +++ b/src/features/dashboard/components/dashboard-shell.tsx @@ -39,7 +39,7 @@ export function DashboardPageFrame({ p="$3.5" pt="$3" pb={flushBottom ? '$0' : '$3.5'} - $md={{ px: '$5', pt: '$4', pb: '$3.5' }} + $large={{ px: '$5', pt: '$4', pb: '$3.5' }} > {children} diff --git a/src/features/dashboard/components/game-account-overview-view.tsx b/src/features/dashboard/components/game-account-overview-view.tsx index 939cf79..c5eccbc 100644 --- a/src/features/dashboard/components/game-account-overview-view.tsx +++ b/src/features/dashboard/components/game-account-overview-view.tsx @@ -9,7 +9,6 @@ import diamondShdImage from '@/assets/images/inventory/original/DIAMOND_SHD.webp import goldImage from '@/assets/images/inventory/original/GOLD.webp'; import recruitTicketImage from '@/assets/images/inventory/original/TKT_RECRUIT.webp'; import { DecorativeBarcode, ItemArtwork, MonoText, TerminalMeterBar, TerminalSectionHeading, TerminalText } from '@/components'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import { ARK_HOST_GAME_STATUS_CODE, type ArkHostGameDetail, type ArkHostGameLogEntry } from '@/schemas/arkhost'; import type { GameAccount } from '@/schemas/game-account'; import { DashboardSummaryFrame, formatDashboardSummaryTitle } from './dashboard-summary-frame'; @@ -44,7 +43,6 @@ export function GameAccountOverviewView({ const { t } = useTranslation('dashboard'); const { t: tCommon } = useTranslation('common'); const [activeSection, setActiveSection] = useState('profile'); - const layoutSize = useLayoutSize(); const colors = getTokens().color; const status = detail?.status; const accountBalances = [ @@ -66,7 +64,7 @@ export function GameAccountOverviewView({ return ( - + - + - + @@ -152,18 +149,16 @@ export function GameAccountOverviewView({ onActivate={setActiveSection} section={balance.section} testID={`overview-balance-frame-${balance.itemIcon}`} - width="48.7%" minW={140} + flexBasis={140} grow={1} p="$3" flexDirection="row" items="center" gap="$3" - $lg={{ width: '23.5%' }} > - + @@ -191,11 +186,10 @@ export function GameAccountOverviewView({ onActivate={setActiveSection} section={metric.section} testID={`overview-operation-metric-frame-${index}`} - width="48.7%" minW={140} + flexBasis={140} grow={1} p="$3" - $lg={{ width: '23.5%' }} > {metric.label} - + (null); const [draftNumeric, setDraftNumeric] = useState(''); @@ -265,7 +267,7 @@ export function GameHostingConfigView({ title={t('hostingConfig.sections.reserves')} trailing={} /> - + - + - + - + - + - + void; selected: boolean; - size: LayoutSize; }) { const artwork = ( onSelect(entry.itemId)} imageOnly={imageOnly} selected={selected} - layoutSize={size} width={itemWidth ?? '100%'} > - {size === 'small' ? ( - - {artwork} - - ) : artwork} + + {artwork} + {imageOnly ? null : ( {entry.item.name} @@ -143,10 +139,11 @@ export const InventoryCell = memo(function InventoryCell({ testID={`inventory-item-quantity-${entry.itemId}`} width="100%" shrink={0} - size={size === 'small' ? '$2' : '$2.5'} - lineHeight={size === 'small' ? '$2.5' : '$3'} + size="$2" + lineHeight="$2.5" color="$appAccent" text="right" + $large={{ size: '$2.5', lineHeight: '$3' }} > {formatInventoryQuantity(entry.quantity)} diff --git a/src/features/dashboard/inventory/components/inventory-view.test.tsx b/src/features/dashboard/inventory/components/inventory-view.test.tsx index 6bf7e99..fb6be9a 100644 --- a/src/features/dashboard/inventory/components/inventory-view.test.tsx +++ b/src/features/dashboard/inventory/components/inventory-view.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render } from '@testing-library/react-native'; import { StyleSheet } from 'react-native'; +import { setMediaState } from '@tamagui/web'; import { TamaguiProvider } from 'tamagui'; import * as v from 'valibot'; @@ -9,16 +10,9 @@ import inventoryGridFilterSmall from '@/assets/images/inventory/grid-filter-smal import { getResponsiveGridLayout } from '@/hooks/use-responsive-grid-rows'; import { itemTableSchema } from '@/schemas/game-data'; import { inventorySchema } from '@/schemas/game-account'; -import type { LayoutSize } from '@/schemas/layout-size'; import { getItemImageUrl } from '@/utils/item-image'; import { EMPTY_INVENTORY, InventoryView } from './inventory-view'; -let mockLayoutSize: LayoutSize = 'small'; - -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => mockLayoutSize, -})); - jest.mock('react-native-reanimated', () => { const reanimated = jest.requireActual('react-native-reanimated'); const reanimatedMock = jest.requireActual('react-native-reanimated/mock'); @@ -126,7 +120,7 @@ describe('getResponsiveGridLayout', () => { describe('InventoryView', () => { beforeEach(() => { jest.useFakeTimers(); - mockLayoutSize = 'small'; + setMediaState({ large: false }); }); afterEach(() => { @@ -256,7 +250,7 @@ describe('InventoryView', () => { }); it('uses the larger cell variant and large minimum width on Large Screen', async () => { - mockLayoutSize = 'large'; + setMediaState({ large: true }); const screen = await render( diff --git a/src/features/dashboard/inventory/components/inventory-view.tsx b/src/features/dashboard/inventory/components/inventory-view.tsx index b7ad397..eb531fc 100644 --- a/src/features/dashboard/inventory/components/inventory-view.tsx +++ b/src/features/dashboard/inventory/components/inventory-view.tsx @@ -1,32 +1,27 @@ import { FlashList } from '@shopify/flash-list'; import { PackageOpen } from 'lucide-react-native'; import { memo, useCallback, useMemo, useState } from 'react'; -import { XStack, YStack, getTokens } from 'tamagui'; +import { XStack, YStack, getTokens, useMedia } from 'tamagui'; -import { ITEM_ARTWORK_SIZE } from '@/components/ui/item-artwork-config'; +import { ITEM_ARTWORK_LARGE_SIZE, ITEM_ARTWORK_SIZE } from '@/components/ui/item-artwork'; import { MonoText, ResponsiveGridRow, TerminalText } from '@/components'; import { getResponsiveGridLayout, useResponsiveGridRows } from '@/hooks/use-responsive-grid-rows'; import type { ItemTable } from '@/schemas/game-data'; import type { Inventory } from '@/schemas/game-account'; -import type { LayoutSize } from '@/schemas/layout-size'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import { InventoryPreviewArtwork } from './inventory-artwork'; import { formatInventoryQuantity, InventoryCell, type InventoryEntry, - INVENTORY_CELL_MIN_WIDTH_TOKEN, + INVENTORY_CELL_LARGE_MIN_WIDTH, + INVENTORY_CELL_MIN_WIDTH, } from './inventory-cell'; const INVENTORY_GRID_GAP_TOKEN = '$2'; -const PREVIEW_ARTWORK_SIZE = { - small: 64, - large: 94, -} as const; -const PREVIEW_FALLBACK_ICON_SIZE = { - small: 28, - large: 34, -} as const; +const PREVIEW_ARTWORK_SIZE = 64; +const PREVIEW_ARTWORK_LARGE_SIZE = 94; +const PREVIEW_FALLBACK_ICON_SIZE = 28; +const PREVIEW_FALLBACK_ICON_LARGE_SIZE = 34; export const EMPTY_INVENTORY: Inventory = {}; @@ -40,13 +35,12 @@ function getItemDescription(value: string | null | undefined): string | undefine const InventoryPreview = memo(function InventoryPreview({ entry, - size, }: { entry: InventoryEntry; - size: LayoutSize; }) { + const { large } = useMedia(); const description = getItemDescription(entry.item.description); - const artworkSize = PREVIEW_ARTWORK_SIZE[size]; + const artworkSize = large ? PREVIEW_ARTWORK_LARGE_SIZE : PREVIEW_ARTWORK_SIZE; return ( {entry.item.name} - + {formatInventoryQuantity(entry.quantity)} {description ? ( {description} @@ -119,7 +117,6 @@ const InventoryRow = memo(function InventoryRow({ gap, imageOnly, itemWidth, - layoutSize, selectedItemId, onSelect, }: { @@ -128,7 +125,6 @@ const InventoryRow = memo(function InventoryRow({ gap: number; imageOnly: boolean; itemWidth: number | undefined; - layoutSize: LayoutSize; selectedItemId: string | null; onSelect: (itemId: string) => void; }) { @@ -145,7 +141,6 @@ const InventoryRow = memo(function InventoryRow({ itemWidth={itemWidth} onSelect={onSelect} selected={entry.itemId === selectedItemId} - size={layoutSize} /> )} /> @@ -161,7 +156,7 @@ export function InventoryView({ inventory: Inventory; itemTable: ItemTable; }) { - const layoutSize = useLayoutSize(); + const { large } = useMedia(); const { entries, byId } = useMemo(() => { const builtEntries: InventoryEntry[] = []; const builtById = new Map(); @@ -195,8 +190,8 @@ export function InventoryView({ }, [accountId]); const tokens = getTokens(); const gridGap = tokens.space[INVENTORY_GRID_GAP_TOKEN].val; - const minimumItemWidth = tokens.size[INVENTORY_CELL_MIN_WIDTH_TOKEN[layoutSize]].val; - const artworkWidth = ITEM_ARTWORK_SIZE[layoutSize]; + const minimumItemWidth = large ? INVENTORY_CELL_LARGE_MIN_WIDTH : INVENTORY_CELL_MIN_WIDTH; + const artworkWidth = large ? ITEM_ARTWORK_LARGE_SIZE : ITEM_ARTWORK_SIZE; const { rows, listWidth, layout, handleLayout, keyExtractor } = useResponsiveGridRows( entries, (width) => getResponsiveGridLayout(width, gridGap, minimumItemWidth), @@ -214,12 +209,11 @@ export function InventoryView({ gap={gridGap} imageOnly={imageOnly} itemWidth={itemWidth} - layoutSize={layoutSize} selectedItemId={extraData ?? null} onSelect={handleSelectItem} /> ), - [gridGap, handleSelectItem, imageOnly, itemWidth, layoutSize, rows.length], + [gridGap, handleSelectItem, imageOnly, itemWidth, rows.length], ); if (!selectedEntry) { @@ -232,7 +226,7 @@ export function InventoryView({ return ( - + {showCells ? ( - + , ); } describe('OperatorCard', () => { + beforeEach(() => { + setMediaState({ large: false }); + }); it('keeps the two required masks and removes redundant portrait wrappers', async () => { const screen = await renderCard(); const edgeFadeMask = screen.getByTestId('operator-card-edge-fade-mask-char_001', { @@ -163,7 +166,8 @@ describe('OperatorCard', () => { }); it('does not create compact artwork on the large layout branch', async () => { - const screen = await renderCard('large'); + setMediaState({ large: true }); + const screen = await renderCard(); expect(screen.getByTestId('operator-card-char_001')).toBeTruthy(); expect(screen.queryByTestId('operator-card-edge-fade-mask-char_001')).toBeNull(); diff --git a/src/features/dashboard/operator-roster/components/operator-card.tsx b/src/features/dashboard/operator-roster/components/operator-card.tsx index 0366b98..77ecec6 100644 --- a/src/features/dashboard/operator-roster/components/operator-card.tsx +++ b/src/features/dashboard/operator-roster/components/operator-card.tsx @@ -2,7 +2,7 @@ import { MaskedView } from '@expo/ui/community/masked-view'; import { Image } from 'expo-image'; import { memo, useMemo } from 'react'; import { StyleSheet } from 'react-native'; -import { XStack, YStack, styled } from 'tamagui'; +import { XStack, YStack, styled, useMedia } from 'tamagui'; import elite0 from '@/assets/images/operators/elite/prerendered/elite_0.webp'; import elite1 from '@/assets/images/operators/elite/prerendered/elite_1.webp'; @@ -19,7 +19,6 @@ import potential4 from '@/assets/images/operators/potential/prerendered/potentia import potential5 from '@/assets/images/operators/potential/prerendered/potential_5.webp'; import { Frame, MonoText, TerminalMeterBar, TerminalText } from '@/components'; import type { Operator } from '@/schemas/game-account'; -import type { LayoutSize } from '@/schemas/layout-size'; import { OPERATOR_PORTRAIT_GEOMETRY } from '../operator-portrait-config'; import { getOperatorPortraitUrl } from '../portrait-image'; @@ -281,16 +280,15 @@ export const OperatorCard = memo(function OperatorCard({ labels, name, operator, - size, }: { labels: OperatorCardLabels; name: string; operator: Operator; - size: LayoutSize; }) { + const { large } = useMedia(); const level = operator.level; - if (size === 'small') { + if (!large) { return ; } diff --git a/src/features/dashboard/operator-roster/components/operator-roster-view.test.tsx b/src/features/dashboard/operator-roster/components/operator-roster-view.test.tsx index 68537d4..2088ba8 100644 --- a/src/features/dashboard/operator-roster/components/operator-roster-view.test.tsx +++ b/src/features/dashboard/operator-roster/components/operator-roster-view.test.tsx @@ -6,25 +6,17 @@ import * as v from 'valibot'; import { i18n } from '@/i18n'; import { operatorSchema } from '@/schemas/game-account'; import type { Operator } from '@/schemas/game-account'; -import type { LayoutSize } from '@/schemas/layout-size'; import { tamaguiConfig } from '../../../../../tamagui.config'; import { OperatorRosterView, type OperatorViewModel } from './operator-roster-view'; -let mockLayoutSize: LayoutSize = 'small'; - -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => mockLayoutSize, -})); - jest.mock('./operator-card', () => { const { Text: MockText, View: MockView } = jest.requireActual('react-native'); return { OPERATOR_CARD_MIN_WIDTH: 140, - OperatorCard: ({ name, operator, size }: { name: string; operator: Operator; size: LayoutSize }) => ( + OperatorCard: ({ name, operator }: { name: string; operator: Operator }) => ( {name} - {size} ), }; @@ -64,10 +56,6 @@ async function renderRoster() { } describe('OperatorRosterView', () => { - afterEach(() => { - mockLayoutSize = 'small'; - }); - it('waits for measurement, then renders stable operator rows', async () => { const screen = await renderRoster(); @@ -82,16 +70,5 @@ describe('OperatorRosterView', () => { expect(screen.getByText('阿米娅')).toBeTruthy(); expect(screen.getByText('德克萨斯')).toBeTruthy(); expect(screen.getByText('能天使')).toBeTruthy(); - expect(screen.getByTestId('operator-card-size-char_001').props.children).toBe('small'); - }); - - it('passes the large layout branch to each card', async () => { - mockLayoutSize = 'large'; - const screen = await renderRoster(); - - await fireEvent(screen.getByTestId('operator-roster-list'), 'layout', gridLayoutEvent(320)); - - expect(screen.getByTestId('operator-card-size-char_001').props.children).toBe('large'); - expect(screen.getByTestId('operator-card-size-char_002').props.children).toBe('large'); }); }); diff --git a/src/features/dashboard/operator-roster/components/operator-roster-view.tsx b/src/features/dashboard/operator-roster/components/operator-roster-view.tsx index 3547b10..5453db1 100644 --- a/src/features/dashboard/operator-roster/components/operator-roster-view.tsx +++ b/src/features/dashboard/operator-roster/components/operator-roster-view.tsx @@ -5,9 +5,7 @@ import { getTokens } from 'tamagui'; import { ResponsiveGridRow } from '@/components'; import { getResponsiveGridLayout, useResponsiveGridRows } from '@/hooks/use-responsive-grid-rows'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import type { Operator } from '@/schemas/game-account'; -import type { LayoutSize } from '@/schemas/layout-size'; import { OperatorCard, OPERATOR_CARD_MIN_WIDTH, type OperatorCardLabels } from './operator-card'; const OPERATOR_ROW_GAP_TOKEN = '$2'; @@ -26,13 +24,11 @@ const OperatorRow = memo(function OperatorRow({ labels, row, gap, - size, }: { isLast: boolean; labels: OperatorCardLabels; row: OperatorViewModel[]; gap: number; - size: LayoutSize; }) { return ( )} /> @@ -58,7 +53,6 @@ export function OperatorRosterView({ operators: readonly OperatorViewModel[]; }) { const { t } = useTranslation('dashboard'); - const layoutSize = useLayoutSize(); const gridGap = getTokens().space[OPERATOR_ROW_GAP_TOKEN].val; const labels = useMemo(() => ({ cellLevel: t('operators.cell.levelLabel'), @@ -91,10 +85,9 @@ export function OperatorRosterView({ labels={labels} row={row} gap={gridGap} - size={layoutSize} /> ), - [gridGap, labels, layoutSize, rows.length], + [gridGap, labels, rows.length], ); return ( diff --git a/src/features/dashboard/queries.ts b/src/features/dashboard/queries.ts index e92d6e9..d83d5c0 100644 --- a/src/features/dashboard/queries.ts +++ b/src/features/dashboard/queries.ts @@ -21,8 +21,7 @@ import type { import type { GameAccount } from '@/schemas/game-account'; import { appStore, useAppStore } from '@/store'; import { FailureError, unwrapResult } from '@/utils/failure-error'; -import { getQueryDependencies } from '@/services/query-dependencies'; -import type { ArkHostFailure, ArkHostSseSubscription } from './api'; +import { arkHostApi, type ArkHostFailure, type ArkHostSseSubscription } from './api'; export const arkHostQueryKeys = { characters: (account: string) => ['arkhost', 'characters', account] as const, @@ -76,7 +75,6 @@ export function useGameAccountsQuery() { queryKey: arkHostQueryKeys.gameAccounts(userId), enabled: session !== null, queryFn: async () => { - const { arkHostApi } = getQueryDependencies(); const result = await arkHostApi.fetchGameList(); const entries = parseArkHostPayload( v.array(arkHostGameListEntrySchema), @@ -91,7 +89,6 @@ export const gameDetailQueryOptions = (account: string) => queryOptions({ queryKey: arkHostQueryKeys.detail(account), queryFn: async () => { - const { arkHostApi } = getQueryDependencies(); const result = await arkHostApi.fetchGameDetail(account); return parseArkHostPayload( v.nullable(arkHostGameDetailSchema), @@ -111,7 +108,6 @@ export const charactersQueryOptions = (account: string) => queryOptions({ queryKey: arkHostQueryKeys.characters(account), queryFn: async () => { - const { arkHostApi } = getQueryDependencies(); const result = await arkHostApi.fetchCharacters(account); return parseArkHostPayload(arkHostCharactersSchema, unwrapResult(result)); }, @@ -128,7 +124,6 @@ export const logsQueryOptions = (account: string) => queryOptions({ queryKey: arkHostQueryKeys.logs(account), queryFn: async () => { - const { arkHostApi } = getQueryDependencies(); const result = await arkHostApi.fetchGameLogs(account, 0); return parseArkHostPayload(arkHostGameLogsSchema, unwrapResult(result)); }, @@ -189,7 +184,6 @@ export function useUpdateGameConfig() { kind: 'business', }); } - const { arkHostApi } = getQueryDependencies(); unwrapResult( await arkHostApi.updateGameConfig(account, parsedPatch.output), ); @@ -243,7 +237,6 @@ export function useArkHostSync() { useEffect(() => { if (!session) return; const userId = session.principal.id; - const { arkHostApi } = getQueryDependencies(); const subscription: ArkHostSseSubscription = arkHostApi.subscribe( session.accessToken, (event) => { diff --git a/src/features/dashboard/resources.test.tsx b/src/features/dashboard/resources.test.tsx index 83db952..fcf1ae9 100644 --- a/src/features/dashboard/resources.test.tsx +++ b/src/features/dashboard/resources.test.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook, waitFor } from '@testing-library/react-native'; import type { PropsWithChildren } from 'react'; -import type { GameResourcesApi } from './api'; +import { gameResourcesApi, type GameResourcesApi } from './api'; import { GAME_RESOURCE_CACHE_KEYS } from './game-resource-cache'; import { bundledCharacterTable, @@ -15,10 +15,6 @@ import { useStageTable, } from './resources'; import { mmkvStateStorage } from '@/lib/mmkv'; -import { - configureQueryDependencies, - resetQueryDependencies, -} from '@/services/query-dependencies'; import { appStore } from '@/store'; function createWrapper() { @@ -44,9 +40,21 @@ function createGameResourcesApi( }; } +function mockGameResourcesApi(api: GameResourcesApi): void { + jest.spyOn(gameResourcesApi, 'fetchCharacter').mockImplementation( + (updatedAt) => api.fetchCharacter(updatedAt), + ); + jest.spyOn(gameResourcesApi, 'fetchItem').mockImplementation( + (updatedAt) => api.fetchItem(updatedAt), + ); + jest.spyOn(gameResourcesApi, 'fetchStage').mockImplementation( + (updatedAt) => api.fetchStage(updatedAt), + ); +} + beforeEach(async () => { - resetQueryDependencies(); - configureQueryDependencies({ gameResourcesApi: createGameResourcesApi() }); + jest.restoreAllMocks(); + mockGameResourcesApi(createGameResourcesApi()); for (const key of Object.values(GAME_RESOURCE_CACHE_KEYS)) { mmkvStateStorage.removeItem(key); } @@ -67,7 +75,7 @@ describe('Game resource queries', () => { }), fetchStage: () => Promise.reject(new Error('offline')), }); - configureQueryDependencies({ gameResourcesApi: api }); + mockGameResourcesApi(api); const { wrapper } = createWrapper(); const { result } = await renderHook( () => ({ @@ -101,7 +109,7 @@ describe('Game resource queries', () => { it('deduplicates concurrent consumers and skips refetches within 24 hours', async () => { const fetchItem = jest.fn().mockResolvedValue({ kind: 'not-modified' }); const api = createGameResourcesApi({ fetchItem }); - configureQueryDependencies({ gameResourcesApi: api }); + mockGameResourcesApi(api); const { wrapper } = createWrapper(); const first = await renderHook(() => ({ item: useItemTable() }), { wrapper }); @@ -133,7 +141,7 @@ describe('Game resource queries', () => { updatedAt, }), }); - configureQueryDependencies({ gameResourcesApi: api }); + mockGameResourcesApi(api); const { wrapper } = createWrapper(); const first = await renderHook(() => ({ item: useItemTable() }), { wrapper, @@ -147,7 +155,7 @@ describe('Game resource queries', () => { expect(JSON.parse(raw)).toEqual({ table: updatedTable, updatedAt }); await first.unmount(); - configureQueryDependencies({ gameResourcesApi: createGameResourcesApi() }); + mockGameResourcesApi(createGameResourcesApi()); const second = await renderHook(() => ({ item: useItemTable() }), { wrapper, }); diff --git a/src/features/dashboard/resources.ts b/src/features/dashboard/resources.ts index ea744aa..64418dc 100644 --- a/src/features/dashboard/resources.ts +++ b/src/features/dashboard/resources.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import type { GameResourceResult } from './api'; +import { gameResourcesApi, type GameResourceResult } from './api'; import { GAME_RESOURCE_CACHE_KEYS, loadCachedGameResource, @@ -12,7 +12,6 @@ import { bundledItemTable, bundledStageTable, } from './game-data'; -import { getQueryDependencies } from '@/services/query-dependencies'; import { characterTableSchema, itemTableSchema, @@ -90,7 +89,7 @@ export function useCharacterTable(): CharacterTable { GAME_RESOURCE_CACHE_KEYS.character, characterTableSchema, characterBundled, - (updatedAt) => getQueryDependencies().gameResourcesApi.fetchCharacter(updatedAt), + (updatedAt) => gameResourcesApi.fetchCharacter(updatedAt), ).data.table; } @@ -100,7 +99,7 @@ export function useItemTable(): ItemTable { GAME_RESOURCE_CACHE_KEYS.item, itemTableSchema, itemBundled, - (updatedAt) => getQueryDependencies().gameResourcesApi.fetchItem(updatedAt), + (updatedAt) => gameResourcesApi.fetchItem(updatedAt), ).data.table; } @@ -110,6 +109,6 @@ export function useStageTable(): StageTable { GAME_RESOURCE_CACHE_KEYS.stage, stageTableSchema, stageBundled, - (updatedAt) => getQueryDependencies().gameResourcesApi.fetchStage(updatedAt), + (updatedAt) => gameResourcesApi.fetchStage(updatedAt), ).data.table; } diff --git a/src/features/navigation/components/large-screen-navigation-sidebar.tsx b/src/features/navigation/components/large-screen-navigation-sidebar.tsx index 88306e9..c577711 100644 --- a/src/features/navigation/components/large-screen-navigation-sidebar.tsx +++ b/src/features/navigation/components/large-screen-navigation-sidebar.tsx @@ -43,8 +43,7 @@ export function LargeScreenNavigationSidebar({ borderRightWidth={1} borderColor="$appBorder" bg="$appSurface" - $md={{ display: 'flex' }} - $xl={{ width: 256 }} + $large={{ display: 'flex', width: 256 }} > diff --git a/src/features/navigation/components/navigation-frame.tsx b/src/features/navigation/components/navigation-frame.tsx index 80be79c..1368cb9 100644 --- a/src/features/navigation/components/navigation-frame.tsx +++ b/src/features/navigation/components/navigation-frame.tsx @@ -1,9 +1,7 @@ import type { ReactNode } from 'react'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { Edge } from 'react-native-safe-area-context'; -import { XStack, YStack } from 'tamagui'; - -import { useLayoutSize } from '@/providers/layout-size-provider'; +import { XStack, YStack, useMedia } from 'tamagui'; import { LargeScreenNavigationSidebar } from './large-screen-navigation-sidebar'; import type { DesktopNavigationItem } from './large-screen-navigation-sidebar'; import type { NavigationScope } from '../navigation-config'; @@ -31,11 +29,11 @@ export function NavigationFrame({ scope, smallScreenEdges, }: NavigationFrameProps) { - const layoutSize = useLayoutSize(); + const { large } = useMedia(); return ( diff --git a/src/features/navigation/components/navigation-header.tsx b/src/features/navigation/components/navigation-header.tsx index 0d7b118..494cf96 100644 --- a/src/features/navigation/components/navigation-header.tsx +++ b/src/features/navigation/components/navigation-header.tsx @@ -40,7 +40,7 @@ export function NavigationHeader({ items="center" flexDirection="row-reverse" gap="$2" - $md={{ flexDirection: 'row', px: '$5', gap: '$4' }} + $large={{ flexDirection: 'row', px: '$5', gap: '$4' }} > - + {title} diff --git a/src/features/navigation/components/settings-pager-tabs.tsx b/src/features/navigation/components/settings-pager-tabs.tsx index f0ebc19..1016e07 100644 --- a/src/features/navigation/components/settings-pager-tabs.tsx +++ b/src/features/navigation/components/settings-pager-tabs.tsx @@ -122,7 +122,6 @@ export function SettingsPagerTabs({ return ( diff --git a/src/features/navigation/navigation-frames.test.tsx b/src/features/navigation/navigation-frames.test.tsx index f2356dd..c4c6cf2 100644 --- a/src/features/navigation/navigation-frames.test.tsx +++ b/src/features/navigation/navigation-frames.test.tsx @@ -4,7 +4,6 @@ import { Text } from 'react-native'; import { TamaguiProvider } from 'tamagui'; import { ROUTES } from '@/constants/routes'; -import type { LayoutSize } from '@/schemas/layout-size'; import { tamaguiConfig } from '../../../tamagui.config'; import { DashboardFrame } from './screens/dashboard-frame'; import { SettingsFrame } from './screens/settings-frame'; @@ -33,7 +32,7 @@ const mockReturnToDashboard = jest.fn(); const mockLogout = jest.fn(); const mockSetBackdropTint = jest.fn(); let mockPathname = '/dashboard/overview'; -let mockLayoutSize: LayoutSize = 'small'; +let mockLarge = false; jest.mock('expo-router', () => ({ usePathname: () => mockPathname, @@ -68,8 +67,9 @@ jest.mock('@/features/dashboard', () => ({ }), })); -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => mockLayoutSize, +jest.mock('tamagui', () => ({ + ...jest.requireActual('tamagui'), + useMedia: () => ({ large: mockLarge }), })); jest.mock('@/features/session', () => ({ @@ -107,7 +107,7 @@ describe('navigation scope frames', () => { beforeEach(() => { jest.clearAllMocks(); mockPathname = '/dashboard/overview'; - mockLayoutSize = 'small'; + mockLarge = false; }); it('pushes the complete Settings screen from Dashboard', async () => { diff --git a/src/features/navigation/screens/settings-frame.tsx b/src/features/navigation/screens/settings-frame.tsx index 6509a3a..9cbe832 100644 --- a/src/features/navigation/screens/settings-frame.tsx +++ b/src/features/navigation/screens/settings-frame.tsx @@ -1,9 +1,7 @@ import type { PropsWithChildren } from 'react'; import { useTranslation } from 'react-i18next'; import { usePathname, useRouter } from 'expo-router'; -import { YStack } from 'tamagui'; - -import { useLayoutSize } from '@/providers/layout-size-provider'; +import { YStack, useMedia } from 'tamagui'; import { NavigationFrame } from '../components/navigation-frame'; import { NavigationHeader } from '../components/navigation-header'; import { @@ -15,7 +13,7 @@ import { useAppLogout, useReturnToDashboard } from '../navigation-actions'; export function SettingsFrame({ children }: PropsWithChildren) { const { t } = useTranslation('navigation'); - const layoutSize = useLayoutSize(); + const { large } = useMedia(); const pathname = usePathname(); const router = useRouter(); const onLogout = useAppLogout(); @@ -32,7 +30,7 @@ export function SettingsFrame({ children }: PropsWithChildren) { if (page && page.id !== activePageId) router.navigate(page.route); }; - const header = layoutSize === 'large' ? ( + const header = large ? ( { - resetQueryDependencies(); + jest.restoreAllMocks(); await act(() => { appStore.getState().logout(); }); @@ -37,7 +34,7 @@ describe('API Node queries', () => { ], ok: true, }); - configureQueryDependencies({ apiNodeAdapter: { queryNodes } }); + jest.spyOn(apiNodeApi, 'queryNodes').mockImplementation(queryNodes); const { wrapper } = createWrapper(); const first = await renderHook(() => useApiNodesQuery(), { wrapper }); @@ -57,7 +54,7 @@ describe('API Node queries', () => { data: [{ id: 'domestic', description: 'Domestic', latencyMs: 20, outcome: 'reachable' }], ok: true, })); - configureQueryDependencies({ apiNodeAdapter: { queryNodes } }); + jest.spyOn(apiNodeApi, 'queryNodes').mockImplementation(queryNodes); const { wrapper } = createWrapper(); const { result } = await renderHook(() => useApiNodesQuery(), { wrapper }); diff --git a/src/features/settings/api-node/queries.ts b/src/features/settings/api-node/queries.ts index b548d36..3fad3e5 100644 --- a/src/features/settings/api-node/queries.ts +++ b/src/features/settings/api-node/queries.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { getQueryDependencies } from '@/services/query-dependencies'; +import { apiNodeApi } from './api'; import { unwrapResult } from '@/utils/failure-error'; import type { ApiNode } from '@/schemas/api-node'; import type { ApiNodeFailure } from './api'; @@ -10,6 +10,6 @@ export function useApiNodesQuery() { queryKey: ['api-nodes'], staleTime: Infinity, queryFn: async () => - unwrapResult(await getQueryDependencies().apiNodeAdapter.queryNodes()), + unwrapResult(await apiNodeApi.queryNodes()), }); } diff --git a/src/features/settings/api-node/screens/network-settings-screen.test.tsx b/src/features/settings/api-node/screens/network-settings-screen.test.tsx index 7040afc..5141519 100644 --- a/src/features/settings/api-node/screens/network-settings-screen.test.tsx +++ b/src/features/settings/api-node/screens/network-settings-screen.test.tsx @@ -9,10 +9,6 @@ import { mockApiNodes } from '@/mocks/api-node'; import { NetworkSettingsScreen } from './network-settings-screen'; import type { ApiNode, ApiNodeId } from '@/schemas/api-node'; -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => 'small', -})); - jest.mock('react-native-reanimated', () => { const reanimated = jest.requireActual('react-native-reanimated'); const reanimatedMock = jest.requireActual('react-native-reanimated/mock'); diff --git a/src/features/settings/api-node/screens/network-settings-screen.tsx b/src/features/settings/api-node/screens/network-settings-screen.tsx index cd80543..84f6acb 100644 --- a/src/features/settings/api-node/screens/network-settings-screen.tsx +++ b/src/features/settings/api-node/screens/network-settings-screen.tsx @@ -2,7 +2,7 @@ import { Activity, Circle, CircleDot, Clock3, RefreshCw, Route, Server } from 'l import { useReducedMotion } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; import { type QueryStatus } from '@tanstack/react-query'; -import { AnimatePresence, Button, RadioGroup, Spinner, XStack, YStack, getTokens } from 'tamagui'; +import { AnimatePresence, Button, RadioGroup, Spinner, XStack, YStack, getTokens, useMedia } from 'tamagui'; import { MonoText, @@ -16,7 +16,6 @@ import { } from '@/components'; import { API_NODE_HOSTS } from '@/constants/api'; import type { ApiNodeHost } from '@/constants/api'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import type { ApiNode, ApiNodeId } from '@/schemas/api-node'; import type { ApiNodeFailure } from '../api'; import { SettingsPage } from '../../components/settings-page'; @@ -52,7 +51,7 @@ export function NetworkSettingsScreen({ }: NetworkSettingsScreenProps) { const { t } = useTranslation('settings'); const colors = getTokens().color; - const layoutSize = useLayoutSize(); + const { large } = useMedia(); const reducedMotion = useReducedMotion(); const nodesById = new Map(nodes.map((node) => [node.id, node])); const buildNode = (host: ApiNodeHost): ApiNode => { @@ -97,8 +96,8 @@ export function NetworkSettingsScreen({ title={t('network.title')} /> )}> - - {layoutSize === 'large' ? ( + + {large ? ( {([ - { t: '$4', r: '$4', width: '$12', height: '$12', opacity: 0.22, scale: isChecking ? 1.08 : 1, transition: reducedMotion ? '0ms' : 'slow', md: { t: '$5', r: '$6', width: '$15', height: '$15' } }, - { t: '$6', r: '$6', width: '$8', height: '$8', opacity: 0.12, scale: 1, transition: '0ms', md: { t: '$8', r: '$9', width: '$10', height: '$10' } }, + { t: '$5', r: '$6', width: '$15', height: '$15', opacity: 0.22, scale: isChecking ? 1.08 : 1, transition: reducedMotion ? '0ms' : 'slow' }, + { t: '$8', r: '$9', width: '$10', height: '$10', opacity: 0.12, scale: 1, transition: '0ms' }, ] as const).map((ring) => ( ))} @@ -129,7 +127,7 @@ export function NetworkSettingsScreen({ {t('network.sessionId')} - + {selectedApiNode.description} @@ -143,7 +141,6 @@ export function NetworkSettingsScreen({ {t(`network.nodes.${selectedApiNode.id}`)} @@ -151,7 +148,7 @@ export function NetworkSettingsScreen({ - + {t('network.latency')} @@ -190,7 +187,7 @@ export function NetworkSettingsScreen({ ) : null} - {layoutSize === 'large' ? ( + {large ? ( {t('network.nodesTitle')} @@ -222,7 +219,7 @@ export function NetworkSettingsScreen({ value={selectedApiNodeId} indicator={} width="100%" flexDirection="column" gap="$3" - $lg={{ flexDirection: 'row' }} + $large={{ flexDirection: 'row' }} > {displayNodes.map((apiNode, index) => { const isSelected = apiNode.id === selectedApiNodeId; @@ -231,7 +228,7 @@ export function NetworkSettingsScreen({ const SelectionIcon = isSelected ? CircleDot : Circle; return ( - + {String(index + 1).padStart(2, '0')} @@ -264,7 +261,7 @@ export function NetworkSettingsScreen({ - + {t(`network.nodes.${apiNode.id}`)} {apiNode.description} @@ -275,7 +272,7 @@ export function NetworkSettingsScreen({ size="$6" lineHeight="$6" fontWeight="900" color={!isChecking ? latencyTone : '$appMuted'} fontVariant={['tabular-nums']} - $md={{ size: '$7', lineHeight: '$7' }} + $large={{ size: '$7', lineHeight: '$7' }} > {!isChecking && isReachable ? apiNode.latencyMs : '--'} diff --git a/src/features/settings/components/settings-page.tsx b/src/features/settings/components/settings-page.tsx index 5e9d81b..ae67fb3 100644 --- a/src/features/settings/components/settings-page.tsx +++ b/src/features/settings/components/settings-page.tsx @@ -1,10 +1,8 @@ import type { PropsWithChildren, ReactNode } from 'react'; -import { ScrollView, YStack } from 'tamagui'; - -import { useLayoutSize } from '@/providers/layout-size-provider'; +import { ScrollView, YStack, useMedia } from 'tamagui'; export function SettingsPage({ children, header }: PropsWithChildren<{ header?: ReactNode }>) { - const layoutSize = useLayoutSize(); + const { large } = useMedia(); return ( - {header && layoutSize === 'large' ? header : null} + {header && large ? header : null} {children} diff --git a/src/features/settings/contributors/components/contributors-operations-roster.tsx b/src/features/settings/contributors/components/contributors-operations-roster.tsx index fd45919..0b4b3f6 100644 --- a/src/features/settings/contributors/components/contributors-operations-roster.tsx +++ b/src/features/settings/contributors/components/contributors-operations-roster.tsx @@ -69,10 +69,7 @@ const RosterRowFrame = styled(XStack, { hoverStyle: { bg: '$appAccentSoft', }, - $xs: { - px: '$3.5', - }, - $md: { + $large: { px: '$4', }, @@ -190,7 +187,7 @@ function ContributorRosterRow({ shrink={1} justify="center" gap="$1" - $xs={{ flexDirection: 'row', items: 'center', gap: '$3' }} + $large={{ flexDirection: 'row', items: 'center', gap: '$3' }} > {member.name} diff --git a/src/features/settings/contributors/components/contributors-tribute.tsx b/src/features/settings/contributors/components/contributors-tribute.tsx index 039e64a..39c7fd9 100644 --- a/src/features/settings/contributors/components/contributors-tribute.tsx +++ b/src/features/settings/contributors/components/contributors-tribute.tsx @@ -242,7 +242,7 @@ export function ContributorsTribute({ onLayout={handleLayout} onPointerEnter={handleHoverIn} onPointerLeave={handleHoverOut} - $md={{ px: '$5', py: '$3' }} + $large={{ px: '$5', py: '$3' }} $platform-web={{ clipPath: 'polygon(0 12px, 12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%)', }} @@ -273,7 +273,7 @@ export function ContributorsTribute({ position="relative" z={1} gap="$4" - $md={{ gap: '$4' }} + $large={{ gap: '$4' }} > {recipientPrefix} @@ -358,9 +356,7 @@ export function ContributorsTribute({ letterSpacing={-0.6} textTransform="uppercase" select="text" - $xxs={{ size: '$8', lineHeight: '$8' }} - $xs={{ size: '$9', lineHeight: '$9' }} - $md={{ size: '$10', lineHeight: '$10' }} + $large={{ size: '$10', lineHeight: '$10' }} > {recipientCallsign} diff --git a/src/features/settings/contributors/screens/contributors-screen.tsx b/src/features/settings/contributors/screens/contributors-screen.tsx index 715d637..29b9e7b 100644 --- a/src/features/settings/contributors/screens/contributors-screen.tsx +++ b/src/features/settings/contributors/screens/contributors-screen.tsx @@ -25,7 +25,7 @@ export function ContributorsScreen() { /> )} > - + ({ - useLayoutSize: () => 'small', -})); - jest.mock('react-native-reanimated', () => { const reanimated = jest.requireActual('react-native-reanimated'); const reanimatedMock = jest.requireActual('react-native-reanimated/mock'); diff --git a/src/features/settings/user-account/screens/account-settings-screen.tsx b/src/features/settings/user-account/screens/account-settings-screen.tsx index 59b97b1..44ff30a 100644 --- a/src/features/settings/user-account/screens/account-settings-screen.tsx +++ b/src/features/settings/user-account/screens/account-settings-screen.tsx @@ -36,7 +36,6 @@ import { passwordChangeIssue, } from '@/schemas/user-account'; import type { PasswordChangeInput } from '@/schemas/user-account'; -import { useLayoutSize } from '@/providers/layout-size-provider'; import { SettingsPage } from '../../components/settings-page'; type PasswordField = 'currentPassword' | 'newPassword' | 'repeatNewPassword'; @@ -151,7 +150,7 @@ function AccountIdentityPanel({ gap="$4" tone="cyan" cornerBrackets - $md={{ p: '$4.5' }} + $large={{ p: '$4.5' }} > @@ -186,7 +185,7 @@ function AccountIdentityPanel({ - + )} > - - {layoutSize === 'small' ? ( - - {t('account.description')} - - ) : null} + + + {t('account.description')} + - - + + - + + {renderPasswordField({ autoComplete: 'new-password', diff --git a/src/providers/layout-size-provider.test.tsx b/src/providers/layout-size-provider.test.tsx deleted file mode 100644 index da6bba9..0000000 --- a/src/providers/layout-size-provider.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { render, screen } from '@testing-library/react-native'; -import { Text } from 'react-native'; - -import { useLayoutSize } from './layout-size-provider'; - -let mockMediaMd = false; - -jest.mock('tamagui', () => ({ - useMedia: () => ({ md: mockMediaMd }), -})); - -function LayoutSizeProbe() { - const layoutSize = useLayoutSize(); - return {layoutSize}; -} - -describe('useLayoutSize', () => { - it.each([ - { mediaMd: false, layoutSize: 'small' }, - { mediaMd: true, layoutSize: 'large' }, - ] as const)('projects Tamagui md=$mediaMd to $layoutSize', async ({ layoutSize, mediaMd }) => { - mockMediaMd = mediaMd; - - await render(); - - expect(screen.getByText(layoutSize)).toBeTruthy(); - }); -}); diff --git a/src/providers/layout-size-provider.tsx b/src/providers/layout-size-provider.tsx deleted file mode 100644 index e83606a..0000000 --- a/src/providers/layout-size-provider.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useMedia } from 'tamagui'; - -import type { LayoutSize } from '@/schemas/layout-size'; - -export function useLayoutSize(): LayoutSize { - const media = useMedia(); - return media.md ? 'large' : 'small'; -} diff --git a/src/schemas/layout-size/index.ts b/src/schemas/layout-size/index.ts deleted file mode 100644 index c344790..0000000 --- a/src/schemas/layout-size/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { layoutSizeSchema } from './layout-size.schema'; -export type { LayoutSize } from './layout-size.schema'; diff --git a/src/schemas/layout-size/layout-size.schema.test.ts b/src/schemas/layout-size/layout-size.schema.test.ts deleted file mode 100644 index c97d2c7..0000000 --- a/src/schemas/layout-size/layout-size.schema.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from 'valibot'; - -import { layoutSizeSchema } from './layout-size.schema'; - -describe('layoutSizeSchema', () => { - it.each(['small', 'large'] as const)('accepts the %s Layout Size', (layoutSize) => { - expect(v.safeParse(layoutSizeSchema, layoutSize).success).toBe(true); - }); - - it.each(['compact', null, {}])('rejects malformed Layout Size input', (input) => { - expect(v.safeParse(layoutSizeSchema, input).success).toBe(false); - }); -}); diff --git a/src/schemas/layout-size/layout-size.schema.ts b/src/schemas/layout-size/layout-size.schema.ts deleted file mode 100644 index 9545961..0000000 --- a/src/schemas/layout-size/layout-size.schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as v from 'valibot'; - -export const layoutSizeSchema = v.picklist(['small', 'large']); - -export type LayoutSize = v.InferOutput; diff --git a/src/services/query-dependencies.ts b/src/services/query-dependencies.ts deleted file mode 100644 index 5e63e9e..0000000 --- a/src/services/query-dependencies.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { AuthAdapter } from '@/features/auth/api'; -import { MockAuthAdapter } from '@/features/auth/api'; -import type { ArkHostApi, GameResourcesApi } from '@/features/dashboard/api'; -import { - MockArkHostApi, - RemoteGameResourcesApi, -} from '@/features/dashboard/api'; -import type { ApiNodeAdapter } from '@/features/settings/api-node/api'; -import { RemoteApiNodeAdapter } from '@/features/settings/api-node/api'; - -export type QueryDependencies = { - apiNodeAdapter: ApiNodeAdapter; - arkHostApi: ArkHostApi; - authAdapter: AuthAdapter; - gameResourcesApi: GameResourcesApi; -}; - -const defaultDependencies: QueryDependencies = { - apiNodeAdapter: new RemoteApiNodeAdapter(), - arkHostApi: new MockArkHostApi(), - authAdapter: new MockAuthAdapter(), - gameResourcesApi: new RemoteGameResourcesApi(), -}; - -let currentDependencies: QueryDependencies = defaultDependencies; - -export function getQueryDependencies(): QueryDependencies { - return currentDependencies; -} - -export function configureQueryDependencies( - overrides: Partial, -): void { - currentDependencies = { ...currentDependencies, ...overrides }; -} - -export function resetQueryDependencies(): void { - currentDependencies = defaultDependencies; -} \ No newline at end of file diff --git a/tamagui.config.ts b/tamagui.config.ts index 8f17bd3..98cfe3e 100644 --- a/tamagui.config.ts +++ b/tamagui.config.ts @@ -77,6 +77,9 @@ const geistMonoFont = createFont({ export const tamaguiConfig = createTamagui({ ...defaultConfig, animations, + media: { + large: { minWidth: 768 }, + }, fonts: { ...defaultConfig.fonts, body: oxaniumFont, diff --git a/tests/route-layouts.test.tsx b/tests/route-layouts.test.tsx index 99b7304..58528dc 100644 --- a/tests/route-layouts.test.tsx +++ b/tests/route-layouts.test.tsx @@ -39,10 +39,6 @@ jest.mock('@/providers', () => ({ AppProvider: mockAppProvider, })); -jest.mock('@/providers/layout-size-provider', () => ({ - useLayoutSize: () => 'small', -})); - jest.mock('@/store', () => ({ useAppStore: (selector: (state: { auth: { session: object | null } }) => unknown) => selector({ auth: { session: mockSession }, From f999b6f1f0970908b7c4d935c76c5f01376f7588 Mon Sep 17 00:00:00 2001 From: jackjieyyy <53422254+jackjieYYY@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:10:06 +1000 Subject: [PATCH 2/3] refactor: update Frame and GameAccount components for improved styling and functionality --- src/components/ui/frame/frame.test.tsx | 24 ++++++- src/components/ui/frame/frame.tsx | 2 +- .../game-account-overview-view.test.tsx | 64 +++++++++++++++++++ .../components/game-account-overview-view.tsx | 6 +- .../dashboard/dashboard-account.test.tsx | 37 ++++++++--- src/features/dashboard/dashboard-account.tsx | 4 +- 6 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 src/features/dashboard/components/game-account-overview-view.test.tsx diff --git a/src/components/ui/frame/frame.test.tsx b/src/components/ui/frame/frame.test.tsx index 8858450..7fd76f0 100644 --- a/src/components/ui/frame/frame.test.tsx +++ b/src/components/ui/frame/frame.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react-native'; -import { Text } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; import { TamaguiProvider } from 'tamagui'; import { tamaguiConfig } from '../../../../tamagui.config'; @@ -16,6 +16,28 @@ function FrameFixture({ selected }: { selected?: boolean }) { } describe('Frame', () => { + it('uses the shared translucent surface by default', async () => { + const screen = await render(); + + expect(StyleSheet.flatten(screen.getByTestId('frame').props.style)).toEqual( + expect.objectContaining({ backgroundColor: 'rgba(29, 32, 34, 0.40)' }), + ); + }); + + it('lets an explicit tone override the shared translucent surface', async () => { + const screen = await render( + + + Content + + , + ); + + expect(StyleSheet.flatten(screen.getByTestId('warning-frame').props.style)).toEqual( + expect.objectContaining({ backgroundColor: 'rgba(255, 157, 54, 0.05)' }), + ); + }); + it('renders four corner brackets only in the selected state', async () => { const screen = await render(); diff --git a/src/components/ui/frame/frame.tsx b/src/components/ui/frame/frame.tsx index f078cc5..c181ae5 100644 --- a/src/components/ui/frame/frame.tsx +++ b/src/components/ui/frame/frame.tsx @@ -5,7 +5,7 @@ const FrameRoot = styled(YStack, { name: 'Frame', position: 'relative', overflow: 'hidden', - bg: '$appSurface', + bg: '$appSurfaceRaisedTranslucent', borderWidth: 1, borderColor: '$appBorder', rounded: '$0', diff --git a/src/features/dashboard/components/game-account-overview-view.test.tsx b/src/features/dashboard/components/game-account-overview-view.test.tsx new file mode 100644 index 0000000..33dd616 --- /dev/null +++ b/src/features/dashboard/components/game-account-overview-view.test.tsx @@ -0,0 +1,64 @@ +import { render } from '@testing-library/react-native'; +import { I18nextProvider } from 'react-i18next'; +import { StyleSheet } from 'react-native'; +import { TamaguiProvider } from 'tamagui'; +import * as v from 'valibot'; + +import { i18n } from '@/i18n'; +import { mockArkHostGameListResponse } from '@/mocks/arkhost'; +import { gameAccountSchema } from '@/schemas/game-account'; +import { tamaguiConfig } from '../../../../tamagui.config'; +import { GameAccountOverviewView } from './game-account-overview-view'; + +jest.mock('expo-image', () => { + const { View } = jest.requireActual('react-native'); + + return { + Image: (props: { [key: string]: unknown }) => , + }; +}); + +const gameAccountEntry = mockArkHostGameListResponse.code === 1 + ? mockArkHostGameListResponse.data[0] + : undefined; + +if (!gameAccountEntry) throw new Error('Expected a Game Account fixture.'); + +const gameAccount = v.parse(gameAccountSchema, { + account: gameAccountEntry.status.account, + ap: gameAccountEntry.status.ap, + avatar: gameAccountEntry.status.avatar, + captchaInfo: gameAccountEntry.captcha_info, + color: 'primary', + config: gameAccountEntry.game_config, + createdAt: gameAccountEntry.status.created_at, + isVerified: gameAccountEntry.status.is_verify, + level: gameAccountEntry.status.level, + nickname: gameAccountEntry.status.nick_name, + platform: gameAccountEntry.status.platform, + statusCode: gameAccountEntry.status.code, + statusText: gameAccountEntry.status.text, + userId: gameAccountEntry.status.uuid, +}); + +describe('GameAccountOverviewView', () => { + it('keeps the profile column within the small-screen content width', async () => { + const screen = await render( + + + + + , + ); + + expect(StyleSheet.flatten(screen.getByTestId('overview-profile-column').props.style)).toEqual( + expect.objectContaining({ width: '100%' }), + ); + }); +}); diff --git a/src/features/dashboard/components/game-account-overview-view.tsx b/src/features/dashboard/components/game-account-overview-view.tsx index c5eccbc..7c09a70 100644 --- a/src/features/dashboard/components/game-account-overview-view.tsx +++ b/src/features/dashboard/components/game-account-overview-view.tsx @@ -64,7 +64,11 @@ export function GameAccountOverviewView({ return ( - + ({ - useLocalSearchParams: jest.fn(() => ({ gameAccountId: 'G16601716973' })), + useGlobalSearchParams: jest.fn(() => ({ gameAccountId: 'G16601716973' })), useRouter: () => ({ setParams: mockRouterSetParams }), })); @@ -50,8 +50,8 @@ jest.mock('./queries', () => ({ useGameAccountsQuery: jest.fn(() => mockGameAccountsQuery), })); -const mockUseLocalSearchParams = jest.mocked( - jest.requireMock('expo-router').useLocalSearchParams, +const mockUseGlobalSearchParams = jest.mocked( + jest.requireMock('expo-router').useGlobalSearchParams, ); function DashboardAccountTestWrapper({ children }: PropsWithChildren) { @@ -63,8 +63,8 @@ function createWrapper() { } beforeEach(() => { - mockUseLocalSearchParams.mockReset(); - mockUseLocalSearchParams.mockReturnValue({ gameAccountId: mockFirstAccount.account }); + mockUseGlobalSearchParams.mockReset(); + mockUseGlobalSearchParams.mockReturnValue({ gameAccountId: mockFirstAccount.account }); mockRouterSetParams.mockReset(); mockGameAccountsQuery = { data: [mockFirstAccount, mockSecondAccount], @@ -84,7 +84,7 @@ describe('DashboardAccountProvider', () => { }); it('ignores non-string account route parameters', async () => { - mockUseLocalSearchParams.mockReturnValue({ gameAccountId: ['G1'] }); + mockUseGlobalSearchParams.mockReturnValue({ gameAccountId: ['G1'] }); const { result } = await renderHook(() => useDashboardAccount(), { wrapper: createWrapper(), @@ -94,7 +94,7 @@ describe('DashboardAccountProvider', () => { }); it('keeps foreign account IDs out of the dashboard context', async () => { - mockUseLocalSearchParams.mockReturnValue({ gameAccountId: 'G9' }); + mockUseGlobalSearchParams.mockReturnValue({ gameAccountId: 'G9' }); const { result } = await renderHook(() => useDashboardAccount(), { wrapper: createWrapper(), @@ -103,4 +103,25 @@ describe('DashboardAccountProvider', () => { expect(result.current.selectedGameAccount).toBeNull(); }); + it('updates the selected account when the active Dashboard URL changes', async () => { + const { result, rerender } = await renderHook(() => useDashboardAccount(), { + wrapper: createWrapper(), + }); + + await act(() => { + result.current.selectGameAccount(mockSecondAccount.account); + }); + + expect(mockRouterSetParams).toHaveBeenCalledWith({ + gameAccountId: mockSecondAccount.account, + }); + expect(result.current.selectedGameAccount?.account).toBe(mockFirstAccount.account); + + mockUseGlobalSearchParams.mockReturnValue({ + gameAccountId: mockSecondAccount.account, + }); + await rerender(undefined); + + expect(result.current.selectedGameAccount?.account).toBe(mockSecondAccount.account); + }); }); diff --git a/src/features/dashboard/dashboard-account.tsx b/src/features/dashboard/dashboard-account.tsx index 7e9ecff..46a84d4 100644 --- a/src/features/dashboard/dashboard-account.tsx +++ b/src/features/dashboard/dashboard-account.tsx @@ -1,4 +1,4 @@ -import { useLocalSearchParams, useRouter } from 'expo-router'; +import { useGlobalSearchParams, useRouter } from 'expo-router'; import { createContext, type PropsWithChildren, @@ -29,7 +29,7 @@ type DashboardAccountContextValue = { const DashboardAccountContext = createContext(null); export function DashboardAccountProvider({ children }: PropsWithChildren) { - const { gameAccountId: routeGameAccountId } = useLocalSearchParams(); + const { gameAccountId: routeGameAccountId } = useGlobalSearchParams(); const router = useRouter(); const gameAccountIdResult = v.safeParse(gameAccountIdSchema, routeGameAccountId); const gameAccountId = gameAccountIdResult.success ? gameAccountIdResult.output : null; From fbc328028e6149363b8e196c5d91c772e0be12fc Mon Sep 17 00:00:00 2001 From: jackjieyyy <53422254+jackjieYYY@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:10:18 +1000 Subject: [PATCH 3/3] refactor: remove redundant test for session refresh behavior in useSessionQueryCacheReset --- .../dashboard/session-query-cache-reset.test.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/features/dashboard/session-query-cache-reset.test.tsx b/src/features/dashboard/session-query-cache-reset.test.tsx index 3b3bc1f..cd5c8f2 100644 --- a/src/features/dashboard/session-query-cache-reset.test.tsx +++ b/src/features/dashboard/session-query-cache-reset.test.tsx @@ -61,20 +61,4 @@ describe('useSessionQueryCacheReset', () => { expect(queryClient.getQueryCache().findAll()).toHaveLength(0); }); }); - - it('keeps the Query cache when the same principal refreshes its session', async () => { - const { queryClient, wrapper } = createWrapper(); - await act(() => { - appStore.getState().setSession(mockActiveSession); - }); - await renderHook(() => useSessionQueryCacheReset(), { wrapper }); - - queryClient.setQueryData(['seed'], { value: 1 }); - - await act(() => { - appStore.getState().setSession(mockActiveSession); - }); - - expect(queryClient.getQueryCache().findAll()).toHaveLength(1); - }); });