From ed25c0311bc05d65263a818bc1d5e784579a137e Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 31 Jul 2026 10:32:49 -0700 Subject: [PATCH 1/2] RD-T43 Expo 56, Livekit 2.10 and Gluestack v5 update --- .github/workflows/react-native-cicd.yml | 2 + .vscode/settings.json | 11 +- __mocks__/@notifee/react-native.web.js | 16 + __mocks__/react-native-gesture-handler.ts | 85 +- babel.config.js | 10 +- electron/main.js | 137 +- electron/preload.js | 20 + global.css | 699 +++++- jest-setup.ts | 2 + jest.config.js | 5 +- jest.resolver.js | 15 + metro.config.js | 12 +- package.json | 40 +- patches/@gluestack-ui+core+5.0.15.patch | 32 + patches/react-native-css-interop+0.1.22.patch | 15 - postcss.config.mjs | 5 + react-native-css-env.d.ts | 3 + src/__tests__/app/call/[id].security.test.tsx | 25 +- src/__tests__/app/call/[id].test.tsx | 22 +- .../app/call/new/what3words.test.tsx | 1 + src/__tests__/app/calls.test.tsx | 1 + src/__tests__/app/index.test.tsx | 1 + src/__tests__/app/login/index.web.test.tsx | 1 + src/__tests__/app/login/login-form.test.tsx | 1 + src/__tests__/app/root/lockscreen.test.tsx | 60 + src/app/(app)/_layout.tsx | 37 +- src/app/(app)/home.tsx | 370 ++- src/app/(app)/home.web.tsx | 314 ++- src/app/(app)/map.tsx | 35 +- src/app/(app)/map.web.tsx | 4 +- src/app/call/[id].tsx | 6 +- src/app/lockscreen.tsx | 23 +- src/app/login/index.web.tsx | 6 +- src/app/maintenance.tsx | 6 +- src/app/weather-alerts/[id].tsx | 2 +- .../bluetooth/bluetooth-audio-modal.tsx | 15 + .../callVideoFeeds/video-player.tsx | 11 +- .../__tests__/call-notes-modal-new.test.tsx | 1 + .../close-call-bottom-sheet.test.tsx | 1 + .../dispatch-selection-basic.test.tsx | 1 + .../dispatch-selection-modal.test.tsx | 1 + src/components/calls/call-card.web.tsx | 2 +- src/components/calls/call-files-modal.tsx | 21 +- src/components/calls/call-form-renderer.tsx | 52 +- src/components/calls/call-images-modal.tsx | 4 +- src/components/calls/call-notes-modal.tsx | 2 +- .../__tests__/check-in-bottom-sheet.test.tsx | 1 + .../__tests__/check-in-history-list.test.tsx | 1 + .../checkIn/__tests__/check-in-tab.test.tsx | 3 + .../__tests__/check-in-timer-card.test.tsx | 1 + src/components/common/loading.tsx | 2 +- src/components/contacts/contact-card.tsx | 2 +- .../contacts/contact-details-sheet.tsx | 4 +- .../__tests__/add-note-bottom-sheet.test.tsx | 1 + .../__tests__/animated-refresh-icon.test.tsx | 1 + .../dispatch-console/active-calls-panel.tsx | 169 +- .../dispatch-console/activity-log-panel.tsx | 130 +- .../animated-refresh-icon.web.tsx | 37 +- .../dashboard-view-toggles.tsx | 33 +- .../dispatch-console/map-widget.tsx | 6 +- .../dispatch-console/notes-panel.tsx | 15 +- .../dispatch-console/panel-header.tsx | 2 +- .../personnel-actions-panel.tsx | 6 +- .../dispatch-console/personnel-panel.tsx | 79 +- .../dispatch-console/resources-panel.tsx | 8 +- .../dispatch-console/stats-header.tsx | 72 +- .../dispatch-console/unit-actions-panel.tsx | 6 +- .../dispatch-console/units-panel.tsx | 82 +- .../incident-command/command-map.tsx | 1 - .../maps/__tests__/pin-actions.test.tsx | 5 +- src/components/maps/map-pins.tsx | 6 +- src/components/maps/pin-marker.tsx | 16 +- src/components/maps/unified-map-view.tsx | 9 +- src/components/maps/unified-map-view.web.tsx | 99 +- .../__tests__/note-details-sheet.test.tsx | 1 + src/components/notes/note-card.tsx | 2 +- .../notifications/NotificationDetail.tsx | 47 +- .../notifications/NotificationInbox.tsx | 23 +- .../__tests__/protocol-details-sheet.test.tsx | 1 + src/components/protocols/protocol-card.tsx | 2 +- .../__tests__/roles-bottom-sheet.test.tsx | 1 + .../login-info-bottom-sheet-simple.test.tsx | 1 + .../login-info-bottom-sheet.test.tsx | 1 + .../server-url-bottom-sheet-simple.test.tsx | 1 + .../__tests__/status-bottom-sheet.test.tsx | 1 + src/components/toast/toast.tsx | 29 +- .../ui/__tests__/bottom-sheet.test.tsx | 1 + src/components/ui/accordion/index.tsx | 77 +- src/components/ui/actionsheet/index.tsx | 188 +- src/components/ui/alert-dialog/index.tsx | 102 +- src/components/ui/alert/index.tsx | 80 +- src/components/ui/avatar/index.tsx | 17 +- src/components/ui/badge/index.tsx | 78 +- src/components/ui/bottomsheet/index.tsx | 77 +- src/components/ui/box/index.tsx | 2 +- src/components/ui/box/index.web.tsx | 10 +- src/components/ui/box/styles.tsx | 3 +- src/components/ui/button/index.tsx | 32 +- src/components/ui/card/index.tsx | 2 +- src/components/ui/card/index.web.tsx | 10 +- src/components/ui/card/styles.tsx | 3 +- src/components/ui/center/index.tsx | 2 +- src/components/ui/center/index.web.tsx | 10 +- src/components/ui/center/styles.tsx | 3 +- src/components/ui/checkbox/index.tsx | 118 +- src/components/ui/divider/index.tsx | 20 +- src/components/ui/drawer/index.tsx | 106 +- src/components/ui/fab/index.tsx | 40 +- src/components/ui/focus-aware-status-bar.tsx | 2 +- src/components/ui/form-control/index.tsx | 100 +- .../ui/gluestack-ui-provider/config.ts | 309 --- .../ui/gluestack-ui-provider/index.tsx | 37 +- .../ui/gluestack-ui-provider/index.web.tsx | 43 +- src/components/ui/grid/index.tsx | 159 +- src/components/ui/grid/index.web.tsx | 2 +- src/components/ui/grid/styles.tsx | 4 +- src/components/ui/heading/index.tsx | 24 +- src/components/ui/heading/index.web.tsx | 2 +- src/components/ui/heading/styles.tsx | 3 +- src/components/ui/hstack/index.tsx | 2 +- src/components/ui/hstack/index.web.tsx | 10 +- src/components/ui/hstack/styles.tsx | 3 +- src/components/ui/icon/index.tsx | 27 +- src/components/ui/icon/index.web.tsx | 6 +- src/components/ui/image-background/index.tsx | 8 +- src/components/ui/image.tsx | 6 +- src/components/ui/image/index.tsx | 8 +- src/components/ui/index.tsx | 6 +- src/components/ui/input/index.tsx | 30 +- src/components/ui/link/index.tsx | 19 +- src/components/ui/menu/index.tsx | 56 +- src/components/ui/modal/index.tsx | 98 +- src/components/ui/popover/index.tsx | 91 +- src/components/ui/portal/index.tsx | 14 +- src/components/ui/pressable/index.tsx | 18 +- src/components/ui/progress/index.tsx | 14 +- src/components/ui/radio/index.tsx | 34 +- src/components/ui/select/index.tsx | 42 +- .../ui/select/select-actionsheet.tsx | 119 +- src/components/ui/skeleton/index.tsx | 84 +- src/components/ui/skeleton/index.web.tsx | 2 +- src/components/ui/skeleton/styles.tsx | 2 +- src/components/ui/slider/index.tsx | 49 +- src/components/ui/spinner/index.tsx | 17 +- src/components/ui/switch/index.tsx | 20 +- src/components/ui/text/index.tsx | 2 +- src/components/ui/text/index.web.tsx | 2 +- src/components/ui/text/styles.tsx | 3 +- src/components/ui/textarea/index.tsx | 32 +- src/components/ui/toast/index.tsx | 28 +- src/components/ui/tooltip/index.tsx | 45 +- .../ui/utils/use-break-point-value.ts | 104 - src/components/ui/vstack/index.tsx | 2 +- src/components/ui/vstack/index.web.tsx | 10 +- src/components/ui/vstack/styles.tsx | 3 +- src/hooks/use-saml-login.ts | 5 +- src/lib/auth/api.tsx | 82 +- src/lib/countly.ts | 42 +- src/lib/dispatch-helpers.ts | 8 +- .../__tests__/use-selected-theme.test.ts | 40 +- src/lib/hooks/use-selected-theme.tsx | 23 +- src/lib/hooks/use-selected-theme.web.tsx | 48 +- src/lib/i18n/utils.web.tsx | 16 +- src/lib/server-url.ts | 6 +- src/lib/storage/index.tsx | 9 +- src/lib/storage/index.web.tsx | 35 +- src/lib/utils.ts | 31 +- .../__tests__/signalr.service.test.ts | 2 +- src/services/audio.service.web.ts | 36 +- src/services/signalr.service.ts | 26 +- src/stores/app/audio-stream-store.ts | 16 +- src/stores/app/livekit-store.ts | 18 +- src/stores/auth/store.tsx | 17 +- src/stores/dispatch/dashboard-view-store.ts | 44 +- src/stores/signalr/signalr-store.ts | 30 +- src/stores/toast/store.ts | 19 +- src/stores/weatherAlerts/store.ts | 4 +- src/translations/ar.json | 3 +- src/translations/de.json | 3 +- src/translations/en.json | 6 +- src/translations/es.json | 3 +- src/translations/fr.json | 3 +- src/translations/it.json | 3 +- src/translations/pl.json | 3 +- src/translations/sv.json | 3 +- src/translations/uk.json | 3 +- tailwind.config.js | 193 -- tsconfig.json | 2 +- webpack.config.js | 27 - yarn.lock | 2207 ++++++----------- 190 files changed, 4409 insertions(+), 4331 deletions(-) create mode 100644 __mocks__/@notifee/react-native.web.js create mode 100644 jest.resolver.js create mode 100644 patches/@gluestack-ui+core+5.0.15.patch delete mode 100644 patches/react-native-css-interop+0.1.22.patch create mode 100644 postcss.config.mjs create mode 100644 react-native-css-env.d.ts delete mode 100644 src/components/ui/gluestack-ui-provider/config.ts delete mode 100644 src/components/ui/utils/use-break-point-value.ts delete mode 100644 tailwind.config.js delete mode 100644 webpack.config.js diff --git a/.github/workflows/react-native-cicd.yml b/.github/workflows/react-native-cicd.yml index 94fb98d..4f3f408 100644 --- a/.github/workflows/react-native-cicd.yml +++ b/.github/workflows/react-native-cicd.yml @@ -564,6 +564,8 @@ jobs: | grep -v "" \ | grep -v "" \ || true)" + # Replace occurrences of "PR" with "Release" + NOTES="$(printf '%s\n' "$NOTES" | sed -E 's/\bPR\b/Release/g')" else NOTES="$(git log -n 5 --pretty=format:'- %s')" fi diff --git a/.vscode/settings.json b/.vscode/settings.json index f78ed7c..38e6da6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,12 +19,19 @@ "statusBar.background": "#8ab9ab", "statusBar.foreground": "#15202b", "statusBarItem.hoverBackground": "#6aa694", - "statusBarItem.remoteBackground": "#8ab9ab", + "statusBarItem.remoteBackground": "#8c5fa0", "statusBarItem.remoteForeground": "#15202b", "titleBar.activeBackground": "#8ab9ab", "titleBar.activeForeground": "#15202b", "titleBar.inactiveBackground": "#8ab9ab99", - "titleBar.inactiveForeground": "#15202b99" + "titleBar.inactiveForeground": "#15202b99", + "activityBarTop.activeBackground": "#aaccc2", + "activityBarTop.background": "#aaccc2", + "activityBarTop.foreground": "#15202b", + "activityBarTop.inactiveForeground": "#15202b99", + "commandCenter.foreground": "#15202b", + "statusBar.debuggingBackground": "#8ab9ab", + "statusBar.debuggingForeground": "#15202b" }, "peacock.color": "#8ab9ab", "cSpell.words": [ diff --git a/__mocks__/@notifee/react-native.web.js b/__mocks__/@notifee/react-native.web.js new file mode 100644 index 0000000..eecf21e --- /dev/null +++ b/__mocks__/@notifee/react-native.web.js @@ -0,0 +1,16 @@ +const AndroidImportance = { + DEFAULT: 3, + HIGH: 4, + LOW: 2, + MIN: 1, + NONE: 0, +}; + +const notifee = { + registerForegroundService: () => {}, + createChannel: async () => 'mock-channel-id', + displayNotification: async () => 'mock-notification-id', + stopForegroundService: async () => undefined, +}; + +module.exports = { default: notifee, AndroidImportance }; diff --git a/__mocks__/react-native-gesture-handler.ts b/__mocks__/react-native-gesture-handler.ts index 925c788..395a0d9 100644 --- a/__mocks__/react-native-gesture-handler.ts +++ b/__mocks__/react-native-gesture-handler.ts @@ -1 +1,84 @@ -module.exports = require('react-native-gesture-handler/src/mocks.js'); +// Manual Jest mock for react-native-gesture-handler. +// RNGH 2.28 no longer ships src/mocks.js (the library mocks moved to +// src/mocks/mocks), so re-export them and add the named exports the app +// imports (ScrollView, GestureHandlerRootView, Gesture, GestureDetector). +/* eslint-disable @typescript-eslint/no-explicit-any */ +const RN = require('react-native'); +const libraryMocks = require('react-native-gesture-handler/src/mocks/mocks'); +const { State } = require('react-native-gesture-handler/src/State'); +const { Directions } = require('react-native-gesture-handler/src/Directions'); + +// Chainable no-op gesture builder (Gesture.Pan().onStart(...).onEnd(...) etc.) +const createChainableGesture = (): any => { + const gesture: any = {}; + const chainableMethods = [ + 'onBegin', + 'onStart', + 'onUpdate', + 'onChange', + 'onEnd', + 'onFinalize', + 'onTouchesDown', + 'onTouchesMove', + 'onTouchesUp', + 'onTouchesCancelled', + 'enabled', + 'shouldCancelWhenOutside', + 'hitSlop', + 'activeOffsetX', + 'activeOffsetY', + 'failOffsetX', + 'failOffsetY', + 'minDistance', + 'minPointers', + 'maxPointers', + 'minDuration', + 'maxDuration', + 'maxDelay', + 'numberOfTaps', + 'maxDistance', + 'runOnJS', + 'simultaneousWithExternalGesture', + 'requireExternalGestureToFail', + 'blocksExternalGesture', + 'withRef', + 'withTestId', + ]; + chainableMethods.forEach((method) => { + gesture[method] = () => gesture; + }); + return gesture; +}; + +const Gesture = { + Pan: createChainableGesture, + Tap: createChainableGesture, + Pinch: createChainableGesture, + Rotation: createChainableGesture, + Fling: createChainableGesture, + LongPress: createChainableGesture, + Native: createChainableGesture, + Manual: createChainableGesture, + Hover: createChainableGesture, + ForceTouch: createChainableGesture, + Simultaneous: (...gestures: any[]) => gestures[0], + Exclusive: (...gestures: any[]) => gestures[0], + Race: (...gestures: any[]) => gestures[0], +}; + +module.exports = { + ...libraryMocks.default, + RawButton: libraryMocks.RawButton, + BaseButton: libraryMocks.BaseButton, + RectButton: libraryMocks.RectButton, + BorderlessButton: libraryMocks.BorderlessButton, + // Named exports used by the app that the library mocks omit + GestureHandlerRootView: RN.View, + ScrollView: RN.ScrollView, + RefreshControl: RN.RefreshControl, + Gesture, + GestureDetector: ({ children }: { children?: any }) => children ?? null, + gestureHandlerRootHOC: (component: any) => component, + State, + Directions, +}; diff --git a/babel.config.js b/babel.config.js index 5d7d5cb..8e0b62e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,13 +1,10 @@ module.exports = function (api) { api.cache(true); - // Check if we're in test environment - const isTest = process.env.NODE_ENV === 'test'; - return { - presets: isTest - ? ['babel-preset-expo'] // No nativewind in test environment - : [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'], + // NativeWind v5 no longer needs a babel preset or jsxImportSource — + // styling is wired through the metro transform (react-native-css). + presets: ['babel-preset-expo'], plugins: [ [ 'module-resolver', @@ -19,7 +16,6 @@ module.exports = function (api) { '@unitools/image': '@unitools/image-expo', '@unitools/router': '@unitools/router-expo', '@unitools/link': '@unitools/link-expo', - '@tailwind.config': './tailwind.config.js', '@assets': './assets', }, extensions: ['.ios.ts', '.android.ts', '.ts', '.ios.tsx', '.android.tsx', '.tsx', '.jsx', '.js', '.json'], diff --git a/electron/main.js b/electron/main.js index 7b08234..7cfec46 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, protocol, net, Notification, ipcMain, session } = require('electron'); +const { app, BrowserWindow, protocol, net, Notification, ipcMain, session, shell, Menu } = require('electron'); const path = require('path'); const url = require('url'); @@ -23,8 +23,52 @@ protocol.registerSchemesAsPrivileged([ }, ]); +let mainWindow = null; +let pendingDeepLink = null; + +// Deep-link scheme matching the app's linking scheme (env.js SCHEME). +// URL schemes are case-insensitive; argv/open-url matching is done lowercased. +const deepLinkScheme = 'ResgridDispatch'; +const deepLinkPrefix = deepLinkScheme.toLowerCase() + '://'; + +app.setAsDefaultProtocolClient(deepLinkScheme); + +function forwardDeepLink(deepLinkUrl) { + if (mainWindow && !mainWindow.webContents.isLoading()) { + mainWindow.webContents.send('deep-link', deepLinkUrl); + } else { + pendingDeepLink = deepLinkUrl; + } +} + +// Single-instance lock: required for the second-instance handler below to +// fire on Windows/Linux instead of spawning a duplicate process. +const gotSingleInstanceLock = app.requestSingleInstanceLock(); +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on('second-instance', (_event, argv) => { + const deepLinkUrl = argv.find((arg) => arg.toLowerCase().startsWith(deepLinkPrefix)); + if (!mainWindow) { + createWindow(); + } else { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + if (deepLinkUrl) { + forwardDeepLink(deepLinkUrl); + } + }); +} + +// macOS deep links arrive via open-url instead of second-instance argv +app.on('open-url', (event, openUrl) => { + event.preventDefault(); + forwardDeepLink(openUrl); +}); + function createWindow() { - const mainWindow = new BrowserWindow({ + mainWindow = new BrowserWindow({ width: 1200, height: 800, title: 'Resgrid Dispatch', @@ -42,6 +86,51 @@ function createWindow() { event.preventDefault(); }); + mainWindow.on('closed', () => { + mainWindow = null; + }); + + // Popups (incl. the OIDC/SSO flow) never create a new BrowserWindow. + // Only http(s) targets are handed to the OS browser, everything else + // is denied outright. + mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => { + try { + const parsed = new URL(targetUrl); + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + shell.openExternal(targetUrl); + } + } catch (err) { + console.error('Blocked invalid window.open URL:', targetUrl, err); + } + return { action: 'deny' }; + }); + + // Block in-window navigation away from the app origin. Dev allows the + // local Metro server only; production allows the app:// scheme only. + mainWindow.webContents.on('will-navigate', (event, targetUrl) => { + let allowed = false; + try { + const parsed = new URL(targetUrl); + allowed = isDev + ? (parsed.protocol === 'http:' || parsed.protocol === 'https:') && + (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') + : parsed.protocol === 'app:'; + } catch (err) { + console.error('Blocked invalid navigation URL:', targetUrl, err); + } + if (!allowed) { + event.preventDefault(); + } + }); + + // Flush any deep link queued before the renderer finished loading + mainWindow.webContents.on('did-finish-load', () => { + if (pendingDeepLink) { + mainWindow.webContents.send('deep-link', pendingDeepLink); + pendingDeepLink = null; + } + }); + // In development, load the local Expo web server // In production, load via custom app:// protocol that serves from dist/ if (isDev) { @@ -64,25 +153,30 @@ app.whenReady().then(() => { if (isDev) { // Dev mode: Metro/webpack needs 'unsafe-eval' for source maps // and hot-reload, blob: for dynamic chunks, ws: for HMR. + // Mapbox GL loads its stylesheet from api.mapbox.com and uses + // blob: workers. connect-src allows plain http/ws for + // self-hosted Resgrid servers without TLS. csp = "default-src 'self' http://localhost:8081;" + " script-src 'self' http://localhost:8081 'unsafe-inline' 'unsafe-eval' blob:;" + - " style-src 'self' http://localhost:8081 'unsafe-inline';" + + " style-src 'self' http://localhost:8081 'unsafe-inline' https://api.mapbox.com;" + " img-src 'self' http://localhost:8081 data: https: blob:;" + " font-src 'self' http://localhost:8081 data:;" + - " connect-src 'self' http://localhost:8081 https: wss: ws:;" + + " connect-src 'self' http://localhost:8081 https: http: wss: ws:;" + " media-src 'self' http://localhost:8081 data: blob:;" + - " worker-src 'self' blob:;"; + " worker-src 'self' blob:;" + + " child-src blob:;"; } else { csp = "default-src 'self' app:;" + " script-src 'self' app: 'unsafe-inline';" + - " style-src 'self' app: 'unsafe-inline';" + - " img-src 'self' app: data: https:;" + + " style-src 'self' app: 'unsafe-inline' https://api.mapbox.com;" + + " img-src 'self' app: data: https: blob:;" + " font-src 'self' app: data:;" + - " connect-src 'self' app: https: wss:;" + - " media-src 'self' app: data:;" + - " worker-src 'self' blob:;"; + " connect-src 'self' app: https: http: wss: ws:;" + + " media-src 'self' app: data: blob:;" + + " worker-src 'self' blob:;" + + " child-src blob:;"; } callback({ @@ -107,11 +201,30 @@ app.whenReady().then(() => { filePath = 'index.html'; } - const fullPath = path.join(distPath, filePath); - return net.fetch(url.pathToFileURL(fullPath).toString()); + // Reject backslashes outright (Windows separator bypass) and + // verify the normalized/resolved path stays inside dist/ so + // ../ segments cannot escape to arbitrary local files. + if (filePath.includes('\\')) { + return new Response('Forbidden', { status: 403 }); + } + + const resolvedPath = path.resolve(distPath, path.normalize(filePath)); + if (resolvedPath !== distPath && !resolvedPath.startsWith(distPath + path.sep)) { + return new Response('Forbidden', { status: 403 }); + } + + return net.fetch(url.pathToFileURL(resolvedPath).toString()); }); } + // Minimal application menu so copy/paste roles (and their keyboard + // shortcuts, especially on macOS) work inside the window. + const menuTemplate = [ + ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []), + { role: 'editMenu' }, + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)); + createWindow(); // ── Notification IPC handlers ────────────────────────────────────── diff --git a/electron/preload.js b/electron/preload.js index e35ca70..6a06dc6 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -49,6 +49,26 @@ ipcRenderer.on('notification:push', (_event, payload) => { } }); +// ── Expose deep-link bridge ──────────────────────────────────────────── +// Lets the renderer subscribe to deep-link URLs (resgriddispatch://...) +// forwarded by the main process from second-instance / open-url events. +contextBridge.exposeInMainWorld('electronDeepLinks', { + /** + * Register a callback that fires when the main process forwards a + * deep-link URL to the renderer. + * @param {(url: string) => void} callback + */ + onDeepLink: (callback) => { + ipcRenderer.on('deep-link', (_event, deepLinkUrl) => { + try { + callback(deepLinkUrl); + } catch (err) { + console.error('Error in deep-link callback:', err); + } + }); + }, +}); + // ── Version information (original preload logic) ─────────────────────── window.addEventListener('DOMContentLoaded', () => { const replaceText = (selector, text) => { diff --git a/global.css b/global.css index 8e5cbbc..aeff712 100644 --- a/global.css +++ b/global.css @@ -1,6 +1,699 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/preflight.css' layer(base); +@import 'tailwindcss/utilities.css'; +@import 'nativewind/theme'; + +/* Class-driven dark mode (GluestackUIProvider toggles the .dark class on web + and on the native root View). */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* ─── Design tokens (light / dark) ─────────────────────────────── + 1. :root — light defaults (all platforms) + 2. @media dark :root — dark defaults; on native this follows + Appearance.getColorScheme() (the provider + sets an override for explicit modes). + 3. :root.dark / :root.light — web-only class override, beats the media + query within the same layer. */ +@layer theme { + :root { + --color-primary-0: 248 250 252; + --color-primary-50: 241 245 249; + --color-primary-100: 226 232 240; + --color-primary-200: 203 213 225; + --color-primary-300: 148 163 184; + --color-primary-400: 100 116 139; + --color-primary-500: 71 85 105; + --color-primary-600: 51 65 85; + --color-primary-700: 45 55 72; + --color-primary-800: 39 49 66; + --color-primary-900: 30 41 59; + --color-primary-950: 15 23 42; + --color-secondary-0: 254 255 255; + --color-secondary-50: 241 242 242; + --color-secondary-100: 231 232 232; + --color-secondary-200: 219 219 219; + --color-secondary-300: 175 176 176; + --color-secondary-400: 114 115 115; + --color-secondary-500: 94 95 95; + --color-secondary-600: 81 82 82; + --color-secondary-700: 63 64 64; + --color-secondary-800: 39 38 38; + --color-secondary-900: 24 23 23; + --color-secondary-950: 11 12 12; + --color-tertiary-0: 255 250 245; + --color-tertiary-50: 255 242 229; + --color-tertiary-100: 255 233 213; + --color-tertiary-200: 254 209 170; + --color-tertiary-300: 253 180 116; + --color-tertiary-400: 251 157 75; + --color-tertiary-500: 231 129 40; + --color-tertiary-600: 215 117 31; + --color-tertiary-700: 180 98 26; + --color-tertiary-800: 130 73 23; + --color-tertiary-900: 108 61 19; + --color-tertiary-950: 84 49 18; + --color-error-0: 254 233 233; + --color-error-50: 254 226 226; + --color-error-100: 254 202 202; + --color-error-200: 252 165 165; + --color-error-300: 248 113 113; + --color-error-400: 239 68 68; + --color-error-500: 230 53 53; + --color-error-600: 220 38 38; + --color-error-700: 185 28 28; + --color-error-800: 153 27 27; + --color-error-900: 127 29 29; + --color-error-950: 83 19 19; + --color-success-0: 228 255 244; + --color-success-50: 202 255 232; + --color-success-100: 162 241 192; + --color-success-200: 132 211 162; + --color-success-300: 102 181 132; + --color-success-400: 72 151 102; + --color-success-500: 52 131 82; + --color-success-600: 42 121 72; + --color-success-700: 32 111 62; + --color-success-800: 22 101 52; + --color-success-900: 20 83 45; + --color-success-950: 27 50 36; + --color-warning-0: 255 253 251; + --color-warning-50: 255 249 245; + --color-warning-100: 255 231 213; + --color-warning-200: 254 205 170; + --color-warning-300: 253 173 116; + --color-warning-400: 251 149 75; + --color-warning-500: 231 120 40; + --color-warning-600: 215 108 31; + --color-warning-700: 180 90 26; + --color-warning-800: 130 68 23; + --color-warning-900: 108 56 19; + --color-warning-950: 84 45 18; + --color-info-0: 236 248 254; + --color-info-50: 199 235 252; + --color-info-100: 162 221 250; + --color-info-200: 124 207 248; + --color-info-300: 87 194 246; + --color-info-400: 50 180 244; + --color-info-500: 13 166 242; + --color-info-600: 11 141 205; + --color-info-700: 9 115 168; + --color-info-800: 7 90 131; + --color-info-900: 5 64 93; + --color-info-950: 3 38 56; + --color-typography-0: 254 254 255; + --color-typography-50: 245 245 245; + --color-typography-100: 229 229 229; + --color-typography-200: 219 219 220; + --color-typography-300: 212 212 212; + --color-typography-400: 163 163 163; + --color-typography-500: 140 140 140; + --color-typography-600: 115 115 115; + --color-typography-700: 82 82 82; + --color-typography-800: 64 64 64; + --color-typography-900: 38 38 39; + --color-typography-950: 23 23 23; + --color-outline-0: 253 254 254; + --color-outline-50: 243 243 243; + --color-outline-100: 230 230 230; + --color-outline-200: 221 220 219; + --color-outline-300: 211 211 211; + --color-outline-400: 165 163 163; + --color-outline-500: 140 141 141; + --color-outline-600: 115 116 116; + --color-outline-700: 83 82 82; + --color-outline-800: 65 65 65; + --color-outline-900: 39 38 36; + --color-outline-950: 26 23 23; + --color-background-0: 255 255 255; + --color-background-50: 246 246 246; + --color-background-100: 242 241 241; + --color-background-200: 220 219 219; + --color-background-300: 213 212 212; + --color-background-400: 162 163 163; + --color-background-500: 142 142 142; + --color-background-600: 116 116 116; + --color-background-700: 83 82 82; + --color-background-800: 65 64 64; + --color-background-900: 39 38 37; + --color-background-950: 24 23 24; + --color-background-error: 254 241 241; + --color-background-warning: 255 244 235; + --color-background-success: 237 252 242; + --color-background-muted: 247 248 247; + --color-background-info: 235 248 254; + --color-indicator-primary: 55 55 55; + --color-indicator-info: 83 153 236; + --color-indicator-error: 185 28 28; + } + + @media (prefers-color-scheme: dark) { + :root { + --color-primary-0: 15 23 42; + --color-primary-50: 30 41 59; + --color-primary-100: 39 49 66; + --color-primary-200: 45 55 72; + --color-primary-300: 51 65 85; + --color-primary-400: 71 85 105; + --color-primary-500: 100 116 139; + --color-primary-600: 71 85 105; + --color-primary-700: 51 65 85; + --color-primary-800: 45 55 72; + --color-primary-900: 39 49 66; + --color-primary-950: 30 41 59; + --color-secondary-0: 11 12 12; + --color-secondary-50: 24 23 23; + --color-secondary-100: 39 38 38; + --color-secondary-200: 63 64 64; + --color-secondary-300: 81 82 82; + --color-secondary-400: 94 95 95; + --color-secondary-500: 114 115 115; + --color-secondary-600: 175 176 176; + --color-secondary-700: 219 219 219; + --color-secondary-800: 231 232 232; + --color-secondary-900: 241 242 242; + --color-secondary-950: 254 255 255; + --color-tertiary-0: 84 49 18; + --color-tertiary-50: 108 61 19; + --color-tertiary-100: 130 73 23; + --color-tertiary-200: 180 98 26; + --color-tertiary-300: 215 117 31; + --color-tertiary-400: 231 129 40; + --color-tertiary-500: 251 157 75; + --color-tertiary-600: 253 180 116; + --color-tertiary-700: 254 209 170; + --color-tertiary-800: 255 233 213; + --color-tertiary-900: 255 242 229; + --color-tertiary-950: 255 250 245; + --color-error-0: 83 19 19; + --color-error-50: 127 29 29; + --color-error-100: 153 27 27; + --color-error-200: 185 28 28; + --color-error-300: 220 38 38; + --color-error-400: 230 53 53; + --color-error-500: 239 68 68; + --color-error-600: 248 113 113; + --color-error-700: 252 165 165; + --color-error-800: 254 202 202; + --color-error-900: 254 226 226; + --color-error-950: 254 233 233; + --color-success-0: 27 50 36; + --color-success-50: 20 83 45; + --color-success-100: 22 101 52; + --color-success-200: 32 111 62; + --color-success-300: 42 121 72; + --color-success-400: 52 131 82; + --color-success-500: 72 151 102; + --color-success-600: 102 181 132; + --color-success-700: 132 211 162; + --color-success-800: 162 241 192; + --color-success-900: 202 255 232; + --color-success-950: 228 255 244; + --color-warning-0: 84 45 18; + --color-warning-50: 108 56 19; + --color-warning-100: 130 68 23; + --color-warning-200: 180 90 26; + --color-warning-300: 215 108 31; + --color-warning-400: 231 120 40; + --color-warning-500: 251 149 75; + --color-warning-600: 253 173 116; + --color-warning-700: 254 205 170; + --color-warning-800: 255 231 213; + --color-warning-900: 255 249 245; + --color-warning-950: 255 253 251; + --color-info-0: 3 38 56; + --color-info-50: 5 64 93; + --color-info-100: 7 90 131; + --color-info-200: 9 115 168; + --color-info-300: 11 141 205; + --color-info-400: 13 166 242; + --color-info-500: 50 180 244; + --color-info-600: 87 194 246; + --color-info-700: 124 207 248; + --color-info-800: 162 221 250; + --color-info-900: 199 235 252; + --color-info-950: 236 248 254; + --color-typography-0: 23 23 23; + --color-typography-50: 38 38 39; + --color-typography-100: 64 64 64; + --color-typography-200: 82 82 82; + --color-typography-300: 115 115 115; + --color-typography-400: 140 140 140; + --color-typography-500: 163 163 163; + --color-typography-600: 212 212 212; + --color-typography-700: 219 219 220; + --color-typography-800: 229 229 229; + --color-typography-900: 245 245 245; + --color-typography-950: 254 254 255; + --color-outline-0: 26 23 23; + --color-outline-50: 39 38 36; + --color-outline-100: 65 65 65; + --color-outline-200: 83 82 82; + --color-outline-300: 115 116 116; + --color-outline-400: 140 141 141; + --color-outline-500: 165 163 163; + --color-outline-600: 211 211 211; + --color-outline-700: 221 220 219; + --color-outline-800: 230 230 230; + --color-outline-900: 243 243 243; + --color-outline-950: 253 254 254; + --color-background-0: 18 18 18; + --color-background-50: 39 38 37; + --color-background-100: 65 64 64; + --color-background-200: 83 82 82; + --color-background-300: 116 116 116; + --color-background-400: 142 142 142; + --color-background-500: 162 163 163; + --color-background-600: 213 212 212; + --color-background-700: 220 219 219; + --color-background-800: 242 241 241; + --color-background-900: 246 246 246; + --color-background-950: 254 254 254; + --color-background-error: 66 43 43; + --color-background-warning: 65 47 35; + --color-background-success: 28 43 33; + --color-background-muted: 51 51 51; + --color-background-info: 26 40 46; + --color-indicator-primary: 247 247 247; + --color-indicator-info: 161 199 245; + --color-indicator-error: 232 70 69; + } + } + + :root.dark { + --color-primary-0: 15 23 42; + --color-primary-50: 30 41 59; + --color-primary-100: 39 49 66; + --color-primary-200: 45 55 72; + --color-primary-300: 51 65 85; + --color-primary-400: 71 85 105; + --color-primary-500: 100 116 139; + --color-primary-600: 71 85 105; + --color-primary-700: 51 65 85; + --color-primary-800: 45 55 72; + --color-primary-900: 39 49 66; + --color-primary-950: 30 41 59; + --color-secondary-0: 11 12 12; + --color-secondary-50: 24 23 23; + --color-secondary-100: 39 38 38; + --color-secondary-200: 63 64 64; + --color-secondary-300: 81 82 82; + --color-secondary-400: 94 95 95; + --color-secondary-500: 114 115 115; + --color-secondary-600: 175 176 176; + --color-secondary-700: 219 219 219; + --color-secondary-800: 231 232 232; + --color-secondary-900: 241 242 242; + --color-secondary-950: 254 255 255; + --color-tertiary-0: 84 49 18; + --color-tertiary-50: 108 61 19; + --color-tertiary-100: 130 73 23; + --color-tertiary-200: 180 98 26; + --color-tertiary-300: 215 117 31; + --color-tertiary-400: 231 129 40; + --color-tertiary-500: 251 157 75; + --color-tertiary-600: 253 180 116; + --color-tertiary-700: 254 209 170; + --color-tertiary-800: 255 233 213; + --color-tertiary-900: 255 242 229; + --color-tertiary-950: 255 250 245; + --color-error-0: 83 19 19; + --color-error-50: 127 29 29; + --color-error-100: 153 27 27; + --color-error-200: 185 28 28; + --color-error-300: 220 38 38; + --color-error-400: 230 53 53; + --color-error-500: 239 68 68; + --color-error-600: 248 113 113; + --color-error-700: 252 165 165; + --color-error-800: 254 202 202; + --color-error-900: 254 226 226; + --color-error-950: 254 233 233; + --color-success-0: 27 50 36; + --color-success-50: 20 83 45; + --color-success-100: 22 101 52; + --color-success-200: 32 111 62; + --color-success-300: 42 121 72; + --color-success-400: 52 131 82; + --color-success-500: 72 151 102; + --color-success-600: 102 181 132; + --color-success-700: 132 211 162; + --color-success-800: 162 241 192; + --color-success-900: 202 255 232; + --color-success-950: 228 255 244; + --color-warning-0: 84 45 18; + --color-warning-50: 108 56 19; + --color-warning-100: 130 68 23; + --color-warning-200: 180 90 26; + --color-warning-300: 215 108 31; + --color-warning-400: 231 120 40; + --color-warning-500: 251 149 75; + --color-warning-600: 253 173 116; + --color-warning-700: 254 205 170; + --color-warning-800: 255 231 213; + --color-warning-900: 255 249 245; + --color-warning-950: 255 253 251; + --color-info-0: 3 38 56; + --color-info-50: 5 64 93; + --color-info-100: 7 90 131; + --color-info-200: 9 115 168; + --color-info-300: 11 141 205; + --color-info-400: 13 166 242; + --color-info-500: 50 180 244; + --color-info-600: 87 194 246; + --color-info-700: 124 207 248; + --color-info-800: 162 221 250; + --color-info-900: 199 235 252; + --color-info-950: 236 248 254; + --color-typography-0: 23 23 23; + --color-typography-50: 38 38 39; + --color-typography-100: 64 64 64; + --color-typography-200: 82 82 82; + --color-typography-300: 115 115 115; + --color-typography-400: 140 140 140; + --color-typography-500: 163 163 163; + --color-typography-600: 212 212 212; + --color-typography-700: 219 219 220; + --color-typography-800: 229 229 229; + --color-typography-900: 245 245 245; + --color-typography-950: 254 254 255; + --color-outline-0: 26 23 23; + --color-outline-50: 39 38 36; + --color-outline-100: 65 65 65; + --color-outline-200: 83 82 82; + --color-outline-300: 115 116 116; + --color-outline-400: 140 141 141; + --color-outline-500: 165 163 163; + --color-outline-600: 211 211 211; + --color-outline-700: 221 220 219; + --color-outline-800: 230 230 230; + --color-outline-900: 243 243 243; + --color-outline-950: 253 254 254; + --color-background-0: 18 18 18; + --color-background-50: 39 38 37; + --color-background-100: 65 64 64; + --color-background-200: 83 82 82; + --color-background-300: 116 116 116; + --color-background-400: 142 142 142; + --color-background-500: 162 163 163; + --color-background-600: 213 212 212; + --color-background-700: 220 219 219; + --color-background-800: 242 241 241; + --color-background-900: 246 246 246; + --color-background-950: 254 254 254; + --color-background-error: 66 43 43; + --color-background-warning: 65 47 35; + --color-background-success: 28 43 33; + --color-background-muted: 51 51 51; + --color-background-info: 26 40 46; + --color-indicator-primary: 247 247 247; + --color-indicator-info: 161 199 245; + --color-indicator-error: 232 70 69; + } + + :root.light { + --color-primary-0: 248 250 252; + --color-primary-50: 241 245 249; + --color-primary-100: 226 232 240; + --color-primary-200: 203 213 225; + --color-primary-300: 148 163 184; + --color-primary-400: 100 116 139; + --color-primary-500: 71 85 105; + --color-primary-600: 51 65 85; + --color-primary-700: 45 55 72; + --color-primary-800: 39 49 66; + --color-primary-900: 30 41 59; + --color-primary-950: 15 23 42; + --color-secondary-0: 254 255 255; + --color-secondary-50: 241 242 242; + --color-secondary-100: 231 232 232; + --color-secondary-200: 219 219 219; + --color-secondary-300: 175 176 176; + --color-secondary-400: 114 115 115; + --color-secondary-500: 94 95 95; + --color-secondary-600: 81 82 82; + --color-secondary-700: 63 64 64; + --color-secondary-800: 39 38 38; + --color-secondary-900: 24 23 23; + --color-secondary-950: 11 12 12; + --color-tertiary-0: 255 250 245; + --color-tertiary-50: 255 242 229; + --color-tertiary-100: 255 233 213; + --color-tertiary-200: 254 209 170; + --color-tertiary-300: 253 180 116; + --color-tertiary-400: 251 157 75; + --color-tertiary-500: 231 129 40; + --color-tertiary-600: 215 117 31; + --color-tertiary-700: 180 98 26; + --color-tertiary-800: 130 73 23; + --color-tertiary-900: 108 61 19; + --color-tertiary-950: 84 49 18; + --color-error-0: 254 233 233; + --color-error-50: 254 226 226; + --color-error-100: 254 202 202; + --color-error-200: 252 165 165; + --color-error-300: 248 113 113; + --color-error-400: 239 68 68; + --color-error-500: 230 53 53; + --color-error-600: 220 38 38; + --color-error-700: 185 28 28; + --color-error-800: 153 27 27; + --color-error-900: 127 29 29; + --color-error-950: 83 19 19; + --color-success-0: 228 255 244; + --color-success-50: 202 255 232; + --color-success-100: 162 241 192; + --color-success-200: 132 211 162; + --color-success-300: 102 181 132; + --color-success-400: 72 151 102; + --color-success-500: 52 131 82; + --color-success-600: 42 121 72; + --color-success-700: 32 111 62; + --color-success-800: 22 101 52; + --color-success-900: 20 83 45; + --color-success-950: 27 50 36; + --color-warning-0: 255 253 251; + --color-warning-50: 255 249 245; + --color-warning-100: 255 231 213; + --color-warning-200: 254 205 170; + --color-warning-300: 253 173 116; + --color-warning-400: 251 149 75; + --color-warning-500: 231 120 40; + --color-warning-600: 215 108 31; + --color-warning-700: 180 90 26; + --color-warning-800: 130 68 23; + --color-warning-900: 108 56 19; + --color-warning-950: 84 45 18; + --color-info-0: 236 248 254; + --color-info-50: 199 235 252; + --color-info-100: 162 221 250; + --color-info-200: 124 207 248; + --color-info-300: 87 194 246; + --color-info-400: 50 180 244; + --color-info-500: 13 166 242; + --color-info-600: 11 141 205; + --color-info-700: 9 115 168; + --color-info-800: 7 90 131; + --color-info-900: 5 64 93; + --color-info-950: 3 38 56; + --color-typography-0: 254 254 255; + --color-typography-50: 245 245 245; + --color-typography-100: 229 229 229; + --color-typography-200: 219 219 220; + --color-typography-300: 212 212 212; + --color-typography-400: 163 163 163; + --color-typography-500: 140 140 140; + --color-typography-600: 115 115 115; + --color-typography-700: 82 82 82; + --color-typography-800: 64 64 64; + --color-typography-900: 38 38 39; + --color-typography-950: 23 23 23; + --color-outline-0: 253 254 254; + --color-outline-50: 243 243 243; + --color-outline-100: 230 230 230; + --color-outline-200: 221 220 219; + --color-outline-300: 211 211 211; + --color-outline-400: 165 163 163; + --color-outline-500: 140 141 141; + --color-outline-600: 115 116 116; + --color-outline-700: 83 82 82; + --color-outline-800: 65 65 65; + --color-outline-900: 39 38 36; + --color-outline-950: 26 23 23; + --color-background-0: 255 255 255; + --color-background-50: 246 246 246; + --color-background-100: 242 241 241; + --color-background-200: 220 219 219; + --color-background-300: 213 212 212; + --color-background-400: 162 163 163; + --color-background-500: 142 142 142; + --color-background-600: 116 116 116; + --color-background-700: 83 82 82; + --color-background-800: 65 64 64; + --color-background-900: 39 38 37; + --color-background-950: 24 23 24; + --color-background-error: 254 241 241; + --color-background-warning: 255 244 235; + --color-background-success: 237 252 242; + --color-background-muted: 247 248 247; + --color-background-info: 235 248 254; + --color-indicator-primary: 55 55 55; + --color-indicator-info: 83 153 236; + --color-indicator-error: 185 28 28; + } +} + +/* ─── Tailwind v4 utilities for the legacy gluestack v2 token scales ── */ +@theme inline { + --color-primary-0: rgb(var(--color-primary-0)); + --color-primary-50: rgb(var(--color-primary-50)); + --color-primary-100: rgb(var(--color-primary-100)); + --color-primary-200: rgb(var(--color-primary-200)); + --color-primary-300: rgb(var(--color-primary-300)); + --color-primary-400: rgb(var(--color-primary-400)); + --color-primary-500: rgb(var(--color-primary-500)); + --color-primary-600: rgb(var(--color-primary-600)); + --color-primary-700: rgb(var(--color-primary-700)); + --color-primary-800: rgb(var(--color-primary-800)); + --color-primary-900: rgb(var(--color-primary-900)); + --color-primary-950: rgb(var(--color-primary-950)); + --color-secondary-0: rgb(var(--color-secondary-0)); + --color-secondary-50: rgb(var(--color-secondary-50)); + --color-secondary-100: rgb(var(--color-secondary-100)); + --color-secondary-200: rgb(var(--color-secondary-200)); + --color-secondary-300: rgb(var(--color-secondary-300)); + --color-secondary-400: rgb(var(--color-secondary-400)); + --color-secondary-500: rgb(var(--color-secondary-500)); + --color-secondary-600: rgb(var(--color-secondary-600)); + --color-secondary-700: rgb(var(--color-secondary-700)); + --color-secondary-800: rgb(var(--color-secondary-800)); + --color-secondary-900: rgb(var(--color-secondary-900)); + --color-secondary-950: rgb(var(--color-secondary-950)); + --color-tertiary-0: rgb(var(--color-tertiary-0)); + --color-tertiary-50: rgb(var(--color-tertiary-50)); + --color-tertiary-100: rgb(var(--color-tertiary-100)); + --color-tertiary-200: rgb(var(--color-tertiary-200)); + --color-tertiary-300: rgb(var(--color-tertiary-300)); + --color-tertiary-400: rgb(var(--color-tertiary-400)); + --color-tertiary-500: rgb(var(--color-tertiary-500)); + --color-tertiary-600: rgb(var(--color-tertiary-600)); + --color-tertiary-700: rgb(var(--color-tertiary-700)); + --color-tertiary-800: rgb(var(--color-tertiary-800)); + --color-tertiary-900: rgb(var(--color-tertiary-900)); + --color-tertiary-950: rgb(var(--color-tertiary-950)); + --color-error-0: rgb(var(--color-error-0)); + --color-error-50: rgb(var(--color-error-50)); + --color-error-100: rgb(var(--color-error-100)); + --color-error-200: rgb(var(--color-error-200)); + --color-error-300: rgb(var(--color-error-300)); + --color-error-400: rgb(var(--color-error-400)); + --color-error-500: rgb(var(--color-error-500)); + --color-error-600: rgb(var(--color-error-600)); + --color-error-700: rgb(var(--color-error-700)); + --color-error-800: rgb(var(--color-error-800)); + --color-error-900: rgb(var(--color-error-900)); + --color-error-950: rgb(var(--color-error-950)); + --color-success-0: rgb(var(--color-success-0)); + --color-success-50: rgb(var(--color-success-50)); + --color-success-100: rgb(var(--color-success-100)); + --color-success-200: rgb(var(--color-success-200)); + --color-success-300: rgb(var(--color-success-300)); + --color-success-400: rgb(var(--color-success-400)); + --color-success-500: rgb(var(--color-success-500)); + --color-success-600: rgb(var(--color-success-600)); + --color-success-700: rgb(var(--color-success-700)); + --color-success-800: rgb(var(--color-success-800)); + --color-success-900: rgb(var(--color-success-900)); + --color-success-950: rgb(var(--color-success-950)); + --color-warning-0: rgb(var(--color-warning-0)); + --color-warning-50: rgb(var(--color-warning-50)); + --color-warning-100: rgb(var(--color-warning-100)); + --color-warning-200: rgb(var(--color-warning-200)); + --color-warning-300: rgb(var(--color-warning-300)); + --color-warning-400: rgb(var(--color-warning-400)); + --color-warning-500: rgb(var(--color-warning-500)); + --color-warning-600: rgb(var(--color-warning-600)); + --color-warning-700: rgb(var(--color-warning-700)); + --color-warning-800: rgb(var(--color-warning-800)); + --color-warning-900: rgb(var(--color-warning-900)); + --color-warning-950: rgb(var(--color-warning-950)); + --color-info-0: rgb(var(--color-info-0)); + --color-info-50: rgb(var(--color-info-50)); + --color-info-100: rgb(var(--color-info-100)); + --color-info-200: rgb(var(--color-info-200)); + --color-info-300: rgb(var(--color-info-300)); + --color-info-400: rgb(var(--color-info-400)); + --color-info-500: rgb(var(--color-info-500)); + --color-info-600: rgb(var(--color-info-600)); + --color-info-700: rgb(var(--color-info-700)); + --color-info-800: rgb(var(--color-info-800)); + --color-info-900: rgb(var(--color-info-900)); + --color-info-950: rgb(var(--color-info-950)); + --color-typography-0: rgb(var(--color-typography-0)); + --color-typography-50: rgb(var(--color-typography-50)); + --color-typography-100: rgb(var(--color-typography-100)); + --color-typography-200: rgb(var(--color-typography-200)); + --color-typography-300: rgb(var(--color-typography-300)); + --color-typography-400: rgb(var(--color-typography-400)); + --color-typography-500: rgb(var(--color-typography-500)); + --color-typography-600: rgb(var(--color-typography-600)); + --color-typography-700: rgb(var(--color-typography-700)); + --color-typography-800: rgb(var(--color-typography-800)); + --color-typography-900: rgb(var(--color-typography-900)); + --color-typography-950: rgb(var(--color-typography-950)); + --color-outline-0: rgb(var(--color-outline-0)); + --color-outline-50: rgb(var(--color-outline-50)); + --color-outline-100: rgb(var(--color-outline-100)); + --color-outline-200: rgb(var(--color-outline-200)); + --color-outline-300: rgb(var(--color-outline-300)); + --color-outline-400: rgb(var(--color-outline-400)); + --color-outline-500: rgb(var(--color-outline-500)); + --color-outline-600: rgb(var(--color-outline-600)); + --color-outline-700: rgb(var(--color-outline-700)); + --color-outline-800: rgb(var(--color-outline-800)); + --color-outline-900: rgb(var(--color-outline-900)); + --color-outline-950: rgb(var(--color-outline-950)); + --color-background-0: rgb(var(--color-background-0)); + --color-background-50: rgb(var(--color-background-50)); + --color-background-100: rgb(var(--color-background-100)); + --color-background-200: rgb(var(--color-background-200)); + --color-background-300: rgb(var(--color-background-300)); + --color-background-400: rgb(var(--color-background-400)); + --color-background-500: rgb(var(--color-background-500)); + --color-background-600: rgb(var(--color-background-600)); + --color-background-700: rgb(var(--color-background-700)); + --color-background-800: rgb(var(--color-background-800)); + --color-background-900: rgb(var(--color-background-900)); + --color-background-950: rgb(var(--color-background-950)); + --color-background-error: rgb(var(--color-background-error)); + --color-background-warning: rgb(var(--color-background-warning)); + --color-background-success: rgb(var(--color-background-success)); + --color-background-muted: rgb(var(--color-background-muted)); + --color-background-info: rgb(var(--color-background-info)); + --color-indicator-primary: rgb(var(--color-indicator-primary)); + --color-indicator-info: rgb(var(--color-indicator-info)); + --color-indicator-error: rgb(var(--color-indicator-error)); +} + +/* ─── Non-color theme extensions (ported from tailwind.config.js) ── */ +@theme { + --font-roboto: Roboto, sans-serif; + --font-weight-extrablack: 950; + --text-2xs: 10px; + --color-typography-white: #ffffff; + --color-typography-gray: #d4d4d4; + --color-typography-black: #181718; + --color-background-light: #fbfbfb; + --color-background-dark: #181719; + --shadow-hard-1: -2px 2px 8px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-2: 0px 3px 10px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-3: 2px 2px 8px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-4: 0px -3px 10px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-5: 0px 2px 10px 0px rgba(38, 38, 38, 0.1); + --shadow-soft-1: 0px 0px 10px rgba(38, 38, 38, 0.1); + --shadow-soft-2: 0px 0px 20px rgba(38, 38, 38, 0.2); + --shadow-soft-3: 0px 0px 30px rgba(38, 38, 38, 0.1); + --shadow-soft-4: 0px 0px 40px rgba(38, 38, 38, 0.1); +} /* Accessible focus styles for keyboard navigation */ .web-input-accessible:focus-visible { diff --git a/jest-setup.ts b/jest-setup.ts index 8b98ae9..6f7a032 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -185,6 +185,8 @@ jest.mock('react-native-permissions', () => ({ // Mock nativewind to avoid react-native-css-interop issues jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), + vars: jest.fn((v: any) => v), cssInterop: jest.fn((Component: any) => Component), useColorScheme: jest.fn(() => ({ colorScheme: 'light', diff --git a/jest.config.js b/jest.config.js index d2487e4..63c1806 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,8 @@ module.exports = { preset: 'jest-expo', + // Reanimated v4 worklets: resolve to the non-native builds in Jest + // (vendored — worklets 0.5.1 does not ship jest/resolver.js) + resolver: '/jest.resolver.js', setupFilesAfterEnv: ['/jest-setup.ts'], testMatch: ['**/?(*.)+(spec|test).ts?(x)'], testPathIgnorePatterns: ['/node_modules/', '\\.\\._.*'], @@ -8,7 +11,7 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleDirectories: ['node_modules', '/'], transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@legendapp/motion|@gluestack-ui|expo-audio|@aptabase/.*|@shopify/flash-list))', + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@legendapp/motion|@gluestack-ui|nativewind|react-native-css|expo-audio|@aptabase/.*|@shopify/flash-list))', ], coverageReporters: ['json-summary', ['text', { file: 'coverage.txt' }], 'cobertura'], reporters: [ diff --git a/jest.resolver.js b/jest.resolver.js new file mode 100644 index 0000000..c889009 --- /dev/null +++ b/jest.resolver.js @@ -0,0 +1,15 @@ +// Reanimated v4 worklets: resolve to the non-native builds in Jest. +// Vendored copy of react-native-worklets/jest/resolver.js (added in worklets +// 0.8.x) — this repo pins react-native-worklets 0.5.1, which does not bundle it. + +/** @type {import('jest-resolve').SyncResolver} */ +module.exports = (request, options) => { + const { defaultResolver } = options; + if (options.basedir.includes('react-native-worklets') || request.includes('react-native-worklets')) { + const workletOptions = { ...options }; + workletOptions.extensions = workletOptions.extensions?.filter((ext) => !ext.includes('native')); + options = workletOptions; + } + + return defaultResolver(request, options); +}; diff --git a/metro.config.js b/metro.config.js index f8c0c68..f2cbb1b 100644 --- a/metro.config.js +++ b/metro.config.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const path = require('path'); const { getSentryExpoConfig } = require('@sentry/react-native/metro'); -const { withNativeWind } = require('nativewind/metro'); +const { withNativewind } = require('nativewind/metro'); const config = getSentryExpoConfig(__dirname, { isCSSEnabled: true, @@ -39,6 +39,14 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { }; } + // Notifee is native-only - mock for web + if (moduleName === '@notifee/react-native') { + return { + type: 'sourceFile', + filePath: path.resolve(__dirname, '__mocks__/@notifee/react-native.web.js'), + }; + } + // MMKV storage mock for web if (moduleName === 'react-native-mmkv') { return { @@ -77,4 +85,4 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { config.resolver.unstable_conditionNames = _.uniq(config.resolver.unstable_conditionNames.concat('browser', 'require', 'react-native')); // <-- and here we can override what we want -module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 }); +module.exports = withNativewind(config, { inlineRem: 16 }); diff --git a/package.json b/package.json index 76d2eb6..2574879 100644 --- a/package.json +++ b/package.json @@ -93,35 +93,8 @@ "@expo/config-plugins": "~54.0.4", "@expo/html-elements": "~0.10.1", "@expo/metro-runtime": "~6.1.2", - "@gluestack-ui/accordion": "~1.0.6", - "@gluestack-ui/actionsheet": "~0.2.44", - "@gluestack-ui/alert": "~0.1.15", - "@gluestack-ui/alert-dialog": "~0.1.30", - "@gluestack-ui/avatar": "~0.1.17", - "@gluestack-ui/button": "~1.0.14", - "@gluestack-ui/checkbox": "~0.1.31", - "@gluestack-ui/divider": "~0.1.9", - "@gluestack-ui/fab": "~0.1.21", - "@gluestack-ui/form-control": "~0.1.18", - "@gluestack-ui/icon": "~0.1.27", - "@gluestack-ui/image": "~0.1.10", - "@gluestack-ui/input": "~0.1.38", - "@gluestack-ui/link": "~0.1.22", - "@gluestack-ui/menu": "~0.2.43", - "@gluestack-ui/modal": "~0.1.35", - "@gluestack-ui/nativewind-utils": "~1.0.26", - "@gluestack-ui/overlay": "~0.1.16", - "@gluestack-ui/popover": "~0.1.49", - "@gluestack-ui/pressable": "~0.1.16", - "@gluestack-ui/progress": "~0.1.16", - "@gluestack-ui/radio": "~0.1.40", - "@gluestack-ui/select": "~0.1.31", - "@gluestack-ui/slider": "~0.1.32", - "@gluestack-ui/spinner": "~0.1.14", - "@gluestack-ui/switch": "~0.1.22", - "@gluestack-ui/textarea": "~0.1.23", - "@gluestack-ui/toast": "~1.0.8", - "@gluestack-ui/tooltip": "~0.1.32", + "@gluestack-ui/core": "^5.0.15", + "@gluestack-ui/utils": "^5.0.6", "@gorhom/bottom-sheet": "~5.0.5", "@hookform/resolvers": "~3.9.0", "@legendapp/motion": "~2.4.0", @@ -188,7 +161,7 @@ "lucide-react-native": "~0.475.0", "mapbox-gl": "^3.15.0", "moti": "~0.29.0", - "nativewind": "~4.1.21", + "nativewind": "5.0.0-preview.4", "react": "19.1.0", "react-dom": "19.1.0", "react-error-boundary": "~4.0.13", @@ -198,6 +171,7 @@ "react-native-base64": "~0.2.1", "react-native-ble-manager": "^12.1.5", "react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971", + "react-native-css": "^3.0.7", "react-native-edge-to-edge": "1.6.0", "react-native-flash-message": "~0.4.2", "react-native-gesture-handler": "~2.28.0", @@ -222,6 +196,7 @@ "@commitlint/cli": "~19.2.2", "@commitlint/config-conventional": "~19.2.2", "@expo/config": "~12.0.12", + "@tailwindcss/postcss": "^4.2.0", "@testing-library/jest-dom": "~6.5.0", "@testing-library/react-native": "~12.9.0", "@types/geojson": "~7946.0.16", @@ -263,7 +238,7 @@ "postinstall-postinstall": "^2.1.0", "prettier": "~3.3.3", "react-native-svg-transformer": "~1.5.1", - "tailwindcss": "3.4.4", + "tailwindcss": "^4.2.0", "ts-jest": "~29.1.2", "ts-node": "~10.9.2", "typescript": "~5.9.2", @@ -300,6 +275,7 @@ "initVersion": "7.0.4" }, "resolutions": { - "form-data": "4.0.4" + "form-data": "4.0.4", + "lightningcss": "1.30.1" } } diff --git a/patches/@gluestack-ui+core+5.0.15.patch b/patches/@gluestack-ui+core+5.0.15.patch new file mode 100644 index 0000000..ab1419a --- /dev/null +++ b/patches/@gluestack-ui+core+5.0.15.patch @@ -0,0 +1,32 @@ +diff --git a/node_modules/@gluestack-ui/core/lib/esm/actionsheet/creator/ActionsheetContent.jsx b/node_modules/@gluestack-ui/core/lib/esm/actionsheet/creator/ActionsheetContent.jsx +index 30962f7..d024fbf 100644 +--- a/node_modules/@gluestack-ui/core/lib/esm/actionsheet/creator/ActionsheetContent.jsx ++++ b/node_modules/@gluestack-ui/core/lib/esm/actionsheet/creator/ActionsheetContent.jsx +@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) { + }; + /* eslint-disable react-native/no-inline-styles */ + import React, { forwardRef } from 'react'; +-import { Animated, Dimensions, AccessibilityInfo, Platform, Keyboard, } from 'react-native'; ++import { Animated, Dimensions, AccessibilityInfo, Platform, Keyboard, useWindowDimensions, } from 'react-native'; + import { ActionsheetContext } from './context'; + import { ActionsheetContentProvider } from './ActionsheetContentContext'; + import { OverlayAnimatePresence } from './OverlayAnimatePresence'; +@@ -19,16 +19,12 @@ import { FocusScope } from '@gluestack-ui/utils/aria'; + import { mergeRefs, findNodeHandle } from '@gluestack-ui/utils/common'; + import { useDialog } from '@gluestack-ui/utils/aria'; + import { usePreventScroll } from '../../overlay/aria'; +-//dimentions not giving proper window height on web +-const windowHeight = Platform.OS === 'web' +- ? typeof window !== 'undefined' +- ? window.innerHeight +- : Dimensions.get('screen').height +- : Dimensions.get('screen').height; ++// Patched: use useWindowDimensions for reactive height (fixes landscape) + function ActionsheetContent(StyledActionsheetContent, AnimatePresence) { + return forwardRef((_a, ref) => { + var { children, _experimentalContent = false, focusScope = true } = _a, props = __rest(_a, ["children", "_experimentalContent", "focusScope"]); + const { visible, handleClose, trapFocus, initialFocusRef, handleCloseBackdrop, finalFocusRef, snapPoints, preventScroll, } = React.useContext(ActionsheetContext); ++ const { height: windowHeight } = useWindowDimensions(); + usePreventScroll({ isDisabled: preventScroll }); + const pan = React.useRef(new Animated.ValueXY()).current; + const contentSheetHeight = React.useRef(0); diff --git a/patches/react-native-css-interop+0.1.22.patch b/patches/react-native-css-interop+0.1.22.patch deleted file mode 100644 index ec9ded3..0000000 --- a/patches/react-native-css-interop+0.1.22.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/react-native-css-interop/dist/css-to-rn/parseDeclaration.js b/node_modules/react-native-css-interop/dist/css-to-rn/parseDeclaration.js -index 1234567..abcdefg 100644 ---- a/node_modules/react-native-css-interop/dist/css-to-rn/parseDeclaration.js -+++ b/node_modules/react-native-css-interop/dist/css-to-rn/parseDeclaration.js -@@ -1756,6 +1756,10 @@ function parseAspectRatio(aspectRatio) { - if (aspectRatio.auto) { - return "auto"; - } -+ // Handle case where aspectRatio.ratio is undefined (e.g., from certain CSS libraries like @novu/js) -+ if (!aspectRatio.ratio) { -+ return undefined; -+ } - else { - if (aspectRatio.ratio[0] === aspectRatio.ratio[1]) { - return 1; diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..a34a3d5 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/react-native-css-env.d.ts b/react-native-css-env.d.ts new file mode 100644 index 0000000..82aac40 --- /dev/null +++ b/react-native-css-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation. diff --git a/src/__tests__/app/call/[id].security.test.tsx b/src/__tests__/app/call/[id].security.test.tsx index fbe07e0..f6557c6 100644 --- a/src/__tests__/app/call/[id].security.test.tsx +++ b/src/__tests__/app/call/[id].security.test.tsx @@ -166,6 +166,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), cssInterop: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); @@ -223,14 +224,27 @@ jest.mock('../../../components/calls/call-notes-modal', () => ({ default: () =>
Notes Modal
, })); +jest.mock('../../../components/calls/call-audio-modal', () => ({ + __esModule: true, + default: () =>
Audio Modal
, +})); + jest.mock('../../../components/calls/close-call-bottom-sheet', () => ({ CloseCallBottomSheet: () =>
Close Call Sheet
, })); +jest.mock('../../../components/calls/reschedule-call-sheet', () => ({ + RescheduleCallSheet: () =>
Reschedule Sheet
, +})); + jest.mock('../../../components/status/status-bottom-sheet', () => ({ StatusBottomSheet: () =>
Status Sheet
, })); +jest.mock('@/components/incident-command/incident-command-tab', () => ({ + IncidentCommandTab: () =>
Incident Command Tab
, +})); + // Mock UI components jest.mock('@/components/ui', () => ({ FocusAwareStatusBar: () => null, @@ -292,8 +306,10 @@ jest.mock('react-native-webview', () => ({ default: () =>
WebView
, })); -// Mock date-fns +// Mock date-fns (keep the real implementations available for components that +// need parse/isValid, only stub format for stable output) jest.mock('date-fns', () => ({ + ...jest.requireActual('date-fns'), format: (date: any, formatStr: string) => `formatted-${formatStr}`, })); @@ -304,10 +320,17 @@ jest.mock('lucide-react-native', () => ({ ImageIcon: ({ size, ...props }: any) =>
Image
, InfoIcon: ({ size, ...props }: any) =>
Info
, LoaderIcon: ({ size, ...props }: any) =>
Loader
, + MapPinIcon: ({ size, ...props }: any) =>
MapPin
, + NavigationIcon: ({ size, ...props }: any) =>
Navigation
, + NetworkIcon: ({ size, ...props }: any) =>
Network
, PaperclipIcon: ({ size, ...props }: any) =>
Paperclip
, RouteIcon: ({ size, ...props }: any) =>
Route
, + ShieldCheckIcon: ({ size, ...props }: any) =>
ShieldCheck
, UserIcon: ({ size, ...props }: any) =>
User
, + UserPlusIcon: ({ size, ...props }: any) =>
UserPlus
, UsersIcon: ({ size, ...props }: any) =>
Users
, + VideoIcon: ({ size, ...props }: any) =>
Video
, + Volume2Icon: ({ size, ...props }: any) =>
Volume2
, })); // Mock react-native-svg diff --git a/src/__tests__/app/call/[id].test.tsx b/src/__tests__/app/call/[id].test.tsx index a1cdf4c..8f92dd9 100644 --- a/src/__tests__/app/call/[id].test.tsx +++ b/src/__tests__/app/call/[id].test.tsx @@ -183,10 +183,18 @@ jest.mock('lucide-react-native', () => ({ FileTextIcon: 'FileTextIcon', ImageIcon: 'ImageIcon', InfoIcon: 'InfoIcon', + LoaderIcon: 'LoaderIcon', + MapPinIcon: 'MapPinIcon', + NavigationIcon: 'NavigationIcon', + NetworkIcon: 'NetworkIcon', PaperclipIcon: 'PaperclipIcon', RouteIcon: 'RouteIcon', + ShieldCheckIcon: 'ShieldCheckIcon', UserIcon: 'UserIcon', + UserPlusIcon: 'UserPlusIcon', UsersIcon: 'UsersIcon', + VideoIcon: 'VideoIcon', + Volume2Icon: 'Volume2Icon', })); // Mock react-native-svg @@ -201,8 +209,10 @@ jest.mock('react-native-svg', () => ({ __esModule: true, })); -// Mock date-fns +// Mock date-fns (keep the real implementations available for components that +// need parse/isValid, only stub format for stable output) jest.mock('date-fns', () => ({ + ...jest.requireActual('date-fns'), format: jest.fn(() => '2024-01-01 12:00'), })); @@ -261,6 +271,12 @@ jest.mock('../../../components/calls/call-notes-modal', () => { }; }); +jest.mock('../../../components/calls/call-audio-modal', () => { + return function CallAudioModal() { + return null; + }; +}); + jest.mock('../../../components/calls/close-call-bottom-sheet', () => ({ CloseCallBottomSheet: () => null, })); @@ -269,6 +285,10 @@ jest.mock('../../../components/status/status-bottom-sheet', () => ({ StatusBottomSheet: () => null, })); +jest.mock('@/components/incident-command/incident-command-tab', () => ({ + IncidentCommandTab: () => null, +})); + jest.mock('@/components/maps/static-map', () => { return function StaticMap() { return null; diff --git a/src/__tests__/app/call/new/what3words.test.tsx b/src/__tests__/app/call/new/what3words.test.tsx index b3a718d..8d2a8da 100644 --- a/src/__tests__/app/call/new/what3words.test.tsx +++ b/src/__tests__/app/call/new/what3words.test.tsx @@ -115,6 +115,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/__tests__/app/calls.test.tsx b/src/__tests__/app/calls.test.tsx index 0d94ff6..f54590a 100644 --- a/src/__tests__/app/calls.test.tsx +++ b/src/__tests__/app/calls.test.tsx @@ -184,6 +184,7 @@ jest.mock('expo-navigation-bar', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: jest.fn(() => ({ colorScheme: 'light' })), })); diff --git a/src/__tests__/app/index.test.tsx b/src/__tests__/app/index.test.tsx index eba6d11..2a04e2d 100644 --- a/src/__tests__/app/index.test.tsx +++ b/src/__tests__/app/index.test.tsx @@ -77,6 +77,7 @@ jest.mock('react-i18next', () => ({ }), })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: jest.fn(() => ({ colorScheme: 'light', })), diff --git a/src/__tests__/app/login/index.web.test.tsx b/src/__tests__/app/login/index.web.test.tsx index a19aaa8..c427842 100644 --- a/src/__tests__/app/login/index.web.test.tsx +++ b/src/__tests__/app/login/index.web.test.tsx @@ -82,6 +82,7 @@ jest.mock('@/stores/app/server-url-store', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light', }), diff --git a/src/__tests__/app/login/login-form.test.tsx b/src/__tests__/app/login/login-form.test.tsx index 9b7cb18..1f1c09b 100644 --- a/src/__tests__/app/login/login-form.test.tsx +++ b/src/__tests__/app/login/login-form.test.tsx @@ -83,6 +83,7 @@ jest.mock('react-i18next', () => ({ // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light', }), diff --git a/src/__tests__/app/root/lockscreen.test.tsx b/src/__tests__/app/root/lockscreen.test.tsx index 903d397..1085939 100644 --- a/src/__tests__/app/root/lockscreen.test.tsx +++ b/src/__tests__/app/root/lockscreen.test.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'expo-router'; import Lockscreen from '../../../app/lockscreen'; import { useAuth } from '@/lib/auth'; +import { verifyPassword } from '@/lib/auth/api'; import useLockscreenStore from '@/stores/lockscreen/store'; // Mock dependencies @@ -21,6 +22,7 @@ jest.mock('react-i18next', () => ({ jest.mock('@/lib/logging', () => ({ logger: { info: jest.fn(), + warn: jest.fn(), error: jest.fn(), }, })); @@ -29,6 +31,10 @@ jest.mock('@/lib/auth', () => ({ useAuth: jest.fn(), })); +jest.mock('@/lib/auth/api', () => ({ + verifyPassword: jest.fn(), +})); + jest.mock('@/stores/lockscreen/store'); const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -55,6 +61,9 @@ describe('Lockscreen', () => { (useLockscreenStore as unknown as jest.Mock).mockReturnValue({ unlock: mockUnlock, }); + + // Default: entered password matches the stored verification hash + (verifyPassword as jest.Mock).mockResolvedValue(true); }); it('should render lockscreen correctly', () => { @@ -205,6 +214,52 @@ describe('Lockscreen', () => { expect(mockUnlock).not.toHaveBeenCalled(); expect(mockReplace).not.toHaveBeenCalled(); }); + + it('should show error and not unlock when password does not match', async () => { + (verifyPassword as jest.Mock).mockResolvedValue(false); + + render( + + + + ); + + const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder'); + const unlockButton = screen.getByText('lockscreen.unlock_button'); + + fireEvent.changeText(passwordInput, 'wrongPassword'); + fireEvent.press(unlockButton); + + await waitFor(() => { + expect(screen.getByText('lockscreen.unlock_failed')).toBeTruthy(); + }); + + expect(mockUnlock).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalledWith('/(app)'); + }); + + it('should require re-login when no verification hash is stored', async () => { + (verifyPassword as jest.Mock).mockResolvedValue(null); + + render( + + + + ); + + const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder'); + const unlockButton = screen.getByText('lockscreen.unlock_button'); + + fireEvent.changeText(passwordInput, 'anyPassword'); + fireEvent.press(unlockButton); + + await waitFor(() => { + expect(screen.getByText('lockscreen.relogin_required')).toBeTruthy(); + }); + + expect(mockUnlock).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalledWith('/(app)'); + }); }); describe('Error handling', () => { @@ -233,6 +288,11 @@ describe('Lockscreen', () => { }); describe('Loading state', () => { + beforeEach(() => { + // Resolve verification after a short delay so the loading state is observable + (verifyPassword as jest.Mock).mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve(true), 100))); + }); + it('should show loading indicator while unlocking', async () => { render( diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index 498e5bd..93dd81d 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -3,13 +3,14 @@ import { NovuProvider } from '@novu/react-native'; import Mapbox from '@rnmapbox/maps'; import { isRunningInExpoGo } from 'expo'; -import { Redirect, Slot } from 'expo-router'; +import { Redirect, Slot, usePathname } from 'expo-router'; import { Menu } from 'lucide-react-native'; import React, { useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, Platform, StyleSheet, Text as RNText, TouchableOpacity, View as RNView } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { DashboardViewToggles } from '@/components/dispatch-console'; import { NotificationButton } from '@/components/notifications/NotificationButton'; import { NotificationInbox } from '@/components/notifications/NotificationInbox'; import SideMenu from '@/components/sidebar/side-menu'; @@ -37,12 +38,14 @@ import { useWeatherAlertsStore } from '@/stores/weatherAlerts/store'; export default function TabLayout() { const { t } = useTranslation(); - const { status } = useAuthStore(); - const { isLocked } = useLockscreenStore(); + const status = useAuthStore((state) => state.status); + const isLocked = useLockscreenStore((state) => state.isLocked); const [isFirstTime, _setIsFirstTime] = useIsFirstTime(); const [isOpen, setIsOpen] = React.useState(false); const [isNotificationsOpen, setIsNotificationsOpen] = React.useState(false); const [webColorScheme, setWebColorScheme] = React.useState<'light' | 'dark'>('light'); + const pathname = usePathname(); + const isDispatchScreen = pathname === '/home' || pathname === '/' || pathname === '/index'; // Get store states first (hooks must be at top level) const config = useCoreStore((state) => state.config); @@ -74,6 +77,7 @@ export default function TabLayout() { // Refs to track initialization state const hasInitialized = useRef(false); const isInitializing = useRef(false); + const initTimedOut = useRef(false); const hasHiddenSplash = useRef(false); const lastSignedInStatus = useRef(null); const parentRef = useRef(null); @@ -110,6 +114,7 @@ export default function TabLayout() { } isInitializing.current = true; + initTimedOut.current = false; logger.info({ message: 'Starting app initialization', context: { @@ -120,7 +125,12 @@ export default function TabLayout() { try { // Set a timeout for initialization to prevent infinite hanging - const initTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Initialization timeout after 30 seconds')), 30000)); + const initTimeout = new Promise((_, reject) => + setTimeout(() => { + initTimedOut.current = true; + reject(new Error('Initialization timeout after 30 seconds')); + }, 30000) + ); const initPromise = (async () => { await useCoreStore.getState().init(); @@ -185,6 +195,21 @@ export default function TabLayout() { }); })(); + // Swallow post-timeout rejections so they don't surface as unhandled, and + // keep isInitializing true until the init promise fully settles + initPromise + .catch((error) => { + if (initTimedOut.current) { + logger.error({ + message: 'App initialization failed after initialization timeout', + context: { error, platform: Platform.OS }, + }); + } + }) + .finally(() => { + isInitializing.current = false; + }); + await Promise.race([initPromise, initTimeout]); } catch (error) { logger.error({ @@ -193,8 +218,6 @@ export default function TabLayout() { }); // Reset initialization state on error so it can be retried hasInitialized.current = false; - } finally { - isInitializing.current = false; } }, [status]); @@ -406,6 +429,7 @@ export default function TabLayout() { {t('app.title', 'Resgrid Responder')} + {isDispatchScreen ? : null} @@ -435,6 +459,7 @@ export default function TabLayout() { {t('app.title', 'Resgrid Responder')} + {isDispatchScreen ? : null} diff --git a/src/app/(app)/home.tsx b/src/app/(app)/home.tsx index 7fcd842..786d504 100644 --- a/src/app/(app)/home.tsx +++ b/src/app/(app)/home.tsx @@ -1,7 +1,7 @@ import { useFocusEffect } from '@react-navigation/native'; import { type Href, router } from 'expo-router'; import { useColorScheme } from 'nativewind'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native'; @@ -15,7 +15,6 @@ import { ActiveCallsPanel, ActivityLogPanel, AddNoteBottomSheet, - DashboardViewToggles, MapWidget, NotesPanel, PersonnelPanel, @@ -28,12 +27,12 @@ import { Box } from '@/components/ui/box'; import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar'; import { HStack } from '@/components/ui/hstack'; import { VStack } from '@/components/ui/vstack'; -import { WeatherAlertBanner } from '@/components/weatherAlerts/weather-alert-banner'; import { useAnalytics } from '@/hooks/use-analytics'; import { logger } from '@/lib/logging'; import { isCallActive, isCallPending, isCallScheduled } from '@/lib/utils'; import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData'; import { type UnitInfoResultData } from '@/models/v4/units/unitInfoResultData'; +import { WeatherAlertSeverity } from '@/models/v4/weatherAlerts/weatherAlertEnums'; import useAuthStore from '@/stores/auth/store'; import { useCallsStore } from '@/stores/calls/store'; import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; @@ -42,9 +41,12 @@ import { useHomeStore } from '@/stores/home/home-store'; import { useNotesStore } from '@/stores/notes/store'; import { usePersonnelStore } from '@/stores/personnel/store'; import { useSignalRStore } from '@/stores/signalr/signalr-store'; +import { useToastStore } from '@/stores/toast/store'; import { useUnitsStore } from '@/stores/units/store'; import { useWeatherAlertsStore } from '@/stores/weatherAlerts/store'; +const SIGNALR_REFETCH_DEBOUNCE_MS = 2000; + export default function DispatchConsole() { const { t } = useTranslation(); const { trackEvent } = useAnalytics(); @@ -54,62 +56,95 @@ export default function DispatchConsole() { const isTablet = Math.min(width, height) >= 600; // Store hooks - const { refreshAll } = useHomeStore(); - const { calls, callPriorities, isLoading: callsLoading, fetchCalls, fetchCallPriorities } = useCallsStore(); - const { units, isLoading: unitsLoading, fetchUnits } = useUnitsStore(); - const { personnel, isLoading: personnelLoading, fetchPersonnel } = usePersonnelStore(); + const refreshAll = useHomeStore((s) => s.refreshAll); + const calls = useCallsStore((s) => s.calls); + const callPriorities = useCallsStore((s) => s.callPriorities); + const callsLoading = useCallsStore((s) => s.isLoading); + const fetchCalls = useCallsStore((s) => s.fetchCalls); + const fetchCallPriorities = useCallsStore((s) => s.fetchCallPriorities); + const units = useUnitsStore((s) => s.units); + const unitsLoading = useUnitsStore((s) => s.isLoading); + const fetchUnits = useUnitsStore((s) => s.fetchUnits); + const personnel = usePersonnelStore((s) => s.personnel); + const personnelLoading = usePersonnelStore((s) => s.isLoading); + const fetchPersonnel = usePersonnelStore((s) => s.fetchPersonnel); const singleList = useDashboardViewStore((s) => s.singleList); - const { notes, isLoading: notesLoading, fetchNotes } = useNotesStore(); - const { lastPersonnelUpdateTimestamp, lastUnitsUpdateTimestamp, lastCallsUpdateTimestamp, lastEventType } = useSignalRStore(); - const { userId } = useAuthStore(); + const notes = useNotesStore((s) => s.notes); + const notesLoading = useNotesStore((s) => s.isLoading); + const fetchNotes = useNotesStore((s) => s.fetchNotes); + const lastPersonnelUpdateTimestamp = useSignalRStore((s) => s.lastPersonnelUpdateTimestamp); + const lastUnitsUpdateTimestamp = useSignalRStore((s) => s.lastUnitsUpdateTimestamp); + const lastCallsUpdateTimestamp = useSignalRStore((s) => s.lastCallsUpdateTimestamp); + const userId = useAuthStore((s) => s.userId); // Weather alerts - const { alerts: weatherAlerts, settings: weatherSettings, fetchActiveAlerts: fetchWeatherAlerts } = useWeatherAlertsStore(); + const weatherAlerts = useWeatherAlertsStore((s) => s.alerts); + const weatherSettings = useWeatherAlertsStore((s) => s.settings); + const fetchWeatherAlerts = useWeatherAlertsStore((s) => s.fetchActiveAlerts); + const showToast = useToastStore((s) => s.showToast); + + // Extreme/Severe counts for the stats bar + const extremeAlertCount = useMemo(() => weatherAlerts.filter((a) => a.Severity === WeatherAlertSeverity.Extreme).length, [weatherAlerts]); + const severeAlertCount = useMemo(() => weatherAlerts.filter((a) => a.Severity === WeatherAlertSeverity.Severe).length, [weatherAlerts]); + + // Show active weather alerts as an auto-dismissing toast (tap to open the alerts list). + // Deduped by a signature of the top alert + count so re-renders don't spam toasts. + const weatherAlertToastSignature = useRef(''); + useEffect(() => { + if (weatherSettings?.WeatherAlertsEnabled === false) return; + if (weatherAlerts.length === 0) { + weatherAlertToastSignature.current = ''; + return; + } + const signature = `${weatherAlerts[0].WeatherAlertId}-${weatherAlerts.length}`; + if (signature === weatherAlertToastSignature.current) return; + weatherAlertToastSignature.current = signature; + const topAlert = weatherAlerts[0]; + const more = weatherAlerts.length > 1 ? ` ${t('weatherAlerts.moreAlerts', { count: weatherAlerts.length - 1 })}` : ''; + showToast('warning', `${topAlert.Event}${more}`, 'weatherAlerts.title', { + duration: 6000, + onPress: () => router.push('/(app)/weather-alerts' as Href), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [weatherAlerts, weatherSettings]); // Dispatch console store - const { - selectedCallId, - selectedUnitId, - selectedPersonnelId, - activityLog, - radioLog, - isTransmitting, - currentChannel, - isCallFilterActive, - selectedCallExtraData, - selectedCallNotes, - isLoadingCallData, - mapCenterLatitude, - mapCenterLongitude, - setSelectedCallId, - setSelectedUnitId, - setSelectedPersonnelId, - toggleCallFilter, - clearCallFilter, - setCallExtraData, - setCallNotes, - setIsLoadingCallData, - setMapCenter, - addActivityLogEntry, - setIsTransmitting, - } = useDispatchConsoleStore(); + const selectedCallId = useDispatchConsoleStore((s) => s.selectedCallId); + const selectedUnitId = useDispatchConsoleStore((s) => s.selectedUnitId); + const selectedPersonnelId = useDispatchConsoleStore((s) => s.selectedPersonnelId); + const activityLog = useDispatchConsoleStore((s) => s.activityLog); + const radioLog = useDispatchConsoleStore((s) => s.radioLog); + const isTransmitting = useDispatchConsoleStore((s) => s.isTransmitting); + const currentChannel = useDispatchConsoleStore((s) => s.currentChannel); + const isCallFilterActive = useDispatchConsoleStore((s) => s.isCallFilterActive); + const selectedCallExtraData = useDispatchConsoleStore((s) => s.selectedCallExtraData); + const selectedCallNotes = useDispatchConsoleStore((s) => s.selectedCallNotes); + const isLoadingCallData = useDispatchConsoleStore((s) => s.isLoadingCallData); + const mapCenterLatitude = useDispatchConsoleStore((s) => s.mapCenterLatitude); + const mapCenterLongitude = useDispatchConsoleStore((s) => s.mapCenterLongitude); + const setSelectedCallId = useDispatchConsoleStore((s) => s.setSelectedCallId); + const setSelectedUnitId = useDispatchConsoleStore((s) => s.setSelectedUnitId); + const setSelectedPersonnelId = useDispatchConsoleStore((s) => s.setSelectedPersonnelId); + const toggleCallFilter = useDispatchConsoleStore((s) => s.toggleCallFilter); + const clearCallFilter = useDispatchConsoleStore((s) => s.clearCallFilter); + const setCallExtraData = useDispatchConsoleStore((s) => s.setCallExtraData); + const setCallNotes = useDispatchConsoleStore((s) => s.setCallNotes); + const setIsLoadingCallData = useDispatchConsoleStore((s) => s.setIsLoadingCallData); + const setMapCenter = useDispatchConsoleStore((s) => s.setMapCenter); + const addActivityLogEntry = useDispatchConsoleStore((s) => s.addActivityLogEntry); + const setIsTransmitting = useDispatchConsoleStore((s) => s.setIsTransmitting); // Local state - const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString('en-US', { hour12: false })); const [isAddingNote, setIsAddingNote] = useState(false); const [isAddNoteSheetOpen, setIsAddNoteSheetOpen] = useState(false); const [selectedPersonnelData, setSelectedPersonnelData] = useState(null); const [selectedUnitData, setSelectedUnitData] = useState(null); const [isCloseCallSheetOpen, setIsCloseCallSheetOpen] = useState(false); - // Update time every second - useEffect(() => { - const timer = setInterval(() => { - setCurrentTime(new Date().toLocaleTimeString('en-US', { hour12: false })); - }, 1000); - - return () => clearInterval(timer); - }, []); + // Debounce timers for SignalR-triggered refetches + const personnelFetchTimer = useRef | null>(null); + const unitsFetchTimer = useRef | null>(null); + const callsFetchTimer = useRef | null>(null); // Initialize data when component mounts useEffect(() => { @@ -161,44 +196,87 @@ export default function DispatchConsole() { // Listen for SignalR personnel updates useEffect(() => { if (lastPersonnelUpdateTimestamp > 0) { - fetchPersonnel(); addActivityLogEntry({ type: 'system', action: t('dispatch.system_update'), description: t('dispatch.data_refreshed'), }); + + if (personnelFetchTimer.current) { + clearTimeout(personnelFetchTimer.current); + } + personnelFetchTimer.current = setTimeout(() => { + personnelFetchTimer.current = null; + fetchPersonnel(); + }, SIGNALR_REFETCH_DEBOUNCE_MS); } + + return () => { + if (personnelFetchTimer.current) { + clearTimeout(personnelFetchTimer.current); + personnelFetchTimer.current = null; + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastPersonnelUpdateTimestamp]); // Listen for SignalR unit updates useEffect(() => { if (lastUnitsUpdateTimestamp > 0) { - fetchUnits(); addActivityLogEntry({ type: 'system', action: t('dispatch.system_update'), description: t('dispatch.data_refreshed'), }); + + if (unitsFetchTimer.current) { + clearTimeout(unitsFetchTimer.current); + } + unitsFetchTimer.current = setTimeout(() => { + unitsFetchTimer.current = null; + fetchUnits(); + }, SIGNALR_REFETCH_DEBOUNCE_MS); } + + return () => { + if (unitsFetchTimer.current) { + clearTimeout(unitsFetchTimer.current); + unitsFetchTimer.current = null; + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastUnitsUpdateTimestamp]); // Listen for SignalR call updates useEffect(() => { if (lastCallsUpdateTimestamp > 0) { - fetchCalls(); addActivityLogEntry({ type: 'system', action: t('dispatch.system_update'), description: t('dispatch.data_refreshed'), }); - // Refresh call data if filter is active - if (isCallFilterActive && selectedCallId) { - fetchCallData(selectedCallId); + if (callsFetchTimer.current) { + clearTimeout(callsFetchTimer.current); } + callsFetchTimer.current = setTimeout(() => { + callsFetchTimer.current = null; + fetchCalls(); + + // Refresh call data if filter is active + const { isCallFilterActive: filterActive, selectedCallId: callId } = useDispatchConsoleStore.getState(); + if (filterActive && callId) { + fetchCallData(callId); + } + }, SIGNALR_REFETCH_DEBOUNCE_MS); } + + return () => { + if (callsFetchTimer.current) { + clearTimeout(callsFetchTimer.current); + callsFetchTimer.current = null; + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastCallsUpdateTimestamp]); @@ -468,6 +546,70 @@ export default function DispatchConsole() { // Determine layout based on screen size and orientation const renderLayout = () => { + // Tablet landscape + single list - 3-column layout with combined resources + if (isTablet && isLandscape && singleList) { + return ( + + {/* Left Column - Calls & Notes */} + + + + + + {/* Center Column - Map */} + + + + + + {/* Right Column - Resources & PTT */} + + + + + + ); + } + // Tablet landscape - 3-column layout if (isTablet && isLandscape) { return ( @@ -545,6 +687,66 @@ export default function DispatchConsole() { ); } + // Tablet portrait + single list - 2-column layout with combined resources + if (isTablet && singleList) { + return ( + + {/* Left Column - Calls & Notes */} + + + + + + {/* Right Column */} + + + + + + + + ); + } + // Tablet portrait - 2-column layout if (isTablet) { return ( @@ -624,10 +826,35 @@ export default function DispatchConsole() { + {singleList ? ( + + ) : null} + {singleList ? ( - + + + ) : ( @@ -664,16 +891,18 @@ export default function DispatchConsole() { - + {!singleList ? ( + + ) : null} router.push('/(app)/weather-alerts' as Href)} /> - {/* Weather Alert Banner */} - {weatherSettings?.WeatherAlertsEnabled !== false && weatherAlerts.length > 0 && router.push('/(app)/weather-alerts' as Href)} />} - {/* Active Call Filter Banner */} {isCallFilterActive && selectedCall && } {/* Main Content */} - - - {renderLayout()} - + {renderLayout()} {/* Audio Stream Bottom Sheet */} @@ -763,4 +988,7 @@ const styles = StyleSheet.create({ scrollContent: { paddingBottom: 20, }, + resourcesPanelBounded: { + maxHeight: 400, + }, }); diff --git a/src/app/(app)/home.web.tsx b/src/app/(app)/home.web.tsx index 2ee2303..0b37ff1 100644 --- a/src/app/(app)/home.web.tsx +++ b/src/app/(app)/home.web.tsx @@ -15,7 +15,6 @@ import { ActiveCallsPanel, ActivityLogPanel, AddNoteBottomSheet, - DashboardViewToggles, MapWidget, NotesPanel, PersonnelPanel, @@ -33,6 +32,7 @@ import { logger } from '@/lib/logging'; import { isCallActive, isCallPending, isCallScheduled } from '@/lib/utils'; import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData'; import { type UnitInfoResultData } from '@/models/v4/units/unitInfoResultData'; +import { WeatherAlertSeverity } from '@/models/v4/weatherAlerts/weatherAlertEnums'; import useAuthStore from '@/stores/auth/store'; import { useCallsStore } from '@/stores/calls/store'; import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; @@ -41,7 +41,9 @@ import { useHomeStore } from '@/stores/home/home-store'; import { useNotesStore } from '@/stores/notes/store'; import { usePersonnelStore } from '@/stores/personnel/store'; import { type SignalREventType, useSignalRStore } from '@/stores/signalr/signalr-store'; +import { useToastStore } from '@/stores/toast/store'; import { useUnitsStore } from '@/stores/units/store'; +import { useWeatherAlertsStore } from '@/stores/weatherAlerts/store'; export default function DispatchConsoleWeb() { const { t } = useTranslation(); @@ -52,44 +54,87 @@ export default function DispatchConsoleWeb() { const isTablet = Math.min(width, height) >= 600; // Store hooks - const { refreshAll } = useHomeStore(); - const { userId } = useAuthStore(); - const { calls, callPriorities, isLoading: callsLoading, fetchCalls, fetchCallPriorities } = useCallsStore(); - const { units, isLoading: unitsLoading, fetchUnits } = useUnitsStore(); - const { personnel, isLoading: personnelLoading, fetchPersonnel } = usePersonnelStore(); + const refreshAll = useHomeStore((s) => s.refreshAll); + const userId = useAuthStore((s) => s.userId); + const calls = useCallsStore((s) => s.calls); + const callPriorities = useCallsStore((s) => s.callPriorities); + const callsLoading = useCallsStore((s) => s.isLoading); + const fetchCalls = useCallsStore((s) => s.fetchCalls); + const fetchCallPriorities = useCallsStore((s) => s.fetchCallPriorities); + const units = useUnitsStore((s) => s.units); + const unitsLoading = useUnitsStore((s) => s.isLoading); + const fetchUnits = useUnitsStore((s) => s.fetchUnits); + const personnel = usePersonnelStore((s) => s.personnel); + const personnelLoading = usePersonnelStore((s) => s.isLoading); + const fetchPersonnel = usePersonnelStore((s) => s.fetchPersonnel); const singleList = useDashboardViewStore((s) => s.singleList); - const { notes, isLoading: notesLoading, fetchNotes } = useNotesStore(); + const notes = useNotesStore((s) => s.notes); + const notesLoading = useNotesStore((s) => s.isLoading); + const fetchNotes = useNotesStore((s) => s.fetchNotes); // SignalR store - subscribe to specific event timestamps - const { lastEventType, lastPersonnelUpdateTimestamp, lastUnitsUpdateTimestamp, lastCallsUpdateTimestamp, isUpdateHubConnected } = useSignalRStore(); + const lastEventType = useSignalRStore((s) => s.lastEventType); + const lastPersonnelUpdateTimestamp = useSignalRStore((s) => s.lastPersonnelUpdateTimestamp); + const lastUnitsUpdateTimestamp = useSignalRStore((s) => s.lastUnitsUpdateTimestamp); + const lastCallsUpdateTimestamp = useSignalRStore((s) => s.lastCallsUpdateTimestamp); + const isUpdateHubConnected = useSignalRStore((s) => s.isUpdateHubConnected); + + // Weather alerts + const weatherAlerts = useWeatherAlertsStore((s) => s.alerts); + const weatherSettings = useWeatherAlertsStore((s) => s.settings); + const fetchWeatherAlerts = useWeatherAlertsStore((s) => s.fetchActiveAlerts); + const showToast = useToastStore((s) => s.showToast); + + // Extreme/Severe counts for the stats bar + const extremeAlertCount = useMemo(() => weatherAlerts.filter((a) => a.Severity === WeatherAlertSeverity.Extreme).length, [weatherAlerts]); + const severeAlertCount = useMemo(() => weatherAlerts.filter((a) => a.Severity === WeatherAlertSeverity.Severe).length, [weatherAlerts]); + + // Show active weather alerts as an auto-dismissing toast (tap to open the alerts list). + // Deduped by a signature of the top alert + count so re-renders don't spam toasts. + const weatherAlertToastSignature = useRef(''); + useEffect(() => { + if (weatherSettings?.WeatherAlertsEnabled === false) return; + if (weatherAlerts.length === 0) { + weatherAlertToastSignature.current = ''; + return; + } + const signature = `${weatherAlerts[0].WeatherAlertId}-${weatherAlerts.length}`; + if (signature === weatherAlertToastSignature.current) return; + weatherAlertToastSignature.current = signature; + const topAlert = weatherAlerts[0]; + const more = weatherAlerts.length > 1 ? ` ${t('weatherAlerts.moreAlerts', { count: weatherAlerts.length - 1 })}` : ''; + showToast('warning', `${topAlert.Event}${more}`, 'weatherAlerts.title', { + duration: 6000, + onPress: () => router.push('/(app)/weather-alerts' as Href), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [weatherAlerts, weatherSettings]); // Dispatch console store - const { - selectedCallId, - selectedUnitId, - selectedPersonnelId, - activityLog, - radioLog, - isTransmitting, - currentChannel, - isCallFilterActive, - selectedCallExtraData, - selectedCallNotes, - isLoadingCallData, - mapCenterLatitude, - mapCenterLongitude, - setSelectedCallId, - setSelectedUnitId, - setSelectedPersonnelId, - addActivityLogEntry, - setIsTransmitting, - toggleCallFilter, - clearCallFilter, - setCallExtraData, - setCallNotes, - setIsLoadingCallData, - setMapCenter, - } = useDispatchConsoleStore(); + const selectedCallId = useDispatchConsoleStore((s) => s.selectedCallId); + const selectedUnitId = useDispatchConsoleStore((s) => s.selectedUnitId); + const selectedPersonnelId = useDispatchConsoleStore((s) => s.selectedPersonnelId); + const activityLog = useDispatchConsoleStore((s) => s.activityLog); + const radioLog = useDispatchConsoleStore((s) => s.radioLog); + const isTransmitting = useDispatchConsoleStore((s) => s.isTransmitting); + const currentChannel = useDispatchConsoleStore((s) => s.currentChannel); + const isCallFilterActive = useDispatchConsoleStore((s) => s.isCallFilterActive); + const selectedCallExtraData = useDispatchConsoleStore((s) => s.selectedCallExtraData); + const selectedCallNotes = useDispatchConsoleStore((s) => s.selectedCallNotes); + const isLoadingCallData = useDispatchConsoleStore((s) => s.isLoadingCallData); + const mapCenterLatitude = useDispatchConsoleStore((s) => s.mapCenterLatitude); + const mapCenterLongitude = useDispatchConsoleStore((s) => s.mapCenterLongitude); + const setSelectedCallId = useDispatchConsoleStore((s) => s.setSelectedCallId); + const setSelectedUnitId = useDispatchConsoleStore((s) => s.setSelectedUnitId); + const setSelectedPersonnelId = useDispatchConsoleStore((s) => s.setSelectedPersonnelId); + const addActivityLogEntry = useDispatchConsoleStore((s) => s.addActivityLogEntry); + const setIsTransmitting = useDispatchConsoleStore((s) => s.setIsTransmitting); + const toggleCallFilter = useDispatchConsoleStore((s) => s.toggleCallFilter); + const clearCallFilter = useDispatchConsoleStore((s) => s.clearCallFilter); + const setCallExtraData = useDispatchConsoleStore((s) => s.setCallExtraData); + const setCallNotes = useDispatchConsoleStore((s) => s.setCallNotes); + const setIsLoadingCallData = useDispatchConsoleStore((s) => s.setIsLoadingCallData); + const setMapCenter = useDispatchConsoleStore((s) => s.setMapCenter); // Local state const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString('en-US', { hour12: false })); @@ -122,6 +167,7 @@ export default function DispatchConsoleWeb() { fetchPersonnel(); fetchNotes(); fetchMapCenter(); + fetchWeatherAlerts(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -144,7 +190,7 @@ export default function DispatchConsoleWeb() { } }; - // Track analytics when view becomes visible + // Refresh data and track analytics when view becomes visible useFocusEffect( useCallback(() => { trackEvent('dispatch_console_viewed', { @@ -153,7 +199,11 @@ export default function DispatchConsoleWeb() { isTablet, platform: 'web', }); - }, [trackEvent, isLandscape, isTablet]) + // Re-fetch core data so downstream panels (including check-in timers) refresh + fetchCalls(); + fetchUnits(); + fetchPersonnel(); + }, [trackEvent, isLandscape, isTablet, fetchCalls, fetchUnits, fetchPersonnel]) ); // Helper function to get event description for activity log @@ -299,10 +349,11 @@ export default function DispatchConsoleWeb() { const activeCalls = calls.filter((c) => isCallActive(c.State)).length; const pendingCalls = calls.filter((c) => isCallPending(c.State)).length; const scheduledCalls = calls.filter((c) => isCallScheduled(c.State)).length; - // Check CurrentStatus (human-readable name) for availability - case insensitive + // Only count units/personnel with explicit 'available' or 'standing by' status (case-insensitive) + // Missing or null statuses are NOT considered available for safety in dispatch scenarios const availableUnits = units.filter((u) => { const status = (u.CurrentStatus || '').toLowerCase(); - return !u.CurrentStatus || status === 'available' || status === 'standing by'; + return status === 'available' || status === 'standing by'; }).length; // Personnel with Available or Standing By status const availablePersonnel = personnel.filter((p) => { @@ -416,7 +467,7 @@ export default function DispatchConsoleWeb() { // Handle expanding map const handleExpandMap = () => { - router.push('/(app)/home/map' as Href); + router.push('/(app)/map' as Href); }; // Handle clearing call filter @@ -524,6 +575,70 @@ export default function DispatchConsoleWeb() { // Determine layout based on screen size and orientation const renderLayout = () => { + // Desktop/Tablet landscape + single list - 3-column layout with combined resources + if (isTablet && isLandscape && singleList) { + return ( + + {/* Left Column - Calls & Notes */} + + + + + + {/* Center Column - Map */} + + + + + + {/* Right Column - Resources & PTT */} + + + + + + ); + } + // Desktop/Tablet landscape - 3-column layout if (isTablet && isLandscape) { return ( @@ -601,6 +716,66 @@ export default function DispatchConsoleWeb() { ); } + // Tablet portrait + single list - 2-column layout with combined resources + if (isTablet && singleList) { + return ( + + {/* Left Column - Calls & Notes */} + + + + + + {/* Right Column */} + + + + + + + + ); + } + // Tablet portrait - 2-column layout if (isTablet) { return ( @@ -680,10 +855,35 @@ export default function DispatchConsoleWeb() { + {singleList ? ( + + ) : null} + {singleList ? ( - + + + ) : ( @@ -720,16 +920,18 @@ export default function DispatchConsoleWeb() { - + {!singleList ? ( + + ) : null} router.push('/(app)/weather-alerts' as Href)} /> {/* Active Call Filter Banner */} {isCallFilterActive && selectedCall && } {/* Main Content */} - - - {renderLayout()} - + {renderLayout()} {/* Audio Stream Bottom Sheet */} @@ -812,4 +1013,7 @@ const styles = StyleSheet.create({ scrollContent: { paddingBottom: 20, }, + resourcesPanelBounded: { + maxHeight: 400, + }, }); diff --git a/src/app/(app)/map.tsx b/src/app/(app)/map.tsx index 6aa3a2c..2390786 100644 --- a/src/app/(app)/map.tsx +++ b/src/app/(app)/map.tsx @@ -46,12 +46,11 @@ export default function Map() { const [isPinDetailModalOpen, setIsPinDetailModalOpen] = useState(false); const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false); const { isActive } = useAppLifecycle(); - const location = useLocationStore((state) => ({ - latitude: state.latitude, - longitude: state.longitude, - heading: state.heading, - isMapLocked: state.isMapLocked, - })); + const latitude = useLocationStore((state) => state.latitude); + const longitude = useLocationStore((state) => state.longitude); + const heading = useLocationStore((state) => state.heading); + const isMapLocked = useLocationStore((state) => state.isMapLocked); + const location = useMemo(() => ({ latitude, longitude, heading, isMapLocked }), [latitude, longitude, heading, isMapLocked]); // Map layers hook const { layers, visibleLayers, isLoading: isLayersLoading, fetchLayers, toggleLayer, showAllLayers, hideAllLayers, getVisibleLayerData } = useMapLayers({ initialLayerType: MapLayerType.ALL, autoFetch: true }); @@ -165,19 +164,19 @@ export default function Map() { useEffect(() => { if (isMapReady && location.latitude && location.longitude) { - logger.info({ - message: 'Location updated and map is ready', - context: { - latitude: location.latitude, - longitude: location.longitude, - heading: location.heading, - isMapLocked: location.isMapLocked, - }, - }); - // When map is locked, always follow the location // When map is unlocked, only follow if user hasn't moved the map if (location.isMapLocked || !hasUserMovedMap) { + logger.info({ + message: 'Location updated and map is ready', + context: { + latitude: location.latitude, + longitude: location.longitude, + heading: location.heading, + isMapLocked: location.isMapLocked, + }, + }); + const cameraConfig: any = { centerCoordinate: [location.longitude, location.latitude], zoomLevel: location.isMapLocked ? 16 : 12, @@ -312,10 +311,10 @@ export default function Map() { } }; - const handlePinPress = (pin: MapMakerInfoData) => { + const handlePinPress = useCallback((pin: MapMakerInfoData) => { setSelectedPin(pin); setIsPinDetailModalOpen(true); - }; + }, []); const handleSetAsCurrentCall = async (pin: MapMakerInfoData) => { try { diff --git a/src/app/(app)/map.web.tsx b/src/app/(app)/map.web.tsx index 7ba754f..d57f647 100644 --- a/src/app/(app)/map.web.tsx +++ b/src/app/(app)/map.web.tsx @@ -127,7 +127,7 @@ export default function MapWeb() { map.current.addControl( new mapboxgl.GeolocateControl({ positionOptions: { - enableHighAccuracy: true, + enableHighAccuracy: false, }, trackUserLocation: true, showUserHeading: true, @@ -242,7 +242,7 @@ export default function MapWeb() { if (!hasValidMapCoordinates(pin)) return; // Create custom marker element using shared utility - const el = createMapMarkerElement(pin, colorScheme); + const el = createMapMarkerElement(pin, colorScheme === 'dark' ? 'dark' : 'light'); // Create popup const popup = new mapboxgl.Popup({ offset: 25 }).setHTML( diff --git a/src/app/call/[id].tsx b/src/app/call/[id].tsx index 43e792b..3c51928 100644 --- a/src/app/call/[id].tsx +++ b/src/app/call/[id].tsx @@ -351,7 +351,7 @@ export default function CallDetail() { title: t('call_detail.tabs.info'), icon: , content: ( - + {t('call_detail.priority')} @@ -634,7 +634,7 @@ export default function CallDetail() { /> {/* Header */} - + {call.Name} ({call.Number}) @@ -702,7 +702,7 @@ export default function CallDetail() { {/* Action Buttons */} - + - ) : playerHtml ? ( + ) : playerSource ? ( <> {isLoading && ( )} - setIsLoading(false)} /> + setIsLoading(false)} /> ) : ( diff --git a/src/components/calls/__tests__/call-notes-modal-new.test.tsx b/src/components/calls/__tests__/call-notes-modal-new.test.tsx index 6ae1177..efd1c30 100644 --- a/src/components/calls/__tests__/call-notes-modal-new.test.tsx +++ b/src/components/calls/__tests__/call-notes-modal-new.test.tsx @@ -20,6 +20,7 @@ jest.mock('@react-navigation/native', () => ({ // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx index bf42075..d0b0856 100644 --- a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx +++ b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx @@ -33,6 +33,7 @@ afterEach(() => { // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/calls/__tests__/dispatch-selection-basic.test.tsx b/src/components/calls/__tests__/dispatch-selection-basic.test.tsx index cbd1760..e65b349 100644 --- a/src/components/calls/__tests__/dispatch-selection-basic.test.tsx +++ b/src/components/calls/__tests__/dispatch-selection-basic.test.tsx @@ -42,6 +42,7 @@ jest.mock('@/stores/dispatch/store', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/calls/__tests__/dispatch-selection-modal.test.tsx b/src/components/calls/__tests__/dispatch-selection-modal.test.tsx index cba6e21..637a477 100644 --- a/src/components/calls/__tests__/dispatch-selection-modal.test.tsx +++ b/src/components/calls/__tests__/dispatch-selection-modal.test.tsx @@ -45,6 +45,7 @@ jest.mock('@/stores/dispatch/store', () => ({ // Mock the color scheme and cssInterop jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/calls/call-card.web.tsx b/src/components/calls/call-card.web.tsx index ff2f3e5..9ae03aa 100644 --- a/src/components/calls/call-card.web.tsx +++ b/src/components/calls/call-card.web.tsx @@ -37,7 +37,7 @@ export const CallCard: React.FC = ({ call, priority }) => { style={{ backgroundColor: bgColor, }} - className={`mb-2 rounded-xl p-2 shadow-sm`} + className={`mb-2 rounded-xl p-2 shadow-xs`} > {/* Header with Call Number and Priority */} diff --git a/src/components/calls/call-files-modal.tsx b/src/components/calls/call-files-modal.tsx index 5d8b4d3..cc9fc8c 100644 --- a/src/components/calls/call-files-modal.tsx +++ b/src/components/calls/call-files-modal.tsx @@ -5,7 +5,7 @@ import * as Sharing from 'expo-sharing'; import { Download, File, X } from 'lucide-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, Pressable } from 'react-native'; +import { Alert, Platform, Pressable } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { getCallAttachmentFile } from '@/api/calls/callFiles'; @@ -106,6 +106,25 @@ export const CallFilesModal: React.FC = ({ isOpen, onClose, // Create a temporary file const fileName = file.FileName || file.Name || `file_${file.Id}`; + + if (Platform.OS === 'web') { + const objectUrl = URL.createObjectURL(fileData); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(objectUrl); + + setDownloadingFiles((prev) => { + const newState = { ...prev }; + delete newState[file.Id]; + return newState; + }); + return; + } + const fileUri = `${FileSystem.documentDirectory}${fileName}`; // Convert blob to base64 diff --git a/src/components/calls/call-form-renderer.tsx b/src/components/calls/call-form-renderer.tsx index e3d18b7..a9ac3ca 100644 --- a/src/components/calls/call-form-renderer.tsx +++ b/src/components/calls/call-form-renderer.tsx @@ -1,5 +1,5 @@ import { useColorScheme } from 'nativewind'; -import React, { useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; import WebView, { type WebViewMessageEvent } from 'react-native-webview'; @@ -87,7 +87,19 @@ export const CallFormRenderer: React.FC = ({ formSchemaJs const isDark = colorScheme === 'dark'; const webviewRef = useRef(null); - const html = buildHtml(formSchemaJson, isDark); + // Validate the schema is non-empty parseable JSON and neutralize `<` so a + // schema containing `` can't break out of the inline script tag + const safeSchemaJson = useMemo(() => { + if (!formSchemaJson || !formSchemaJson.trim()) return null; + try { + JSON.parse(formSchemaJson); + return formSchemaJson.replace(/ (safeSchemaJson ? buildHtml(safeSchemaJson, isDark) : null), [safeSchemaJson, isDark]); const handleMessage = (event: WebViewMessageEvent) => { try { @@ -100,23 +112,25 @@ export const CallFormRenderer: React.FC = ({ formSchemaJs return ( - ( - - - - )} - startInLoadingState - /> + {html ? ( + ( + + + + )} + startInLoadingState + /> + ) : null} ); }; diff --git a/src/components/calls/call-images-modal.tsx b/src/components/calls/call-images-modal.tsx index 4635f26..71e1e45 100644 --- a/src/components/calls/call-images-modal.tsx +++ b/src/components/calls/call-images-modal.tsx @@ -265,7 +265,7 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call const handleViewableItemsChanged = useRef(({ viewableItems }: any) => { if (viewableItems.length > 0) { - setActiveIndex(viewableItems[0].index || 0); + setActiveIndex(viewableItems[0].index ?? 0); } }).current; @@ -387,7 +387,7 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call ref={flatListRef} data={validImages} renderItem={renderImageItem} - keyExtractor={(item) => item?.Id || `image-${Math.random()}`} + keyExtractor={(item, index) => item?.Id || item?.Url || `image-${index}`} horizontal pagingEnabled showsHorizontalScrollIndicator={false} diff --git a/src/components/calls/call-notes-modal.tsx b/src/components/calls/call-notes-modal.tsx index 2d0db06..0d6733d 100644 --- a/src/components/calls/call-notes-modal.tsx +++ b/src/components/calls/call-notes-modal.tsx @@ -142,7 +142,7 @@ const CallNotesModal = ({ isOpen, onClose, callId }: CallNotesModalProps) => { ) : filteredNotes.length > 0 ? ( filteredNotes.map((note) => ( - + {note.Note} {note.FullName} diff --git a/src/components/checkIn/__tests__/check-in-bottom-sheet.test.tsx b/src/components/checkIn/__tests__/check-in-bottom-sheet.test.tsx index 7b5dd25..1a79baf 100644 --- a/src/components/checkIn/__tests__/check-in-bottom-sheet.test.tsx +++ b/src/components/checkIn/__tests__/check-in-bottom-sheet.test.tsx @@ -10,6 +10,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), cssInterop: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/components/checkIn/__tests__/check-in-history-list.test.tsx b/src/components/checkIn/__tests__/check-in-history-list.test.tsx index c9ae725..3ebcf66 100644 --- a/src/components/checkIn/__tests__/check-in-history-list.test.tsx +++ b/src/components/checkIn/__tests__/check-in-history-list.test.tsx @@ -10,6 +10,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/components/checkIn/__tests__/check-in-tab.test.tsx b/src/components/checkIn/__tests__/check-in-tab.test.tsx index 6d98989..e7e6654 100644 --- a/src/components/checkIn/__tests__/check-in-tab.test.tsx +++ b/src/components/checkIn/__tests__/check-in-tab.test.tsx @@ -10,6 +10,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), cssInterop: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); @@ -17,12 +18,14 @@ jest.mock('nativewind', () => ({ const mockStore = { timerStatuses: [], checkInHistory: [], + callPersonnelStatuses: [], isLoadingStatuses: false, isLoadingHistory: false, statusError: null, fetchTimerStatuses: jest.fn(), fetchResolvedTimers: jest.fn(), fetchCheckInHistory: jest.fn(), + fetchCallPersonnelStatuses: jest.fn(), toggleTimers: jest.fn(), startPolling: jest.fn(), stopPolling: jest.fn(), diff --git a/src/components/checkIn/__tests__/check-in-timer-card.test.tsx b/src/components/checkIn/__tests__/check-in-timer-card.test.tsx index 41275af..268c8c8 100644 --- a/src/components/checkIn/__tests__/check-in-timer-card.test.tsx +++ b/src/components/checkIn/__tests__/check-in-timer-card.test.tsx @@ -15,6 +15,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), cssInterop: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/components/common/loading.tsx b/src/components/common/loading.tsx index 78dba51..0d36c4d 100644 --- a/src/components/common/loading.tsx +++ b/src/components/common/loading.tsx @@ -114,7 +114,7 @@ export const Skeleton: React.FC = ({ width = '100%', height = 20, const radiusMap = { none: 'rounded-none', - sm: 'rounded-sm', + sm: 'rounded-xs', md: 'rounded-md', lg: 'rounded-lg', full: 'rounded-full', diff --git a/src/components/contacts/contact-card.tsx b/src/components/contacts/contact-card.tsx index de2673d..5ed7462 100644 --- a/src/components/contacts/contact-card.tsx +++ b/src/components/contacts/contact-card.tsx @@ -32,7 +32,7 @@ export const ContactCard: React.FC = ({ contact, onPress }) => const displayName = getDisplayName(contact); return ( - onPress(contact.ContactId)}> + onPress(contact.ContactId)}> {contact.ImageUrl ? ( diff --git a/src/components/contacts/contact-details-sheet.tsx b/src/components/contacts/contact-details-sheet.tsx index 84c6f25..980ac20 100644 --- a/src/components/contacts/contact-details-sheet.tsx +++ b/src/components/contacts/contact-details-sheet.tsx @@ -207,12 +207,12 @@ export const ContactDetailsSheet: React.FC = () => { {/* Tab Navigation */} - setActiveTab('details')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'details' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setActiveTab('details')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'details' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('contacts.tabs.details')} - setActiveTab('notes')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'notes' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setActiveTab('notes')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'notes' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('contacts.tabs.notes')} diff --git a/src/components/dispatch-console/__tests__/add-note-bottom-sheet.test.tsx b/src/components/dispatch-console/__tests__/add-note-bottom-sheet.test.tsx index efc6e55..dffb689 100644 --- a/src/components/dispatch-console/__tests__/add-note-bottom-sheet.test.tsx +++ b/src/components/dispatch-console/__tests__/add-note-bottom-sheet.test.tsx @@ -20,6 +20,7 @@ afterEach(() => { // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/dispatch-console/__tests__/animated-refresh-icon.test.tsx b/src/components/dispatch-console/__tests__/animated-refresh-icon.test.tsx index cce86ce..8770312 100644 --- a/src/components/dispatch-console/__tests__/animated-refresh-icon.test.tsx +++ b/src/components/dispatch-console/__tests__/animated-refresh-icon.test.tsx @@ -12,6 +12,7 @@ jest.mock('react-native-reanimated', () => { // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/components/dispatch-console/active-calls-panel.tsx b/src/components/dispatch-console/active-calls-panel.tsx index 59bb4fe..d8b001b 100644 --- a/src/components/dispatch-console/active-calls-panel.tsx +++ b/src/components/dispatch-console/active-calls-panel.tsx @@ -2,7 +2,7 @@ import { type Href, router } from 'expo-router'; import { AlertTriangle, Clock, ExternalLink, MapPin, Navigation, Plus, Radio, Search, UserPlus, X } from 'lucide-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Animated, Easing, Modal, Platform, Pressable, ScrollView, StyleSheet, Text as RNText, TextInput, View } from 'react-native'; +import { Animated, Easing, FlatList, Modal, Platform, Pressable, ScrollView, StyleSheet, Text as RNText, TextInput, View } from 'react-native'; import { getCallExtraData, updateCall } from '@/api/calls/calls'; import { DispatchSelectionModal } from '@/components/calls/dispatch-selection-modal'; @@ -21,6 +21,7 @@ import { type DispatchedEventResultData } from '@/models/v4/calls/dispatchedEven import { CheckInTimerStatus } from '@/models/v4/checkIn/checkInEnums'; import { useCallsStore } from '@/stores/calls/store'; import { useCheckInStore } from '@/stores/checkIn/store'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { useDispatchConsoleStore } from '@/stores/dispatch/dispatch-console-store'; import { type DispatchSelection } from '@/stores/dispatch/store'; import { useSecurityStore } from '@/stores/security/store'; @@ -255,6 +256,8 @@ interface ActiveCallsPanelProps { selectedCallId?: string; onSelectCall?: (callId: string) => void; isFilterActive?: boolean; + /** Flex weight when expanded; collapses to header height so siblings absorb the freed space. */ + flexWeight?: number; } // Call item wrapper for selection and interaction @@ -266,10 +269,10 @@ const CallItemWrapper: React.FC<{ dispatches?: DispatchedEventResultData[]; isLoadingDispatches?: boolean; overdueEntityIds?: Set; - onPress: () => void; - onOpenDetails: () => void; - onDispatchMore?: () => void; -}> = ({ call, priority, isSelected, isFilterActive, dispatches, isLoadingDispatches, overdueEntityIds, onPress, onOpenDetails, onDispatchMore }) => { + onSelect: (callId: string) => void; + onOpenDetails: (callId: string) => void; + onDispatchMore?: (call: CallResultData) => void; +}> = React.memo(({ call, priority, isSelected, isFilterActive, dispatches, isLoadingDispatches, overdueEntityIds, onSelect, onOpenDetails, onDispatchMore }) => { void isFilterActive; // kept in props for selection badge only const { t } = useTranslation(); const bgColor = priority?.Color || '#6b7280'; @@ -279,7 +282,7 @@ const CallItemWrapper: React.FC<{ const wrapperStyle = isSelected && isFilterActive ? StyleSheet.flatten([styles.callItemWrapper, styles.selectedCall]) : styles.callItemWrapper; return ( - + onSelect(call.CallId)}> {/* Selection indicator */} {isSelected && isFilterActive ? ( @@ -296,7 +299,7 @@ const CallItemWrapper: React.FC<{ { e.stopPropagation(); - onOpenDetails(); + onOpenDetails(call.CallId); }} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} style={StyleSheet.flatten([styles.detailsButton, { backgroundColor: `${bgColor}dd` }])} @@ -307,7 +310,7 @@ const CallItemWrapper: React.FC<{ { e.stopPropagation(); - onDispatchMore(); + onDispatchMore(call); }} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} style={StyleSheet.flatten([styles.dispatchMoreButton, { backgroundColor: `${bgColor}dd` }])} @@ -320,9 +323,11 @@ const CallItemWrapper: React.FC<{ ); -}; +}); + +CallItemWrapper.displayName = 'CallItemWrapper'; -export const ActiveCallsPanel: React.FC = ({ selectedCallId, onSelectCall, isFilterActive = false }) => { +export const ActiveCallsPanel: React.FC = ({ selectedCallId, onSelectCall, isFilterActive = false, flexWeight }) => { const { t } = useTranslation(); const { canUserCreateCalls } = useSecurityStore(); const showToast = useToastStore((state) => state.showToast); @@ -339,7 +344,8 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall }); return ids; }, [allTimerStatuses]); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('active-calls')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [searchQuery, setSearchQuery] = useState(''); // Per-call dispatches cache (fetched eagerly for all active calls) @@ -400,27 +406,33 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall batchEpochs.set(id, epochCounterRef.current); }); - Promise.all( - toFetch.map((callId) => - getCallExtraData(callId) - .then((res) => ({ callId, dispatches: res?.Data?.Dispatches ?? ([] as DispatchedEventResultData[]) })) - .catch(() => ({ callId, dispatches: null as DispatchedEventResultData[] | null })) - ) - ).then((results) => { - setCallDispatchesMap((prev) => { - const next = { ...prev }; - results.forEach(({ callId, dispatches }) => { - // Skip update if a newer request superseded this one for this callId. - if (callFetchEpochsRef.current.get(callId) !== batchEpochs.get(callId)) return; - if (dispatches !== null) { - next[callId] = dispatches; - } else { - // Fetch failed – remove from the fetched set so the next refresh can retry - fetchedCallIdsRef.current.delete(callId); - } + const runBatches = async () => { + // Bound the fan-out so console load does not fire N parallel requests + const CONCURRENCY = 4; + for (let i = 0; i < toFetch.length; i += CONCURRENCY) { + const chunk = toFetch.slice(i, i + CONCURRENCY); + const results = await Promise.all( + chunk.map((callId) => + getCallExtraData(callId) + .then((res) => ({ callId, dispatches: res?.Data?.Dispatches ?? ([] as DispatchedEventResultData[]) })) + .catch(() => ({ callId, dispatches: null as DispatchedEventResultData[] | null })) + ) + ); + setCallDispatchesMap((prev) => { + const next = { ...prev }; + results.forEach(({ callId, dispatches }) => { + // Skip update if a newer request superseded this one for this callId. + if (callFetchEpochsRef.current.get(callId) !== batchEpochs.get(callId)) return; + if (dispatches !== null) { + next[callId] = dispatches; + } else { + // Fetch failed – remove from the fetched set so the next refresh can retry + fetchedCallIdsRef.current.delete(callId); + } + }); + return next; }); - return next; - }); + } // Always clear loading state so spinners are never permanently stuck, // even if a concurrent batch started after this one. setLoadingCallIds((prev) => { @@ -430,7 +442,8 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall }); return next; }); - }); + }; + void runBatches(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [calls]); @@ -470,6 +483,17 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall router.push(`/call/${callId}` as Href); }, []); + const handleSelectCall = useCallback( + (callId: string) => { + onSelectCall?.(callId); + }, + [onSelectCall] + ); + + const handleDispatchMore = useCallback((call: CallResultData) => { + setDispatchTargetCall(call); + }, []); + // Dispatch additional resources to the targeted call. Picked resources are unioned with the call's // existing dispatches so nothing is un-dispatched; the server notifies only the newly-added resources. const handleDispatchAdditional = async (selection: DispatchSelection) => { @@ -516,15 +540,38 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall fetchCallPriorities(); }, [fetchCalls, fetchCallPriorities]); + const renderCallItem = useCallback( + ({ item }: { item: CallResultData }) => ( + + ), + [getPriority, selectedCallId, isFilterActive, callDispatchesMap, loadingCallIds, overdueEntityIds, handleSelectCall, handleOpenCallDetails, canUserCreateCalls, handleDispatchMore] + ); + + const keyExtractor = useCallback((item: CallResultData) => item.CallId, []); + return ( - + setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('active-calls', !isCollapsed)} rightContent={ @@ -560,8 +607,8 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall ) : null} - - {error ? ( + {error ? ( + {error} @@ -569,31 +616,27 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall {t('common.retry')} - ) : activeCalls.length === 0 ? ( - - - {t('dispatch.no_active_calls')} - - ) : ( - activeCalls.map((call) => ( - { - onSelectCall?.(call.CallId); - }} - onOpenDetails={() => handleOpenCallDetails(call.CallId)} - onDispatchMore={canUserCreateCalls ? () => setDispatchTargetCall(call) : undefined} - /> - )) - )} - + + ) : ( + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={activeCalls} + renderItem={renderCallItem} + keyExtractor={keyExtractor} + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + ListEmptyComponent={ + + + {t('dispatch.no_active_calls')} + + } + /> + )} ) : null} @@ -616,9 +659,11 @@ const styles = StyleSheet.create({ }, content: { flex: 1, - padding: 8, maxHeight: 300, }, + contentContainer: { + padding: 8, + }, callItemWrapper: { marginBottom: 8, }, diff --git a/src/components/dispatch-console/activity-log-panel.tsx b/src/components/dispatch-console/activity-log-panel.tsx index 5907fe7..a255f74 100644 --- a/src/components/dispatch-console/activity-log-panel.tsx +++ b/src/components/dispatch-console/activity-log-panel.tsx @@ -3,7 +3,7 @@ import { AlertTriangle, ArrowRight, ChevronRight, Clock, CloudLightning, Filter, import { useColorScheme } from 'nativewind'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Pressable, ScrollView, StyleSheet, View } from 'react-native'; +import { FlatList, Platform, Pressable, ScrollView, StyleSheet, View } from 'react-native'; import { Badge } from '@/components/ui/badge'; import { Box } from '@/components/ui/box'; @@ -18,6 +18,7 @@ import { type UnitInfoResultData } from '@/models/v4/units/unitInfoResultData'; import { SEVERITY_COLORS, WeatherAlertSeverity } from '@/models/v4/weatherAlerts/weatherAlertEnums'; import { useCallsStore } from '@/stores/calls/store'; import { useCheckInStore } from '@/stores/checkIn/store'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { type RadioLogEntry } from '@/stores/dispatch/dispatch-console-store'; import { usePersonnelActionsStore } from '@/stores/dispatch/personnel-actions-store'; import { useUnitActionsStore } from '@/stores/dispatch/unit-actions-store'; @@ -125,7 +126,7 @@ const formatDuration = (seconds: number | null) => { return `${mins}m ${secs}s`; }; -const ActivityLogItem: React.FC<{ entry: ActivityLogEntry }> = ({ entry }) => { +const ActivityLogItem: React.FC<{ entry: ActivityLogEntry }> = React.memo(({ entry }) => { const IconComponent = getIconForType(entry.type); const color = getColorForType(entry.type); @@ -148,9 +149,11 @@ const ActivityLogItem: React.FC<{ entry: ActivityLogEntry }> = ({ entry }) => { ); -}; +}); + +ActivityLogItem.displayName = 'ActivityLogItem'; -const CallActivityItem: React.FC<{ activity: DispatchedEventResultData }> = ({ activity }) => { +const CallActivityItem: React.FC<{ activity: DispatchedEventResultData }> = React.memo(({ activity }) => { const getActivityIcon = () => { if (activity.Type === 'Unit' || activity.Type === 'u') return Truck; if (activity.Type === 'Personnel' || activity.Type === 'p') return User; @@ -194,9 +197,11 @@ const CallActivityItem: React.FC<{ activity: DispatchedEventResultData }> = ({ a ); -}; +}); -const RadioLogItem: React.FC<{ entry: RadioLogEntry }> = ({ entry }) => { +CallActivityItem.displayName = 'CallActivityItem'; + +const RadioLogItem: React.FC<{ entry: RadioLogEntry }> = React.memo(({ entry }) => { const { t } = useTranslation(); const color = entry.isActive ? '#22c55e' : '#6b7280'; @@ -226,7 +231,9 @@ const RadioLogItem: React.FC<{ entry: RadioLogEntry }> = ({ entry }) => { ); -}; +}); + +RadioLogItem.displayName = 'RadioLogItem'; interface ActionButtonProps { icon: React.ElementType; @@ -301,7 +308,8 @@ export const ActivityLogPanel: React.FC = ({ onAddCallNote, }) => { const { t } = useTranslation(); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('activity-log')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [activeTab, setActiveTab] = useState('activity'); // Personnel actions store @@ -399,10 +407,11 @@ export const ActivityLogPanel: React.FC = ({ // Get all active call IDs to fetch timer statuses for const activeCallIds = useMemo(() => calls.map((c) => parseInt(c.CallId)).filter((id) => !isNaN(id) && id > 0), [calls]); + // Stable key derived from the ID set (order-independent) so the polling + // interval only resets when the actual set of active calls changes, not on + // every SignalR-driven `calls` array identity change. + const activeCallIdsKey = useMemo(() => [...activeCallIds].sort((a, b) => a - b).join(','), [activeCallIds]); - // Fetch check-in timer statuses and restart polling whenever the calls list updates. - // Using the `calls` reference as a dependency ensures re-fetch after navigation back - // (when calls are refetched on focus) even if the call IDs themselves haven't changed. useEffect(() => { if (activeCallIds.length > 0) { fetchTimerStatusesForCalls(activeCallIds); @@ -411,7 +420,8 @@ export const ActivityLogPanel: React.FC = ({ return () => { stopCheckInPolling(); }; - }, [calls, activeCallIds, fetchTimerStatusesForCalls, startCheckInPolling, stopCheckInPolling]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeCallIdsKey, fetchTimerStatusesForCalls, startCheckInPolling, stopCheckInPolling]); // When call filter is active, show only timers for the selected call const filteredTimerStatuses = useMemo(() => { @@ -470,35 +480,75 @@ export const ActivityLogPanel: React.FC = ({ } }, [activeTab, radioLog.length, activityCount, filteredTimerStatuses.length, urgentCheckInCount, weatherAlertCount]); + const renderActivityItem = useCallback(({ item }: { item: ActivityLogEntry }) => , []); + const activityKeyExtractor = useCallback((item: ActivityLogEntry) => item.id, []); + const renderRadioItem = useCallback(({ item }: { item: RadioLogEntry }) => , []); + const radioKeyExtractor = useCallback((item: RadioLogEntry) => item.id, []); + const renderCallActivityItem = useCallback(({ item }: { item: DispatchedEventResultData }) => , []); + const callActivityKeyExtractor = useCallback((item: DispatchedEventResultData, index: number) => `${item.Id || index}-${item.Timestamp}`, []); + const renderActivityContent = () => { if (useCallActivity) { // Show call-specific activity from API - return callActivity!.map((activity, index) => ); - } - - if (filteredEntries.length === 0) { return ( - - - {isCallFilterActive ? t('dispatch.no_call_activity') : t('dispatch.no_activity')} - + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={callActivity!} + renderItem={renderCallActivityItem} + keyExtractor={callActivityKeyExtractor} + initialNumToRender={15} + maxToRenderPerBatch={15} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + /> ); } - return filteredEntries.map((entry) => ); + return ( + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={filteredEntries} + renderItem={renderActivityItem} + keyExtractor={activityKeyExtractor} + initialNumToRender={15} + maxToRenderPerBatch={15} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + ListEmptyComponent={ + + + {isCallFilterActive ? t('dispatch.no_call_activity') : t('dispatch.no_activity')} + + } + /> + ); }; const renderRadioContent = () => { - if (radioLog.length === 0) { - return ( - - - {t('dispatch.no_radio_activity')} - - ); - } - - return radioLog.map((entry) => ); + return ( + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={radioLog} + renderItem={renderRadioItem} + keyExtractor={radioKeyExtractor} + initialNumToRender={15} + maxToRenderPerBatch={15} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + ListEmptyComponent={ + + + {t('dispatch.no_radio_activity')} + + } + /> + ); }; const renderActionsContent = () => { @@ -658,6 +708,8 @@ export const ActivityLogPanel: React.FC = ({ } }; + const isVirtualizedTab = activeTab === 'activity' || activeTab === 'radio'; + return ( = ({ iconColor={activeTab === 'radio' ? '#22c55e' : activeTab === 'actions' ? '#f59e0b' : activeTab === 'checkins' ? '#ef4444' : activeTab === 'weather' ? '#F57C00' : '#6b7280'} count={getHeaderCount()} isCollapsed={isCollapsed} - onToggleCollapse={() => setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('activity-log', !isCollapsed)} rightContent={ isCallFilterActive && activeTab === 'activity' ? ( @@ -713,9 +765,13 @@ export const ActivityLogPanel: React.FC = ({ {/* Tab Content */} - - {renderTabContent()} - + {isVirtualizedTab ? ( + renderTabContent() + ) : ( + + {renderTabContent()} + + )} ) : null} @@ -737,9 +793,11 @@ export const ActivityLogPanel: React.FC = ({ const styles = StyleSheet.create({ content: { flex: 1, - padding: 8, maxHeight: 350, }, + contentContainer: { + padding: 8, + }, iconContainer: { width: 24, height: 24, diff --git a/src/components/dispatch-console/animated-refresh-icon.web.tsx b/src/components/dispatch-console/animated-refresh-icon.web.tsx index 3d98539..9821e48 100644 --- a/src/components/dispatch-console/animated-refresh-icon.web.tsx +++ b/src/components/dispatch-console/animated-refresh-icon.web.tsx @@ -1,6 +1,6 @@ import { RefreshCw } from 'lucide-react-native'; import { useColorScheme } from 'nativewind'; -import React, { useEffect, useRef, useState } from 'react'; +import React from 'react'; interface AnimatedRefreshIconProps { isLoading: boolean; @@ -9,47 +9,16 @@ interface AnimatedRefreshIconProps { export const AnimatedRefreshIcon: React.FC = ({ isLoading, size = 14 }) => { const { colorScheme } = useColorScheme(); - const [rotation, setRotation] = useState(0); - const animationRef = useRef(null); - const startTimeRef = useRef(0); - - useEffect(() => { - if (isLoading) { - startTimeRef.current = performance.now(); - - const animate = (currentTime: number) => { - const elapsed = currentTime - startTimeRef.current; - const degrees = (elapsed / 1000) * 360; // Full rotation per second - setRotation(degrees % 360); - animationRef.current = requestAnimationFrame(animate); - }; - - animationRef.current = requestAnimationFrame(animate); - } else { - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - animationRef.current = null; - } - setRotation(0); - } - - return () => { - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - } - }; - }, [isLoading]); - const iconColor = colorScheme === 'dark' ? '#9ca3af' : '#6b7280'; return (
+
); diff --git a/src/components/dispatch-console/dashboard-view-toggles.tsx b/src/components/dispatch-console/dashboard-view-toggles.tsx index 5a22ebf..7560c62 100644 --- a/src/components/dispatch-console/dashboard-view-toggles.tsx +++ b/src/components/dispatch-console/dashboard-view-toggles.tsx @@ -7,35 +7,45 @@ import { Icon } from '@/components/ui/icon'; import { Text } from '@/components/ui/text'; import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; +interface DashboardViewTogglesProps { + /** Render inside the app header: tighter spacing, no bottom margin. */ + inHeader?: boolean; + /** Header has a solid colored background: use light text/pills for inactive state. */ + onColoredBg?: boolean; +} + /** Compact toggle bar for the dispatch dashboard: filter to available resources and/or merge them into a single list. */ -export const DashboardViewToggles: React.FC = () => { +export const DashboardViewToggles: React.FC = ({ inHeader = false, onColoredBg = false }) => { const { t } = useTranslation(); const availableOnly = useDashboardViewStore((s) => s.availableOnly); const singleList = useDashboardViewStore((s) => s.singleList); const toggleAvailableOnly = useDashboardViewStore((s) => s.toggleAvailableOnly); const toggleSingleList = useDashboardViewStore((s) => s.toggleSingleList); + const inactiveTextClass = onColoredBg ? 'text-white' : 'text-gray-700 dark:text-gray-300'; + const inactiveIconClass = onColoredBg ? 'text-white' : 'text-gray-600 dark:text-gray-300'; + return ( - + - - {t('dispatch.available_only')} + + {t('dispatch.available_only')} - - {t('dispatch.single_list')} + + {t('dispatch.single_list')} ); @@ -48,6 +58,10 @@ const styles = StyleSheet.create({ marginBottom: 8, flexWrap: 'wrap', }, + rowInHeader: { + marginBottom: 0, + flexWrap: 'nowrap', + }, pill: { flexDirection: 'row', alignItems: 'center', @@ -62,4 +76,7 @@ const styles = StyleSheet.create({ pillInactive: { backgroundColor: 'rgba(148, 163, 184, 0.18)', }, + pillInactiveHeader: { + backgroundColor: 'rgba(255, 255, 255, 0.2)', + }, }); diff --git a/src/components/dispatch-console/map-widget.tsx b/src/components/dispatch-console/map-widget.tsx index b9747d7..90a3be3 100644 --- a/src/components/dispatch-console/map-widget.tsx +++ b/src/components/dispatch-console/map-widget.tsx @@ -11,6 +11,7 @@ import { Icon } from '@/components/ui/icon'; import { Text } from '@/components/ui/text'; import { MapLayerType, useMapLayers } from '@/hooks/use-map-layers'; import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { PanelHeader } from './panel-header'; @@ -34,7 +35,8 @@ interface MapWidgetProps { export const MapWidget: React.FC = ({ pins, onExpandMap, onRefresh, onCenterOnLocation, onPinPress, autoFetchPins = true, children }) => { const { t } = useTranslation(); const { colorScheme } = useColorScheme(); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('map')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false); // Map layers hook @@ -100,7 +102,7 @@ export const MapWidget: React.FC = ({ pins, onExpandMap, onRefre icon={MapIcon} iconColor="#14b8a6" isCollapsed={isCollapsed} - onToggleCollapse={() => setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('map', !isCollapsed)} rightContent={ {onRefresh ? ( diff --git a/src/components/dispatch-console/notes-panel.tsx b/src/components/dispatch-console/notes-panel.tsx index 3cf987a..f22e207 100644 --- a/src/components/dispatch-console/notes-panel.tsx +++ b/src/components/dispatch-console/notes-panel.tsx @@ -13,6 +13,7 @@ import { VStack } from '@/components/ui/vstack'; import { formatDateForDisplay, parseDateISOString, stripHtmlTags } from '@/lib/utils'; import { type CallNoteResultData } from '@/models/v4/callNotes/callNoteResultData'; import { type NoteResultData } from '@/models/v4/notes/noteResultData'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { AnimatedRefreshIcon } from './animated-refresh-icon'; import { PanelHeader } from './panel-header'; @@ -28,6 +29,8 @@ interface NotesPanelProps { callNotes?: CallNoteResultData[]; onAddCallNote?: (note: string) => void; isAddingNote?: boolean; + /** Flex weight when expanded; collapses to header height so siblings absorb the freed space. */ + flexWeight?: number; } const NoteItem: React.FC<{ @@ -77,9 +80,10 @@ const CallNoteItem: React.FC<{ ); }; -export const NotesPanel: React.FC = ({ notes, isLoading, onRefresh, onSelectNote, onNewNote, isCallFilterActive, callNotes, onAddCallNote, isAddingNote }) => { +export const NotesPanel: React.FC = ({ notes, isLoading, onRefresh, onSelectNote, onNewNote, isCallFilterActive, callNotes, onAddCallNote, isAddingNote, flexWeight }) => { const { t } = useTranslation(); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('notes')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [newNoteText, setNewNoteText] = useState(''); const [searchQuery, setSearchQuery] = useState(''); @@ -118,14 +122,17 @@ export const NotesPanel: React.FC = ({ notes, isLoading, onRefr const notesCount = displayNotes ? filteredCallNotes?.length || 0 : filteredNotes.length; return ( - + setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('notes', !isCollapsed)} rightContent={ {isCallFilterActive ? ( diff --git a/src/components/dispatch-console/panel-header.tsx b/src/components/dispatch-console/panel-header.tsx index 22b12b3..d477abd 100644 --- a/src/components/dispatch-console/panel-header.tsx +++ b/src/components/dispatch-console/panel-header.tsx @@ -39,7 +39,7 @@ export const PanelHeader: React.FC = ({ title, icon, iconColor ) : null} - {onToggleCollapse ? : null} + {onToggleCollapse ? : null} diff --git a/src/components/dispatch-console/personnel-actions-panel.tsx b/src/components/dispatch-console/personnel-actions-panel.tsx index 57ba35c..2208d1f 100644 --- a/src/components/dispatch-console/personnel-actions-panel.tsx +++ b/src/components/dispatch-console/personnel-actions-panel.tsx @@ -640,21 +640,21 @@ export const PersonnelActionsPanel: React.FC = ({ pe <> {destinationConfig.showCalls ? ( - setDestinationTab('calls')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'calls' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('calls')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'calls' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('dispatch.calls')} ({activeCalls.length}) ) : null} {destinationConfig.showStations ? ( - setDestinationTab('stations')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'stations' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('stations')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'stations' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('dispatch.stations')} ({availableStations.length}) ) : null} {destinationConfig.showPois ? ( - setDestinationTab('pois')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'pois' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('pois')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'pois' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('menu.pois')} ({availablePois.length}) diff --git a/src/components/dispatch-console/personnel-panel.tsx b/src/components/dispatch-console/personnel-panel.tsx index 2b60f28..542dfaf 100644 --- a/src/components/dispatch-console/personnel-panel.tsx +++ b/src/components/dispatch-console/personnel-panel.tsx @@ -3,7 +3,7 @@ import { Circle, ExternalLink, Filter, MapPin, Plus, Search, User, Users, X } fr import { useColorScheme } from 'nativewind'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native'; +import { FlatList, Platform, Pressable, StyleSheet, TextInput, View } from 'react-native'; import { Badge } from '@/components/ui/badge'; import { Box } from '@/components/ui/box'; @@ -15,7 +15,7 @@ import { isPersonnelDispatch } from '@/lib/dispatch-types'; import { isPersonnelAvailable } from '@/lib/resource-availability'; import { type DispatchedEventResultData } from '@/models/v4/calls/dispatchedEventResultData'; import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData'; -import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { AnimatedRefreshIcon } from './animated-refresh-icon'; import { PanelHeader } from './panel-header'; @@ -37,16 +37,16 @@ const PersonnelItem: React.FC<{ person: PersonnelInfoResultData; isSelected: boolean; isOnCall?: boolean; - onPress: () => void; - onSetStatus?: () => void; -}> = ({ person, isSelected, isOnCall, onPress, onSetStatus }) => { + onSelect: (personnelId: string) => void; + onSetStatusForPersonnel?: (personnelId: string, personnelName: string) => void; +}> = React.memo(({ person, isSelected, isOnCall, onSelect, onSetStatusForPersonnel }) => { const { t } = useTranslation(); const statusColor = person.StatusColor || '#6b7280'; const staffingColor = person.StaffingColor || '#6b7280'; const hasDestination = person.StatusDestinationName && person.StatusDestinationName.trim() !== ''; return ( - + onSelect(person.UserId)}> @@ -103,11 +103,11 @@ const PersonnelItem: React.FC<{ {person.Staffing || t('dispatch.unknown')} - {onSetStatus ? ( + {onSetStatusForPersonnel ? ( { e.stopPropagation(); - onSetStatus(); + onSetStatusForPersonnel(person.UserId, `${person.FirstName} ${person.LastName}`); }} style={styles.statusButton} > @@ -119,7 +119,9 @@ const PersonnelItem: React.FC<{ ); -}; +}); + +PersonnelItem.displayName = 'PersonnelItem'; export const PersonnelPanel: React.FC = ({ personnel, @@ -133,7 +135,8 @@ export const PersonnelPanel: React.FC = ({ onSetPersonnelStatusForCall, }) => { const { t } = useTranslation(); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('personnel')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [searchQuery, setSearchQuery] = useState(''); const availableOnly = useDashboardViewStore((s) => s.availableOnly); const singleList = useDashboardViewStore((s) => s.singleList); @@ -231,6 +234,24 @@ export const PersonnelPanel: React.FC = ({ // Count on-duty personnel const onDutyCount = displayedPersonnel.filter((p) => p.Staffing && p.Staffing.toLowerCase() !== 'off duty').length; + const renderPersonnelItem = useCallback( + ({ item }: { item: PersonnelInfoResultData }) => { + const fullName = `${item.FirstName} ${item.LastName}`.toLowerCase(); + return ( + + ); + }, + [selectedPersonnelId, dispatchedPersonnelIds, dispatchedPersonnelNames, selectedCallId, handleSelectPersonnel, isCallFilterActive, onSetPersonnelStatusForCall] + ); + + const keyExtractor = useCallback((item: PersonnelInfoResultData) => item.UserId, []); + // When "single list" is on, the combined ResourcesPanel (rendered from the units slot) already // includes personnel, so this panel hides itself to avoid duplication. if (singleList) { @@ -245,7 +266,7 @@ export const PersonnelPanel: React.FC = ({ iconColor="#8b5cf6" count={displayedPersonnel.length} isCollapsed={isCollapsed} - onToggleCollapse={() => setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('personnel', !isCollapsed)} rightContent={ @@ -292,28 +313,24 @@ export const PersonnelPanel: React.FC = ({ ) : null} - - {displayedPersonnel.length === 0 ? ( + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={displayedPersonnel} + renderItem={renderPersonnelItem} + keyExtractor={keyExtractor} + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + ListEmptyComponent={ {isCallFilterActive ? t('dispatch.no_personnel_on_call') : t('dispatch.no_personnel')} - ) : ( - displayedPersonnel.map((person) => { - const fullName = `${person.FirstName} ${person.LastName}`.toLowerCase(); - return ( - handleSelectPersonnel(person.UserId)} - onSetStatus={isCallFilterActive && onSetPersonnelStatusForCall ? () => onSetPersonnelStatusForCall(person.UserId, `${person.FirstName} ${person.LastName}`) : undefined} - /> - ); - }) - )} - + } + /> ) : null}
@@ -328,9 +345,11 @@ const styles = StyleSheet.create({ }, content: { flex: 1, - padding: 8, maxHeight: 300, }, + contentContainer: { + padding: 8, + }, avatar: { width: 32, height: 32, diff --git a/src/components/dispatch-console/resources-panel.tsx b/src/components/dispatch-console/resources-panel.tsx index f5a7670..43ddc81 100644 --- a/src/components/dispatch-console/resources-panel.tsx +++ b/src/components/dispatch-console/resources-panel.tsx @@ -15,7 +15,7 @@ import { isPersonnelAvailable, isUnitAvailable } from '@/lib/resource-availabili import { type DispatchedEventResultData } from '@/models/v4/calls/dispatchedEventResultData'; import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData'; import { type UnitInfoResultData } from '@/models/v4/units/unitInfoResultData'; -import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { usePersonnelStore } from '@/stores/personnel/store'; import { useUnitsStore } from '@/stores/units/store'; @@ -77,7 +77,8 @@ export const ResourcesPanel: React.FC = ({ const personnelLoading = usePersonnelStore((s) => s.isLoading); const fetchPersonnel = usePersonnelStore((s) => s.fetchPersonnel); const availableOnly = useDashboardViewStore((s) => s.availableOnly); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('resources')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [searchQuery, setSearchQuery] = useState(''); // Unit names dispatched to the call — used for on-call highlighting (mirrors UnitsPanel). @@ -251,7 +252,7 @@ export const ResourcesPanel: React.FC = ({ iconColor="#6366f1" count={rows.length} isCollapsed={isCollapsed} - onToggleCollapse={() => setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('resources', !isCollapsed)} rightContent={ @@ -363,7 +364,6 @@ const styles = StyleSheet.create({ content: { flex: 1, padding: 8, - maxHeight: 300, }, icon: { width: 28, diff --git a/src/components/dispatch-console/stats-header.tsx b/src/components/dispatch-console/stats-header.tsx index fe28482..f7cfa0b 100644 --- a/src/components/dispatch-console/stats-header.tsx +++ b/src/components/dispatch-console/stats-header.tsx @@ -1,8 +1,8 @@ -import { AlertTriangle, CalendarClock, Clock, Phone, Truck, User, Users } from 'lucide-react-native'; +import { AlertTriangle, CalendarClock, Clock, CloudLightning, Phone, Truck, User, Users } from 'lucide-react-native'; import { useColorScheme } from 'nativewind'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, View } from 'react-native'; +import { Pressable, StyleSheet, View } from 'react-native'; import { Box } from '@/components/ui/box'; import { HStack } from '@/components/ui/hstack'; @@ -21,26 +21,29 @@ interface StatItemProps { bgClassName?: string; bgColor?: string; darkBgColor?: string; + onPress?: () => void; } -const StatItem: React.FC = ({ icon, label, value, color, darkColor, bgClassName, bgColor, darkBgColor }) => { +const StatItem: React.FC = ({ icon, label, value, color, darkColor, bgClassName, bgColor, darkBgColor, onPress }) => { const { colorScheme } = useColorScheme(); const isDark = colorScheme === 'dark'; const displayColor = isDark ? darkColor : color; const backgroundColor = bgColor || darkBgColor ? (isDark ? darkBgColor : bgColor) : undefined; return ( - - - - - - - {value} - - {label} - - + + + + + + + + {value} + + {label} + + + ); }; @@ -51,15 +54,38 @@ interface StatsHeaderProps { unitsAvailable: number; personnelAvailable: number; personnelOnDuty: number; - currentTime: string; weatherLatitude?: number | null; weatherLongitude?: number | null; + extremeAlerts?: number; + severeAlerts?: number; + onWeatherAlertsPress?: () => void; } -export const StatsHeader: React.FC = ({ activeCalls, pendingCalls, scheduledCalls, unitsAvailable, personnelAvailable, personnelOnDuty, currentTime, weatherLatitude, weatherLongitude }) => { +export const StatsHeader: React.FC = ({ + activeCalls, + pendingCalls, + scheduledCalls, + unitsAvailable, + personnelAvailable, + personnelOnDuty, + weatherLatitude, + weatherLongitude, + extremeAlerts = 0, + severeAlerts = 0, + onWeatherAlertsPress, +}) => { const { t } = useTranslation(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === 'dark'; + const [currentTime, setCurrentTime] = React.useState(new Date().toLocaleTimeString('en-US', { hour12: false })); + + React.useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date().toLocaleTimeString('en-US', { hour12: false })); + }, 1000); + + return () => clearInterval(timer); + }, []); return ( @@ -82,6 +108,20 @@ export const StatsHeader: React.FC = ({ activeCalls, pendingCa {/* Personnel On Duty */} + {/* Weather Alerts (Extreme / Severe) - only shown when such alerts are active */} + {extremeAlerts + severeAlerts > 0 ? ( + + ) : null} + {/* Current Time & Weather */} diff --git a/src/components/dispatch-console/unit-actions-panel.tsx b/src/components/dispatch-console/unit-actions-panel.tsx index a8c1899..101c355 100644 --- a/src/components/dispatch-console/unit-actions-panel.tsx +++ b/src/components/dispatch-console/unit-actions-panel.tsx @@ -513,21 +513,21 @@ export const UnitActionsPanel: React.FC = ({ unit: unitPr <> {destinationConfig.showCalls ? ( - setDestinationTab('calls')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'calls' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('calls')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'calls' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('dispatch.calls')} ({activeCalls.length}) ) : null} {destinationConfig.showStations ? ( - setDestinationTab('stations')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'stations' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('stations')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'stations' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('dispatch.stations')} ({availableStations.length}) ) : null} {destinationConfig.showPois ? ( - setDestinationTab('pois')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'pois' ? 'bg-white shadow-sm dark:bg-gray-700' : ''}`}> + setDestinationTab('pois')} className={`flex-1 rounded-md px-3 py-2 ${destinationTab === 'pois' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}> {t('menu.pois')} ({availablePois.length}) diff --git a/src/components/dispatch-console/units-panel.tsx b/src/components/dispatch-console/units-panel.tsx index ad1ddd0..84f1b3b 100644 --- a/src/components/dispatch-console/units-panel.tsx +++ b/src/components/dispatch-console/units-panel.tsx @@ -1,8 +1,8 @@ import { type Href, router } from 'expo-router'; import { Circle, ExternalLink, Filter, MapPin, Plus, Search, Truck, X } from 'lucide-react-native'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native'; +import { FlatList, Platform, Pressable, StyleSheet, TextInput, View } from 'react-native'; import { Badge } from '@/components/ui/badge'; import { Box } from '@/components/ui/box'; @@ -15,7 +15,7 @@ import { isUnitAvailable } from '@/lib/resource-availability'; import { type DispatchedEventResultData } from '@/models/v4/calls/dispatchedEventResultData'; import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData'; import { type UnitInfoResultData } from '@/models/v4/units/unitInfoResultData'; -import { useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; +import { selectCardCollapsed, useDashboardViewStore } from '@/stores/dispatch/dashboard-view-store'; import { AnimatedRefreshIcon } from './animated-refresh-icon'; import { PanelHeader } from './panel-header'; @@ -58,15 +58,15 @@ const UnitItem: React.FC<{ unit: UnitInfoResultData; isSelected: boolean; isOnCall?: boolean; - onPress: () => void; - onSetStatus?: () => void; -}> = ({ unit, isSelected, isOnCall, onPress, onSetStatus }) => { + onSelect?: (unitId: string) => void; + onSetStatusForUnit?: (unitId: string, unitName: string) => void; +}> = React.memo(({ unit, isSelected, isOnCall, onSelect, onSetStatusForUnit }) => { const { t } = useTranslation(); const statusColor = getStatusColor(unit.CurrentStatusColor, unit.CurrentStatusId); const hasDestination = unit.CurrentDestinationName && unit.CurrentDestinationName.trim() !== ''; return ( - + onSelect?.(unit.UnitId)}> @@ -120,11 +120,11 @@ const UnitItem: React.FC<{ GPS ) : null} - {onSetStatus ? ( + {onSetStatusForUnit ? ( { e.stopPropagation(); - onSetStatus(); + onSetStatusForUnit(unit.UnitId, unit.Name); }} style={styles.statusButton} > @@ -136,7 +136,9 @@ const UnitItem: React.FC<{ ); -}; +}); + +UnitItem.displayName = 'UnitItem'; export const UnitsPanel: React.FC = ({ units, @@ -153,7 +155,8 @@ export const UnitsPanel: React.FC = ({ onSetPersonnelStatusForCall, }) => { const { t } = useTranslation(); - const [isCollapsed, setIsCollapsed] = useState(false); + const isCollapsed = useDashboardViewStore(selectCardCollapsed('units')); + const setCardCollapsed = useDashboardViewStore((s) => s.setCardCollapsed); const [searchQuery, setSearchQuery] = useState(''); const availableOnly = useDashboardViewStore((s) => s.availableOnly); const singleList = useDashboardViewStore((s) => s.singleList); @@ -200,6 +203,28 @@ export const UnitsPanel: React.FC = ({ // Count available units const availableUnits = displayedUnits.filter((u) => !u.CurrentStatusId || u.CurrentStatusId === 'available').length; + const handleSelectUnit = useCallback( + (unitId: string) => { + onSelectUnit?.(unitId); + }, + [onSelectUnit] + ); + + const renderUnitItem = useCallback( + ({ item }: { item: UnitInfoResultData }) => ( + + ), + [selectedUnitId, dispatchedUnitNames, selectedCallId, handleSelectUnit, isCallFilterActive, onSetUnitStatusForCall] + ); + + const keyExtractor = useCallback((item: UnitInfoResultData) => item.UnitId, []); + // When "single list" is on, units + personnel are shown together in the combined ResourcesPanel // (rendered from the units slot); the Personnel panel hides itself. if (singleList) { @@ -226,7 +251,7 @@ export const UnitsPanel: React.FC = ({ iconColor="#3b82f6" count={displayedUnits.length} isCollapsed={isCollapsed} - onToggleCollapse={() => setIsCollapsed(!isCollapsed)} + onToggleCollapse={() => setCardCollapsed('units', !isCollapsed)} rightContent={ @@ -269,25 +294,24 @@ export const UnitsPanel: React.FC = ({ ) : null} - - {displayedUnits.length === 0 ? ( + + style={styles.content} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + data={displayedUnits} + renderItem={renderUnitItem} + keyExtractor={keyExtractor} + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={7} + removeClippedSubviews={Platform.OS !== 'web'} + ListEmptyComponent={ {isCallFilterActive ? t('dispatch.no_units_on_call') : t('dispatch.no_units')} - ) : ( - displayedUnits.map((unit) => ( - onSelectUnit?.(unit.UnitId)} - onSetStatus={isCallFilterActive && onSetUnitStatusForCall ? () => onSetUnitStatusForCall(unit.UnitId, unit.Name) : undefined} - /> - )) - )} - + } + /> ) : null} @@ -305,9 +329,11 @@ const styles = StyleSheet.create({ }, content: { flex: 1, - padding: 8, maxHeight: 300, }, + contentContainer: { + padding: 8, + }, statusIndicator: { width: 28, height: 28, diff --git a/src/components/incident-command/command-map.tsx b/src/components/incident-command/command-map.tsx index f6e788c..6c33f06 100644 --- a/src/components/incident-command/command-map.tsx +++ b/src/components/incident-command/command-map.tsx @@ -217,4 +217,3 @@ const styles = StyleSheet.create({ flex: 1, }, }); - diff --git a/src/components/maps/__tests__/pin-actions.test.tsx b/src/components/maps/__tests__/pin-actions.test.tsx index 3b35cb9..af932f2 100644 --- a/src/components/maps/__tests__/pin-actions.test.tsx +++ b/src/components/maps/__tests__/pin-actions.test.tsx @@ -21,6 +21,7 @@ jest.mock('react-i18next', () => ({ // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); @@ -149,7 +150,7 @@ const mockCallPin = { Latitude: 40.7128, Longitude: -74.0060, ImagePath: 'call', - Type: 1, + Type: 0, InfoWindowContent: 'Medical emergency at Main St', Color: '#ff0000', zIndex: 1, @@ -452,7 +453,7 @@ describe('Pin Actions Integration Tests', () => { const callPinByType = { ...mockCallPin, ImagePath: 'other', - Type: 1, + Type: 0, }; render( diff --git a/src/components/maps/map-pins.tsx b/src/components/maps/map-pins.tsx index 5baf377..c0e04d3 100644 --- a/src/components/maps/map-pins.tsx +++ b/src/components/maps/map-pins.tsx @@ -19,18 +19,18 @@ interface MapPinsProps { */ const BOTTOM_CENTER_ANCHOR = { x: 0.5, y: 1.0 }; -const MapPins: React.FC = ({ pins, onPinPress }) => { +const MapPins = React.memo(({ pins, onPinPress }) => { return ( <> {pins .filter((pin) => hasValidMapCoordinates(pin)) .map((pin) => ( - onPinPress?.(pin)} /> + ))} ); -}; +}); export default MapPins; diff --git a/src/components/maps/pin-marker.tsx b/src/components/maps/pin-marker.tsx index ccc23d8..ad25868 100644 --- a/src/components/maps/pin-marker.tsx +++ b/src/components/maps/pin-marker.tsx @@ -1,6 +1,6 @@ import type Mapbox from '@rnmapbox/maps'; import { useColorScheme } from 'nativewind'; -import React from 'react'; +import React, { useCallback } from 'react'; import { Image, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Path } from 'react-native-svg'; @@ -15,7 +15,7 @@ interface PinMarkerProps { pin: MapMakerInfoData; size?: number; markerRef?: Mapbox.PointAnnotation | null; - onPress?: () => void; + onPress?: (pin: MapMakerInfoData) => void; } /** @@ -30,9 +30,13 @@ const POI_MARKER_HEIGHT = 48; const POI_ICON_TOP_OFFSET = 10; const POI_ICON_FONT_SIZE = 14; -const PinMarker: React.FC = ({ pin, size = 32, onPress }) => { +const PinMarker = React.memo(({ pin, size = 32, onPress }) => { const { colorScheme } = useColorScheme(); + const handlePress = useCallback(() => { + onPress?.(pin); + }, [onPress, pin]); + const isPoiMapPin = isPoiMarker({ type: pin.Type, poiTypeId: pin.PoiTypeId, @@ -85,7 +89,7 @@ const PinMarker: React.FC = ({ pin, size = 32, onPress }) => { if (isPoiMapPin) { return ( - + {/* SVG background shape */} @@ -107,14 +111,14 @@ const PinMarker: React.FC = ({ pin, size = 32, onPress }) => { // Non-POI legacy marker rendering return ( - + {pin.Title} ); -}; +}); const styles = StyleSheet.create({ container: { diff --git a/src/components/maps/unified-map-view.tsx b/src/components/maps/unified-map-view.tsx index a9b6105..197e2b7 100644 --- a/src/components/maps/unified-map-view.tsx +++ b/src/components/maps/unified-map-view.tsx @@ -1,7 +1,7 @@ import Mapbox, { type CircleLayerStyle, type FillLayerStyle, type LineLayerStyle } from '@rnmapbox/maps'; import { type Feature, type FeatureCollection, type GeoJsonProperties, type Geometry } from 'geojson'; import { useColorScheme } from 'nativewind'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { getMapDataAndMarkers } from '@/api/mapping/mapping'; @@ -58,10 +58,9 @@ export const UnifiedMapView: React.FC = ({ const signalRDebounceTimer = useRef | null>(null); const signalRAbortController = useRef(null); - const location = useLocationStore((state) => ({ - latitude: state.latitude, - longitude: state.longitude, - })); + const latitude = useLocationStore((state) => state.latitude); + const longitude = useLocationStore((state) => state.longitude); + const location = useMemo(() => ({ latitude, longitude }), [latitude, longitude]); // Use external pins if provided, otherwise use internal pins const mapPins = externalPins ?? internalPins; diff --git a/src/components/maps/unified-map-view.web.tsx b/src/components/maps/unified-map-view.web.tsx index e69f103..355ca98 100644 --- a/src/components/maps/unified-map-view.web.tsx +++ b/src/components/maps/unified-map-view.web.tsx @@ -55,17 +55,14 @@ export const UnifiedMapView: React.FC = ({ const { colorScheme } = useColorScheme(); const mapContainer = useRef(null); const map = useRef(null); - const markersRef = useRef([]); + const markersRef = useRef>(new Map()); + const markerMetaRef = useRef>(new Map()); + const onPinPressRef = useRef(onPinPress); const layerIdsRef = useRef([]); const sourceIdsRef = useRef([]); const [isMapReady, setIsMapReady] = useState(false); const [internalPins, setInternalPins] = useState([]); - const location = useLocationStore((state) => ({ - latitude: state.latitude, - longitude: state.longitude, - })); - // Use external pins if provided, otherwise use internal pins const mapPins = externalPins ?? internalPins; @@ -92,13 +89,14 @@ export const UnifiedMapView: React.FC = ({ mapboxgl.accessToken = Env.MAPBOX_PUBKEY; - const initialCenter: [number, number] = location.longitude && location.latitude ? [location.longitude, location.latitude] : [-98.5795, 39.8283]; + const { latitude, longitude } = useLocationStore.getState(); + const initialCenter: [number, number] = longitude && latitude ? [longitude, latitude] : [-98.5795, 39.8283]; map.current = new mapboxgl.Map({ container: mapContainer.current, style: getMapStyle(), center: initialCenter, - zoom: location.latitude && location.longitude ? 12 : 3, + zoom: latitude && longitude ? 12 : 3, interactive, }); @@ -108,7 +106,7 @@ export const UnifiedMapView: React.FC = ({ map.current.addControl( new mapboxgl.GeolocateControl({ positionOptions: { - enableHighAccuracy: true, + enableHighAccuracy: false, }, trackUserLocation: true, showUserHeading: true, @@ -121,13 +119,17 @@ export const UnifiedMapView: React.FC = ({ onMapReady?.(); }); + const markers = markersRef.current; + const markerMeta = markerMetaRef.current; + return () => { - markersRef.current.forEach((marker) => marker.remove()); - markersRef.current = []; + markers.forEach((marker) => marker.remove()); + markers.clear(); + markerMeta.clear(); map.current?.remove(); map.current = null; }; - }, [getMapStyle, location.latitude, location.longitude, interactive, showUserLocation, onMapReady]); + }, [getMapStyle, interactive, showUserLocation, onMapReady]); // Update map style when theme changes useEffect(() => { @@ -209,39 +211,78 @@ export const UnifiedMapView: React.FC = ({ }; }, [autoFetchPins]); + // Keep a ref to the latest onPinPress so marker click handlers always call the + // current callback without forcing a marker rebuild when its identity changes. + useEffect(() => { + onPinPressRef.current = onPinPress; + }, [onPinPress]); + // Update markers when mapPins change useEffect(() => { if (!map.current || !isMapReady) return; - // Remove existing markers - markersRef.current.forEach((marker) => marker.remove()); - markersRef.current = []; + const theme = colorScheme === 'dark' ? 'dark' : 'light'; + const seenPinIds = new Set(); + + const buildPopupHtml = (pin: MapMakerInfoData) => + `
+

${pin.Title}

+ ${getMapPinSummary(pin) ? `

${getMapPinSummary(pin)}

` : ''} +

+ ${pin.Latitude.toFixed(6)}, ${pin.Longitude.toFixed(6)} +

+
`; - // Add new markers mapPins.forEach((pin) => { if (!hasValidMapCoordinates(pin)) return; + seenPinIds.add(pin.Id); + + const signature = `${theme}:${pin.Title}:${pin.Type}:${pin.PoiTypeId}:${pin.LayerId}:${pin.ImagePath}:${pin.PoiImage}:${pin.Marker}:${pin.Color}:${pin.Address}:${pin.Note}:${pin.PoiTypeName}:${pin.InfoWindowContent}`; + const existing = markersRef.current.get(pin.Id); + + if (existing) { + const meta = markerMetaRef.current.get(pin.Id); + const moved = !meta || meta.latitude !== pin.Latitude || meta.longitude !== pin.Longitude; + + if (moved) { + existing.setLngLat([pin.Longitude, pin.Latitude]); + } + + if (meta && meta.signature === signature) { + if (moved) { + existing.getPopup()?.setHTML(buildPopupHtml(pin)); + markerMetaRef.current.set(pin.Id, { signature, latitude: pin.Latitude, longitude: pin.Longitude }); + } + return; + } + + existing.remove(); + markersRef.current.delete(pin.Id); + } // Create custom marker element using shared utility - const el = createMapMarkerElement(pin, colorScheme, () => { - onPinPress?.(pin); + const el = createMapMarkerElement(pin, theme, () => { + onPinPressRef.current?.(pin); }); // Create popup - const popup = new mapboxgl.Popup({ offset: 25 }).setHTML( - `
-

${pin.Title}

- ${getMapPinSummary(pin) ? `

${getMapPinSummary(pin)}

` : ''} -

- ${pin.Latitude.toFixed(6)}, ${pin.Longitude.toFixed(6)} -

-
` - ); + const popup = new mapboxgl.Popup({ offset: 25 }).setHTML(buildPopupHtml(pin)); const marker = new mapboxgl.Marker({ element: el }).setLngLat([pin.Longitude, pin.Latitude]).setPopup(popup).addTo(map.current!); - markersRef.current.push(marker); + markersRef.current.set(pin.Id, marker); + markerMetaRef.current.set(pin.Id, { signature, latitude: pin.Latitude, longitude: pin.Longitude }); + }); + + // Remove stale markers + markersRef.current.forEach((marker, pinId) => { + if (!seenPinIds.has(pinId)) { + marker.remove(); + markersRef.current.delete(pinId); + markerMetaRef.current.delete(pinId); + } }); - }, [mapPins, isMapReady, colorScheme, onPinPress]); + }, [mapPins, isMapReady, colorScheme]); // Update layers when visibility changes useEffect(() => { diff --git a/src/components/notes/__tests__/note-details-sheet.test.tsx b/src/components/notes/__tests__/note-details-sheet.test.tsx index ba21c30..f517aba 100644 --- a/src/components/notes/__tests__/note-details-sheet.test.tsx +++ b/src/components/notes/__tests__/note-details-sheet.test.tsx @@ -22,6 +22,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/notes/note-card.tsx b/src/components/notes/note-card.tsx index 5500877..149bc70 100644 --- a/src/components/notes/note-card.tsx +++ b/src/components/notes/note-card.tsx @@ -17,7 +17,7 @@ interface NoteCardProps { export const NoteCard: React.FC = ({ note, onPress }) => { return ( onPress(note.NoteId)}> - + {note.Title} diff --git a/src/components/notifications/NotificationDetail.tsx b/src/components/notifications/NotificationDetail.tsx index 254dff3..b80aa37 100644 --- a/src/components/notifications/NotificationDetail.tsx +++ b/src/components/notifications/NotificationDetail.tsx @@ -1,8 +1,7 @@ import { useNotifications } from '@novu/react-native'; import { ArrowLeft, Calendar, ExternalLink, Trash2 } from 'lucide-react-native'; -import { colorScheme } from 'nativewind'; import React, { useEffect } from 'react'; -import { Animated, Dimensions, Platform, Pressable, SafeAreaView, StatusBar, StyleSheet, Text, View } from 'react-native'; +import { Animated, Appearance, Dimensions, Platform, Pressable, SafeAreaView, StatusBar, StyleSheet, Text, View } from 'react-native'; // Define the interface directly in this file interface NotificationPayload { @@ -208,8 +207,8 @@ const styles = StyleSheet.create({ right: 0, width: SIDEBAR_WIDTH, height: '100%', - backgroundColor: colorScheme.get() === 'dark' ? '#171717' : '#fff', - shadowColor: colorScheme.get() === 'dark' ? '#262626' : '#e5e5e5', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#171717' : '#fff', + shadowColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#e5e5e5', shadowOffset: { width: -2, height: 0, @@ -229,14 +228,14 @@ const styles = StyleSheet.create({ padding: 16, paddingTop: Platform.OS === 'android' ? STATUS_BAR_HEIGHT + 16 : 16, borderBottomWidth: 1, - borderBottomColor: colorScheme.get() === 'dark' ? '#333333' : '#e5e5e5', + borderBottomColor: Appearance.getColorScheme() === 'dark' ? '#333333' : '#e5e5e5', }, headerTitle: { fontSize: 18, fontWeight: 'bold', flex: 1, textAlign: 'center', - color: colorScheme.get() === 'dark' ? '#f3f4f6' : '#111827', + color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', }, backButton: { padding: 8, @@ -259,12 +258,12 @@ const styles = StyleSheet.create({ }, dateText: { fontSize: 14, - color: colorScheme.get() === 'dark' ? '#9ca3af' : '#6b7280', + color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', marginLeft: 6, }, timeText: { fontSize: 14, - color: colorScheme.get() === 'dark' ? '#9ca3af' : '#6b7280', + color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', }, typeTag: { alignSelf: 'flex-start', @@ -279,28 +278,28 @@ const styles = StyleSheet.create({ textTransform: 'uppercase', }, typeTagDefault: { - backgroundColor: colorScheme.get() === 'dark' ? '#374151' : '#e5e7eb', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#374151' : '#e5e7eb', }, typeTagInfo: { - backgroundColor: colorScheme.get() === 'dark' ? '#1e40af' : '#dbeafe', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#1e40af' : '#dbeafe', }, typeTagSuccess: { - backgroundColor: colorScheme.get() === 'dark' ? '#065f46' : '#d1fae5', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#065f46' : '#d1fae5', }, typeTagWarning: { - backgroundColor: colorScheme.get() === 'dark' ? '#92400e' : '#fef3c7', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#92400e' : '#fef3c7', }, typeTagAlert: { - backgroundColor: colorScheme.get() === 'dark' ? '#991b1b' : '#fee2e2', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#991b1b' : '#fee2e2', }, title: { fontSize: 20, fontWeight: 'bold', marginBottom: 12, - color: colorScheme.get() === 'dark' ? '#f3f4f6' : '#111827', + color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', }, bodyContainer: { - backgroundColor: colorScheme.get() === 'dark' ? '#262626' : '#f9fafb', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#f9fafb', padding: 16, borderRadius: 8, marginBottom: 20, @@ -308,19 +307,19 @@ const styles = StyleSheet.create({ body: { fontSize: 16, lineHeight: 24, - color: colorScheme.get() === 'dark' ? '#e5e5e5' : '#374151', + color: Appearance.getColorScheme() === 'dark' ? '#e5e5e5' : '#374151', }, metadataDetailsContainer: { marginTop: 10, padding: 16, - backgroundColor: colorScheme.get() === 'dark' ? '#262626' : '#f9fafb', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#f9fafb', borderRadius: 8, }, metadataTitle: { fontSize: 16, fontWeight: '600', marginBottom: 10, - color: colorScheme.get() === 'dark' ? '#f3f4f6' : '#111827', + color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', }, metadataItem: { flexDirection: 'row', @@ -330,31 +329,31 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '500', marginRight: 8, - color: colorScheme.get() === 'dark' ? '#9ca3af' : '#6b7280', + color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', }, metadataValue: { fontSize: 14, flex: 1, - color: colorScheme.get() === 'dark' ? '#e5e5e5' : '#111827', + color: Appearance.getColorScheme() === 'dark' ? '#e5e5e5' : '#111827', }, buttonText: { fontSize: 16, fontWeight: '500', - color: colorScheme.get() === 'dark' ? '#3b82f6' : '#2563eb', + color: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#2563eb', }, referenceButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 24, - backgroundColor: colorScheme.get() === 'dark' ? '#1e3a8a' : '#dbeafe', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#1e3a8a' : '#dbeafe', padding: 12, borderRadius: 8, borderWidth: 1, - borderColor: colorScheme.get() === 'dark' ? '#3b82f6' : '#60a5fa', + borderColor: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#60a5fa', }, referenceButtonIcon: { marginRight: 8, - color: colorScheme.get() === 'dark' ? '#3b82f6' : '#2563eb', + color: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#2563eb', }, }); diff --git a/src/components/notifications/NotificationInbox.tsx b/src/components/notifications/NotificationInbox.tsx index 43daf40..8dfdb6d 100644 --- a/src/components/notifications/NotificationInbox.tsx +++ b/src/components/notifications/NotificationInbox.tsx @@ -1,8 +1,7 @@ import { useNotifications } from '@novu/react-native'; import { CheckCircle, ChevronRight, Circle, ExternalLink, MoreVertical, Trash2, X } from 'lucide-react-native'; -import { colorScheme } from 'nativewind'; import React, { useEffect, useRef, useState } from 'react'; -import { ActivityIndicator, Animated, Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, StatusBar, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, Animated, Appearance, Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, StatusBar, StyleSheet, View } from 'react-native'; import { deleteMessage } from '@/api/novu/inbox'; import { NotificationDetail } from '@/components/notifications/NotificationDetail'; @@ -355,8 +354,8 @@ const styles = StyleSheet.create({ right: 0, width: SIDEBAR_WIDTH, height: '100%', - backgroundColor: colorScheme.get() === 'dark' ? '#171717' : '#fff', - shadowColor: colorScheme.get() === 'dark' ? '#262626' : '#e5e5e5', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#171717' : '#fff', + shadowColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#e5e5e5', shadowOffset: { width: -2, height: 0, @@ -402,7 +401,7 @@ const styles = StyleSheet.create({ selectionCount: { fontSize: 16, fontWeight: '600', - color: colorScheme.get() === 'dark' ? '#ffffff' : '#000000', + color: Appearance.getColorScheme() === 'dark' ? '#ffffff' : '#000000', }, selectionActions: { flexDirection: 'row', @@ -413,14 +412,14 @@ const styles = StyleSheet.create({ alignItems: 'center', padding: 16, borderBottomWidth: 1, - borderBottomColor: colorScheme.get() === 'dark' ? '#333333' : '#eee', + borderBottomColor: Appearance.getColorScheme() === 'dark' ? '#333333' : '#eee', position: 'relative', }, unreadNotificationItem: { - backgroundColor: colorScheme.get() === 'dark' ? '#262626' : '#f0f7ff', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#f0f7ff', }, selectedNotificationItem: { - backgroundColor: colorScheme.get() === 'dark' ? '#1e3a8a' : '#dbeafe', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#1e3a8a' : '#dbeafe', }, unreadIndicator: { position: 'absolute', @@ -428,7 +427,7 @@ const styles = StyleSheet.create({ top: 0, width: 4, height: '100%', - backgroundColor: colorScheme.get() === 'dark' ? '#60a5fa' : '#3b82f6', + backgroundColor: Appearance.getColorScheme() === 'dark' ? '#60a5fa' : '#3b82f6', }, selectionIndicator: { marginRight: 12, @@ -440,15 +439,15 @@ const styles = StyleSheet.create({ notificationBody: { fontSize: 16, marginBottom: 4, - color: colorScheme.get() === 'dark' ? '#e5e5e5' : '#333333', + color: Appearance.getColorScheme() === 'dark' ? '#e5e5e5' : '#333333', }, unreadNotificationText: { fontWeight: '600', - color: colorScheme.get() === 'dark' ? '#ffffff' : '#000000', + color: Appearance.getColorScheme() === 'dark' ? '#ffffff' : '#000000', }, timestamp: { fontSize: 12, - color: colorScheme.get() === 'dark' ? '#a3a3a3' : '#666', + color: Appearance.getColorScheme() === 'dark' ? '#a3a3a3' : '#666', }, actionButtons: { flexDirection: 'row', diff --git a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx index 86f3c65..74eb30e 100644 --- a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx +++ b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx @@ -21,6 +21,7 @@ jest.mock('@/lib/utils', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/protocols/protocol-card.tsx b/src/components/protocols/protocol-card.tsx index e9eb4aa..0cd23aa 100644 --- a/src/components/protocols/protocol-card.tsx +++ b/src/components/protocols/protocol-card.tsx @@ -17,7 +17,7 @@ interface ProtocolCardProps { export const ProtocolCard: React.FC = ({ protocol, onPress }) => { return ( onPress(protocol.Id)} testID={`protocol-card-${protocol.Id}`}> - + {protocol.Name} diff --git a/src/components/roles/__tests__/roles-bottom-sheet.test.tsx b/src/components/roles/__tests__/roles-bottom-sheet.test.tsx index 25b6801..795c26b 100644 --- a/src/components/roles/__tests__/roles-bottom-sheet.test.tsx +++ b/src/components/roles/__tests__/roles-bottom-sheet.test.tsx @@ -47,6 +47,7 @@ jest.mock('react-i18next', () => ({ // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), cssInterop: jest.fn(), })); diff --git a/src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx b/src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx index fefa4b9..4035edc 100644 --- a/src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx +++ b/src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx @@ -17,6 +17,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light', }), diff --git a/src/components/settings/__tests__/login-info-bottom-sheet.test.tsx b/src/components/settings/__tests__/login-info-bottom-sheet.test.tsx index fefa4b9..4035edc 100644 --- a/src/components/settings/__tests__/login-info-bottom-sheet.test.tsx +++ b/src/components/settings/__tests__/login-info-bottom-sheet.test.tsx @@ -17,6 +17,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light', }), diff --git a/src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx b/src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx index 00c487c..514176d 100644 --- a/src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx +++ b/src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx @@ -9,6 +9,7 @@ jest.mock('react-i18next', () => ({ })); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: () => ({ colorScheme: 'light' }), })); diff --git a/src/components/status/__tests__/status-bottom-sheet.test.tsx b/src/components/status/__tests__/status-bottom-sheet.test.tsx index 20fc558..b348550 100644 --- a/src/components/status/__tests__/status-bottom-sheet.test.tsx +++ b/src/components/status/__tests__/status-bottom-sheet.test.tsx @@ -110,6 +110,7 @@ jest.mock('@expo/html-elements', () => { }); jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: jest.fn(() => ({ colorScheme: 'light' })), cssInterop: jest.fn((component: any) => component), })); diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx index 95b88f7..6f84eb5 100644 --- a/src/components/toast/toast.tsx +++ b/src/components/toast/toast.tsx @@ -1,26 +1,37 @@ +import { X } from 'lucide-react-native'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { Pressable } from 'react-native'; +import { HStack } from '@/components/ui/hstack'; import { VStack } from '@/components/ui/vstack'; import { type ToastType, useToastStore } from '../../stores/toast/store'; import { Toast, ToastDescription, ToastTitle } from '../ui/toast'; export const ToastMessage: React.FC<{ - //id: string; + id: string; type: ToastType; title?: string; message: string; -}> = ({ /*id,*/ type, title, message }) => { - //const { removeToast } = useToastStore(); + onPress?: () => void; +}> = ({ id, type, title, message, onPress }) => { + const removeToast = useToastStore((state) => state.removeToast); const { t } = useTranslation(); return ( - - - {title && {t(title)}} - {t(message)} - - + + + + + {title && {t(title)}} + {t(message)} + + removeToast(id)} hitSlop={8} testID={`toast-dismiss-${id}`}> + + + + + ); }; diff --git a/src/components/ui/__tests__/bottom-sheet.test.tsx b/src/components/ui/__tests__/bottom-sheet.test.tsx index 2e16eca..ef4588b 100644 --- a/src/components/ui/__tests__/bottom-sheet.test.tsx +++ b/src/components/ui/__tests__/bottom-sheet.test.tsx @@ -6,6 +6,7 @@ import { CustomBottomSheet } from '../bottom-sheet'; // Mock nativewind jest.mock('nativewind', () => ({ + styled: jest.fn((Component: any) => Component), useColorScheme: jest.fn(() => ({ colorScheme: 'light' })), cssInterop: jest.fn(), })); diff --git a/src/components/ui/accordion/index.tsx b/src/components/ui/accordion/index.tsx index 51f629b..ab6d0b2 100644 --- a/src/components/ui/accordion/index.tsx +++ b/src/components/ui/accordion/index.tsx @@ -1,14 +1,11 @@ 'use client'; import { H3 } from '@expo/html-elements'; -import { createAccordion } from '@gluestack-ui/accordion'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { cssInterop } from 'nativewind'; -import React, { useMemo } from 'react'; +import { createAccordion } from '@gluestack-ui/core/accordion/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; +import React from 'react'; import { Platform, Pressable, Text, type TextProps, View } from 'react-native'; -import { Svg } from 'react-native-svg'; const SCOPE = 'ACCORDION'; /** Styles */ @@ -78,75 +75,24 @@ const accordionTriggerStyle = tva({ base: 'w-full py-5 px-5 flex-row justify-between items-center web:outline-none focus:outline-none data-[disabled=true]:opacity-40 data-[disabled=true]:cursor-not-allowed data-[focus-visible=true]:bg-background-50', }); -type IPrimitiveIcon = { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; - -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon & React.ComponentPropsWithoutRef>( - ({ height, width, fill, color, size, stroke = 'currentColor', as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - const colorProps = stroke === 'currentColor' && color !== undefined ? color : stroke; - - if (AsComp) { - return ; - } - return ; - } -); - -const Root = Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContextAndStates(View, SCOPE); +const Root = withStyleContext(View, SCOPE); const Header = (Platform.OS === 'web' ? H3 : View) as React.ComponentType; +const StyledUIIcon = styled(UIIcon, { className: 'style' }); + /** Creator */ const UIAccordion = createAccordion({ Root: Root, Item: View, Header: Header, Trigger: Pressable, - Icon: PrimitiveIcon, + Icon: StyledUIIcon, TitleText: Text, ContentText: Text, Content: View, }); -cssInterop(UIAccordion, { className: 'style' }); -cssInterop(UIAccordion.Item, { className: 'style' }); -cssInterop(UIAccordion.Header, { className: 'style' }); -cssInterop(UIAccordion.Trigger, { className: 'style' }); -cssInterop(UIAccordion.Icon, { className: 'style' }); -cssInterop(UIAccordion.TitleText, { className: 'style' }); -cssInterop(UIAccordion.Content, { className: 'style' }); -cssInterop(UIAccordion.ContentText, { className: 'style' }); -// @ts-ignore -cssInterop(UIAccordion.Icon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, -}); - type IAccordionProps = React.ComponentPropsWithoutRef & VariantProps; type IAccordionItemProps = React.ComponentPropsWithoutRef & VariantProps; @@ -158,6 +104,8 @@ type IAccordionContentTextProps = React.ComponentPropsWithoutRef & React.ComponentPropsWithoutRef & { as?: React.ElementType; + height?: number; + width?: number; }; type IAccordionHeaderProps = React.ComponentPropsWithoutRef & VariantProps; @@ -169,7 +117,8 @@ type IAccordionTitleTextProps = React.ComponentPropsWithoutRef, IAccordionProps>(({ className, variant = 'filled', size = 'md', ...props }, ref) => { - return ; + const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]); + return ; }); const AccordionItem = React.forwardRef, IAccordionItemProps>(({ className, ...props }, ref) => { diff --git a/src/components/ui/actionsheet/index.tsx b/src/components/ui/actionsheet/index.tsx index 0c2a650..d5c92ec 100644 --- a/src/components/ui/actionsheet/index.tsx +++ b/src/components/ui/actionsheet/index.tsx @@ -1,55 +1,33 @@ 'use client'; import { H4 } from '@expo/html-elements'; -import { createActionsheet } from '@gluestack-ui/actionsheet'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStates } from '@gluestack-ui/nativewind-utils/withStates'; -import { AnimatePresence, createMotionAnimatedComponent, Motion } from '@legendapp/motion'; +import { createActionsheet } from '@gluestack-ui/core/actionsheet/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { AnimatePresence, createMotionAnimatedComponent, Motion, type MotionComponentProps } from '@legendapp/motion'; import { FlashList } from '@shopify/flash-list'; -import { cssInterop } from 'nativewind'; -import React, { type ComponentType, type RefAttributes, useMemo } from 'react'; -import { Platform, Pressable, type PressableProps, ScrollView, SectionList, Text, View, VirtualizedList } from 'react-native'; -import { Svg } from 'react-native-svg'; - -type IPrimitiveIcon = { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon & React.ComponentPropsWithoutRef>( - ({ height, width, fill, color, size, stroke = 'currentColor', as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - const colorProps = stroke === 'currentColor' && color !== undefined ? color : stroke; - - if (AsComp) { - return ; - } - return ; - } -); +import { styled } from 'nativewind'; +import React from 'react'; +import { Pressable, type PressableProps, ScrollView, SectionList, Text, View, type ViewStyle, VirtualizedList } from 'react-native'; -const ItemWrapper = React.forwardRef, PressableProps>(({ ...props }, ref) => { +const ItemWrapper = React.forwardRef, PressableProps>(function ItemWrapper({ ...props }, ref) { return ; }); -const AnimatedPressable = createMotionAnimatedComponent(Pressable); +type IMotionViewProps = React.ComponentProps & MotionComponentProps; + +const MotionView = Motion.View as React.ComponentType; + +type IAnimatedPressableProps = React.ComponentProps & MotionComponentProps; + +const AnimatedPressable = createMotionAnimatedComponent(Pressable) as React.ComponentType; + +const StyledUIIcon = styled(UIIcon, { className: 'style' }); export const UIActionsheet = createActionsheet({ Root: View, - Content: Motion.View, - Item: Platform.OS === 'web' ? ItemWrapper : (withStates(ItemWrapper) as ComponentType>), + Content: MotionView, + Item: ItemWrapper, ItemText: Text, DragIndicator: View, IndicatorWrapper: View, @@ -59,53 +37,10 @@ export const UIActionsheet = createActionsheet({ FlatList: FlashList, SectionList: SectionList, SectionHeaderText: H4, - Icon: PrimitiveIcon, + Icon: StyledUIIcon, AnimatePresence: AnimatePresence, }); -cssInterop(UIActionsheet, { className: 'style' }); -cssInterop(UIActionsheet.Content, { className: 'style' }); -cssInterop(ItemWrapper, { className: 'style' }); -cssInterop(UIActionsheet.ItemText, { className: 'style' }); -cssInterop(UIActionsheet.DragIndicator, { className: 'style' }); -cssInterop(UIActionsheet.DragIndicatorWrapper, { className: 'style' }); -cssInterop(UIActionsheet.Backdrop, { className: 'style' }); -cssInterop(UIActionsheet.ScrollView, { - className: 'style', - contentContainerClassName: 'contentContainerStyle', - indicatorClassName: 'indicatorStyle', -}); -cssInterop(UIActionsheet.VirtualizedList, { - className: 'style', - ListFooterComponentClassName: 'ListFooterComponentStyle', - ListHeaderComponentClassName: 'ListHeaderComponentStyle', - contentContainerClassName: 'contentContainerStyle', - indicatorClassName: 'indicatorStyle', -}); -cssInterop(UIActionsheet.FlatList, { - className: 'style', - ListFooterComponentClassName: 'ListFooterComponentStyle', - ListHeaderComponentClassName: 'ListHeaderComponentStyle', - columnWrapperClassName: 'columnWrapperStyle', - contentContainerClassName: 'contentContainerStyle', - indicatorClassName: 'indicatorStyle', -}); -cssInterop(UIActionsheet.SectionList, { className: 'style' }); -cssInterop(UIActionsheet.SectionHeaderText, { className: 'style' }); -cssInterop(UIActionsheet.Icon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, -}); - const actionsheetStyle = tva({ base: 'w-full h-full web:pointer-events-none' }); const actionsheetContentStyle = tva({ @@ -265,9 +200,11 @@ type IActionsheetIconProps = VariantProps & React.ComponentPropsWithoutRef & { className?: string; as?: React.ElementType; + height?: number; + width?: number; }; -const Actionsheet = React.forwardRef, IActionsheetProps>(({ className, ...props }, ref) => { +const Actionsheet = React.forwardRef, IActionsheetProps>(function Actionsheet({ className, ...props }, ref) { return ( , IAc ); }); -const ActionsheetContent = React.forwardRef, IActionsheetContentProps>(({ className, ...props }, ref) => { +const ActionsheetContent = React.forwardRef, IActionsheetContentProps>(function ActionsheetContent({ className, ...props }, ref) { return ( , IActionsheetItemProps>(({ className, ...props }, ref) => { +const ActionsheetItem = React.forwardRef, IActionsheetItemProps>(function ActionsheetItem({ className, ...props }, ref) { return ( , IActionsheetItemTextProps>(({ isTruncated, bold, underline, strikeThrough, size = 'sm', className, ...props }, ref) => { +const ActionsheetItemText = React.forwardRef, IActionsheetItemTextProps>(function ActionsheetItemText( + { isTruncated, bold, underline, strikeThrough, size = 'sm', className, ...props }, + ref +) { return ( , IActionsheetDragIndicatorProps>(({ className, ...props }, ref) => { +const ActionsheetDragIndicator = React.forwardRef, IActionsheetDragIndicatorProps>(function ActionsheetDragIndicator({ className, ...props }, ref) { return ( , IActionsheetDragIndicatorWrapperProps>(({ className, ...props }, ref) => { +const ActionsheetDragIndicatorWrapper = React.forwardRef, IActionsheetDragIndicatorWrapperProps>(function ActionsheetDragIndicatorWrapper( + { className, ...props }, + ref +) { return ( , IActionsheetBackdropProps>(({ className, ...props }, ref) => { +const ActionsheetBackdrop = React.forwardRef, IActionsheetBackdropProps>(function ActionsheetBackdrop({ className, ...props }, ref) { return ( , IActionsheetScrollViewProps>(({ className, ...props }, ref) => { +const ActionsheetScrollView = React.forwardRef, IActionsheetScrollViewProps>(function ActionsheetScrollView({ className, ...props }, ref) { return ( , IActionsheetVirtualizedListProps>(({ className, ...props }, ref) => { +const ActionsheetVirtualizedList = React.forwardRef, IActionsheetVirtualizedListProps>(function ActionsheetVirtualizedList({ className, ...props }, ref) { return ( , IActionsheetFlatListProps>(({ className, ...props }, ref) => { +const ActionsheetFlatList = React.forwardRef, IActionsheetFlatListProps>(function ActionsheetFlatList({ className, style, ...props }, ref) { + return ( + + + + ); +}); + +const ActionsheetSectionList = React.forwardRef, IActionsheetSectionListProps>(function ActionsheetSectionList({ className, ...props }, ref) { return ( - , IActionsheetSectionListProps>(({ className, ...props }, ref) => { +const ActionsheetSectionHeaderText = React.forwardRef, IActionsheetSectionHeaderTextProps>(function ActionsheetSectionHeaderText( + { className, isTruncated, bold, underline, strikeThrough, size, sub, italic, highlight, ...props }, + ref +) { return ( - , IActionsheetSectionHeaderTextProps>( - ({ className, isTruncated, bold, underline, strikeThrough, size, sub, italic, highlight, ...props }, ref) => { - return ( - - ); - } -); - -const ActionsheetIcon = React.forwardRef, IActionsheetIconProps>(({ className, size = 'sm', ...props }, ref) => { +const ActionsheetIcon = React.forwardRef, IActionsheetIconProps>(function ActionsheetIcon({ className, size = 'sm', ...props }, ref) { if (typeof size === 'number') { return ; } else if ((props.height !== undefined || props.width !== undefined) && size === undefined) { diff --git a/src/components/ui/alert-dialog/index.tsx b/src/components/ui/alert-dialog/index.tsx index ad7b0f7..08021c3 100644 --- a/src/components/ui/alert-dialog/index.tsx +++ b/src/components/ui/alert-dialog/index.tsx @@ -1,41 +1,28 @@ 'use client'; -import { createAlertDialog } from '@gluestack-ui/alert-dialog'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { AnimatePresence, createMotionAnimatedComponent, Motion } from '@legendapp/motion'; -import { cssInterop } from 'nativewind'; +import { createAlertDialog } from '@gluestack-ui/core/alert-dialog/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Platform, Pressable, ScrollView, View } from 'react-native'; - -const AnimatedPressable = createMotionAnimatedComponent(Pressable); +import { Pressable, ScrollView, View } from 'react-native'; +import Animated, { Easing, FadeIn, FadeOut, ZoomIn } from 'react-native-reanimated'; const SCOPE = 'ALERT_DIALOG'; +const RootComponent = withStyleContext(View, SCOPE); + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); +const AnimatedView = Animated.createAnimatedComponent(View); + const UIAccessibleAlertDialog = createAlertDialog({ - Root: Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContextAndStates(View, SCOPE), + Root: RootComponent, Body: ScrollView, - Content: Motion.View, + Content: AnimatedView, CloseButton: Pressable, Header: View, Footer: View, Backdrop: AnimatedPressable, - AnimatePresence: AnimatePresence, }); -cssInterop(UIAccessibleAlertDialog, { className: 'style' }); -cssInterop(UIAccessibleAlertDialog.Content, { className: 'style' }); -cssInterop(UIAccessibleAlertDialog.CloseButton, { className: 'style' }); -cssInterop(UIAccessibleAlertDialog.Header, { className: 'style' }); -cssInterop(UIAccessibleAlertDialog.Footer, { className: 'style' }); -cssInterop(UIAccessibleAlertDialog.Body, { - className: 'style', - contentContainerClassName: 'contentContainerStyle', - indicatorClassName: 'indicatorStyle', -}); -cssInterop(UIAccessibleAlertDialog.Backdrop, { className: 'style' }); - const alertDialogStyle = tva({ base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none', parentVariants: { @@ -77,7 +64,7 @@ const alertDialogFooterStyle = tva({ const alertDialogBodyStyle = tva({ base: '' }); const alertDialogBackdropStyle = tva({ - base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default', + base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark/50 web:cursor-default', }); type IAlertDialogProps = React.ComponentPropsWithoutRef & VariantProps; @@ -94,38 +81,23 @@ type IAlertDialogBodyProps = React.ComponentPropsWithoutRef & VariantProps & { className?: string }; -const AlertDialog = React.forwardRef, IAlertDialogProps>(({ className, size = 'md', ...props }, ref) => { - return ; +const AlertDialog = React.forwardRef, IAlertDialogProps>(function AlertDialog({ className, size = 'md', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); -const AlertDialogContent = React.forwardRef, IAlertDialogContentProps>(({ className, size, ...props }, ref) => { +const AlertDialogContent = React.forwardRef, IAlertDialogContentProps>(function AlertDialogContent({ className, size, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); return ( , IAlertDialogCloseButtonProps>(({ className, ...props }, ref) => { +const AlertDialogCloseButton = React.forwardRef, IAlertDialogCloseButtonProps>(function AlertDialogCloseButton({ className, ...props }, ref) { return ( , IAlertDialogHeaderProps>(({ className, ...props }, ref) => { +const AlertDialogHeader = React.forwardRef, IAlertDialogHeaderProps>(function AlertDialogHeader({ className, ...props }, ref) { return ( , IAlertDialogFooterProps>(({ className, ...props }, ref) => { +const AlertDialogFooter = React.forwardRef, IAlertDialogFooterProps>(function AlertDialogFooter({ className, ...props }, ref) { return ( , IAlertDialogBodyProps>(({ className, ...props }, ref) => { +const AlertDialogBody = React.forwardRef, IAlertDialogBodyProps>(function AlertDialogBody({ className, ...props }, ref) { return ( , IAlertDialogBackdropProps>(({ className, ...props }, ref) => { +const AlertDialogBackdrop = React.forwardRef, IAlertDialogBackdropProps>(function AlertDialogBackdrop({ className, ...props }, ref) { return ( & { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; - -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>(({ height, width, fill, color, size, stroke, as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - let colorProps = {}; - if (color) { - colorProps = { ...colorProps, color: color }; - } - if (stroke) { - colorProps = { ...colorProps, stroke: stroke }; - } - if (fill) { - colorProps = { ...colorProps, fill: fill }; - } - if (AsComp) { - return ; - } - return ; -}); - -const IconWrapper = React.forwardRef, IPrimitiveIcon>(({ ...props }, ref) => { - return ; -}); +const StyledUIIcon = styled(UIIcon, { className: 'style' }); export const UIAlert = createAlert({ Root: withStyleContext(View, SCOPE), Text: Text, - Icon: IconWrapper, -}); - -cssInterop(UIAlert, { className: 'style' }); -//@ts-ignore -cssInterop(UIAlert.Text, { className: 'style' }); -cssInterop(IconWrapper, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, + Icon: StyledUIIcon, }); type IAlertProps = Omit, 'context'> & VariantProps; const Alert = React.forwardRef, IAlertProps>(({ className, variant = 'solid', action = 'muted', ...props }, ref) => { - return ; + const contextValue = React.useMemo(() => ({ variant, action }), [variant, action]); + return ; }); type IAlertTextProps = React.ComponentPropsWithoutRef & VariantProps; @@ -197,7 +141,11 @@ const AlertText = React.forwardRef, IAlert ); }); -type IAlertIconProps = React.ComponentPropsWithoutRef & VariantProps; +type IAlertIconProps = React.ComponentPropsWithoutRef & + VariantProps & { + height?: number; + width?: number; + }; const AlertIcon = React.forwardRef, IAlertIconProps>(({ className, size = 'md', ...props }, ref) => { const { action: parentAction } = useStyleContext(SCOPE); diff --git a/src/components/ui/avatar/index.tsx b/src/components/ui/avatar/index.tsx index 56fbe1e..a747da6 100644 --- a/src/components/ui/avatar/index.tsx +++ b/src/components/ui/avatar/index.tsx @@ -1,11 +1,9 @@ 'use client'; -import { createAvatar } from '@gluestack-ui/avatar'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { cssInterop } from 'nativewind'; +import { createAvatar } from '@gluestack-ui/core/avatar/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { Image, Platform, Text, View } from 'react-native'; + const SCOPE = 'AVATAR'; const UIAvatar = createAvatar({ @@ -16,12 +14,6 @@ const UIAvatar = createAvatar({ FallbackText: Text, }); -cssInterop(UIAvatar, { className: 'style' }); -cssInterop(UIAvatar.Badge, { className: 'style' }); -cssInterop(UIAvatar.Group, { className: 'style' }); -cssInterop(UIAvatar.Image, { className: 'style' }); -cssInterop(UIAvatar.FallbackText, { className: 'style' }); - const avatarStyle = tva({ base: 'rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5', variants: { @@ -76,7 +68,8 @@ const avatarImageStyle = tva({ type IAvatarProps = Omit, 'context'> & VariantProps; export const Avatar = React.forwardRef, IAvatarProps>(({ className, size = 'md', ...props }, ref) => { - return ; + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); type IAvatarBadgeProps = React.ComponentPropsWithoutRef & VariantProps; diff --git a/src/components/ui/badge/index.tsx b/src/components/ui/badge/index.tsx index d6bf306..bc2a36a 100644 --- a/src/components/ui/badge/index.tsx +++ b/src/components/ui/badge/index.tsx @@ -1,11 +1,10 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { cssInterop } from 'nativewind'; +import { type PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React, { useMemo } from 'react'; import { Text, View } from 'react-native'; -import { Svg } from 'react-native-svg'; + const SCOPE = 'BADGE'; const badgeStyle = tva({ @@ -90,70 +89,15 @@ const badgeIconStyle = tva({ }, }); -type IPrimitiveIcon = React.ComponentPropsWithoutRef & { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; - -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>(({ height, width, fill, color, size, stroke, as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - let colorProps = {}; - if (color) { - colorProps = { ...colorProps, color: color }; - } - if (stroke) { - colorProps = { ...colorProps, stroke: stroke }; - } - if (fill) { - colorProps = { ...colorProps, fill: fill }; - } - if (AsComp) { - return ; - } - return ; -}); - const ContextView = withStyleContext(View, SCOPE); -cssInterop(ContextView, { className: 'style' }); -cssInterop(PrimitiveIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, -}); + +const StyledUIIcon = styled(UIIcon, { className: 'style' }); type IBadgeProps = React.ComponentPropsWithoutRef & VariantProps; const Badge = ({ children, action = 'info', variant = 'solid', size = 'md', className, ...props }: { className?: string } & IBadgeProps) => { + const contextValue = useMemo(() => ({ action, variant, size }), [action, variant, size]); return ( - + {children} ); @@ -187,12 +131,12 @@ const BadgeIcon = React.forwardRef, IBadg const { size: parentSize, action: parentAction } = useStyleContext(SCOPE); if (typeof size === 'number') { - return ; + return ; } else if ((props.height !== undefined || props.width !== undefined) && size === undefined) { - return ; + return ; } return ( - ; + bottomSheetRef: React.RefObject | null; handleClose: () => void; handleOpen: () => void; }>({ visible: false, - bottomSheetRef: { current: null }, + bottomSheetRef: null, handleClose: () => {}, handleOpen: () => {}, }); @@ -64,18 +65,17 @@ export const BottomSheet = ({ snapToIndex = 1, onOpen, onClose, ...props }: { sn onClose && onClose(); }, [onClose]); - return ( - - {props.children} - + const contextValue = useMemo( + () => ({ + visible, + bottomSheetRef: bottomSheetRef as React.RefObject, + handleClose, + handleOpen, + }), + [visible, handleClose, handleOpen] ); + + return {props.children}; }; export const BottomSheetPortal = ({ @@ -125,42 +125,48 @@ export const BottomSheetTrigger = ({ className, ...props }: PressableProps & { c }; type IBottomSheetBackdrop = React.ComponentProps; +const StyledGorhomBottomSheetBackdrop = styled(GorhomBottomSheetBackdrop, { + className: 'style', +}); + export const BottomSheetBackdrop = ({ disappearsOnIndex = -1, appearsOnIndex = 1, className, ...props }: Partial & { className?: string }) => { return ( - ); }; -cssInterop(GorhomBottomSheetBackdrop, { className: 'style' }); - type IBottomSheetDragIndicator = React.ComponentProps; +const StyledBottomSheetHandle = styled(BottomSheetHandle, { + className: 'style', +}); + export const BottomSheetDragIndicator = ({ children, className, ...props }: Partial & { className?: string }) => { return ( - {children} - + ); }; -cssInterop(BottomSheetHandle, { className: 'style' }); - type IBottomSheetContent = React.ComponentProps; +const StyledGorhomBottomSheetView = styled(GorhomBottomSheetView, { + className: 'style', +}); + export const BottomSheetContent = ({ ...props }: IBottomSheetContent) => { const { handleClose, visible } = useContext(BottomSheetContext); const keyDownHandlers = useMemo(() => { @@ -179,9 +185,8 @@ export const BottomSheetContent = ({ ...props }: IBottomSheetContent) => { if (Platform.OS === 'web') return ( - { {props.children} )} - + ); return ( - {props.children} - + ); }; -cssInterop(GorhomBottomSheetView, { className: 'style' }); - export const BottomSheetItem = ({ children, className, @@ -247,8 +249,3 @@ export const BottomSheetScrollView = GorhomBottomSheetScrollView; export const BottomSheetFlatList = GorhomBottomSheetFlatList; export const BottomSheetSectionList = GorhomBottomSheetSectionList; export const BottomSheetTextInput = GorhomBottomSheetInput; - -cssInterop(GorhomBottomSheetInput, { className: 'style' }); -cssInterop(GorhomBottomSheetScrollView, { className: 'style' }); -cssInterop(GorhomBottomSheetFlatList, { className: 'style' }); -cssInterop(GorhomBottomSheetSectionList, { className: 'style' }); diff --git a/src/components/ui/box/index.tsx b/src/components/ui/box/index.tsx index 8a3e8d4..9444e52 100644 --- a/src/components/ui/box/index.tsx +++ b/src/components/ui/box/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { View, type ViewProps } from 'react-native'; diff --git a/src/components/ui/box/index.web.tsx b/src/components/ui/box/index.web.tsx index aa0fa86..b5180fc 100644 --- a/src/components/ui/box/index.web.tsx +++ b/src/components/ui/box/index.web.tsx @@ -1,12 +1,14 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; +import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'; import { boxStyle } from './styles'; -type IBoxProps = React.ComponentPropsWithoutRef<'div'> & VariantProps & { className?: string }; +type IBoxProps = React.ComponentPropsWithoutRef<'div'> & VariantProps & { className?: string; style?: StyleProp }; -const Box = React.forwardRef(({ className, ...props }, ref) => { - return
; +const Box = React.forwardRef(({ className, style, ...props }, ref) => { + const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style; + return
; }); Box.displayName = 'Box'; diff --git a/src/components/ui/box/styles.tsx b/src/components/ui/box/styles.tsx index 8492d50..c85440d 100644 --- a/src/components/ui/box/styles.tsx +++ b/src/components/ui/box/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'flex flex-col relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none' : ''; diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx index 9927328..c76b1bc 100644 --- a/src/components/ui/button/index.tsx +++ b/src/components/ui/button/index.tsx @@ -1,10 +1,8 @@ 'use client'; -import { createButton } from '@gluestack-ui/button'; -import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { cssInterop } from 'nativewind'; +import { createButton } from '@gluestack-ui/core/button/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { ActivityIndicator, Pressable, Text, View } from 'react-native'; @@ -12,25 +10,16 @@ const SCOPE = 'BUTTON'; const Root = withStyleContext(Pressable, SCOPE); +const StyledUIIcon = styled(UIIcon, { + className: 'style', +}); + const UIButton = createButton({ Root: Root, Text, Group: View, Spinner: ActivityIndicator, - Icon: UIIcon, -}); - -cssInterop(PrimitiveIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, - }, + Icon: StyledUIIcon, }); const buttonStyle = tva({ @@ -246,7 +235,8 @@ const buttonGroupStyle = tva({ type IButtonProps = Omit, 'context'> & VariantProps & { className?: string }; const Button = React.forwardRef, IButtonProps>(function Button({ className, variant = 'solid', size = 'md', action = 'primary', ...props }, ref) { - return ; + const contextValue = React.useMemo(() => ({ variant, size, action }), [variant, size, action]); + return ; }); type IButtonTextProps = React.ComponentPropsWithoutRef & VariantProps & { className?: string }; diff --git a/src/components/ui/card/index.tsx b/src/components/ui/card/index.tsx index 46b0091..225ebc7 100644 --- a/src/components/ui/card/index.tsx +++ b/src/components/ui/card/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { View, type ViewProps } from 'react-native'; diff --git a/src/components/ui/card/index.web.tsx b/src/components/ui/card/index.web.tsx index 3ffa137..ce8e560 100644 --- a/src/components/ui/card/index.web.tsx +++ b/src/components/ui/card/index.web.tsx @@ -1,12 +1,14 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; +import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'; import { cardStyle } from './styles'; -type ICardProps = React.ComponentPropsWithoutRef<'div'> & VariantProps; +type ICardProps = React.ComponentPropsWithoutRef<'div'> & VariantProps & { style?: StyleProp }; -const Card = React.forwardRef(({ className, size = 'md', variant = 'elevated', ...props }, ref) => { - return
; +const Card = React.forwardRef(({ className, size = 'md', variant = 'elevated', style, ...props }, ref) => { + const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style; + return
; }); Card.displayName = 'Card'; diff --git a/src/components/ui/card/styles.tsx b/src/components/ui/card/styles.tsx index c8c4a40..2a13a00 100644 --- a/src/components/ui/card/styles.tsx +++ b/src/components/ui/card/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'flex flex-col relative z-0' : ''; export const cardStyle = tva({ diff --git a/src/components/ui/center/index.tsx b/src/components/ui/center/index.tsx index b45ed65..abd7b13 100644 --- a/src/components/ui/center/index.tsx +++ b/src/components/ui/center/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { View, type ViewProps } from 'react-native'; diff --git a/src/components/ui/center/index.web.tsx b/src/components/ui/center/index.web.tsx index 0a010ef..cb464b8 100644 --- a/src/components/ui/center/index.web.tsx +++ b/src/components/ui/center/index.web.tsx @@ -1,12 +1,14 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; +import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'; import { centerStyle } from './styles'; -type ICenterProps = React.ComponentPropsWithoutRef<'div'> & VariantProps; +type ICenterProps = React.ComponentPropsWithoutRef<'div'> & VariantProps & { style?: StyleProp }; -const Center = React.forwardRef(({ className, ...props }, ref) => { - return
; +const Center = React.forwardRef(({ className, style, ...props }, ref) => { + const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style; + return
; }); Center.displayName = 'Center'; diff --git a/src/components/ui/center/styles.tsx b/src/components/ui/center/styles.tsx index 9a6841b..d851322 100644 --- a/src/components/ui/center/styles.tsx +++ b/src/components/ui/center/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'flex flex-col relative z-0' : ''; diff --git a/src/components/ui/checkbox/index.tsx b/src/components/ui/checkbox/index.tsx index 5889890..2fab364 100644 --- a/src/components/ui/checkbox/index.tsx +++ b/src/components/ui/checkbox/index.tsx @@ -1,91 +1,40 @@ 'use client'; -import { createCheckbox } from '@gluestack-ui/checkbox'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStates } from '@gluestack-ui/nativewind-utils/withStates'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { cssInterop } from 'nativewind'; -import React, { type ComponentType, type RefAttributes, useMemo } from 'react'; -import type { TextProps, ViewProps } from 'react-native'; -import { Pressable, Text, View } from 'react-native'; -import { Platform } from 'react-native'; -import { Svg } from 'react-native-svg'; - -const IndicatorWrapper = React.forwardRef, ViewProps>(({ ...props }, ref) => { +import { createCheckbox } from '@gluestack-ui/core/checkbox/creator'; +import type { IPrimitiveIcon, Svg } from '@gluestack-ui/core/icon/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; +import React from 'react'; +import type { ColorValue, TextProps, ViewProps } from 'react-native'; +import { Platform, Pressable, Text, View } from 'react-native'; + +const IndicatorWrapper = React.forwardRef, ViewProps>(function IndicatorWrapper({ ...props }, ref) { return ; }); -const LabelWrapper = React.forwardRef, TextProps>(({ ...props }, ref) => { +const LabelWrapper = React.forwardRef, TextProps>(function LabelWrapper({ ...props }, ref) { return ; }); -type IPrimitiveIcon = React.ComponentPropsWithoutRef & { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; - -const IconWrapper = React.forwardRef, IPrimitiveIcon>(({ ...props }, ref) => { - return ; +const StyledUIIcon = styled(UIIcon, { + className: 'style', }); -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>(({ height, width, fill, color, size, stroke, as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - let colorProps = {}; - if (color) { - colorProps = { ...colorProps, color: color }; - } - if (stroke) { - colorProps = { ...colorProps, stroke: stroke }; - } - if (fill) { - colorProps = { ...colorProps, fill: fill }; - } - if (AsComp) { - return ; - } - return ; +type IIconWrapperRef = (IPrimitiveIcon & React.RefAttributes) | { fill?: ColorValue; stroke?: ColorValue }; + +const IconWrapper = React.forwardRef>(function IconWrapper({ ...props }, ref) { + return ; }); const SCOPE = 'CHECKBOX'; -const UICheckbox = createCheckbox({ - // @ts-ignore - Root: Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContextAndStates(Pressable, SCOPE), - Group: Platform.OS === 'web' ? View : (withStates(View) as ComponentType>), - Icon: Platform.OS === 'web' ? IconWrapper : (withStates(IconWrapper) as ComponentType>), - Label: Platform.OS === 'web' ? LabelWrapper : (withStates(LabelWrapper) as ComponentType>), - Indicator: Platform.OS === 'web' ? IndicatorWrapper : (withStates(IndicatorWrapper) as ComponentType>), -}); -cssInterop(UICheckbox, { className: 'style' }); -cssInterop(UICheckbox.Group, { className: 'style' }); -cssInterop(LabelWrapper, { className: 'style' }); -cssInterop(IndicatorWrapper, { className: 'style' }); -cssInterop(IconWrapper, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, +const UICheckbox = createCheckbox({ + // @ts-expect-error : internal implementation for r-19/react-native-web + Root: Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContext(Pressable, SCOPE), + Group: View, + Icon: IconWrapper, + Label: LabelWrapper, + Indicator: IndicatorWrapper, }); const checkboxStyle = tva({ @@ -137,7 +86,8 @@ const CheckboxGroup = UICheckbox.Group; type ICheckboxProps = React.ComponentPropsWithoutRef & VariantProps; -const Checkbox = React.forwardRef, ICheckboxProps>(({ className, size = 'md', ...props }, ref) => { +const Checkbox = React.forwardRef, ICheckboxProps>(function Checkbox({ className, size = 'md', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size }), [size]); return ( , ICheckbox size, })} {...props} - context={{ - size, - }} + context={contextValue} ref={ref} /> ); @@ -155,7 +103,7 @@ const Checkbox = React.forwardRef, ICheckbox type ICheckboxIndicatorProps = React.ComponentPropsWithoutRef & VariantProps; -const CheckboxIndicator = React.forwardRef, ICheckboxIndicatorProps>(({ className, ...props }, ref) => { +const CheckboxIndicator = React.forwardRef, ICheckboxIndicatorProps>(function CheckboxIndicator({ className, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); return ( @@ -173,7 +121,7 @@ const CheckboxIndicator = React.forwardRef & VariantProps; -const CheckboxLabel = React.forwardRef, ICheckboxLabelProps>(({ className, ...props }, ref) => { +const CheckboxLabel = React.forwardRef, ICheckboxLabelProps>(function CheckboxLabel({ className, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); return ( ); }); -type ICheckboxIconProps = React.ComponentPropsWithoutRef & VariantProps; +type ICheckboxIconProps = React.ComponentPropsWithoutRef & + VariantProps & { + height?: number; + width?: number; + }; -const CheckboxIcon = React.forwardRef, ICheckboxIconProps>(({ className, size, ...props }, ref) => { +const CheckboxIcon = React.forwardRef, ICheckboxIconProps>(function CheckboxIcon({ className, size, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); if (typeof size === 'number') { diff --git a/src/components/ui/divider/index.tsx b/src/components/ui/divider/index.tsx index 0f9a36c..a78fa4b 100644 --- a/src/components/ui/divider/index.tsx +++ b/src/components/ui/divider/index.tsx @@ -1,10 +1,8 @@ 'use client'; -import { createDivider } from '@gluestack-ui/divider'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { cssInterop } from 'nativewind'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { View } from 'react-native'; +import { Platform, View } from 'react-native'; const dividerStyle = tva({ base: 'bg-background-200', @@ -16,17 +14,15 @@ const dividerStyle = tva({ }, }); -const UIDivider = createDivider({ Root: View }); +type IUIDividerProps = React.ComponentPropsWithoutRef & VariantProps; -cssInterop(UIDivider, { className: 'style' }); - -type IUIDividerProps = React.ComponentPropsWithoutRef & VariantProps; - -const Divider = React.forwardRef, IUIDividerProps>(({ className, orientation = 'horizontal', ...props }, ref) => { +const Divider = React.forwardRef, IUIDividerProps>(({ className, orientation = 'horizontal', ...props }, ref) => { return ( - & Variant type IDrawerCloseButtonProps = React.ComponentProps & VariantProps & { className?: string }; -const Drawer = React.forwardRef, IDrawerProps>(({ className, size = 'sm', anchor = 'left', ...props }, ref) => { - return ; +const Drawer = React.forwardRef, IDrawerProps>(function Drawer({ className, size = 'sm', anchor = 'left', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size, anchor }), [size, anchor]); + return ; }); -const DrawerBackdrop = React.forwardRef, IDrawerBackdropProps>(({ className, ...props }, ref) => { +const DrawerBackdrop = React.forwardRef, IDrawerBackdropProps>(function DrawerBackdrop({ className, ...props }, ref) { return ( , IDrawerContentProps>(({ className, ...props }, ref) => { +const DrawerContent = React.forwardRef, IDrawerContentProps>(function DrawerContent({ className, ...props }, ref) { const { size: parentSize, anchor: parentAnchor } = useStyleContext(SCOPE); - const drawerHeight = screenHeight * (sizes[parentSize] || sizes.md); - const drawerWidth = screenWidth * (sizes[parentSize] || sizes.md); - - const isHorizontal = parentAnchor === 'left' || parentAnchor === 'right'; - - const initialObj = isHorizontal ? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth } : { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }; - - const animateObj = isHorizontal ? { x: 0 } : { y: 0 }; + // Calculate positioning classes + const customClass = parentAnchor === 'left' || parentAnchor === 'right' ? `top-0 ${parentAnchor === 'left' ? 'left-0' : 'right-0'}` : `left-0 ${parentAnchor === 'top' ? 'top-0' : 'bottom-0'}`; - const exitObj = isHorizontal ? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth } : { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }; + // Select entering and exiting animations based on anchor + const enteringAnimation = + parentAnchor === 'left' + ? SlideInLeft.duration(200).easing(Easing.in(Easing.cubic)) + : parentAnchor === 'right' + ? SlideInRight.duration(200) + : parentAnchor === 'top' + ? SlideInUp.duration(200) + : SlideInDown.duration(200); - const customClass = isHorizontal ? `top-0 ${parentAnchor === 'left' ? 'left-0' : 'right-0'}` : `left-0 ${parentAnchor === 'top' ? 'top-0' : 'bottom-0'}`; + const exitingAnimation = parentAnchor === 'left' ? SlideOutLeft.duration(200) : parentAnchor === 'right' ? SlideOutRight.duration(200) : parentAnchor === 'top' ? SlideOutUp.duration(200) : SlideOutDown.duration(200); return ( ); }); -const DrawerHeader = React.forwardRef, IDrawerHeaderProps>(({ className, ...props }, ref) => { +const DrawerHeader = React.forwardRef, IDrawerHeaderProps>(function DrawerHeader({ className, ...props }, ref) { return ( , ); }); -const DrawerBody = React.forwardRef, IDrawerBodyProps>(({ className, ...props }, ref) => { +const DrawerBody = React.forwardRef, IDrawerBodyProps>(function DrawerBody({ className, ...props }, ref) { return ( , IDra ); }); -const DrawerFooter = React.forwardRef, IDrawerFooterProps>(({ className, ...props }, ref) => { +const DrawerFooter = React.forwardRef, IDrawerFooterProps>(function DrawerFooter({ className, ...props }, ref) { return ( , ); }); -const DrawerCloseButton = React.forwardRef, IDrawerCloseButtonProps>(({ className, ...props }, ref) => { +const DrawerCloseButton = React.forwardRef, IDrawerCloseButtonProps>(function DrawerCloseButton({ className, ...props }, ref) { return ( , 'context'> & VariantProps; const Fab = React.forwardRef, IFabProps>(({ size = 'md', placement = 'bottom right', className, ...props }, ref) => { - return ; + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); type IFabLabelProps = React.ComponentPropsWithoutRef & VariantProps; diff --git a/src/components/ui/focus-aware-status-bar.tsx b/src/components/ui/focus-aware-status-bar.tsx index b3012dd..cf50324 100644 --- a/src/components/ui/focus-aware-status-bar.tsx +++ b/src/components/ui/focus-aware-status-bar.tsx @@ -64,5 +64,5 @@ export const FocusAwareStatusBar = ({ hidden = false }: Props) => { if (Platform.OS === 'web') return null; // Only render SystemBars when focused and on supported platforms - return isFocused && (Platform.OS === 'android' || Platform.OS === 'ios') ? : null; + return isFocused && (Platform.OS === 'android' || Platform.OS === 'ios') ? : null; }; diff --git a/src/components/ui/form-control/index.tsx b/src/components/ui/form-control/index.tsx index c16b6d0..35d4909 100644 --- a/src/components/ui/form-control/index.tsx +++ b/src/components/ui/form-control/index.tsx @@ -1,51 +1,13 @@ 'use client'; -import { createFormControl } from '@gluestack-ui/form-control'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { cssInterop } from 'nativewind'; -import React, { useMemo } from 'react'; +import { createFormControl } from '@gluestack-ui/core/form-control/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; +import React from 'react'; import { Text, View } from 'react-native'; -import { Svg } from 'react-native-svg'; const SCOPE = 'FORM_CONTROL'; -type IPrimitiveIcon = React.ComponentPropsWithoutRef & { - height?: number | string; - width?: number | string; - fill?: string; - color?: string; - size?: number | string; - stroke?: string; - as?: React.ElementType; - className?: string; -}; - -const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>(({ height, width, fill, color, size, stroke, as: AsComp, ...props }, ref) => { - const sizeProps = useMemo(() => { - if (size) return { size }; - if (height && width) return { height, width }; - if (height) return { height }; - if (width) return { width }; - return {}; - }, [size, height, width]); - - let colorProps = {}; - if (color) { - colorProps = { ...colorProps, color: color }; - } - if (stroke) { - colorProps = { ...colorProps, stroke: stroke }; - } - if (fill) { - colorProps = { ...colorProps, fill: fill }; - } - if (AsComp) { - return ; - } - return ; -}); - const formControlStyle = tva({ base: 'flex flex-col', variants: { @@ -245,7 +207,7 @@ const formControlLabelAstrickStyle = tva({ type IFormControlLabelAstrickProps = React.ComponentPropsWithoutRef & VariantProps; -const FormControlLabelAstrick = React.forwardRef, IFormControlLabelAstrickProps>(({ className, ...props }, ref) => { +const FormControlLabelAstrick = React.forwardRef, IFormControlLabelAstrickProps>(({ className, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); return ( @@ -260,11 +222,13 @@ const FormControlLabelAstrick = React.forwardRef, ); }); +const StyledUIIcon = styled(UIIcon, { className: 'style' }); + export const UIFormControl = createFormControl({ Root: withStyleContext(View, SCOPE), Error: View, ErrorText: Text, - ErrorIcon: PrimitiveIcon, + ErrorIcon: StyledUIIcon, Label: View, LabelText: Text, LabelAstrick: FormControlLabelAstrick, @@ -272,42 +236,22 @@ export const UIFormControl = createFormControl({ HelperText: Text, }); -cssInterop(UIFormControl, { className: 'style' }); -cssInterop(UIFormControl.Error, { className: 'style' }); -cssInterop(UIFormControl.Error.Text, { className: 'style' }); -cssInterop(UIFormControl.Label, { className: 'style' }); -cssInterop(UIFormControl.Label.Text, { className: 'style' }); -cssInterop(UIFormControl.Helper, { className: 'style' }); -cssInterop(UIFormControl.Helper.Text, { className: 'style' }); -cssInterop(UIFormControl.Error.Icon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - // @ts-ignore - fill: true, - color: true, - stroke: true, - }, - }, -}); - type IFormControlProps = React.ComponentProps & VariantProps; -const FormControl = React.forwardRef, IFormControlProps>(({ className, size = 'md', ...props }, ref) => { - return ; +const FormControl = React.forwardRef, IFormControlProps>(({ className, size = 'md', ...props }, ref) => { + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); type IFormControlErrorProps = React.ComponentProps & VariantProps; -const FormControlError = React.forwardRef, IFormControlErrorProps>(({ className, ...props }, ref) => { +const FormControlError = React.forwardRef, IFormControlErrorProps>(({ className, ...props }, ref) => { return ; }); type IFormControlErrorTextProps = React.ComponentProps & VariantProps; -const FormControlErrorText = React.forwardRef, IFormControlErrorTextProps>(({ className, size, ...props }, ref) => { +const FormControlErrorText = React.forwardRef, IFormControlErrorTextProps>(({ className, size, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); return ( & VariantProps; -const FormControlErrorIcon = React.forwardRef, IFormControlErrorIconProps>(({ className, size, ...props }, ref) => { +type IFormControlErrorIconProps = React.ComponentProps & + VariantProps & { + height?: number; + width?: number; + }; +const FormControlErrorIcon = React.forwardRef, IFormControlErrorIconProps>(({ className, size, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); if (typeof size === 'number') { @@ -345,13 +293,13 @@ const FormControlErrorIcon = React.forwardRef & VariantProps; -const FormControlLabel = React.forwardRef, IFormControlLabelProps>(({ className, ...props }, ref) => { +const FormControlLabel = React.forwardRef, IFormControlLabelProps>(({ className, ...props }, ref) => { return ; }); type IFormControlLabelTextProps = React.ComponentProps & VariantProps; -const FormControlLabelText = React.forwardRef, IFormControlLabelTextProps>(({ className, size, ...props }, ref) => { +const FormControlLabelText = React.forwardRef, IFormControlLabelTextProps>(({ className, size, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); return ( @@ -369,7 +317,7 @@ const FormControlLabelText = React.forwardRef & VariantProps; -const FormControlHelper = React.forwardRef, IFormControlHelperProps>(({ className, ...props }, ref) => { +const FormControlHelper = React.forwardRef, IFormControlHelperProps>(({ className, ...props }, ref) => { return ( & VariantProps; -const FormControlHelperText = React.forwardRef, IFormControlHelperTextProps>(({ className, size, ...props }, ref) => { +const FormControlHelperText = React.forwardRef, IFormControlHelperTextProps>(({ className, size, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); return ( diff --git a/src/components/ui/gluestack-ui-provider/config.ts b/src/components/ui/gluestack-ui-provider/config.ts deleted file mode 100644 index 5cc0ce3..0000000 --- a/src/components/ui/gluestack-ui-provider/config.ts +++ /dev/null @@ -1,309 +0,0 @@ -'use client'; -import { vars } from 'nativewind'; - -export const config = { - light: vars({ - '--color-primary-0': '248 250 252', - '--color-primary-50': '241 245 249', - '--color-primary-100': '226 232 240', - '--color-primary-200': '203 213 225', - '--color-primary-300': '148 163 184', - '--color-primary-400': '100 116 139', - '--color-primary-500': '71 85 105', - '--color-primary-600': '51 65 85', - '--color-primary-700': '45 55 72', - '--color-primary-800': '39 49 66', - '--color-primary-900': '30 41 59', - '--color-primary-950': '15 23 42', - - /* Secondary */ - '--color-secondary-0': '254 255 255', - '--color-secondary-50': '241 242 242', - '--color-secondary-100': '231 232 232', - '--color-secondary-200': '219 219 219', - '--color-secondary-300': '175 176 176', - '--color-secondary-400': '114 115 115', - '--color-secondary-500': '94 95 95', - '--color-secondary-600': '81 82 82', - '--color-secondary-700': '63 64 64', - '--color-secondary-800': '39 38 38', - '--color-secondary-900': '24 23 23', - '--color-secondary-950': '11 12 12', - - /* Tertiary */ - '--color-tertiary-0': '255 250 245', - '--color-tertiary-50': '255 242 229', - '--color-tertiary-100': '255 233 213', - '--color-tertiary-200': '254 209 170', - '--color-tertiary-300': '253 180 116', - '--color-tertiary-400': '251 157 75', - '--color-tertiary-500': '231 129 40', - '--color-tertiary-600': '215 117 31', - '--color-tertiary-700': '180 98 26', - '--color-tertiary-800': '130 73 23', - '--color-tertiary-900': '108 61 19', - '--color-tertiary-950': '84 49 18', - - /* Error */ - '--color-error-0': '254 233 233', - '--color-error-50': '254 226 226', - '--color-error-100': '254 202 202', - '--color-error-200': '252 165 165', - '--color-error-300': '248 113 113', - '--color-error-400': '239 68 68', - '--color-error-500': '230 53 53', - '--color-error-600': '220 38 38', - '--color-error-700': '185 28 28', - '--color-error-800': '153 27 27', - '--color-error-900': '127 29 29', - '--color-error-950': '83 19 19', - - /* Success */ - '--color-success-0': '228 255 244', - '--color-success-50': '202 255 232', - '--color-success-100': '162 241 192', - '--color-success-200': '132 211 162', - '--color-success-300': '102 181 132', - '--color-success-400': '72 151 102', - '--color-success-500': '52 131 82', - '--color-success-600': '42 121 72', - '--color-success-700': '32 111 62', - '--color-success-800': '22 101 52', - '--color-success-900': '20 83 45', - '--color-success-950': '27 50 36', - - /* Warning */ - '--color-warning-0': '255 253 251', - '--color-warning-50': '255 249 245', - '--color-warning-100': '255 231 213', - '--color-warning-200': '254 205 170', - '--color-warning-300': '253 173 116', - '--color-warning-400': '251 149 75', - '--color-warning-500': '231 120 40', - '--color-warning-600': '215 108 31', - '--color-warning-700': '180 90 26', - '--color-warning-800': '130 68 23', - '--color-warning-900': '108 56 19', - '--color-warning-950': '84 45 18', - - /* Info */ - '--color-info-0': '236 248 254', - '--color-info-50': '199 235 252', - '--color-info-100': '162 221 250', - '--color-info-200': '124 207 248', - '--color-info-300': '87 194 246', - '--color-info-400': '50 180 244', - '--color-info-500': '13 166 242', - '--color-info-600': '11 141 205', - '--color-info-700': '9 115 168', - '--color-info-800': '7 90 131', - '--color-info-900': '5 64 93', - '--color-info-950': '3 38 56', - - /* Typography */ - '--color-typography-0': '254 254 255', - '--color-typography-50': '245 245 245', - '--color-typography-100': '229 229 229', - '--color-typography-200': '219 219 220', - '--color-typography-300': '212 212 212', - '--color-typography-400': '163 163 163', - '--color-typography-500': '140 140 140', - '--color-typography-600': '115 115 115', - '--color-typography-700': '82 82 82', - '--color-typography-800': '64 64 64', - '--color-typography-900': '38 38 39', - '--color-typography-950': '23 23 23', - - /* Outline */ - '--color-outline-0': '253 254 254', - '--color-outline-50': '243 243 243', - '--color-outline-100': '230 230 230', - '--color-outline-200': '221 220 219', - '--color-outline-300': '211 211 211', - '--color-outline-400': '165 163 163', - '--color-outline-500': '140 141 141', - '--color-outline-600': '115 116 116', - '--color-outline-700': '83 82 82', - '--color-outline-800': '65 65 65', - '--color-outline-900': '39 38 36', - '--color-outline-950': '26 23 23', - - /* Background */ - '--color-background-0': '255 255 255', - '--color-background-50': '246 246 246', - '--color-background-100': '242 241 241', - '--color-background-200': '220 219 219', - '--color-background-300': '213 212 212', - '--color-background-400': '162 163 163', - '--color-background-500': '142 142 142', - '--color-background-600': '116 116 116', - '--color-background-700': '83 82 82', - '--color-background-800': '65 64 64', - '--color-background-900': '39 38 37', - '--color-background-950': '24 23 24', - - /* Background Special */ - '--color-background-error': '254 241 241', - '--color-background-warning': '255 244 235', - '--color-background-success': '237 252 242', - '--color-background-muted': '247 248 247', - '--color-background-info': '235 248 254', - - /* Focus Ring Indicator */ - '--color-indicator-primary': '55 55 55', - '--color-indicator-info': '83 153 236', - '--color-indicator-error': '185 28 28', - }), - dark: vars({ - '--color-primary-0': '15 23 42', - '--color-primary-50': '30 41 59', - '--color-primary-100': '39 49 66', - '--color-primary-200': '45 55 72', - '--color-primary-300': '51 65 85', - '--color-primary-400': '71 85 105', - '--color-primary-500': '100 116 139', - '--color-primary-600': '71 85 105', - '--color-primary-700': '51 65 85', - '--color-primary-800': '45 55 72', - '--color-primary-900': '39 49 66', - '--color-primary-950': '30 41 59', - - /* Secondary */ - '--color-secondary-0': '11 12 12', - '--color-secondary-50': '24 23 23', - '--color-secondary-100': '39 38 38', - '--color-secondary-200': '63 64 64', - '--color-secondary-300': '81 82 82', - '--color-secondary-400': '94 95 95', - '--color-secondary-500': '114 115 115', - '--color-secondary-600': '175 176 176', - '--color-secondary-700': '219 219 219', - '--color-secondary-800': '231 232 232', - '--color-secondary-900': '241 242 242', - '--color-secondary-950': '254 255 255', - - /* Tertiary */ - '--color-tertiary-0': '84 49 18', - '--color-tertiary-50': '108 61 19', - '--color-tertiary-100': '130 73 23', - '--color-tertiary-200': '180 98 26', - '--color-tertiary-300': '215 117 31', - '--color-tertiary-400': '231 129 40', - '--color-tertiary-500': '251 157 75', - '--color-tertiary-600': '253 180 116', - '--color-tertiary-700': '254 209 170', - '--color-tertiary-800': '255 233 213', - '--color-tertiary-900': '255 242 229', - '--color-tertiary-950': '255 250 245', - - /* Error */ - '--color-error-0': '83 19 19', - '--color-error-50': '127 29 29', - '--color-error-100': '153 27 27', - '--color-error-200': '185 28 28', - '--color-error-300': '220 38 38', - '--color-error-400': '230 53 53', - '--color-error-500': '239 68 68', - '--color-error-600': '248 113 113', - '--color-error-700': '252 165 165', - '--color-error-800': '254 202 202', - '--color-error-900': '254 226 226', - '--color-error-950': '254 233 233', - - /* Success */ - '--color-success-0': '27 50 36', - '--color-success-50': '20 83 45', - '--color-success-100': '22 101 52', - '--color-success-200': '32 111 62', - '--color-success-300': '42 121 72', - '--color-success-400': '52 131 82', - '--color-success-500': '72 151 102', - '--color-success-600': '102 181 132', - '--color-success-700': '132 211 162', - '--color-success-800': '162 241 192', - '--color-success-900': '202 255 232', - '--color-success-950': '228 255 244', - - /* Warning */ - '--color-warning-0': '84 45 18', - '--color-warning-50': '108 56 19', - '--color-warning-100': '130 68 23', - '--color-warning-200': '180 90 26', - '--color-warning-300': '215 108 31', - '--color-warning-400': '231 120 40', - '--color-warning-500': '251 149 75', - '--color-warning-600': '253 173 116', - '--color-warning-700': '254 205 170', - '--color-warning-800': '255 231 213', - '--color-warning-900': '255 249 245', - '--color-warning-950': '255 253 251', - - /* Info */ - '--color-info-0': '3 38 56', - '--color-info-50': '5 64 93', - '--color-info-100': '7 90 131', - '--color-info-200': '9 115 168', - '--color-info-300': '11 141 205', - '--color-info-400': '13 166 242', - '--color-info-500': '50 180 244', - '--color-info-600': '87 194 246', - '--color-info-700': '124 207 248', - '--color-info-800': '162 221 250', - '--color-info-900': '199 235 252', - '--color-info-950': '236 248 254', - - /* Typography */ - '--color-typography-0': '23 23 23', - '--color-typography-50': '38 38 39', - '--color-typography-100': '64 64 64', - '--color-typography-200': '82 82 82', - '--color-typography-300': '115 115 115', - '--color-typography-400': '140 140 140', - '--color-typography-500': '163 163 163', - '--color-typography-600': '212 212 212', - '--color-typography-700': '219 219 220', - '--color-typography-800': '229 229 229', - '--color-typography-900': '245 245 245', - '--color-typography-950': '254 254 255', - - /* Outline */ - '--color-outline-0': '26 23 23', - '--color-outline-50': '39 38 36', - '--color-outline-100': '65 65 65', - '--color-outline-200': '83 82 82', - '--color-outline-300': '115 116 116', - '--color-outline-400': '140 141 141', - '--color-outline-500': '165 163 163', - '--color-outline-600': '211 211 211', - '--color-outline-700': '221 220 219', - '--color-outline-800': '230 230 230', - '--color-outline-900': '243 243 243', - '--color-outline-950': '253 254 254', - - /* Background */ - '--color-background-0': '18 18 18', - '--color-background-50': '39 38 37', - '--color-background-100': '65 64 64', - '--color-background-200': '83 82 82', - '--color-background-300': '116 116 116', - '--color-background-400': '142 142 142', - '--color-background-500': '162 163 163', - '--color-background-600': '213 212 212', - '--color-background-700': '220 219 219', - '--color-background-800': '242 241 241', - '--color-background-900': '246 246 246', - '--color-background-950': '254 254 254', - - /* Background Special */ - '--color-background-error': '66 43 43', - '--color-background-warning': '65 47 35', - '--color-background-success': '28 43 33', - '--color-background-muted': '51 51 51', - '--color-background-info': '26 40 46', - - /* Focus Ring Indicator */ - '--color-indicator-primary': '247 247 247', - '--color-indicator-info': '161 199 245', - '--color-indicator-error': '232 70 69', - }), -}; diff --git a/src/components/ui/gluestack-ui-provider/index.tsx b/src/components/ui/gluestack-ui-provider/index.tsx index aa11fc6..6b283a4 100644 --- a/src/components/ui/gluestack-ui-provider/index.tsx +++ b/src/components/ui/gluestack-ui-provider/index.tsx @@ -1,29 +1,24 @@ -import { OverlayProvider } from '@gluestack-ui/overlay'; -import { ToastProvider } from '@gluestack-ui/toast'; -import { colorScheme as colorSchemeNW } from 'nativewind'; -import React from 'react'; -import { type ColorSchemeName, useColorScheme, View, type ViewProps } from 'react-native'; +'use client'; +import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; +import { ToastProvider } from '@gluestack-ui/core/toast/creator'; +import React, { useEffect } from 'react'; +import { Appearance, useColorScheme, View, type ViewProps } from 'react-native'; -import { config } from './config'; +export type ModeType = 'light' | 'dark' | 'system'; -type ModeType = 'light' | 'dark' | 'system'; +export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: ModeType; children?: React.ReactNode; style?: ViewProps['style'] }) { + // Tokens (--color-*) flip through the prefers-color-scheme media query, + // which react-native-css drives from Appearance. The className wrapper + // drives the class-based `dark:` variant (see @custom-variant in global.css). + const osScheme = useColorScheme(); + const resolvedScheme: 'light' | 'dark' = mode === 'system' ? (osScheme === 'dark' ? 'dark' : 'light') : mode; -const getColorSchemeName = (colorScheme: ColorSchemeName, mode: ModeType): 'light' | 'dark' => { - if (mode === 'system') { - return colorScheme ?? 'light'; - } - return mode; -}; - -export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: 'light' | 'dark' | 'system'; children?: React.ReactNode; style?: ViewProps['style'] }) { - const colorScheme = useColorScheme(); - - const colorSchemeName = getColorSchemeName(colorScheme, mode); - - colorSchemeNW.set(mode); + useEffect(() => { + Appearance.setColorScheme(mode === 'system' ? null : mode); + }, [mode]); return ( - + {props.children} diff --git a/src/components/ui/gluestack-ui-provider/index.web.tsx b/src/components/ui/gluestack-ui-provider/index.web.tsx index 5a37510..fe52a89 100644 --- a/src/components/ui/gluestack-ui-provider/index.web.tsx +++ b/src/components/ui/gluestack-ui-provider/index.web.tsx @@ -1,35 +1,15 @@ 'use client'; -import { setFlushStyles } from '@gluestack-ui/nativewind-utils/flush'; -import { OverlayProvider } from '@gluestack-ui/overlay'; -import { ToastProvider } from '@gluestack-ui/toast'; +import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; +import { ToastProvider } from '@gluestack-ui/core/toast/creator'; import React, { useEffect, useLayoutEffect } from 'react'; -import { config } from './config'; import { script } from './script'; -const variableStyleTagId = 'nativewind-style'; -const createStyle = (styleTagId: string) => { - const style = document.createElement('style'); - style.id = styleTagId; - style.appendChild(document.createTextNode('')); - return style; -}; +export type ModeType = 'light' | 'dark' | 'system'; export const useSafeLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; -export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: 'light' | 'dark' | 'system'; children?: React.ReactNode }) { - let cssVariablesWithMode = ``; - Object.keys(config).forEach((configKey) => { - cssVariablesWithMode += configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`; - const cssVariables = Object.keys(config[configKey as keyof typeof config]).reduce((acc: string, curr: string) => { - acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `; - return acc; - }, ''); - cssVariablesWithMode += `${cssVariables} \n}`; - }); - - setFlushStyles(cssVariablesWithMode); - +export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: ModeType; children?: React.ReactNode }) { const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => { script(e.matches ? 'dark' : 'light'); }, []); @@ -54,21 +34,6 @@ export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: 'ligh return () => media.removeListener(handleMediaQuery); }, [handleMediaQuery]); - useSafeLayoutEffect(() => { - if (typeof window !== 'undefined') { - const documentElement = document.documentElement; - if (documentElement) { - const head = documentElement.querySelector('head'); - let style = head?.querySelector(`[id='${variableStyleTagId}']`); - if (!style) { - style = createStyle(variableStyleTagId); - style.innerHTML = cssVariablesWithMode; - if (head) head.appendChild(style); - } - } - } - }, []); - return ( <> ); }); -cssInterop(Grid, { - className: { - target: 'style', - nativeStyleToProp: { - gap: 'gap', - rowGap: 'rowGap', - columnGap: 'columnGap', - flexDirection: 'flexDirection', - padding: 'padding', - paddingLeft: 'paddingLeft', - paddingRight: 'paddingRight', - paddingStart: 'paddingStart', - paddingEnd: 'paddingEnd', - borderWidth: 'borderWidth', - borderLeftWidth: 'borderLeftWidth', - borderRightWidth: 'borderRightWidth', - }, - }, -}); - type IGridItemProps = ViewProps & VariantProps & { index?: number; @@ -217,32 +141,24 @@ type IGridItemProps = ViewProps & }; const GridItem = forwardRef, IGridItemProps>(function GridItem({ className, _extra, ...props }, ref) { - const [flexBasisValue, setFlexBasisValue] = useState('auto'); - - const { calculatedWidth, numColumns, itemsPerRow, flexDirection, gap, columnGap } = useContext(GridContext); - + const { calculatedWidth, numColumns, flexDirection, gap } = useContext(GridContext); const gridItemClass = _extra?.className; const responsiveColSpan = (useBreakpointValue(generateResponsiveColSpans({ gridItemClassName: gridItemClass })) ?? 1) as number; - useEffect(() => { - if (!flexDirection?.includes('column') && calculatedWidth && numColumns > 0 && responsiveColSpan > 0) { - // find out in which row of itemsPerRow the current item's index is - const row = Object.keys(itemsPerRow).find((key) => { - return itemsPerRow[key].includes(props?.index); - }); - - const rowColsCount = itemsPerRow[row as string]?.length; - - const space = columnGap || gap || 0; - - const gutterOffset = space * (rowColsCount === 1 && responsiveColSpan < numColumns ? 2 : rowColsCount - 1); - - const flexBasisVal = Math.min((((calculatedWidth - gutterOffset) * responsiveColSpan) / numColumns / calculatedWidth) * 100, 100) + '%'; - - setFlexBasisValue(flexBasisVal); + const flexBasisValue = useMemo(() => { + if (!calculatedWidth || !numColumns || responsiveColSpan <= 0) { + return 'auto'; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [calculatedWidth, responsiveColSpan, numColumns, columnGap, gap, flexDirection]); + if (flexDirection?.includes('column')) { + return 'auto'; + } + // CSS grid-equivalent formula: + // colWidth = (W - gap*(numColumns-1)) / numColumns + // itemWidth = colSpan*colWidth + (colSpan-1)*gap + // Simplified: colSpan*(W + gap)/numColumns - gap + const itemWidth = (responsiveColSpan * (calculatedWidth + gap)) / numColumns - gap; + return Math.max(0, Math.floor(itemWidth)); + }, [calculatedWidth, responsiveColSpan, numColumns, gap, flexDirection]); return ( , IGridItemProps>(fun {...props} style={[ { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - flexBasis: flexBasisValue as any, + width: typeof flexBasisValue === 'number' ? flexBasisValue : undefined, + flexBasis: typeof flexBasisValue === 'string' ? flexBasisValue : undefined, + flexShrink: 0, + flexGrow: 0, }, props.style, ]} @@ -266,5 +184,4 @@ const GridItem = forwardRef, IGridItemProps>(fun Grid.displayName = 'Grid'; GridItem.displayName = 'GridItem'; - export { Grid, GridItem }; diff --git a/src/components/ui/grid/index.web.tsx b/src/components/ui/grid/index.web.tsx index ccfab7e..e9bfba6 100644 --- a/src/components/ui/grid/index.web.tsx +++ b/src/components/ui/grid/index.web.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { gridItemStyle, gridStyle } from './styles'; diff --git a/src/components/ui/grid/styles.tsx b/src/components/ui/grid/styles.tsx index a21f4ac..188942a 100644 --- a/src/components/ui/grid/styles.tsx +++ b/src/components/ui/grid/styles.tsx @@ -1,5 +1,5 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { isWeb } from '@gluestack-ui/utils/nativewind-utils'; const gridBaseStyle = isWeb ? 'grid grid-cols-12' : 'box-border flex-row flex-wrap justify-start'; const gridItemBaseStyle = isWeb ? 'w-auto col-span-1' : ''; diff --git a/src/components/ui/heading/index.tsx b/src/components/ui/heading/index.tsx index d381691..1eefc79 100644 --- a/src/components/ui/heading/index.tsx +++ b/src/components/ui/heading/index.tsx @@ -1,24 +1,24 @@ -import { H1, H2, H3, H4, H5, H6 } from '@expo/html-elements'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { cssInterop } from 'nativewind'; +import { H1 as H1Base, H2 as H2Base, H3 as H3Base, H4 as H4Base, H5 as H5Base, H6 as H6Base } from '@expo/html-elements'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React, { forwardRef, memo } from 'react'; import { headingStyle } from './styles'; type IHeadingProps = VariantProps & - React.ComponentPropsWithoutRef & { + React.ComponentPropsWithoutRef & { as?: React.ElementType; }; -cssInterop(H1, { className: 'style' }); -cssInterop(H2, { className: 'style' }); -cssInterop(H3, { className: 'style' }); -cssInterop(H4, { className: 'style' }); -cssInterop(H5, { className: 'style' }); -cssInterop(H6, { className: 'style' }); +const H1 = styled(H1Base, { className: 'style' }); +const H2 = styled(H2Base, { className: 'style' }); +const H3 = styled(H3Base, { className: 'style' }); +const H4 = styled(H4Base, { className: 'style' }); +const H5 = styled(H5Base, { className: 'style' }); +const H6 = styled(H6Base, { className: 'style' }); const MappedHeading = memo( - forwardRef, IHeadingProps>(function MappedHeading({ size, className, isTruncated, bold, underline, strikeThrough, sub, italic, highlight, ...props }, ref) { + forwardRef, IHeadingProps>(function MappedHeading({ size, className, isTruncated, bold, underline, strikeThrough, sub, italic, highlight, ...props }, ref) { switch (size) { case '5xl': case '4xl': @@ -161,7 +161,7 @@ const MappedHeading = memo( ); const Heading = memo( - forwardRef, IHeadingProps>(function Heading({ className, size = 'lg', as: AsComp, ...props }, ref) { + forwardRef, IHeadingProps>(function Heading({ className, size = 'lg', as: AsComp, ...props }, ref) { const { isTruncated, bold, underline, strikeThrough, sub, italic, highlight } = props; if (AsComp) { diff --git a/src/components/ui/heading/index.web.tsx b/src/components/ui/heading/index.web.tsx index 2163065..77ec2aa 100644 --- a/src/components/ui/heading/index.web.tsx +++ b/src/components/ui/heading/index.web.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React, { forwardRef, memo } from 'react'; import { headingStyle } from './styles'; diff --git a/src/components/ui/heading/styles.tsx b/src/components/ui/heading/styles.tsx index 58fee19..33b8f76 100644 --- a/src/components/ui/heading/styles.tsx +++ b/src/components/ui/heading/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'font-sans tracking-sm bg-transparent border-0 box-border display-inline list-none margin-0 padding-0 position-relative text-start no-underline whitespace-pre-wrap word-wrap-break-word' : ''; export const headingStyle = tva({ diff --git a/src/components/ui/hstack/index.tsx b/src/components/ui/hstack/index.tsx index 67c0f61..664dea6 100644 --- a/src/components/ui/hstack/index.tsx +++ b/src/components/ui/hstack/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import type { ViewProps } from 'react-native'; import { View } from 'react-native'; diff --git a/src/components/ui/hstack/index.web.tsx b/src/components/ui/hstack/index.web.tsx index 2f457f5..03778e4 100644 --- a/src/components/ui/hstack/index.web.tsx +++ b/src/components/ui/hstack/index.web.tsx @@ -1,12 +1,14 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; +import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'; import { hstackStyle } from './styles'; -type IHStackProps = React.ComponentPropsWithoutRef<'div'> & VariantProps; +type IHStackProps = React.ComponentPropsWithoutRef<'div'> & VariantProps & { style?: StyleProp }; -const HStack = React.forwardRef, IHStackProps>(({ className, space, reversed, ...props }, ref) => { - return
; +const HStack = React.forwardRef, IHStackProps>(({ className, space, reversed, style, ...props }, ref) => { + const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style; + return
; }); HStack.displayName = 'HStack'; diff --git a/src/components/ui/hstack/styles.tsx b/src/components/ui/hstack/styles.tsx index 8543515..0c3bee8 100644 --- a/src/components/ui/hstack/styles.tsx +++ b/src/components/ui/hstack/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'flex relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none' : ''; diff --git a/src/components/ui/icon/index.tsx b/src/components/ui/icon/index.tsx index 8cb28b5..e2bebbb 100644 --- a/src/components/ui/icon/index.tsx +++ b/src/components/ui/icon/index.tsx @@ -1,9 +1,7 @@ 'use client'; -import { createIcon } from '@gluestack-ui/icon'; -import { type IPrimitiveIcon, PrimitiveIcon, Svg } from '@gluestack-ui/icon'; -import { type VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { cssInterop } from 'nativewind'; +import { createIcon, type IPrimitiveIcon, PrimitiveIcon, Svg } from '@gluestack-ui/core/icon/creator'; +import { tva, type VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { Path } from 'react-native-svg'; @@ -25,28 +23,17 @@ const iconStyle = tva({ }, }); -cssInterop(UIIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, - }, -}); +const StyledUIIcon = styled(UIIcon, { className: 'style' }); type IIConProps = IPrimitiveIcon & VariantProps & React.ComponentPropsWithoutRef; const Icon = React.forwardRef, IIConProps>(function Icon({ size = 'md', className, ...props }, ref) { if (typeof size === 'number') { - return ; + return ; } else if ((props.height !== undefined || props.width !== undefined) && size === undefined) { - return ; + return ; } - return ; + return ; }); export { Icon }; diff --git a/src/components/ui/icon/index.web.tsx b/src/components/ui/icon/index.web.tsx index 2cb93c8..28d5394 100644 --- a/src/components/ui/icon/index.web.tsx +++ b/src/components/ui/icon/index.web.tsx @@ -1,8 +1,6 @@ 'use client'; -import { createIcon } from '@gluestack-ui/icon'; -import { PrimitiveIcon, Svg } from '@gluestack-ui/icon'; -import { type VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { createIcon, PrimitiveIcon, Svg } from '@gluestack-ui/core/icon/creator'; +import { tva, type VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; export const UIIcon = createIcon({ diff --git a/src/components/ui/image-background/index.tsx b/src/components/ui/image-background/index.tsx index 3d550c7..f553b7a 100644 --- a/src/components/ui/image-background/index.tsx +++ b/src/components/ui/image-background/index.tsx @@ -1,11 +1,11 @@ 'use client'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { ImageBackground as RNImageBackground } from 'react-native'; const imageBackgroundStyle = tva({}); -export const ImageBackground = React.forwardRef, React.ComponentProps>(({ className, ...props }, ref) => { +const ImageBackground = React.forwardRef, React.ComponentProps>(function ImageBackground({ className, ...props }, ref) { return ( ); }); + +ImageBackground.displayName = 'ImageBackground'; + +export { ImageBackground }; diff --git a/src/components/ui/image.tsx b/src/components/ui/image.tsx index c2f99f1..a8c7a64 100644 --- a/src/components/ui/image.tsx +++ b/src/components/ui/image.tsx @@ -1,16 +1,16 @@ import type { ImageProps } from 'expo-image'; import { Image as NImage } from 'expo-image'; -import { cssInterop } from 'nativewind'; +import { styled } from 'nativewind'; import * as React from 'react'; export type ImgProps = ImageProps & { className?: string; }; -cssInterop(NImage, { className: 'style' }); +const StyledNImage = styled(NImage, { className: 'style' }); export const Image = ({ style, className, placeholder = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4', ...props }: ImgProps) => { - return ; + return ; }; export const preloadImages = (sources: string[]) => { diff --git a/src/components/ui/image/index.tsx b/src/components/ui/image/index.tsx index 66fae4f..91a10c0 100644 --- a/src/components/ui/image/index.tsx +++ b/src/components/ui/image/index.tsx @@ -1,8 +1,7 @@ 'use client'; -import { createImage } from '@gluestack-ui/image'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { cssInterop } from 'nativewind'; +import { createImage } from '@gluestack-ui/core/image/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { Image as RNImage, Platform } from 'react-native'; @@ -23,7 +22,6 @@ const imageStyle = tva({ }); const UIImage = createImage({ Root: RNImage }); -cssInterop(UIImage, { className: 'style' }); type ImageProps = VariantProps & React.ComponentProps; const Image = React.forwardRef, ImageProps & { className?: string }>(({ size = 'md', className, ...props }, ref) => { diff --git a/src/components/ui/index.tsx b/src/components/ui/index.tsx index 4847d9e..5183a7b 100644 --- a/src/components/ui/index.tsx +++ b/src/components/ui/index.tsx @@ -1,4 +1,4 @@ -import { cssInterop } from 'nativewind'; +import { styled } from 'nativewind'; import Svg from 'react-native-svg'; //export * from './button'; @@ -18,8 +18,8 @@ export * from './focus-aware-status-bar'; export { ActivityIndicator, Pressable, ScrollView, TouchableOpacity, View } from 'react-native'; export { SafeAreaView } from 'react-native-safe-area-context'; -//Apply cssInterop to Svg to resolve className string into style -cssInterop(Svg, { +// Svg with className resolved into style (NativeWind v5 styled wrapper) +export const StyledSvg = styled(Svg, { className: { target: 'style', }, diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index 522df05..a9fa059 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -1,35 +1,22 @@ 'use client'; -import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon'; -import { createInput } from '@gluestack-ui/input'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { cssInterop } from 'nativewind'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { createInput } from '@gluestack-ui/core/input/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { Pressable, TextInput, View } from 'react-native'; const SCOPE = 'INPUT'; +const StyledUIIcon = styled(UIIcon, { className: 'style' }); + const UIInput = createInput({ Root: withStyleContext(View, SCOPE), - Icon: UIIcon, + Icon: StyledUIIcon, Slot: Pressable, Input: TextInput, }); -cssInterop(PrimitiveIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, - }, -}); - const inputStyle = tva({ base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center', @@ -100,7 +87,8 @@ const inputFieldStyle = tva({ type IInputProps = React.ComponentProps & VariantProps & { className?: string }; const Input = React.forwardRef, IInputProps>(function Input({ className, variant = 'outline', size = 'md', ...props }, ref) { - return ; + const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]); + return ; }); type IInputIconProps = React.ComponentProps & diff --git a/src/components/ui/link/index.tsx b/src/components/ui/link/index.tsx index 289a52d..7a8c96e 100644 --- a/src/components/ui/link/index.tsx +++ b/src/components/ui/link/index.tsx @@ -1,22 +1,15 @@ 'use client'; -import { createLink } from '@gluestack-ui/link'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStates } from '@gluestack-ui/nativewind-utils/withStates'; -import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { cssInterop } from 'nativewind'; +import { createLink } from '@gluestack-ui/core/link/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Platform, Pressable } from 'react-native'; +import { Pressable } from 'react-native'; import { Text } from 'react-native'; export const UILink = createLink({ - Root: Platform.OS === 'web' ? withStyleContext(Pressable) : withStyleContextAndStates(Pressable), - Text: Platform.OS === 'web' ? Text : withStates(Text), + Root: withStyleContext(Pressable), + Text: Text, }); -cssInterop(UILink, { className: 'style' }); -cssInterop(UILink.Text, { className: 'style' }); - const linkStyle = tva({ base: 'group/link web:outline-0 data-[disabled=true]:web:cursor-not-allowed data-[focus-visible=true]:web:ring-2 data-[focus-visible=true]:web:ring-indicator-primary data-[focus-visible=true]:web:outline-0 data-[disabled=true]:opacity-4 ', }); diff --git a/src/components/ui/menu/index.tsx b/src/components/ui/menu/index.tsx index a42c5c7..c3e806d 100644 --- a/src/components/ui/menu/index.tsx +++ b/src/components/ui/menu/index.tsx @@ -1,15 +1,12 @@ 'use client'; -import { createMenu } from '@gluestack-ui/menu'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { AnimatePresence, Motion, type MotionComponentProps } from '@legendapp/motion'; -import { cssInterop } from 'nativewind'; +import { createMenu } from '@gluestack-ui/core/menu/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Pressable, Text, View, type ViewStyle } from 'react-native'; +import { Pressable, ScrollView, Text, View } from 'react-native'; +import Animated, { FadeOut, ZoomIn } from 'react-native-reanimated'; -type IMotionViewProps = React.ComponentProps & MotionComponentProps; - -const MotionView = Motion.View as React.ComponentType; +const AnimatedView = Animated.createAnimatedComponent(ScrollView); const menuStyle = tva({ base: 'rounded-md bg-background-0 border border-outline-100 p-1 shadow-hard-5', @@ -104,40 +101,27 @@ const Item = React.forwardRef, IMenuItemPro const Separator = React.forwardRef, React.ComponentPropsWithoutRef & VariantProps>(function Separator({ className, ...props }, ref) { return ; }); + export const UIMenu = createMenu({ - Root: MotionView, + Root: AnimatedView, Item: Item, Label: Text, Backdrop: BackdropPressable, - AnimatePresence: AnimatePresence, Separator: Separator, }); -cssInterop(MotionView, { className: 'style' }); - type IMenuProps = React.ComponentProps & VariantProps & { className?: string }; type IMenuItemLabelProps = React.ComponentProps & VariantProps & { className?: string }; const Menu = React.forwardRef, IMenuProps>(function Menu({ className, ...props }, ref) { return ( & VariantPr type IModalCloseButtonProps = React.ComponentProps & VariantProps & { className?: string }; -const Modal = React.forwardRef, IModalProps>(({ className, size = 'md', ...props }, ref) => ( - -)); +const Modal = React.forwardRef, IModalProps>(function Modal({ className, size = 'md', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; +}); -const ModalBackdrop = React.forwardRef, IModalBackdropProps>(({ className, ...props }, ref) => { +const ModalBackdrop = React.forwardRef, IModalBackdropProps>(function ModalBackdrop({ className, ...props }, ref) { return ( ); }); -const ModalContent = React.forwardRef, IModalContentProps>(({ className, size, ...props }, ref) => { +const ModalContent = React.forwardRef, IModalContentProps>(function ModalContent({ className, size, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); return ( , ); }); -const ModalHeader = React.forwardRef, IModalHeaderProps>(({ className, ...props }, ref) => { +const ModalHeader = React.forwardRef, IModalHeaderProps>(function ModalHeader({ className, ...props }, ref) { return ( , IM ); }); -const ModalBody = React.forwardRef, IModalBodyProps>(({ className, ...props }, ref) => { +const ModalBody = React.forwardRef, IModalBodyProps>(function ModalBody({ className, ...props }, ref) { return ( , IModal ); }); -const ModalFooter = React.forwardRef, IModalFooterProps>(({ className, ...props }, ref) => { +const ModalFooter = React.forwardRef, IModalFooterProps>(function ModalFooter({ className, ...props }, ref) { return ( , IM ); }); -const ModalCloseButton = React.forwardRef, IModalCloseButtonProps>(({ className, ...props }, ref) => { +const ModalCloseButton = React.forwardRef, IModalCloseButtonProps>(function ModalCloseButton({ className, ...props }, ref) { return ( & MotionComponentProps; - -const AnimatedPressable = createMotionAnimatedComponent(Pressable) as React.ComponentType; +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); +const AnimatedView = Animated.createAnimatedComponent(View); const SCOPE = 'POPOVER'; -type IMotionViewProps = React.ComponentProps & MotionComponentProps; - -const MotionView = Motion.View as React.ComponentType; - const UIPopover = createPopover({ - Root: withStyleContext(View, SCOPE), - Arrow: MotionView, + Root: withStyleContext(AnimatedView, SCOPE), + Arrow: View, Backdrop: AnimatedPressable, Body: ScrollView, CloseButton: Pressable, - Content: MotionView, + Content: View, Footer: View, Header: View, - AnimatePresence: AnimatePresence, }); -cssInterop(MotionView, { className: 'style' }); -cssInterop(AnimatedPressable, { className: 'style' }); - const popoverStyle = tva({ base: 'group/popover w-full h-full justify-center items-center web:pointer-events-none', variants: { @@ -116,7 +105,21 @@ type IPopoverBackdropProps = React.ComponentProps & V type IPopoverCloseButtonProps = React.ComponentProps & VariantProps & { className?: string }; const Popover = React.forwardRef, IPopoverProps>(function Popover({ className, size = 'md', placement = 'bottom', ...props }, ref) { - return ; + const contextValue = React.useMemo(() => ({ size, placement }), [size, placement]); + return ( + + ); }); const PopoverContent = React.forwardRef, IPopoverContentProps>(function PopoverContent({ className, size, ...props }, ref) { @@ -125,17 +128,6 @@ const PopoverContent = React.forwardRef return ( , React.ComponentProps>(({ ...props }: React.ComponentProps, ref) => { - return ; +const Portal = React.forwardRef, React.ComponentProps>(function Portal({ ...props }, ref) { + return ; }); + +Portal.displayName = 'Portal'; + +export { Portal }; diff --git a/src/components/ui/pressable/index.tsx b/src/components/ui/pressable/index.tsx index 57adb68..fe259c8 100644 --- a/src/components/ui/pressable/index.tsx +++ b/src/components/ui/pressable/index.tsx @@ -1,25 +1,21 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { createPressable } from '@gluestack-ui/pressable'; -import { cssInterop } from 'nativewind'; +import { createPressable } from '@gluestack-ui/core/pressable/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Platform, Pressable as RNPressable } from 'react-native'; +import { Pressable as RNPressable } from 'react-native'; const UIPressable = createPressable({ - Root: Platform.OS === 'web' ? withStyleContext(RNPressable) : withStyleContextAndStates(RNPressable), + Root: withStyleContext(RNPressable), }); const pressableStyle = tva({ base: 'data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-indicator-info data-[focus-visible=true]:ring-2 data-[disabled=true]:opacity-40', }); -cssInterop(UIPressable, { className: 'style' }); - type IPressableProps = Omit, 'context'> & VariantProps; -const Pressable = React.forwardRef, IPressableProps>(({ className, ...props }, ref) => { +const Pressable = React.forwardRef, IPressableProps>(function Pressable({ className, ...props }, ref) { return ( & React.ComponentProps< type IProgressFilledTrackProps = VariantProps & React.ComponentProps; export const Progress = React.forwardRef, IProgressProps>(({ className, size = 'md', ...props }, ref) => { - return ; + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); export const ProgressFilledTrack = React.forwardRef, IProgressFilledTrackProps>(({ className, ...props }, ref) => { diff --git a/src/components/ui/radio/index.tsx b/src/components/ui/radio/index.tsx index 82c8381..b2dccea 100644 --- a/src/components/ui/radio/index.tsx +++ b/src/components/ui/radio/index.tsx @@ -1,36 +1,27 @@ 'use client'; -import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { createRadio } from '@gluestack-ui/radio'; -import { cssInterop } from 'nativewind'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { createRadio } from '@gluestack-ui/core/radio/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { Platform, Pressable, Text, View } from 'react-native'; const SCOPE = 'Radio'; +const StyledIcon = styled(UIIcon, { + className: { + target: 'style', + }, +}); + const UIRadio = createRadio({ Root: (Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContext(Pressable, SCOPE)) as ReturnType>, Group: View, - Icon: UIIcon, + Icon: StyledIcon, Indicator: View, Label: Text, }); -cssInterop(PrimitiveIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, - }, -}); - const radioStyle = tva({ base: 'group/radio flex-row justify-start items-center web:cursor-pointer data-[disabled=true]:web:cursor-not-allowed', variants: { @@ -90,7 +81,8 @@ const radioLabelStyle = tva({ type IRadioProps = Omit, 'context'> & VariantProps; const Radio = React.forwardRef, IRadioProps>(function Radio({ className, size = 'md', ...props }, ref) { - return ; + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); type IRadioGroupProps = React.ComponentProps & VariantProps; diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx index 57729ff..d8dae42 100644 --- a/src/components/ui/select/index.tsx +++ b/src/components/ui/select/index.tsx @@ -1,11 +1,11 @@ 'use client'; -import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { createSelect } from '@gluestack-ui/select'; -import { cssInterop } from 'nativewind'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import { createSelect } from '@gluestack-ui/core/select/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { Pressable, TextInput, View } from 'react-native'; @@ -83,12 +83,18 @@ const selectInputStyle = tva({ }, }); +const StyledIcon = styled(UIIcon, { + className: { + target: 'style', + }, +}); + const UISelect = createSelect( { Root: View, Trigger: withStyleContext(SelectTriggerWrapper), Input: TextInput, - Icon: UIIcon, + Icon: StyledIcon, }, { Portal: Actionsheet, @@ -106,25 +112,6 @@ const UISelect = createSelect( } ); -cssInterop(UISelect, { className: 'style' }); -cssInterop(UISelect.Input, { - className: { target: 'style', nativeStyleToProp: { textAlign: true } }, -}); -cssInterop(SelectTriggerWrapper, { className: 'style' }); - -cssInterop(PrimitiveIcon, { - className: { - target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, - }, -}); - type ISelectProps = VariantProps & React.ComponentProps & { className?: string }; const Select = React.forwardRef, ISelectProps>(function Select({ className, ...props }, ref) { @@ -142,6 +129,7 @@ const Select = React.forwardRef, ISelectProp type ISelectTriggerProps = VariantProps & React.ComponentProps & { className?: string }; const SelectTrigger = React.forwardRef, ISelectTriggerProps>(function SelectTrigger({ className, size = 'md', variant = 'outline', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size, variant }), [size, variant]); return ( ); diff --git a/src/components/ui/select/select-actionsheet.tsx b/src/components/ui/select/select-actionsheet.tsx index a0bd0f6..7fe823c 100644 --- a/src/components/ui/select/select-actionsheet.tsx +++ b/src/components/ui/select/select-actionsheet.tsx @@ -1,16 +1,15 @@ 'use client'; import { H4 } from '@expo/html-elements'; -import { createActionsheet } from '@gluestack-ui/actionsheet'; -import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; +import { createActionsheet } from '@gluestack-ui/core/actionsheet/creator'; +import { UIIcon } from '@gluestack-ui/core/icon/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import { AnimatePresence, createMotionAnimatedComponent, Motion, type MotionComponentProps } from '@legendapp/motion'; -import { FlashList } from '@shopify/flash-list'; -import { cssInterop } from 'nativewind'; +import { styled } from 'nativewind'; import React from 'react'; -import { Pressable, ScrollView, SectionList, Text, View, type ViewStyle, VirtualizedList } from 'react-native'; +import { FlatList, Pressable, ScrollView, SectionList, Text, View, type ViewStyle, VirtualizedList } from 'react-native'; type IAnimatedPressableProps = React.ComponentProps & MotionComponentProps; @@ -20,64 +19,58 @@ type IMotionViewProps = React.ComponentProps & MotionComponentProps const MotionView = Motion.View as React.ComponentType; -export const UIActionsheet = createActionsheet({ - Root: View, - Content: Motion.View, - Item: AnimatedPressable, - ItemText: Text, - DragIndicator: View, - IndicatorWrapper: View, - Backdrop: AnimatedPressable, - ScrollView: ScrollView, - VirtualizedList: VirtualizedList, - FlatList: FlashList, - SectionList: SectionList, - SectionHeaderText: H4, - Icon: UIIcon, - AnimatePresence: AnimatePresence, -}); - -cssInterop(UIActionsheet, { className: 'style' }); -cssInterop(UIActionsheet.Content, { className: 'style' }); -cssInterop(UIActionsheet.Item, { className: 'style' }); -cssInterop(UIActionsheet.ItemText, { className: 'style' }); -cssInterop(UIActionsheet.DragIndicator, { className: 'style' }); -cssInterop(UIActionsheet.DragIndicatorWrapper, { className: 'style' }); -cssInterop(UIActionsheet.Backdrop, { className: 'style' }); -cssInterop(UIActionsheet.ScrollView, { - className: 'style', - contentContainerClassName: 'contentContainerStyle', - indicatorClassName: 'indicatorStyle', -}); -cssInterop(UIActionsheet.VirtualizedList, { +const StyledMotionView = styled(MotionView, { className: 'style' }); +// Cast to a minimal prop surface: `styled`'s mapping constraint computes +// dot-notation paths over every prop, which exceeds TS's union limit for +// ScrollView's full props (TS2590). The returned component is untyped by +// `styled` anyway, so this cast is lossless. +const StyledScrollView = styled(ScrollView as unknown as React.ComponentType>, { className: 'style', - ListFooterComponentClassName: 'ListFooterComponentStyle', - ListHeaderComponentClassName: 'ListHeaderComponentStyle', contentContainerClassName: 'contentContainerStyle', indicatorClassName: 'indicatorStyle', }); -cssInterop(UIActionsheet.FlatList, { +const StyledVirtualizedList = styled(VirtualizedList as unknown as React.ComponentType>, { className: 'style', ListFooterComponentClassName: 'ListFooterComponentStyle', ListHeaderComponentClassName: 'ListHeaderComponentStyle', - columnWrapperClassName: 'columnWrapperStyle', contentContainerClassName: 'contentContainerStyle', indicatorClassName: 'indicatorStyle', }); -cssInterop(UIActionsheet.SectionList, { className: 'style' }); -cssInterop(UIActionsheet.SectionHeaderText, { className: 'style' }); -cssInterop(PrimitiveIcon, { +const StyledFlatList = styled( + FlatList as unknown as React.ComponentType>, + { + className: 'style', + ListFooterComponentClassName: 'ListFooterComponentStyle', + ListHeaderComponentClassName: 'ListHeaderComponentStyle', + columnWrapperClassName: 'columnWrapperStyle', + contentContainerClassName: 'contentContainerStyle', + indicatorClassName: 'indicatorStyle', + } +); +const StyledIcon = styled(UIIcon, { className: { target: 'style', - nativeStyleToProp: { - height: true, - width: true, - fill: true, - color: 'classNameColor', - stroke: true, - }, }, }); +const StyledSectionList = styled(SectionList as unknown as React.ComponentType>, { className: 'style' }); +const StyledSectionHeaderText = styled(H4, { className: 'style' }); + +export const UIActionsheet = createActionsheet({ + Root: View, + Content: withStyleContext(StyledMotionView), + Item: withStyleContext(Pressable), + ItemText: Text, + DragIndicator: View, + IndicatorWrapper: View, + Backdrop: AnimatedPressable, + ScrollView: StyledScrollView, + VirtualizedList: StyledVirtualizedList, + FlatList: StyledFlatList, + SectionList: StyledSectionList, + SectionHeaderText: StyledSectionHeaderText, + Icon: StyledIcon, + AnimatePresence: AnimatePresence, +}); const actionsheetStyle = tva({ base: 'w-full h-full web:pointer-events-none' }); @@ -305,10 +298,10 @@ const ActionsheetItemText = React.forwardRef & }; const Skeleton = forwardRef, ISkeletonProps>(({ className, variant, children, startColor = 'bg-background-200', isLoaded = false, speed = 2, ...props }, ref) => { - const pulseAnim = new Animated.Value(1); + const isWeb = Platform.OS === 'web'; + const pulseAnim = useRef(new Animated.Value(1)).current; + const animRef = useRef(null); const customTimingFunction = Easing.bezier(0.4, 0, 0.6, 1); const fadeDuration = 0.6; - const animationDuration = (fadeDuration * 10000) / speed; // Convert seconds to milliseconds + const animationDuration = (fadeDuration * 10000) / speed; - const pulse = Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1, // Start with opacity 1 - duration: animationDuration / 2, // Third of the animation duration - easing: customTimingFunction, - useNativeDriver: Platform.OS !== 'web', - }), - Animated.timing(pulseAnim, { - toValue: 0.75, - duration: animationDuration / 2, // Third of the animation duration - easing: customTimingFunction, - useNativeDriver: Platform.OS !== 'web', - }), - Animated.timing(pulseAnim, { - toValue: 1, - duration: animationDuration / 2, // Third of the animation duration - easing: customTimingFunction, - useNativeDriver: Platform.OS !== 'web', - }), - ]); + useEffect(() => { + // On web, use CSS animation instead to avoid Animated.loop JS driver overhead + if (isWeb) return; + + if (!isLoaded) { + const pulse = Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1, + duration: animationDuration / 2, + easing: customTimingFunction, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 0.75, + duration: animationDuration / 2, + easing: customTimingFunction, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: animationDuration / 2, + easing: customTimingFunction, + useNativeDriver: true, + }), + ]); + animRef.current = Animated.loop(pulse); + animRef.current.start(); + } else { + animRef.current?.stop(); + animRef.current = null; + } + + return () => { + animRef.current?.stop(); + animRef.current = null; + }; + }, [isLoaded, isWeb, animationDuration, pulseAnim, customTimingFunction]); if (!isLoaded) { - Animated.loop(pulse).start(); + // On web, use a CSS keyframe animation to avoid JS-driven Animated.loop + if (isWeb) { + return ( + + ); + } return ( , ISkeletonProps>(({ cl /> ); } else { - Animated.loop(pulse).stop(); - return children; } }); diff --git a/src/components/ui/skeleton/index.web.tsx b/src/components/ui/skeleton/index.web.tsx index 92a5af6..b9a02de 100644 --- a/src/components/ui/skeleton/index.web.tsx +++ b/src/components/ui/skeleton/index.web.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { skeletonStyle, skeletonTextStyle } from './styles'; diff --git a/src/components/ui/skeleton/styles.tsx b/src/components/ui/skeleton/styles.tsx index fdd0eae..e02397e 100644 --- a/src/components/ui/skeleton/styles.tsx +++ b/src/components/ui/skeleton/styles.tsx @@ -1,4 +1,4 @@ -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; export const skeletonStyle = tva({ base: 'w-full h-full', diff --git a/src/components/ui/slider/index.tsx b/src/components/ui/slider/index.tsx index 97c432f..ba7fe99 100644 --- a/src/components/ui/slider/index.tsx +++ b/src/components/ui/slider/index.tsx @@ -1,32 +1,26 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStates } from '@gluestack-ui/nativewind-utils/withStates'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { createSlider } from '@gluestack-ui/slider'; -import { cssInterop } from 'nativewind'; -import React, { type ComponentType, type RefAttributes } from 'react'; -import { Pressable, type ViewProps } from 'react-native'; -import { Platform, View } from 'react-native'; - -const ThumbWrapper = React.forwardRef, React.ComponentProps>((props, ref) => ); - -const FilledTrackWrapper = React.forwardRef, React.ComponentProps>((props, ref) => ); +import { createSlider } from '@gluestack-ui/core/slider/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; +import React from 'react'; +import { Pressable, View } from 'react-native'; const SCOPE = 'SLIDER'; +const Root = withStyleContext(View, SCOPE); export const UISlider = createSlider({ - Root: Platform.OS === 'web' ? withStyleContext(View, SCOPE) : withStyleContextAndStates(View, SCOPE), - Thumb: Platform.OS === 'web' ? ThumbWrapper : (withStates(View) as ComponentType>), + Root: Root, + Thumb: View, Track: Pressable, - FilledTrack: Platform.OS === 'web' ? FilledTrackWrapper : (withStates(View) as ComponentType>), + FilledTrack: View, ThumbInteraction: View, }); -cssInterop(UISlider, { className: 'style' }); -cssInterop(ThumbWrapper, { className: 'style' }); -cssInterop(UISlider.Track, { className: 'style' }); -cssInterop(FilledTrackWrapper, { className: 'style' }); +// Narrow cast: styled's mapping constraint computes dot-notation paths over +// every prop, which exceeds TS's union limit for Pressable's full props +// (TS2590). The returned component is untyped by `styled` anyway. +const StyledTrack = styled(UISlider.Track as unknown as React.ComponentType>, { + className: 'style', +}) as unknown as typeof UISlider.Track & React.ComponentType<{ className?: string }>; const sliderStyle = tva({ base: 'justify-center items-center data-[disabled=true]:web:opacity-40 data-[disabled=true]:web:pointer-events-none', @@ -158,7 +152,8 @@ const sliderFilledTrackStyle = tva({ type ISliderProps = React.ComponentProps & VariantProps; -export const Slider = React.forwardRef, ISliderProps>(({ className, size = 'md', orientation = 'horizontal', isReversed = false, ...props }, ref) => { +export const Slider = React.forwardRef, ISliderProps>(({ className, size = 'md', orientation = 'horizontal', isReversed = false, ...props }, ref) => { + const contextValue = React.useMemo(() => ({ size, orientation, isReversed }), [size, orientation, isReversed]); return ( , ISlide isReversed, class: className, })} - context={{ size, orientation, isReversed }} + context={contextValue} /> ); }); type ISliderThumbProps = React.ComponentProps & VariantProps; -export const SliderThumb = React.forwardRef, ISliderThumbProps>(({ className, size, ...props }, ref) => { +export const SliderThumb = React.forwardRef, ISliderThumbProps>(({ className, size, ...props }, ref) => { const { size: parentSize } = useStyleContext(SCOPE); return ( @@ -198,11 +193,11 @@ export const SliderThumb = React.forwardRef & VariantProps; -export const SliderTrack = React.forwardRef, ISliderTrackProps>(({ className, ...props }, ref) => { +export const SliderTrack = React.forwardRef, ISliderTrackProps>(({ className, ...props }, ref) => { const { orientation: parentOrientation, size: parentSize, isReversed } = useStyleContext(SCOPE); return ( - & VariantProps; -export const SliderFilledTrack = React.forwardRef, ISliderFilledTrackProps>(({ className, ...props }, ref) => { +export const SliderFilledTrack = React.forwardRef, ISliderFilledTrackProps>(({ className, ...props }, ref) => { const { orientation: parentOrientation } = useStyleContext(SCOPE); return ( diff --git a/src/components/ui/spinner/index.tsx b/src/components/ui/spinner/index.tsx index 400b165..8f6236f 100644 --- a/src/components/ui/spinner/index.tsx +++ b/src/components/ui/spinner/index.tsx @@ -1,22 +1,19 @@ 'use client'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { createSpinner } from '@gluestack-ui/spinner'; -import { cssInterop } from 'nativewind'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { styled } from 'nativewind'; import React from 'react'; import { ActivityIndicator } from 'react-native'; -const UISpinner = createSpinner({ Root: ActivityIndicator }); - -cssInterop(UISpinner, { - className: { target: 'style', nativeStyleToProp: { color: true } }, +const StyledActivityIndicator = styled(ActivityIndicator, { + className: { target: 'style', nativeStyleMapping: { color: true } }, }); const spinnerStyle = tva({}); -type ISpinnerProps = React.ComponentProps; +type ISpinnerProps = React.ComponentProps; -const Spinner = React.forwardRef, ISpinnerProps>(({ className, color, ...props }, ref) => { - return ; +const Spinner = React.forwardRef, ISpinnerProps>(({ className, color, ...props }, ref) => { + return ; }); Spinner.displayName = 'Spinner'; diff --git a/src/components/ui/switch/index.tsx b/src/components/ui/switch/index.tsx index c3abf63..1c8be86 100644 --- a/src/components/ui/switch/index.tsx +++ b/src/components/ui/switch/index.tsx @@ -1,23 +1,13 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { createSwitch } from '@gluestack-ui/switch'; -import { cssInterop } from 'nativewind'; +import { createSwitch } from '@gluestack-ui/core/switch/creator'; +import { tva, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Platform, Switch as RNSwitch } from 'react-native'; - -const SwitchWrapper = React.forwardRef, React.ComponentProps>(({ ...props }, ref) => { - return ; -}); +import { Switch as RNSwitch } from 'react-native'; const UISwitch = createSwitch({ - Root: Platform.OS === 'web' ? withStyleContext(SwitchWrapper) : withStyleContextAndStates(SwitchWrapper), + Root: withStyleContext(RNSwitch), }); -cssInterop(SwitchWrapper, { className: 'style' }); - const switchStyle = tva({ base: 'data-[focus=true]:outline-0 data-[focus=true]:ring-2 data-[focus=true]:ring-indicator-primary web:cursor-pointer disabled:cursor-not-allowed data-[disabled=true]:opacity-40 data-[invalid=true]:border-error-700 data-[invalid=true]:rounded-xl data-[invalid=true]:border-2', @@ -31,7 +21,7 @@ const switchStyle = tva({ }); type ISwitchProps = React.ComponentProps & VariantProps; -const Switch = React.forwardRef, ISwitchProps>(({ className, size = 'md', ...props }, ref) => { +const Switch = React.forwardRef, ISwitchProps>(function Switch({ className, size = 'md', ...props }, ref) { return ; }); diff --git a/src/components/ui/text/index.tsx b/src/components/ui/text/index.tsx index 8fd91eb..cf5def0 100644 --- a/src/components/ui/text/index.tsx +++ b/src/components/ui/text/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { Text as RNText } from 'react-native'; diff --git a/src/components/ui/text/index.web.tsx b/src/components/ui/text/index.web.tsx index 158cddf..7b63841 100644 --- a/src/components/ui/text/index.web.tsx +++ b/src/components/ui/text/index.web.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { textStyle } from './styles'; diff --git a/src/components/ui/text/styles.tsx b/src/components/ui/text/styles.tsx index af02bd0..c02b46c 100644 --- a/src/components/ui/text/styles.tsx +++ b/src/components/ui/text/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'font-sans tracking-sm my-0 bg-transparent border-0 box-border display-inline list-none margin-0 padding-0 position-relative text-start no-underline whitespace-pre-wrap word-wrap-break-word' diff --git a/src/components/ui/textarea/index.tsx b/src/components/ui/textarea/index.tsx index dfe6e9a..4a6d8d6 100644 --- a/src/components/ui/textarea/index.tsx +++ b/src/components/ui/textarea/index.tsx @@ -1,27 +1,13 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStates } from '@gluestack-ui/nativewind-utils/withStates'; -import { useStyleContext, withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { createTextarea } from '@gluestack-ui/textarea'; -import { cssInterop } from 'nativewind'; +import { createTextarea } from '@gluestack-ui/core/textarea/creator'; +import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; -import { Platform, TextInput, View } from 'react-native'; - -const TextareaWrapper = React.forwardRef, React.ComponentProps>(({ ...props }, ref) => { - return ; -}); +import { TextInput, View } from 'react-native'; const SCOPE = 'TEXTAREA'; const UITextarea = createTextarea({ - Root: Platform.OS === 'web' ? withStyleContext(TextareaWrapper, SCOPE) : withStyleContextAndStates(TextareaWrapper, SCOPE), - Input: Platform.OS === 'web' ? TextInput : withStates(TextInput), -}); - -cssInterop(TextareaWrapper, { className: 'style' }); -cssInterop(UITextarea.Input, { - className: { target: 'style', nativeStyleToProp: { textAlign: true } }, + Root: withStyleContext(View, SCOPE), + Input: TextInput, }); const textareaStyle = tva({ @@ -55,19 +41,21 @@ const textareaInputStyle = tva({ type ITextareaProps = React.ComponentProps & VariantProps; -const Textarea = React.forwardRef, ITextareaProps>(({ className, variant = 'default', size = 'md', ...props }, ref) => { - return ; +const Textarea = React.forwardRef, ITextareaProps>(function Textarea({ className, variant = 'default', size = 'md', ...props }, ref) { + const contextValue = React.useMemo(() => ({ size }), [size]); + return ; }); type ITextareaInputProps = React.ComponentProps & VariantProps; -const TextareaInput = React.forwardRef, ITextareaInputProps>(({ className, ...props }, ref) => { +const TextareaInput = React.forwardRef, ITextareaInputProps>(function TextareaInput({ className, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); return ( & MotionComponentProps; - -const MotionView = Motion.View as React.ComponentType; - -const useToast = createToastHook(MotionView, AnimatePresence); +import { AccessibilityInfo, Text, View } from 'react-native'; +import Animated, { SlideInUp } from 'react-native-reanimated'; +const useToast = createToastHook(View); const SCOPE = 'TOAST'; - -cssInterop(MotionView, { className: 'style' }); - +const AnimatedView = Animated.createAnimatedComponent(View); const toastStyle = tva({ base: 'p-4 m-1 rounded-md gap-1 web:pointer-events-auto shadow-hard-5 border-outline-100', variants: { @@ -143,13 +134,14 @@ const toastDescriptionStyle = tva({ }, }); -const Root = withStyleContext(View, SCOPE); +const Root = withStyleContext(AnimatedView, SCOPE); type IToastProps = React.ComponentProps & { className?: string; } & VariantProps; const Toast = React.forwardRef, IToastProps>(function Toast({ className, variant = 'solid', action = 'muted', ...props }, ref) { - return ; + const contextValue = React.useMemo(() => ({ variant, action }), [variant, action]); + return ; }); type IToastTitleProps = React.ComponentProps & { diff --git a/src/components/ui/tooltip/index.tsx b/src/components/ui/tooltip/index.tsx index 7883c1b..4ace896 100644 --- a/src/components/ui/tooltip/index.tsx +++ b/src/components/ui/tooltip/index.tsx @@ -1,17 +1,22 @@ 'use client'; -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; -import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext'; -import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; -import { createTooltip } from '@gluestack-ui/tooltip'; -import { AnimatePresence, Motion } from '@legendapp/motion'; -import { cssInterop } from 'nativewind'; +import { createTooltip } from '@gluestack-ui/core/tooltip/creator'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import { AnimatePresence, Motion, type MotionComponentProps } from '@legendapp/motion'; +import { styled } from 'nativewind'; import React from 'react'; -import { Platform, Text, View } from 'react-native'; +import { Text, type View, type ViewStyle } from 'react-native'; + +type IMotionViewProps = React.ComponentProps & MotionComponentProps; + +const MotionView = Motion.View as React.ComponentType; + +const StyledMotionView = styled(MotionView, { className: 'style' }); export const UITooltip = createTooltip({ - Root: Platform.OS === 'web' ? withStyleContext(View) : withStyleContextAndStates(View), - Content: Motion.View, + Root: withStyleContext(StyledMotionView), + Content: MotionView, Text: Text, AnimatePresence: AnimatePresence, }); @@ -29,9 +34,7 @@ const tooltipTextStyle = tva({ variants: { isTruncated: { - true: { - props: 'line-clamp-1 truncate', - }, + true: 'line-clamp-1 truncate', }, bold: { true: 'font-bold', @@ -67,19 +70,15 @@ const tooltipTextStyle = tva({ }, }); -cssInterop(UITooltip, { className: 'style' }); -cssInterop(UITooltip.Content, { className: 'style' }); -cssInterop(UITooltip.Text, { className: 'style' }); - type ITooltipProps = React.ComponentProps & VariantProps & { className?: string }; type ITooltipContentProps = React.ComponentProps & VariantProps & { className?: string }; type ITooltipTextProps = React.ComponentProps & VariantProps & { className?: string }; -export const Tooltip = React.forwardRef, ITooltipProps>(({ className, ...props }, ref) => { +const Tooltip = React.forwardRef, ITooltipProps>(function Tooltip({ className, ...props }, ref) { return ; }); -export const TooltipContent = React.forwardRef, ITooltipContentProps & { className?: string }>(({ className, ...props }, ref) => { +const TooltipContent = React.forwardRef, ITooltipContentProps & { className?: string }>(function TooltipContent({ className, ...props }, ref) { return ( , ITooltipTextProps & { className?: string }>(({ size, className, ...props }, ref) => { +const TooltipText = React.forwardRef, ITooltipTextProps & { className?: string }>(function TooltipText({ size, className, ...props }, ref) { return ; }); + +Tooltip.displayName = 'Tooltip'; +TooltipContent.displayName = 'TooltipContent'; +TooltipText.displayName = 'TooltipText'; + +export { Tooltip, TooltipContent, TooltipText }; diff --git a/src/components/ui/utils/use-break-point-value.ts b/src/components/ui/utils/use-break-point-value.ts deleted file mode 100644 index c2a33a3..0000000 --- a/src/components/ui/utils/use-break-point-value.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Dimensions, useWindowDimensions } from 'react-native'; -import * as tailwindConfig from 'tailwind.config'; -import resolveConfig from 'tailwindcss/resolveConfig'; - -const TailwindTheme = resolveConfig(tailwindConfig as any); -const screenSize = TailwindTheme.theme.screens; - -type breakpoints = keyof typeof screenSize | 'default'; - -type MediaQueriesBreakpoints = { - key: breakpoints; - breakpoint: number; - isValid: boolean; - value?: unknown; -}; - -type BreakPointValue = Partial>; - -const resolveScreenWidth: Record = { - default: 0, -}; - -Object.entries(screenSize).forEach(([key, value]) => { - if (typeof value === 'string') { - resolveScreenWidth[key] = parseInt(value.replace('px', ''), 10); - } -}); - -export const getBreakPointValue = (values: BreakPointValue, width: number): unknown => { - if (typeof values !== 'object') return values; - - let finalBreakPointResolvedValue: unknown; - const mediaQueriesBreakpoints: MediaQueriesBreakpoints[] = [ - { - key: 'default', - breakpoint: 0, - isValid: true, - }, - ]; - Object.keys(resolveScreenWidth).forEach((key) => { - const isValid = isValidBreakpoint(resolveScreenWidth[key], width); - - mediaQueriesBreakpoints.push({ - key: key, - breakpoint: resolveScreenWidth[key], - isValid: isValid, - }); - }); - - mediaQueriesBreakpoints.sort((a: MediaQueriesBreakpoints, b: MediaQueriesBreakpoints) => a.breakpoint - b.breakpoint); - - mediaQueriesBreakpoints.forEach((breakpoint: MediaQueriesBreakpoints, index: number) => { - breakpoint.value = values.hasOwnProperty(breakpoint.key) ? values[breakpoint.key] : mediaQueriesBreakpoints[index - 1]?.value || mediaQueriesBreakpoints[0]?.value; - }); - - const lastValidObject = getLastValidObject(mediaQueriesBreakpoints); - - if (!lastValidObject) { - finalBreakPointResolvedValue = values; - } else { - finalBreakPointResolvedValue = lastValidObject.value; - } - return finalBreakPointResolvedValue; -}; - -export function useBreakpointValue(values: BreakPointValue): unknown { - const { width } = useWindowDimensions(); - - const [currentBreakPointValue, setCurrentBreakPointValue] = useState(getBreakPointValue(values, width)); - - useEffect(() => { - if (typeof values === 'object') { - const finalBreakPointResolvedValue = getBreakPointValue(values, width); - setCurrentBreakPointValue(finalBreakPointResolvedValue); - } - }, [values, width]); - - if (typeof values !== 'object') return values; - - return currentBreakPointValue; -} - -export function isValidBreakpoint(breakPointWidth: number, width: number = Dimensions.get('window')?.width || 0) { - const windowWidth = width; - - return windowWidth >= breakPointWidth; -} - -function getLastValidObject( - mediaQueries: { - key: breakpoints; - breakpoint: number; - isValid: boolean; - value?: unknown; - }[] -) { - for (let i = mediaQueries.length - 1; i >= 0; i--) { - if (mediaQueries[i].isValid) { - return mediaQueries[i]; - } - } - return null; // No valid object found -} diff --git a/src/components/ui/vstack/index.tsx b/src/components/ui/vstack/index.tsx index d3d690f..53dc1e4 100644 --- a/src/components/ui/vstack/index.tsx +++ b/src/components/ui/vstack/index.tsx @@ -1,4 +1,4 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; import { View } from 'react-native'; diff --git a/src/components/ui/vstack/index.web.tsx b/src/components/ui/vstack/index.web.tsx index 90061d6..4e056a5 100644 --- a/src/components/ui/vstack/index.web.tsx +++ b/src/components/ui/vstack/index.web.tsx @@ -1,12 +1,14 @@ -import type { VariantProps } from '@gluestack-ui/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import React from 'react'; +import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'; import { vstackStyle } from './styles'; -type IVStackProps = React.ComponentProps<'div'> & VariantProps; +type IVStackProps = React.ComponentProps<'div'> & VariantProps & { style?: StyleProp }; -const VStack = React.forwardRef, IVStackProps>(function VStack({ className, space, reversed, ...props }, ref) { - return
; +const VStack = React.forwardRef, IVStackProps>(function VStack({ className, space, reversed, style, ...props }, ref) { + const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style; + return
; }); VStack.displayName = 'VStack'; diff --git a/src/components/ui/vstack/styles.tsx b/src/components/ui/vstack/styles.tsx index ac5d68d..aead1c1 100644 --- a/src/components/ui/vstack/styles.tsx +++ b/src/components/ui/vstack/styles.tsx @@ -1,5 +1,4 @@ -import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb'; -import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { isWeb, tva } from '@gluestack-ui/utils/nativewind-utils'; const baseStyle = isWeb ? 'flex flex-col relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none' : ''; diff --git a/src/hooks/use-saml-login.ts b/src/hooks/use-saml-login.ts index 662840a..772996e 100644 --- a/src/hooks/use-saml-login.ts +++ b/src/hooks/use-saml-login.ts @@ -36,7 +36,8 @@ export function useSamlLogin(idpSsoUrl: string, username: string, departmentId?: const samlResponse = parsed.queryParams?.saml_response as string | undefined; if (!samlResponse) { - logger.error({ message: 'SSO SAML: No saml_response in deep link', context: { url } }); + // Never log the raw deep link - its query params can carry the saml_response token + logger.error({ message: 'SSO SAML: No saml_response in deep link', context: { path: parsed.path } }); return null; } @@ -49,7 +50,7 @@ export function useSamlLogin(idpSsoUrl: string, username: string, departmentId?: return result.authResponse; } catch (error) { - logger.error({ message: 'SSO SAML: Deep link handling threw exception', context: { error } }); + logger.error({ message: 'SSO SAML: Deep link handling threw exception', context: { message: error instanceof Error ? error.message : String(error) } }); return null; } } diff --git a/src/lib/auth/api.tsx b/src/lib/auth/api.tsx index 3efba55..140c2d2 100644 --- a/src/lib/auth/api.tsx +++ b/src/lib/auth/api.tsx @@ -1,13 +1,81 @@ import { Env } from '@env'; -import axios from 'axios'; -import { randomUUID } from 'expo-crypto'; +import axios, { AxiosError } from 'axios'; +import { CryptoDigestAlgorithm, digestStringAsync, randomUUID } from 'expo-crypto'; import queryString from 'query-string'; import { logger } from '@/lib/logging'; +import { getItem, removeItem, setItem } from '../storage'; import { getBaseApiUrl } from '../storage/app'; import type { AuthResponse, LoginCredentials, LoginResponse } from './types'; +// Strip any query string from a URL so tokens/credentials in params are never logged. +const sanitizeUrl = (url?: string): string | undefined => url?.split('?')[0]; + +// Extract only non-sensitive fields from an axios error. Never log error.config, +// config.data or response bodies for the token endpoints - they contain credentials. +const sanitizeAuthError = (error: unknown): Record => { + if (error instanceof AxiosError) { + return { + message: error.message, + status: error.response?.status, + url: sanitizeUrl(error.config?.url), + }; + } + return { message: error instanceof Error ? error.message : String(error) }; +}; + +const PASSWORD_VERIFICATION_HASH_KEY = 'PASSWORD_VERIFICATION_HASH'; +const PASSWORD_VERIFICATION_SALT_KEY = 'PASSWORD_VERIFICATION_SALT'; + +// Store a salted SHA-256 hash of the password after a successful password-grant +// login so the lockscreen can verify the password offline. This is a verification +// cache only - the password itself is never stored. +export const storePasswordVerificationHash = async (password: string): Promise => { + try { + let salt = getItem(PASSWORD_VERIFICATION_SALT_KEY); + if (!salt) { + salt = randomUUID(); + await setItem(PASSWORD_VERIFICATION_SALT_KEY, salt); + } + + const hash = await digestStringAsync(CryptoDigestAlgorithm.SHA256, `${salt}:${password}`); + await setItem(PASSWORD_VERIFICATION_HASH_KEY, hash); + } catch (error) { + logger.error({ + message: 'Failed to store password verification hash', + context: { message: error instanceof Error ? error.message : String(error) }, + }); + } +}; + +// Verify a password against the stored salted hash. Returns null when no hash is +// stored (e.g. SSO/OIDC login), true on match and false on mismatch. +export const verifyPassword = async (password: string): Promise => { + try { + const salt = getItem(PASSWORD_VERIFICATION_SALT_KEY); + const storedHash = getItem(PASSWORD_VERIFICATION_HASH_KEY); + + if (!salt || !storedHash) { + return null; + } + + const hash = await digestStringAsync(CryptoDigestAlgorithm.SHA256, `${salt}:${password}`); + return hash === storedHash; + } catch (error) { + logger.error({ + message: 'Failed to verify password', + context: { message: error instanceof Error ? error.message : String(error) }, + }); + return null; + } +}; + +export const clearPasswordVerificationHash = async (): Promise => { + await removeItem(PASSWORD_VERIFICATION_HASH_KEY); + await removeItem(PASSWORD_VERIFICATION_SALT_KEY); +}; + const authApi = axios.create({ headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -19,7 +87,7 @@ authApi.interceptors.request.use((config) => { config.baseURL = getBaseApiUrl(); logger.info({ message: 'Auth API request interceptor', - context: { baseURL: config.baseURL, url: config.url }, + context: { baseURL: config.baseURL, url: sanitizeUrl(config.url) }, }); return config; }); @@ -59,7 +127,7 @@ export const loginRequest = async (credentials: LoginCredentials): Promise Promise; } +// Persisted random device id for the web sender (analytics must never throw) +const getWebDeviceId = (): string => { + try { + const stored = window.localStorage.getItem('countly_device_id'); + if (stored) return stored; + const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`; + window.localStorage.setItem('countly_device_id', id); + return id; + } catch { + return 'unknown'; + } +}; + +const sendWebEvent = (eventName: string, segmentation: Record, count: number): void => { + try { + const appKey = Env.COUNTLY_APP_KEY; + const serverUrl = Env.COUNTLY_SERVER_URL; + if (!appKey || !serverUrl || typeof fetch === 'undefined') return; + + const params = new URLSearchParams({ + app_key: appKey, + device_id: getWebDeviceId(), + events: JSON.stringify([{ key: eventName, count, segmentation }]), + }); + + fetch(`${serverUrl}/i?${params.toString()}`, { method: 'POST', keepalive: true }).catch(() => {}); + } catch { + // Analytics must never throw + } +}; + /** * Platform-aware Countly wrapper - * Provides a no-op implementation for web platform + * Provides a minimal direct-post implementation for web platform */ let Countly: CountlyInterface; if (Platform.OS === 'web') { - // Web fallback - no-op implementation Countly = { events: { - recordEvent: () => { - // No-op for web - // Could optionally send to a web analytics service here - }, + recordEvent: sendWebEvent, }, init: async () => { - // No-op + // No-op - the web sender reads config lazily per event }, initWithConfig: async () => { // No-op diff --git a/src/lib/dispatch-helpers.ts b/src/lib/dispatch-helpers.ts index 1da7dd7..25abdf6 100644 --- a/src/lib/dispatch-helpers.ts +++ b/src/lib/dispatch-helpers.ts @@ -60,7 +60,13 @@ export function mergeDispatchSelections(existing: DispatchSelection, added: Disp * Shared by the call-detail screens and the dispatch console so every "add resources" entry point * submits an identical, field-preserving payload. */ -export function buildAddResourcesUpdateRequest(call: CallResultData, existingDispatches: DispatchedEventResultData[] | undefined | null, added: DispatchSelection, callFormData?: string, plusCode?: string): UpdateCallRequest { +export function buildAddResourcesUpdateRequest( + call: CallResultData, + existingDispatches: DispatchedEventResultData[] | undefined | null, + added: DispatchSelection, + callFormData?: string, + plusCode?: string +): UpdateCallRequest { const merged = mergeDispatchSelections(dispatchesToSelection(existingDispatches), added); let latitude: number | undefined; diff --git a/src/lib/hooks/__tests__/use-selected-theme.test.ts b/src/lib/hooks/__tests__/use-selected-theme.test.ts index c54e92d..de191c9 100644 --- a/src/lib/hooks/__tests__/use-selected-theme.test.ts +++ b/src/lib/hooks/__tests__/use-selected-theme.test.ts @@ -1,7 +1,7 @@ -import { colorScheme } from 'nativewind'; +import { Appearance } from 'react-native'; import { storage } from '../../storage'; -import { loadSelectedTheme, useSelectedTheme } from '../use-selected-theme'; +import { loadSelectedTheme } from '../use-selected-theme'; // Mock the storage module jest.mock('../../storage', () => ({ @@ -11,40 +11,33 @@ jest.mock('../../storage', () => ({ }, })); -// Mock nativewind -jest.mock('nativewind', () => ({ - colorScheme: { - set: jest.fn(), - }, - useColorScheme: () => ({ - colorScheme: 'system', - setColorScheme: jest.fn(), - }), -})); - // Mock react-native-mmkv jest.mock('react-native-mmkv', () => ({ useMMKVString: jest.fn(() => ['system', jest.fn()]), })); const mockedStorage = storage as jest.Mocked; -const mockedColorScheme = colorScheme as jest.Mocked; describe('loadSelectedTheme', () => { + let setColorSchemeSpy: jest.SpyInstance; + beforeEach(() => { jest.clearAllMocks(); - console.log = jest.fn(); + setColorSchemeSpy = jest.spyOn(Appearance, 'setColorScheme').mockImplementation(() => {}); console.error = jest.fn(); }); + afterEach(() => { + setColorSchemeSpy.mockRestore(); + }); + it('should load and apply saved theme from storage', () => { mockedStorage.getString.mockReturnValue('dark'); loadSelectedTheme(); expect(mockedStorage.getString).toHaveBeenCalledWith('SELECTED_THEME'); - expect(mockedColorScheme.set).toHaveBeenCalledWith('dark'); - expect(console.log).toHaveBeenCalledWith('Loading selected theme:', 'dark'); + expect(setColorSchemeSpy).toHaveBeenCalledWith('dark'); }); it('should handle no saved theme gracefully', () => { @@ -53,8 +46,7 @@ describe('loadSelectedTheme', () => { loadSelectedTheme(); expect(mockedStorage.getString).toHaveBeenCalledWith('SELECTED_THEME'); - expect(mockedColorScheme.set).toHaveBeenCalledWith('dark'); - expect(console.log).toHaveBeenCalledWith('No custom theme found, defaulting to dark mode'); + expect(setColorSchemeSpy).not.toHaveBeenCalled(); }); it('should handle storage errors gracefully', () => { @@ -66,7 +58,7 @@ describe('loadSelectedTheme', () => { loadSelectedTheme(); expect(console.error).toHaveBeenCalledWith('Failed to load selected theme:', error); - expect(mockedColorScheme.set).not.toHaveBeenCalled(); + expect(setColorSchemeSpy).not.toHaveBeenCalled(); }); it('should apply light theme correctly', () => { @@ -74,16 +66,14 @@ describe('loadSelectedTheme', () => { loadSelectedTheme(); - expect(mockedColorScheme.set).toHaveBeenCalledWith('light'); - expect(console.log).toHaveBeenCalledWith('Loading selected theme:', 'light'); + expect(setColorSchemeSpy).toHaveBeenCalledWith('light'); }); - it('should apply system theme correctly', () => { + it('should clear the override for system theme', () => { mockedStorage.getString.mockReturnValue('system'); loadSelectedTheme(); - expect(mockedColorScheme.set).toHaveBeenCalledWith('system'); - expect(console.log).toHaveBeenCalledWith('Loading selected theme:', 'system'); + expect(setColorSchemeSpy).toHaveBeenCalledWith(null); }); }); diff --git a/src/lib/hooks/use-selected-theme.tsx b/src/lib/hooks/use-selected-theme.tsx index e8f3d19..6ca9494 100644 --- a/src/lib/hooks/use-selected-theme.tsx +++ b/src/lib/hooks/use-selected-theme.tsx @@ -1,11 +1,19 @@ -import { colorScheme, useColorScheme } from 'nativewind'; import React from 'react'; +import { Appearance } from 'react-native'; import { useMMKVString } from 'react-native-mmkv'; import { storage } from '../storage'; const SELECTED_THEME = 'SELECTED_THEME'; export type ColorSchemeType = 'light' | 'dark' | 'system'; + +const applyColorScheme = (t: ColorSchemeType) => { + // NativeWind v5: theme overrides go through the standard Appearance API + // (null clears the override on RN 0.81). GluestackUIProvider picks the value + // up via useColorScheme and applies the web class / native class wrapper. + Appearance.setColorScheme(t === 'system' ? null : t); +}; + /** * this hooks should only be used while selecting the theme * This hooks will return the selected theme which is stored in MMKV @@ -14,18 +22,17 @@ export type ColorSchemeType = 'light' | 'dark' | 'system'; * */ export const useSelectedTheme = () => { - const { colorScheme: _color, setColorScheme } = useColorScheme(); const [theme, _setTheme] = useMMKVString(SELECTED_THEME, storage); const setSelectedTheme = React.useCallback( (t: ColorSchemeType) => { - setColorScheme(t); + applyColorScheme(t); _setTheme(t); }, - [setColorScheme, _setTheme] + [_setTheme] ); - const selectedTheme = (theme ?? 'dark') as ColorSchemeType; + const selectedTheme = (theme ?? 'system') as ColorSchemeType; return { selectedTheme, setSelectedTheme } as const; }; // to be used in the root file to load the selected theme from MMKV @@ -33,11 +40,7 @@ export const loadSelectedTheme = () => { try { const theme = storage.getString(SELECTED_THEME); if (theme !== undefined) { - console.log('Loading selected theme:', theme); - colorScheme.set(theme as ColorSchemeType); - } else { - console.log('No custom theme found, defaulting to dark mode'); - colorScheme.set('dark'); + applyColorScheme(theme as ColorSchemeType); } } catch (error) { console.error('Failed to load selected theme:', error); diff --git a/src/lib/hooks/use-selected-theme.web.tsx b/src/lib/hooks/use-selected-theme.web.tsx index e3d33ca..5a76d63 100644 --- a/src/lib/hooks/use-selected-theme.web.tsx +++ b/src/lib/hooks/use-selected-theme.web.tsx @@ -1,11 +1,30 @@ -import { colorScheme, useColorScheme } from 'nativewind'; import React, { useEffect, useState } from 'react'; const SELECTED_THEME = 'SELECTED_THEME'; export type ColorSchemeType = 'light' | 'dark' | 'system'; +const getSystemColorMode = (): 'light' | 'dark' => { + if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + return 'light'; +}; + +// NativeWind v5 web: the class-based `dark:` variant is driven by the +// element class (see @custom-variant in global.css), so apply the resolved +// scheme directly to the document element — same approach as the +// GluestackUIProvider web script. react-native-web's Appearance has no +// setColorScheme, so the native Appearance override path is unavailable here. +const applyColorScheme = (t: ColorSchemeType) => { + if (typeof document === 'undefined') return; + const documentElement = document.documentElement; + const resolved = t === 'system' ? getSystemColorMode() : t; + documentElement.classList.remove(resolved === 'light' ? 'dark' : 'light'); + documentElement.classList.add(resolved); + documentElement.style.colorScheme = resolved; +}; + export const useSelectedTheme = () => { - const { colorScheme: _color, setColorScheme } = useColorScheme(); const [theme, setThemeState] = useState(undefined); useEffect(() => { @@ -17,14 +36,11 @@ export const useSelectedTheme = () => { } }, []); - const setSelectedTheme = React.useCallback( - (t: ColorSchemeType) => { - setColorScheme(t); - setThemeState(t); - if (typeof localStorage !== 'undefined') localStorage.setItem(SELECTED_THEME, t); - }, - [setColorScheme] - ); + const setSelectedTheme = React.useCallback((t: ColorSchemeType) => { + applyColorScheme(t); + setThemeState(t); + if (typeof localStorage !== 'undefined') localStorage.setItem(SELECTED_THEME, t); + }, []); const selectedTheme = (theme ?? 'dark') as ColorSchemeType; return { selectedTheme, setSelectedTheme } as const; @@ -36,15 +52,9 @@ export const loadSelectedTheme = () => { // Not in a browser environment (e.g. Electron preload / SSR), skip return; } - const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem(SELECTED_THEME) : null; - if (storedTheme) { - console.log('Loading selected theme:', storedTheme); - colorScheme.set(storedTheme as ColorSchemeType); - } else { - console.log('No custom theme found, defaulting to dark mode'); - colorScheme.set('dark'); - } - return; + const storedTheme = typeof localStorage !== 'undefined' ? (localStorage.getItem(SELECTED_THEME) as ColorSchemeType | null) : null; + // Default to dark mode when no custom theme has been chosen + applyColorScheme(storedTheme ?? 'dark'); } catch (error) { console.error('Failed to load selected theme:', error); } diff --git a/src/lib/i18n/utils.web.tsx b/src/lib/i18n/utils.web.tsx index bd1f064..4fb8b87 100644 --- a/src/lib/i18n/utils.web.tsx +++ b/src/lib/i18n/utils.web.tsx @@ -44,15 +44,23 @@ export const useSelectedLanguage = () => { const [language, setLangState] = useState(undefined); useEffect(() => { - const storedLang = localStorage.getItem(LOCAL); - if (storedLang) { - setLangState(storedLang); + try { + const storedLang = localStorage.getItem(LOCAL); + if (storedLang) { + setLangState(storedLang); + } + } catch (e) { + console.warn('localStorage.getItem failed', e); } }, []); const setLanguage = useCallback((lang: Language) => { setLangState(lang); - localStorage.setItem(LOCAL, lang); + try { + localStorage.setItem(LOCAL, lang); + } catch (e) { + console.warn('localStorage.setItem failed', e); + } if (lang !== undefined) changeLanguage(lang as Language); }, []); diff --git a/src/lib/server-url.ts b/src/lib/server-url.ts index c555024..bfbef9c 100644 --- a/src/lib/server-url.ts +++ b/src/lib/server-url.ts @@ -10,8 +10,10 @@ import { Env } from '@/lib/env'; // Sentinel value used for the "Custom" option in the hosted-site dropdown. export const CUSTOM_SERVER_VALUE = '__custom__'; -// Matches a URL that starts with http:// or https://. -export const URL_PATTERN = /^https?:\/\/.+/; +// Matches https:// URLs. Plain http:// is only allowed for local development +// hosts (localhost / 127.0.0.1) - anything else would send the password grant +// and bearer tokens in cleartext. +export const URL_PATTERN = /^(https:\/\/.+|http:\/\/(localhost|127\.0\.0\.1)(:\d+)?([/?#].*)?)$/i; // The API path suffix (e.g. /api/v4) appended to every base URL. const API_PATH_SUFFIX = `/api/${Env.API_VERSION}`; diff --git a/src/lib/storage/index.tsx b/src/lib/storage/index.tsx index 4e9a456..26ed0ff 100644 --- a/src/lib/storage/index.tsx +++ b/src/lib/storage/index.tsx @@ -16,8 +16,13 @@ if (Platform.OS === 'web') { const IS_FIRST_TIME = 'IS_FIRST_TIME'; export function getItem(key: string): T | null { - const value = storage.getString(key); - return value ? JSON.parse(value) : null; + try { + const value = storage.getString(key); + return value ? JSON.parse(value) : null; + } catch (error) { + console.error('Error reading from storage', { key, error }); + return null; + } } export async function setItem(key: string, value: T) { diff --git a/src/lib/storage/index.web.tsx b/src/lib/storage/index.web.tsx index 4a79709..d2a1977 100644 --- a/src/lib/storage/index.web.tsx +++ b/src/lib/storage/index.web.tsx @@ -16,10 +16,33 @@ export const storage: any = { return null; } }, - set: (key: string, value: string) => { + getNumber: (key: string): number | undefined => { + if (!isLocalStorageAvailable) return undefined; + try { + const value = localStorage.getItem(key); + if (value === null) return undefined; + const num = Number(value); + return isNaN(num) ? undefined : num; + } catch (e) { + console.warn('localStorage.getNumber failed', e); + return undefined; + } + }, + getBoolean: (key: string): boolean | undefined => { + if (!isLocalStorageAvailable) return undefined; + try { + const value = localStorage.getItem(key); + if (value === null) return undefined; + return value === 'true'; + } catch (e) { + console.warn('localStorage.getBoolean failed', e); + return undefined; + } + }, + set: (key: string, value: string | number | boolean) => { if (!isLocalStorageAvailable) return; try { - localStorage.setItem(key, value); + localStorage.setItem(key, String(value)); } catch (e) { console.warn('localStorage.setItem failed', e); } @@ -32,6 +55,14 @@ export const storage: any = { console.warn('localStorage.removeItem failed', e); } }, + clearAll: () => { + if (!isLocalStorageAvailable) return; + try { + localStorage.clear(); + } catch (e) { + console.warn('localStorage.clearAll failed', e); + } + }, getAllKeys: (): string[] => { if (!isLocalStorageAvailable) return []; try { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9e1ceea..7a55c9a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -116,6 +116,9 @@ export function getAvatarUrl(userId: string) { } export function invertColor(hex: string, bw: boolean): string { + if (!hex) { + return '#000000'; + } if (hex.indexOf('#') === 0) { hex = hex.slice(1); } @@ -128,8 +131,12 @@ export function invertColor(hex: string, bw: boolean): string { hex = h0 + h0 + h1 + h1 + h2 + h2; } } - if (hex.length !== 6) { - throw new Error('Invalid HEX color.'); + // strip alpha channel from 8-digit hex. + if (hex.length === 8) { + hex = hex.slice(0, 6); + } + if (!/^[0-9a-fA-F]{6}$/.test(hex)) { + return '#000000'; } let r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), @@ -239,9 +246,13 @@ export function parseDateISOString(s: string | undefined | null): Date | null { // Ensure we have all required parts const [year, month, day, hour = '0', minute = '0', second = '0'] = b; if (!year || !month || !day) { - throw new Error('Invalid date string format'); + return null; } - return new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10), parseInt(hour, 10), parseInt(minute, 10), parseInt(second, 10)); + const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10), parseInt(hour, 10), parseInt(minute, 10), parseInt(second, 10)); + if (isNaN(date.getTime())) { + return null; + } + return date; } export function getDate(date: string): string { @@ -473,6 +484,10 @@ export function getTimeAgo(time: any, floor: number = 0): string { time = +new Date(); } + if (typeof time !== 'number' || isNaN(time)) { + return 'Unknown'; + } + const timeFormats = [ [60, 'seconds', 1], // 60 [120, '1 minute ago', '1 minute from now'], // 60*2 @@ -522,7 +537,7 @@ export function getTimeAgo(time: any, floor: number = 0): string { } } } - return time; + return time.toString(); } export function getTimeAgoUtc(time: any): string { @@ -545,6 +560,10 @@ export function getTimeAgoUtc(time: any): string { time = +new Date(); } + if (typeof time !== 'number' || isNaN(time)) { + return 'Unknown'; + } + const currentDate = new Date(); time = Number(new Date(time).getTime() + 0 * 60 * 1000); @@ -593,5 +612,5 @@ export function getTimeAgoUtc(time: any): string { } } } - return time; + return time.toString(); } diff --git a/src/services/__tests__/signalr.service.test.ts b/src/services/__tests__/signalr.service.test.ts index a55dd67..46d17f1 100644 --- a/src/services/__tests__/signalr.service.test.ts +++ b/src/services/__tests__/signalr.service.test.ts @@ -106,7 +106,7 @@ describe('SignalRService', () => { }) ); expect(mockBuilderInstance.withAutomaticReconnect).toHaveBeenCalledWith([0, 2000, 5000, 10000, 30000]); - expect(mockBuilderInstance.configureLogging).toHaveBeenCalledWith(LogLevel.Information); + expect(mockBuilderInstance.configureLogging).toHaveBeenCalledWith(LogLevel.Warning); expect(mockConnection.start).toHaveBeenCalled(); }); diff --git a/src/services/audio.service.web.ts b/src/services/audio.service.web.ts index 093590f..f3913fa 100644 --- a/src/services/audio.service.web.ts +++ b/src/services/audio.service.web.ts @@ -34,8 +34,8 @@ class AudioService { } try { - // Create Web Audio API context - this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + // Create Web Audio API context lazily on first use + this.ensureAudioContext(); // Pre-load audio assets await this.preloadAudioAssets(); @@ -53,6 +53,26 @@ class AudioService { } } + private ensureAudioContext(): AudioContext | null { + if (this.audioContext || typeof window === 'undefined') { + return this.audioContext; + } + + this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + + // Autoplay policy: contexts created before a user gesture start suspended - + // resume once the user interacts with the page + const resumeOnGesture = () => { + if (this.audioContext && this.audioContext.state === 'suspended') { + this.audioContext.resume().catch(() => {}); + } + }; + window.addEventListener('pointerdown', resumeOnGesture, { once: true }); + window.addEventListener('keydown', resumeOnGesture, { once: true }); + + return this.audioContext; + } + public async preloadAudioAssets(): Promise { try { // Web audio files would be loaded from the assets directory @@ -99,7 +119,9 @@ class AudioService { } private async playAudioBuffer(bufferName: string): Promise { - if (!this.audioContext || !this.audioBuffers.has(bufferName)) { + const audioContext = this.ensureAudioContext(); + + if (!audioContext || !this.audioBuffers.has(bufferName)) { logger.warn({ message: `Audio buffer not found: ${bufferName}`, }); @@ -110,9 +132,13 @@ class AudioService { const buffer = this.audioBuffers.get(bufferName); if (!buffer) return; - const source = this.audioContext.createBufferSource(); + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } + + const source = audioContext.createBufferSource(); source.buffer = buffer; - source.connect(this.audioContext.destination); + source.connect(audioContext.destination); source.start(0); } catch (error) { logger.error({ diff --git a/src/services/signalr.service.ts b/src/services/signalr.service.ts index ac52858..ad23e9a 100644 --- a/src/services/signalr.service.ts +++ b/src/services/signalr.service.ts @@ -3,6 +3,7 @@ import { Platform } from 'react-native'; import { Env } from '@/lib/env'; import { logger } from '@/lib/logging'; +import { isElectron } from '@/lib/platform'; import useAuthStore from '@/stores/auth/store'; export interface SignalRHubConfig { @@ -101,6 +102,12 @@ class SignalRService { return; } + // In Electron, minimizing the window sets visibilityState to 'hidden', + // which would stall reconnects; skip visibility-based handling there + if (isElectron()) { + return; + } + this.visibilityChangeHandler = () => { const wasVisible = this.isPageVisible; this.isPageVisible = document.visibilityState === 'visible'; @@ -380,7 +387,7 @@ class SignalRService { } ) .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) - .configureLogging(LogLevel.Information); + .configureLogging(LogLevel.Warning); const connection = connectionBuilder.build(); @@ -563,7 +570,7 @@ class SignalRService { accessTokenFactory: () => token, }) .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) - .configureLogging(LogLevel.Information) + .configureLogging(LogLevel.Warning) .build(); // Set up event handlers @@ -682,8 +689,8 @@ class SignalRService { // Remove the timeout from tracking this.reconnectTimeouts.delete(hubName); - // On web, check if page is visible before reconnecting - if (Platform.OS === 'web' && !this.isPageVisible) { + // On web (except Electron, where minimize hides the window), check if page is visible before reconnecting + if (Platform.OS === 'web' && !isElectron() && !this.isPageVisible) { logger.debug({ message: `Skipping reconnection for hub ${hubName} - page is not visible`, }); @@ -969,7 +976,16 @@ class SignalRService { } private emit(event: string, data: unknown): void { - this.eventListeners.get(event)?.forEach((callback) => callback(data)); + this.eventListeners.get(event)?.forEach((callback) => { + try { + callback(data); + } catch (error) { + logger.error({ + message: `Error in event listener for event: ${event}`, + context: { error }, + }); + } + }); } /** diff --git a/src/stores/app/audio-stream-store.ts b/src/stores/app/audio-stream-store.ts index 440552d..98259dc 100644 --- a/src/stores/app/audio-stream-store.ts +++ b/src/stores/app/audio-stream-store.ts @@ -5,6 +5,16 @@ import { getDepartmentAudioStreams } from '@/api/voice'; import { logger } from '@/lib/logging'; import { type DepartmentAudioResultStreamData } from '@/models/v4/voice/departmentAudioResultStreamData'; +// Tracks the pending stream-restart timeout so it can be cancelled when playback stops +let replayTimeout: ReturnType | null = null; + +const clearReplayTimeout = () => { + if (replayTimeout) { + clearTimeout(replayTimeout); + replayTimeout = null; + } +}; + interface AudioStreamState { // Available streams availableStreams: DepartmentAudioResultStreamData[]; @@ -132,7 +142,9 @@ export const useAudioStreamStore = create((set, get) => ({ // For live streams, try to reconnect const { currentStream } = get(); if (currentStream?.Id === stream.Id) { - setTimeout(async () => { + clearReplayTimeout(); + replayTimeout = setTimeout(async () => { + replayTimeout = null; try { await sound.replayAsync(); } catch (replayError) { @@ -194,6 +206,8 @@ export const useAudioStreamStore = create((set, get) => ({ stopStream: async () => { try { + clearReplayTimeout(); + const { soundObject, currentStream } = get(); if (soundObject) { diff --git a/src/stores/app/livekit-store.ts b/src/stores/app/livekit-store.ts index ce13d19..132f2d0 100644 --- a/src/stores/app/livekit-store.ts +++ b/src/stores/app/livekit-store.ts @@ -1,9 +1,21 @@ -import notifee, { AndroidImportance } from '@notifee/react-native'; +import type notifeeType from '@notifee/react-native'; +import type { AndroidImportance as AndroidImportanceType } from '@notifee/react-native'; import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from 'expo-audio'; import { Room, RoomEvent, Track } from 'livekit-client'; import { Platform } from 'react-native'; import { create } from 'zustand'; +// Notifee is native-only - conditionally require it so web module evaluation doesn't crash +let notifee: typeof notifeeType | null = null; +let AndroidImportance: typeof AndroidImportanceType | null = null; + +if (Platform.OS === 'android') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + notifee = require('@notifee/react-native').default; + // eslint-disable-next-line @typescript-eslint/no-var-requires + AndroidImportance = require('@notifee/react-native').AndroidImportance; +} + import { getCanConnectToVoiceSession, getDepartmentVoiceSettings } from '../../api/voice'; import { logger } from '../../lib/logging'; import { type DepartmentVoiceChannelResultData } from '../../models/v4/voice/departmentVoiceResultData'; @@ -430,7 +442,7 @@ export const useLiveKitStore = create((set, get) => ({ }, startAndroidForegroundService: async () => { - if (Platform.OS !== 'android') return; + if (Platform.OS !== 'android' || !notifee || !AndroidImportance) return; try { logger.debug({ @@ -477,7 +489,7 @@ export const useLiveKitStore = create((set, get) => ({ }, stopAndroidForegroundService: async () => { - if (Platform.OS !== 'android') return; + if (Platform.OS !== 'android' || !notifee) return; try { logger.debug({ diff --git a/src/stores/auth/store.tsx b/src/stores/auth/store.tsx index 72d672f..862a482 100644 --- a/src/stores/auth/store.tsx +++ b/src/stores/auth/store.tsx @@ -6,7 +6,7 @@ import { createJSONStorage, persist } from 'zustand/middleware'; import { logger } from '@/lib/logging'; -import { loginRequest, refreshTokenRequest } from '../../lib/auth/api'; +import { clearPasswordVerificationHash, loginRequest, refreshTokenRequest, storePasswordVerificationHash } from '../../lib/auth/api'; import type { AuthResponse, AuthState, LoginCredentials } from '../../lib/auth/types'; import { type ProfileModel } from '../../lib/auth/types'; @@ -60,7 +60,12 @@ const useAuthStore = create()( if (!response.authResponse || !response.authResponse.id_token) { logger.error({ message: 'Login: Missing auth response or id_token', - context: { authResponse: response.authResponse }, + context: { + hasAuthResponse: !!response.authResponse, + hasIdToken: !!response.authResponse?.id_token, + hasAccessToken: !!response.authResponse?.access_token, + hasRefreshToken: !!response.authResponse?.refresh_token, + }, }); throw new Error('Invalid authentication response: missing token data'); } @@ -95,6 +100,9 @@ const useAuthStore = create()( userId: profileData.sub, }); + // Cache a salted hash of the password for lockscreen verification + await storePasswordVerificationHash(credentials.password); + logger.info({ message: 'Login: State updated to signedIn', context: { userId: profileData.sub }, @@ -140,6 +148,8 @@ const useAuthStore = create()( message: 'Logout: Clearing auth state', }); + await clearPasswordVerificationHash(); + set({ accessToken: null, refreshToken: null, @@ -195,6 +205,9 @@ const useAuthStore = create()( try { set({ status: 'loading', error: null }); + // SSO logins have no password - drop any cached password verification hash + await clearPasswordVerificationHash(); + const tokenToDecode = authResponse.id_token || authResponse.access_token; let profileData: ProfileModel; diff --git a/src/stores/dispatch/dashboard-view-store.ts b/src/stores/dispatch/dashboard-view-store.ts index 062fce4..e87fcf4 100644 --- a/src/stores/dispatch/dashboard-view-store.ts +++ b/src/stores/dispatch/dashboard-view-store.ts @@ -1,4 +1,7 @@ import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; + +import { zustandStorage } from '@/lib/storage'; /** * Dispatch-dashboard resource-view preferences. @@ -9,21 +12,46 @@ import { create } from 'zustand'; * the separate Units and Personnel panels. * * The resource panels read these directly so no prop-drilling is needed across the responsive layouts. + * + * `collapsedCards` tracks each dashboard card's collapsed state. Together with the view flags above + * it is persisted to device/browser storage so the console looks the same when the user signs back in. */ +export type DashboardCardKey = 'active-calls' | 'units' | 'personnel' | 'notes' | 'resources' | 'map' | 'activity-log'; + interface DashboardViewState { availableOnly: boolean; singleList: boolean; + collapsedCards: Partial>; toggleAvailableOnly: () => void; toggleSingleList: () => void; setAvailableOnly: (value: boolean) => void; setSingleList: (value: boolean) => void; + setCardCollapsed: (card: DashboardCardKey, collapsed: boolean) => void; } -export const useDashboardViewStore = create((set) => ({ - availableOnly: false, - singleList: false, - toggleAvailableOnly: () => set((state) => ({ availableOnly: !state.availableOnly })), - toggleSingleList: () => set((state) => ({ singleList: !state.singleList })), - setAvailableOnly: (value) => set({ availableOnly: value }), - setSingleList: (value) => set({ singleList: value }), -})); +export const useDashboardViewStore = create()( + persist( + (set) => ({ + availableOnly: false, + singleList: false, + collapsedCards: {}, + toggleAvailableOnly: () => set((state) => ({ availableOnly: !state.availableOnly })), + toggleSingleList: () => set((state) => ({ singleList: !state.singleList })), + setAvailableOnly: (value) => set({ availableOnly: value }), + setSingleList: (value) => set({ singleList: value }), + setCardCollapsed: (card, collapsed) => set((state) => ({ collapsedCards: { ...state.collapsedCards, [card]: collapsed } })), + }), + { + name: 'dashboard-view-storage', + storage: createJSONStorage(() => zustandStorage), + partialize: (state) => ({ + availableOnly: state.availableOnly, + singleList: state.singleList, + collapsedCards: state.collapsedCards, + }), + } + ) +); + +/** Selector helper: collapsed flag for a single card (defaults to expanded). */ +export const selectCardCollapsed = (card: DashboardCardKey) => (state: DashboardViewState) => state.collapsedCards[card] ?? false; diff --git a/src/stores/signalr/signalr-store.ts b/src/stores/signalr/signalr-store.ts index 0242d63..5ed3eca 100644 --- a/src/stores/signalr/signalr-store.ts +++ b/src/stores/signalr/signalr-store.ts @@ -223,7 +223,15 @@ export const useSignalRStore = create((set, get) => ({ ], }); - await signalRService.invoke(Env.CHANNEL_HUB_NAME, 'connect', parseInt(securityStore.getState().rights?.DepartmentId ?? '0')); + const departmentId = Number(securityStore.getState().rights?.DepartmentId ?? '0'); + if (Number.isFinite(departmentId)) { + await signalRService.invoke(Env.CHANNEL_HUB_NAME, 'connect', departmentId); + } else { + logger.error({ + message: 'Invalid DepartmentId, skipping update hub connect invoke', + context: { departmentId: securityStore.getState().rights?.DepartmentId }, + }); + } // Create and register handlers with stored references for cleanup updateHubHandlers.personnelStatusUpdated = (message: unknown) => { @@ -231,7 +239,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'personnelStatusUpdated', context: { message }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'personnelStatusUpdated', lastPersonnelUpdateTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'personnelStatusUpdated', lastPersonnelUpdateTimestamp: Date.now() }); }; signalRService.on('personnelStatusUpdated', updateHubHandlers.personnelStatusUpdated); @@ -240,7 +248,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'personnelStaffingUpdated', context: { message }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'personnelStaffingUpdated', lastPersonnelUpdateTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'personnelStaffingUpdated', lastPersonnelUpdateTimestamp: Date.now() }); }; signalRService.on('personnelStaffingUpdated', updateHubHandlers.personnelStaffingUpdated); @@ -259,7 +267,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'callsUpdated', context: { message, now }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: now, lastEventType: 'callsUpdated', lastCallsUpdateTimestamp: now }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: now, lastEventType: 'callsUpdated', lastCallsUpdateTimestamp: now }); }; signalRService.on('callsUpdated', updateHubHandlers.callsUpdated); @@ -268,7 +276,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'callAdded', context: { message }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'callAdded', lastCallsUpdateTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'callAdded', lastCallsUpdateTimestamp: Date.now() }); }; signalRService.on('callAdded', updateHubHandlers.callAdded); @@ -277,7 +285,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'callClosed', context: { message }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'callClosed', lastCallsUpdateTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'callClosed', lastCallsUpdateTimestamp: Date.now() }); }; signalRService.on('callClosed', updateHubHandlers.callClosed); @@ -286,7 +294,7 @@ export const useSignalRStore = create((set, get) => ({ message: 'checkInUpdated', context: { message }, }); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'checkInUpdated', lastCheckInUpdateTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'checkInUpdated', lastCheckInUpdateTimestamp: Date.now() }); }; signalRService.on('checkInUpdated', updateHubHandlers.checkInUpdated); @@ -317,7 +325,7 @@ export const useSignalRStore = create((set, get) => ({ // Lazy import to avoid circular dependency const { useWeatherAlertsStore } = require('../weatherAlerts/store'); useWeatherAlertsStore.getState().handleAlertReceived(alertId); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertReceived', lastWeatherAlertTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertReceived', lastWeatherAlertTimestamp: Date.now() }); }; signalRService.on('weatherAlertReceived', updateHubHandlers.weatherAlertReceived); @@ -333,7 +341,7 @@ export const useSignalRStore = create((set, get) => ({ } const { useWeatherAlertsStore } = require('../weatherAlerts/store'); useWeatherAlertsStore.getState().handleAlertUpdated(alertId); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertUpdated', lastWeatherAlertTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertUpdated', lastWeatherAlertTimestamp: Date.now() }); }; signalRService.on('weatherAlertUpdated', updateHubHandlers.weatherAlertUpdated); @@ -349,7 +357,7 @@ export const useSignalRStore = create((set, get) => ({ } const { useWeatherAlertsStore } = require('../weatherAlerts/store'); useWeatherAlertsStore.getState().handleAlertExpired(alertId); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertExpired', lastWeatherAlertTimestamp: Date.now() }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: Date.now(), lastEventType: 'weatherAlertExpired', lastWeatherAlertTimestamp: Date.now() }); }; signalRService.on('weatherAlertExpired', updateHubHandlers.weatherAlertExpired); @@ -362,7 +370,7 @@ export const useSignalRStore = create((set, get) => ({ // call" notification — the client refetches the affected command board. const callId = extractAlertId(message); const now = Date.now(); - set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: now, lastEventType: 'incidentCommandUpdated', lastIncidentCommandUpdateTimestamp: now }); + set({ lastUpdateMessage: null, lastUpdateTimestamp: now, lastEventType: 'incidentCommandUpdated', lastIncidentCommandUpdateTimestamp: now }); if (callId) { // Lazy import to avoid circular dependency const { useIncidentCommandStore } = require('../incident-command/store'); diff --git a/src/stores/toast/store.ts b/src/stores/toast/store.ts index f58856e..e3a0f7f 100644 --- a/src/stores/toast/store.ts +++ b/src/stores/toast/store.ts @@ -7,27 +7,36 @@ interface ToastMessage { type: ToastType; title?: string; message: string; + /** Tap handler - e.g. navigate to a detail screen. */ + onPress?: () => void; + /** Auto-dismiss delay in ms (default 3000). */ + duration?: number; +} + +interface ToastOptions { + onPress?: () => void; + duration?: number; } interface ToastStore { toasts: ToastMessage[]; - showToast: (type: ToastType, message: string, title?: string) => void; + showToast: (type: ToastType, message: string, title?: string, options?: ToastOptions) => void; removeToast: (id: string) => void; } export const useToastStore = create((set) => ({ toasts: [], - showToast: (type, message, title) => { + showToast: (type, message, title, options) => { const id = Math.random().toString(36).substring(7); set((state) => ({ - toasts: [...state.toasts, { id, type, message, title }], + toasts: [...state.toasts, { id, type, message, title, onPress: options?.onPress, duration: options?.duration }], })); - // Auto remove toast after 3 seconds + // Auto remove toast after the requested duration (default 3 seconds) setTimeout(() => { set((state) => ({ toasts: state.toasts.filter((toast) => toast.id !== id), })); - }, 3000); + }, options?.duration ?? 3000); }, removeToast: (id) => { set((state) => ({ diff --git a/src/stores/weatherAlerts/store.ts b/src/stores/weatherAlerts/store.ts index 6e6063e..f25b5d2 100644 --- a/src/stores/weatherAlerts/store.ts +++ b/src/stores/weatherAlerts/store.ts @@ -25,7 +25,9 @@ import { type WeatherAlertZoneResultData } from '@/models/v4/weatherAlerts/weath function sortAlerts(a: WeatherAlertResultData, b: WeatherAlertResultData): number { if (a.Severity !== b.Severity) return a.Severity - b.Severity; - return new Date(b.EffectiveUtc).getTime() - new Date(a.EffectiveUtc).getTime(); + const timeA = new Date(a.EffectiveUtc).getTime(); + const timeB = new Date(b.EffectiveUtc).getTime(); + return (isNaN(timeB) ? 0 : timeB) - (isNaN(timeA) ? 0 : timeA); } interface WeatherAlertsState { diff --git a/src/translations/ar.json b/src/translations/ar.json index fdd19eb..cdb376c 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -1265,7 +1265,8 @@ "source_type_national_weather_service": "خدمة الطقس الوطنية", "source_type_environment_canada": "هيئة البيئة الكندية", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "تنبيهات الطقس" }, "videoFeeds": { "title": "بث الفيديو", diff --git a/src/translations/de.json b/src/translations/de.json index d797900..abd2f8b 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Nationaler Wetterdienst", "source_type_environment_canada": "Umweltbehörde Kanada", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Wetterwarnungen" }, "videoFeeds": { "title": "Videoübertragungen", diff --git a/src/translations/en.json b/src/translations/en.json index d7c7216..a3edfe5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -503,7 +503,7 @@ "zip": "Zip Code" }, "form": { - "invalid_url": "Please enter a valid URL starting with http:// or https://", + "invalid_url": "Please enter a valid URL starting with https:// (http:// is only allowed for localhost)", "required": "This field is required" }, "livekit": { @@ -591,6 +591,7 @@ "title": "Lock Screen", "unlock_button": "Unlock", "unlock_failed": "Failed to unlock. Please try again.", + "relogin_required": "Password verification is unavailable for this session. Please log in again.", "unlocking": "Unlocking...", "welcome_back": "Welcome Back" }, @@ -1264,7 +1265,8 @@ "source_type_national_weather_service": "National Weather Service", "source_type_environment_canada": "Environment Canada", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Weather Alerts" }, "videoFeeds": { "title": "Video Feeds", diff --git a/src/translations/es.json b/src/translations/es.json index daf18d7..7d217bd 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -1269,7 +1269,8 @@ "source_type_national_weather_service": "Servicio Meteorológico Nacional", "source_type_environment_canada": "Medio Ambiente de Canadá", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Alertas Meteorológicas" }, "videoFeeds": { "title": "Transmisiones de Video", diff --git a/src/translations/fr.json b/src/translations/fr.json index 978b0aa..74da6c8 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Service météorologique national", "source_type_environment_canada": "Environnement Canada", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Alertes météo" }, "videoFeeds": { "title": "Flux vidéo", diff --git a/src/translations/it.json b/src/translations/it.json index 3d16b59..a641401 100644 --- a/src/translations/it.json +++ b/src/translations/it.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Servizio meteorologico nazionale", "source_type_environment_canada": "Ambiente Canada", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Allerte meteo" }, "videoFeeds": { "title": "Feed video", diff --git a/src/translations/pl.json b/src/translations/pl.json index 1728c4f..ff3f2ca 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Krajowa Służba Pogodowa", "source_type_environment_canada": "Środowisko Kanady", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Alerty pogodowe" }, "videoFeeds": { "title": "Transmisje wideo", diff --git a/src/translations/sv.json b/src/translations/sv.json index a2bb077..6756ae6 100644 --- a/src/translations/sv.json +++ b/src/translations/sv.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Nationella vädertjänsten", "source_type_environment_canada": "Kanadas miljömyndighet", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Vädervarningar" }, "videoFeeds": { "title": "Videoflöden", diff --git a/src/translations/uk.json b/src/translations/uk.json index 4853c54..3338f06 100644 --- a/src/translations/uk.json +++ b/src/translations/uk.json @@ -1264,7 +1264,8 @@ "source_type_national_weather_service": "Національна метеорологічна служба", "source_type_environment_canada": "Міністерство довкілля Канади", "source_type_meteoalarm": "MeteoAlarm" - } + }, + "stats_label": "Погодні попередження" }, "videoFeeds": { "title": "Відеопотоки", diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 873fc78..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-nocheck -const gluestackPlugin = require('@gluestack-ui/nativewind-utils/tailwind-plugin'); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class', - content: ['./src/**/*.{html,js,jsx,ts,tsx}', './src/components/**/*.{html,js,jsx,ts,tsx,mdx}', './src/hooks/**/*.{html,js,jsx,ts,tsx,mdx}'], - presets: [require('nativewind/preset')], - theme: { - extend: { - colors: { - primary: { - 0: 'rgb(var(--color-primary-0)/)', - 50: 'rgb(var(--color-primary-50)/)', - 100: 'rgb(var(--color-primary-100)/)', - 200: 'rgb(var(--color-primary-200)/)', - 300: 'rgb(var(--color-primary-300)/)', - 400: 'rgb(var(--color-primary-400)/)', - 500: 'rgb(var(--color-primary-500)/)', - 600: 'rgb(var(--color-primary-600)/)', - 700: 'rgb(var(--color-primary-700)/)', - 800: 'rgb(var(--color-primary-800)/)', - 900: 'rgb(var(--color-primary-900)/)', - 950: 'rgb(var(--color-primary-950)/)', - }, - secondary: { - 0: 'rgb(var(--color-secondary-0)/)', - 50: 'rgb(var(--color-secondary-50)/)', - 100: 'rgb(var(--color-secondary-100)/)', - 200: 'rgb(var(--color-secondary-200)/)', - 300: 'rgb(var(--color-secondary-300)/)', - 400: 'rgb(var(--color-secondary-400)/)', - 500: 'rgb(var(--color-secondary-500)/)', - 600: 'rgb(var(--color-secondary-600)/)', - 700: 'rgb(var(--color-secondary-700)/)', - 800: 'rgb(var(--color-secondary-800)/)', - 900: 'rgb(var(--color-secondary-900)/)', - 950: 'rgb(var(--color-secondary-950)/)', - }, - tertiary: { - 50: 'rgb(var(--color-tertiary-50)/)', - 100: 'rgb(var(--color-tertiary-100)/)', - 200: 'rgb(var(--color-tertiary-200)/)', - 300: 'rgb(var(--color-tertiary-300)/)', - 400: 'rgb(var(--color-tertiary-400)/)', - 500: 'rgb(var(--color-tertiary-500)/)', - 600: 'rgb(var(--color-tertiary-600)/)', - 700: 'rgb(var(--color-tertiary-700)/)', - 800: 'rgb(var(--color-tertiary-800)/)', - 900: 'rgb(var(--color-tertiary-900)/)', - 950: 'rgb(var(--color-tertiary-950)/)', - }, - error: { - 0: 'rgb(var(--color-error-0)/)', - 50: 'rgb(var(--color-error-50)/)', - 100: 'rgb(var(--color-error-100)/)', - 200: 'rgb(var(--color-error-200)/)', - 300: 'rgb(var(--color-error-300)/)', - 400: 'rgb(var(--color-error-400)/)', - 500: 'rgb(var(--color-error-500)/)', - 600: 'rgb(var(--color-error-600)/)', - 700: 'rgb(var(--color-error-700)/)', - 800: 'rgb(var(--color-error-800)/)', - 900: 'rgb(var(--color-error-900)/)', - 950: 'rgb(var(--color-error-950)/)', - }, - success: { - 0: 'rgb(var(--color-success-0)/)', - 50: 'rgb(var(--color-success-50)/)', - 100: 'rgb(var(--color-success-100)/)', - 200: 'rgb(var(--color-success-200)/)', - 300: 'rgb(var(--color-success-300)/)', - 400: 'rgb(var(--color-success-400)/)', - 500: 'rgb(var(--color-success-500)/)', - 600: 'rgb(var(--color-success-600)/)', - 700: 'rgb(var(--color-success-700)/)', - 800: 'rgb(var(--color-success-800)/)', - 900: 'rgb(var(--color-success-900)/)', - 950: 'rgb(var(--color-success-950)/)', - }, - warning: { - 0: 'rgb(var(--color-warning-0)/)', - 50: 'rgb(var(--color-warning-50)/)', - 100: 'rgb(var(--color-warning-100)/)', - 200: 'rgb(var(--color-warning-200)/)', - 300: 'rgb(var(--color-warning-300)/)', - 400: 'rgb(var(--color-warning-400)/)', - 500: 'rgb(var(--color-warning-500)/)', - 600: 'rgb(var(--color-warning-600)/)', - 700: 'rgb(var(--color-warning-700)/)', - 800: 'rgb(var(--color-warning-800)/)', - 900: 'rgb(var(--color-warning-900)/)', - 950: 'rgb(var(--color-warning-950)/)', - }, - info: { - 0: 'rgb(var(--color-info-0)/)', - 50: 'rgb(var(--color-info-50)/)', - 100: 'rgb(var(--color-info-100)/)', - 200: 'rgb(var(--color-info-200)/)', - 300: 'rgb(var(--color-info-300)/)', - 400: 'rgb(var(--color-info-400)/)', - 500: 'rgb(var(--color-info-500)/)', - 600: 'rgb(var(--color-info-600)/)', - 700: 'rgb(var(--color-info-700)/)', - 800: 'rgb(var(--color-info-800)/)', - 900: 'rgb(var(--color-info-900)/)', - 950: 'rgb(var(--color-info-950)/)', - }, - typography: { - 0: 'rgb(var(--color-typography-0)/)', - 50: 'rgb(var(--color-typography-50)/)', - 100: 'rgb(var(--color-typography-100)/)', - 200: 'rgb(var(--color-typography-200)/)', - 300: 'rgb(var(--color-typography-300)/)', - 400: 'rgb(var(--color-typography-400)/)', - 500: 'rgb(var(--color-typography-500)/)', - 600: 'rgb(var(--color-typography-600)/)', - 700: 'rgb(var(--color-typography-700)/)', - 800: 'rgb(var(--color-typography-800)/)', - 900: 'rgb(var(--color-typography-900)/)', - 950: 'rgb(var(--color-typography-950)/)', - white: '#FFFFFF', - gray: '#D4D4D4', - black: '#181718', - }, - outline: { - 0: 'rgb(var(--color-outline-0)/)', - 50: 'rgb(var(--color-outline-50)/)', - 100: 'rgb(var(--color-outline-100)/)', - 200: 'rgb(var(--color-outline-200)/)', - 300: 'rgb(var(--color-outline-300)/)', - 400: 'rgb(var(--color-outline-400)/)', - 500: 'rgb(var(--color-outline-500)/)', - 600: 'rgb(var(--color-outline-600)/)', - 700: 'rgb(var(--color-outline-700)/)', - 800: 'rgb(var(--color-outline-800)/)', - 900: 'rgb(var(--color-outline-900)/)', - 950: 'rgb(var(--color-outline-950)/)', - }, - background: { - 0: 'rgb(var(--color-background-0)/)', - 50: 'rgb(var(--color-background-50)/)', - 100: 'rgb(var(--color-background-100)/)', - 200: 'rgb(var(--color-background-200)/)', - 300: 'rgb(var(--color-background-300)/)', - 400: 'rgb(var(--color-background-400)/)', - 500: 'rgb(var(--color-background-500)/)', - 600: 'rgb(var(--color-background-600)/)', - 700: 'rgb(var(--color-background-700)/)', - 800: 'rgb(var(--color-background-800)/)', - 900: 'rgb(var(--color-background-900)/)', - 950: 'rgb(var(--color-background-950)/)', - error: 'rgb(var(--color-background-error)/)', - warning: 'rgb(var(--color-background-warning)/)', - muted: 'rgb(var(--color-background-muted)/)', - success: 'rgb(var(--color-background-success)/)', - info: 'rgb(var(--color-background-info)/)', - light: '#FBFBFB', - dark: '#181719', - }, - indicator: { - primary: 'rgb(var(--color-indicator-primary)/)', - info: 'rgb(var(--color-indicator-info)/)', - error: 'rgb(var(--color-indicator-error)/)', - }, - }, - fontFamily: { - heading: undefined, - body: undefined, - mono: undefined, - roboto: ['Roboto', 'sans-serif'], - }, - fontWeight: { - extrablack: '950', - }, - fontSize: { - '2xs': '10px', - }, - boxShadow: { - 'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)', - 'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)', - 'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)', - 'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)', - 'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)', - 'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)', - 'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)', - 'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)', - 'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)', - }, - }, - }, - plugins: [gluestackPlugin], -}; diff --git a/tsconfig.json b/tsconfig.json index b575069..84baaca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,5 +40,5 @@ "ios", "storybookDocsComponents" ], - "include": ["src/**/*.ts", "src/**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts", "__mocks__/**/*.ts", "app.config.ts", "jest-setup.ts", "jest-platform-setup.ts", "__tests__/**/*.ts", "__tests__/**/*.tsx", "types/**/*.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts", "react-native-css-env.d.ts", "__mocks__/**/*.ts", "app.config.ts", "jest-setup.ts", "jest-platform-setup.ts", "__tests__/**/*.ts", "__tests__/**/*.tsx", "types/**/*.ts"] } diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 287eba6..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-env node */ -const createExpoWebpackConfigAsync = require('@expo/webpack-config'); - -module.exports = async function (env, argv) { - const config = await createExpoWebpackConfigAsync(env, argv); - - // Ensure import.meta is properly transformed - config.module.rules.forEach((rule) => { - if (rule.oneOf) { - rule.oneOf.forEach((oneOfRule) => { - if (oneOfRule.use && Array.isArray(oneOfRule.use) && oneOfRule.use.some((use) => use.loader && use.loader.includes('babel-loader'))) { - oneOfRule.exclude = (input) => { - // Don't exclude problematic packages from transpilation - const shouldTranspile = /node_modules\/(acorn|cjs-module-lexer|@eslint|sucrase)/.test(input); - if (shouldTranspile) { - return false; - } - // Exclude other node_modules - return /node_modules/.test(input); - }; - } - }); - } - }); - - return config; -}; diff --git a/yarn.lock b/yarn.lock index a87e207..d3c69af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,34 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== +"@adobe/react-spectrum-ui@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz#31651a1c664b014bd7b4f8a2265e26ae418aecec" + integrity sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA== + +"@adobe/react-spectrum-workflow@2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz#3f5adc5fe2bae25416805b7d92c1ff5bd1e061f0" + integrity sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg== + +"@adobe/react-spectrum@^3.47.0": + version "3.47.2" + resolved "https://registry.yarnpkg.com/@adobe/react-spectrum/-/react-spectrum-3.47.2.tgz#0d2795b43eaa8bf469bfc2c7019cc4533879d2d5" + integrity sha512-QxsE7bPBGpzwYofACF0RAL/Zs3p0u9HNvCDf9LEN87rEGFpkagIB+T+TSZ0xbl6BJ6z7S02m3zAFk3s9FoHJpQ== + dependencies: + "@internationalized/date" "^3.12.2" + "@react-types/shared" "^3.36.0" + "@spectrum-icons/ui" "^3.7.1" + "@spectrum-icons/workflow" "^4.3.1" + "@swc/helpers" "^0.5.0" + client-only "^0.0.1" + clsx "^2.0.0" + react-aria "3.50.0" + react-aria-components "1.19.0" + react-stately "3.48.0" + react-transition-group "^4.4.5" + use-sync-external-store "^1.6.0" + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" @@ -225,7 +253,7 @@ "@babel/traverse" "^7.28.5" "@babel/types" "^7.28.5" -"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.25.9", "@babel/helper-module-imports@^7.27.1": +"@babel/helper-module-imports@^7.25.9", "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== @@ -929,11 +957,16 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.27.1" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.7": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== +"@babel/runtime@^7.24.4", "@babel/runtime@^7.27.1", "@babel/runtime@^7.5.5": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" + integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== + "@babel/template@^7.25.0", "@babel/template@^7.26.9", "@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.3.3": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" @@ -965,7 +998,7 @@ "@babel/types" "^7.28.4" debug "^4.3.1" -"@babel/traverse@^7.23.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.26.10", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4": +"@babel/traverse@^7.25.3", "@babel/traverse@^7.26.10", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== @@ -991,7 +1024,7 @@ "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.23.0", "@babel/types@^7.25.2", "@babel/types@^7.26.10", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.3.3": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.25.2", "@babel/types@^7.26.10", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.3.3": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== @@ -1312,6 +1345,14 @@ minimatch "^9.0.3" plist "^3.1.0" +"@emnapi/core@^1.11.1": + version "1.11.3" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.3.tgz#5e95348a42cd1e06f0b9aa380cd74091daa4d520" + integrity sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg== + dependencies: + "@emnapi/wasi-threads" "1.2.3" + tslib "^2.4.0" + "@emnapi/core@^1.4.3": version "1.5.0" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" @@ -1320,6 +1361,13 @@ "@emnapi/wasi-threads" "1.1.0" tslib "^2.4.0" +"@emnapi/runtime@^1.11.1": + version "1.11.3" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.3.tgz#84257ae3b0531eb2aec1ffa23d70700da007ba95" + integrity sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA== + dependencies: + tslib "^2.4.0" + "@emnapi/runtime@^1.4.3": version "1.5.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" @@ -1334,6 +1382,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/wasi-threads@1.2.3", "@emnapi/wasi-threads@^1.2.2": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz#c9bf72fd4be5b928aee894820e8d814ed73916e9" + integrity sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g== + dependencies: + tslib "^2.4.0" + "@emotion/is-prop-valid@^0.8.2": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" @@ -1764,7 +1819,7 @@ dependencies: tslib "^2.8.0" -"@formatjs/icu-messageformat-parser@2.11.2", "@formatjs/icu-messageformat-parser@^2.0.18": +"@formatjs/icu-messageformat-parser@^2.0.18": version "2.11.2" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz#85aea211bea40aa81ee1d44ac7accc3cf5500a73" integrity sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA== @@ -1788,335 +1843,61 @@ dependencies: tslib "^2.8.0" -"@gluestack-ui/accordion@~1.0.6": - version "1.0.14" - resolved "https://registry.yarnpkg.com/@gluestack-ui/accordion/-/accordion-1.0.14.tgz#22c1192557c9fcf958daf7a63fa17d35bbc6cbfe" - integrity sha512-dpqlSnIZk1grZOPtLoMYh08OGmRgl/Sjpv1KltPdpjNaG2Gt7DPAmT/8l9wVuJYMOfdsLOsBOfzGo5eECZ9WFg== - dependencies: - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/accordion" "^0.0.2" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/actionsheet@~0.2.44": - version "0.2.53" - resolved "https://registry.yarnpkg.com/@gluestack-ui/actionsheet/-/actionsheet-0.2.53.tgz#09608d32f373f99c0dff9fb62856895a843f1a05" - integrity sha512-93qHvq6BHezJ7wt2lce4OQ38wXCGsDtglj5nlmwo2T41vj4ubOtDVoSUhXT+hfH0EmRr0TxFNeFqIgesO46qVw== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.22" - "@gluestack-ui/transitions" "^0.1.11" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/dialog" "^0.0.5" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/alert-dialog@~0.1.30": - version "0.1.38" - resolved "https://registry.yarnpkg.com/@gluestack-ui/alert-dialog/-/alert-dialog-0.1.38.tgz#fd63bdbdf2ddd627aa3e55e4758a2c0be61b411c" - integrity sha512-qarZlXlmGHwjfOFaEJgLpNstDd38b7TBMdYtpb6xXIvOA6W6IyNnnKYPNasOiUlNrW8qvqrZrZc+xJfK5USr0w== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.22" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/dialog" "^0.0.5" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/alert@~0.1.15": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@gluestack-ui/alert/-/alert-0.1.16.tgz#cede162e04c3dcb07a3da797221852bb7c127311" - integrity sha512-vE0litmicuKFJFp9TY3mRuZmyH/E24IdHlC0f3FZjfa+AzeOS+bZdKMRix2EYkvBGrA6uNffWUuHeXWQGMRVJQ== - -"@gluestack-ui/avatar@~0.1.17": - version "0.1.18" - resolved "https://registry.yarnpkg.com/@gluestack-ui/avatar/-/avatar-0.1.18.tgz#bb890a7b889060e01faf549f59a47d11c04ada1c" - integrity sha512-VA9XwtavYLYCWrjxHc2u9gRpV97cPRcr/6KJ4tLiMiQbiRL1b4zckiL+/F39fB6xjUOUQHl3Fjo/Yd8swa0MBg== - dependencies: - "@gluestack-ui/utils" "^0.1.14" - -"@gluestack-ui/button@~1.0.14": - version "1.0.14" - resolved "https://registry.yarnpkg.com/@gluestack-ui/button/-/button-1.0.14.tgz#c8c2d60888df7aa472485792a6c848a194a933e0" - integrity sha512-Rqv5PY18jzeVACBwsVxXlyDbHjBiSXGVqY29VO6KHZGMMokCPW6MXYXT8ZpriLxMc1DRkgTtpTVYZCJBFaYTbg== - dependencies: - "@gluestack-ui/utils" "0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/checkbox@~0.1.31": - version "0.1.39" - resolved "https://registry.yarnpkg.com/@gluestack-ui/checkbox/-/checkbox-0.1.39.tgz#1c84a3a19120b9ab9ea1c9c6ac7e8771902bd194" - integrity sha512-CPL9+g9qIFuGGQAQuFySATTxrqGMhIx/ASpBdl/YC50Ec/d7gMZlnKww3N1og3a6JqMG8yeWwbH+GH3z07XsfA== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/utils" "^0.1.15" - "@react-aria/visually-hidden" "^3.8.6" - "@react-native-aria/checkbox" "^0.2.10" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/utils" "0.2.12" - "@react-stately/checkbox" "^3.4.2" - -"@gluestack-ui/divider@~0.1.9": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@gluestack-ui/divider/-/divider-0.1.10.tgz#506ec45f9b5183a0e3b293dbff7b7da19edcfda8" - integrity sha512-/hZx1Rmy4Pgln9AwAprAEQcxYPEHjHSNF4xCUWlK/q0peyiMT5Nagt54VnxykVn5A0b2zg5QKP0pOqOF9xeE6w== - -"@gluestack-ui/fab@~0.1.21": - version "0.1.28" - resolved "https://registry.yarnpkg.com/@gluestack-ui/fab/-/fab-0.1.28.tgz#603da60deabea09e71baddb82a4abe61262b9d7d" - integrity sha512-NTzTN08KyX3iqcC5LiRqymrO+TlCzfcjDS5GeO0Y+1AiH7loKwWwDm0PFn3TbflfycRTG0jw8RkUqs2Ckov/mg== - dependencies: - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/form-control@^0.1.19", "@gluestack-ui/form-control@~0.1.18": - version "0.1.19" - resolved "https://registry.yarnpkg.com/@gluestack-ui/form-control/-/form-control-0.1.19.tgz#7a75b65f5ec89da95c30442750d6f7af71db6868" - integrity sha512-6YbPbi/RZrXc5DyVPbxPV17FYaBoEl1yAdSwut8iE6n+yQekjluINrh2q5ZPWF2SGmyo7VSNcL85yeU5I97xHg== - dependencies: - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/hooks@0.1.13": - version "0.1.13" - resolved "https://registry.yarnpkg.com/@gluestack-ui/hooks/-/hooks-0.1.13.tgz#4f1494850bb6e95a1eaeef10c724c83adfe6954f" - integrity sha512-2x5EGcAuvdWFoOaqBRJsilR9Nx07tDvuAr3MI5SOmDdn7Bjzi+3CVAZwwDE2nGwAL3ldJvbAlzVkiNCw4TsSeA== - -"@gluestack-ui/icon@~0.1.27": - version "0.1.27" - resolved "https://registry.yarnpkg.com/@gluestack-ui/icon/-/icon-0.1.27.tgz#6a2241bcdf3d15e53e7d191922792b634e290b52" - integrity sha512-mIsfgdVUihtRORoeDxn3kMFph5XOXK+Scnj1JXLufbt7h9kbfaZLGhYDc9cp2zpPc1Xki9SC1yXYfw8y4FAAdA== - dependencies: - "@gluestack-ui/provider" "^0.1.19" - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/image@~0.1.10": - version "0.1.17" - resolved "https://registry.yarnpkg.com/@gluestack-ui/image/-/image-0.1.17.tgz#b92c788bea557c5a4e34231c5430398cc1220011" - integrity sha512-hcyTVBUw55bSjEpyynIZJVm3Ff6lcjKXFT55f890oy2PNGyFdsKr6V+t/jy5n/2fwbv1eR2KU0uDDiI2vmBh+A== - dependencies: - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/input@~0.1.38": - version "0.1.38" - resolved "https://registry.yarnpkg.com/@gluestack-ui/input/-/input-0.1.38.tgz#8430b5f4901e59475a99d5e3f473bd5b0d248a19" - integrity sha512-NzwDOXkkMYzBQ0h7UnhKA2h54/qlxDxMFGXykkmYOl7mc7QJc1aJaveo4yMHtpYvcQG17xLyD+Z+5CQYA76nvw== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/link@~0.1.22": - version "0.1.29" - resolved "https://registry.yarnpkg.com/@gluestack-ui/link/-/link-0.1.29.tgz#5dcba644772570dc9165e8b36862c70bd49ea8fb" - integrity sha512-7rNx0puyHr+LG4moy/OfMybwVIxQ/ES4H/xyskvcvylCtbxPqN+0oSQ9WA4CISeipwK4x+2aQQRvjasYt9rXgA== - dependencies: - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/menu@~0.2.43": - version "0.2.43" - resolved "https://registry.yarnpkg.com/@gluestack-ui/menu/-/menu-0.2.43.tgz#54cbaef5d9f4e1c1a9d5b1e809023d1e8b20cebd" - integrity sha512-RjThV7nqGOFnsdCMG/JHs2qrVvtjw3MwKJJPD0bAJa+rhBEk2n9tbEFHPR6KWWNUmSnP3aWSZldT5Y87JiVKNA== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.22" - "@gluestack-ui/utils" "^0.1.15" - "@react-aria/menu" "^3.14.1" - "@react-aria/overlays" "^3.19.0" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/menu" "0.2.15" - "@react-native-aria/overlays" "^0.3.15" - "@react-stately/utils" "^3.6.0" - react-stately "^3.21.0" - -"@gluestack-ui/modal@~0.1.35": - version "0.1.41" - resolved "https://registry.yarnpkg.com/@gluestack-ui/modal/-/modal-0.1.41.tgz#e8c04fd9ac7598dd0f2a2ff15f8526b9cceab2d2" - integrity sha512-pEO5h6TxEpYyU/CEhRD/pAkuBsDfqnFUe51AxK7w6FLjBuTKA5P0wsQ1pfcovJeykkpreG+MiUvNPIo3MW/Lfw== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.22" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/dialog" "^0.0.5" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/overlays" "^0.3.15" - -"@gluestack-ui/nativewind-utils@~1.0.26": - version "1.0.28" - resolved "https://registry.yarnpkg.com/@gluestack-ui/nativewind-utils/-/nativewind-utils-1.0.28.tgz#54ec885c5683ce7a18aeafbd31ef00e17913ef11" - integrity sha512-POWYUK99Y9zRbDbTv0/FF6YtPmymaAmqgWKU3QYFjraK3HPAgF9HjovJVEN8SycEDylJAnACw53AoHFQ3Bxh+A== - dependencies: - find-yarn-workspace-root "^2.0.0" - patch-package "8.0.0" +"@gluestack-ui/core@^5.0.15": + version "5.0.15" + resolved "https://registry.yarnpkg.com/@gluestack-ui/core/-/core-5.0.15.tgz#bb95c99c5083487a46b0cf27730c8473579650a9" + integrity sha512-H9dOYlofbraLbdxt0B0WFuWp+n4ZBNJcFTThiC6y5gvVZlrOoWLG67OehcAsPr7iD+yiV3dAXMkVjRZyWltnAQ== + dependencies: + "@react-aria/checkbox" "^3.16.3" + "@react-aria/focus" "^3.21.3" + "@react-aria/interactions" "^3.26.0" + "@react-aria/label" "^3.7.23" + "@react-aria/menu" "^3.19.4" + "@react-aria/overlays" "^3.31.0" + "@react-aria/radio" "^3.12.3" + "@react-aria/selection" "^3.27.0" + "@react-aria/slider" "^3.8.3" + "@react-aria/switch" "^3.7.10" + "@react-aria/utils" "^3.32.0" + "@react-aria/visually-hidden" "^3.8.29" + "@react-stately/checkbox" "^3.7.4" + "@react-stately/collections" "^3.12.9" + "@react-stately/menu" "^3.9.10" + "@react-stately/overlays" "^3.6.22" + "@react-stately/radio" "^3.11.4" + "@react-stately/slider" "^3.7.4" + "@react-stately/toggle" "^3.9.4" + "@react-stately/tree" "^3.9.5" + "@react-stately/utils" "^3.11.0" + "@react-types/button" "^3.14.1" + "@react-types/checkbox" "^3.10.2" + "@react-types/menu" "^3.10.5" + "@react-types/overlays" "^3.9.2" + "@react-types/radio" "^3.9.2" + "@react-types/shared" "^3.33.0" + "@react-types/slider" "^3.8.2" + "@react-types/switch" "^3.5.15" + date-fns "^4.1.0" + +"@gluestack-ui/utils@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@gluestack-ui/utils/-/utils-5.0.6.tgz#2f3f55574aac2a879527c705410fb8af21e78582" + integrity sha512-GjVIic+ea0XIrxgTzuiGqd/QiDyd/nPypIleKuMUthpTNX5MWz+dSTQf0GAXY0XeMxqGPlmX3T2o3uVTaevIcQ== + dependencies: + "@react-aria/dialog" "^3.5.27" + "@react-aria/focus" "^3.21.3" + "@react-aria/interactions" "^3.26.0" + "@react-aria/ssr" "^3.9.9" + "@react-aria/utils" "^3.32.0" + "@react-stately/toggle" "^3.9.4" + "@react-types/checkbox" "^3.10.2" + "@react-types/dialog" "^3.5.22" + "@react-types/shared" "^3.33.0" + dom-helpers "^6.0.1" + react-aria "^3.48.0" + react-stately "^3.46.0" tailwind-variants "0.1.20" -"@gluestack-ui/overlay@0.1.22", "@gluestack-ui/overlay@^0.1.16", "@gluestack-ui/overlay@^0.1.20", "@gluestack-ui/overlay@^0.1.22", "@gluestack-ui/overlay@~0.1.16": - version "0.1.22" - resolved "https://registry.yarnpkg.com/@gluestack-ui/overlay/-/overlay-0.1.22.tgz#b0c03eb88f37e1564b6522f3d021052dcce86f4a" - integrity sha512-ttlSjO/ysN9NuTq5JPoVwOAU8aKMaj738xTCxOVgBRr1+kbBUVpmBnygVlC6PvHvJF1A00a0ZTzw48CIVL5aNw== - dependencies: - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/overlays" "^0.3.15" - -"@gluestack-ui/popover@~0.1.49": - version "0.1.49" - resolved "https://registry.yarnpkg.com/@gluestack-ui/popover/-/popover-0.1.49.tgz#38fa4971938d5a84c74fab9e305c0293dc7c36c5" - integrity sha512-AI73AXEPoIPGpwhQICn9BiQIJzCDGSb4RT6OQI2V3TKlBYM+RodZvK0RKMipEsROBQwNWbcJoPJRkH9jwDVrBA== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "0.1.22" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/dialog" "^0.0.5" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/overlays" "0.3.15" - -"@gluestack-ui/pressable@~0.1.16": - version "0.1.23" - resolved "https://registry.yarnpkg.com/@gluestack-ui/pressable/-/pressable-0.1.23.tgz#4324aa4bdbb6e5844416d7cdcc7f62f9f6956c89" - integrity sha512-y7Sqwwe4+nIM5pECr3UT9qx7MMyuJHt1od6dfB/K+S2X91uZgTJEw7PUQgvOW6Jr8dBrStxFiTWfHmDqX/FVOQ== - dependencies: - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - -"@gluestack-ui/progress@~0.1.16": - version "0.1.18" - resolved "https://registry.yarnpkg.com/@gluestack-ui/progress/-/progress-0.1.18.tgz#9a659115b4579fee057c2d13bbc52e9e080a6958" - integrity sha512-WQCLdvqoiIIQnLBXG7+HDTREMbUfEy9rV7FuBMcBBqEFILuVT1W4hyuXLz/CPrUoCWZJ8OUKvLZvaWWzW8pA9Q== - dependencies: - "@gluestack-ui/utils" "^0.1.14" - -"@gluestack-ui/provider@^0.1.19": - version "0.1.19" - resolved "https://registry.yarnpkg.com/@gluestack-ui/provider/-/provider-0.1.19.tgz#145c038c401ce250274c384e69a91f78ee1ff73c" - integrity sha512-+MbL2VpGK7kkSNccgNeMay/nslDA4BzgjHFWwnys+dSvyCMgt7yfSxrfWziTvAyaQ4cRkq/lXCfJK0B1nuCZ4A== - dependencies: - "@react-native-aria/interactions" "0.2.16" - tsconfig "7" - typescript "^5.6.3" - -"@gluestack-ui/radio@~0.1.40": - version "0.1.40" - resolved "https://registry.yarnpkg.com/@gluestack-ui/radio/-/radio-0.1.40.tgz#cfbb709db8170d9a7dc70decdb7a3522fb0e9e67" - integrity sha512-UJ6i3qpbBVsSWrxQi889yXczPjmfhfHCplMK2n4xkEfsYcjDVKpronVfqbJ41gf27fBzyLJFnCec3uPL4+obSA== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/utils" "^0.1.15" - "@react-aria/visually-hidden" "^3.7.0" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/radio" "^0.2.13" - "@react-stately/radio" "^3.8.1" - -"@gluestack-ui/react-native-aria@^0.1.6": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@gluestack-ui/react-native-aria/-/react-native-aria-0.1.7.tgz#f982cbb071fa844afcf3816873dce300dc754843" - integrity sha512-mdK/M6RQafAYiHyJnDNjaE9+ttr9eg48f2InmO3rvcrjsdaLcu/vwHlwrO9N3xYcb3wb9z2+YZ4OIgtymFNTOw== - dependencies: - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/select@~0.1.31": - version "0.1.31" - resolved "https://registry.yarnpkg.com/@gluestack-ui/select/-/select-0.1.31.tgz#b0f91fdaf0124db5fa3cdd0e3a7460db9406eff0" - integrity sha512-d8vfdCK4VFBYEDYMCTRYcZCWvYaf7UWMJGg2uMnutHA8Y4sDXjKR5P821xKdh75QVkmZRZm1MdzSWFvXUzAlgg== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/slider@~0.1.32": - version "0.1.32" - resolved "https://registry.yarnpkg.com/@gluestack-ui/slider/-/slider-0.1.32.tgz#65d166ff51a5a94c27a68fc1ccf2d4d373bd1824" - integrity sha512-g0e7dAGOYYARlL3cdHe3mhN71j85TnqUgK/xOYWjVDE0U+atIXxxTVEXeO0ZPGJ3YUOUUAInIVGaa0xvnjEkYg== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/utils" "^0.1.15" - "@react-aria/visually-hidden" "^3.8.1" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/slider" "^0.2.12" - "@react-stately/slider" "^3.2.4" - -"@gluestack-ui/spinner@~0.1.14": - version "0.1.15" - resolved "https://registry.yarnpkg.com/@gluestack-ui/spinner/-/spinner-0.1.15.tgz#712276227568476f55672b7bbe0082478c0c0a65" - integrity sha512-jkXOGhna05HM2tcPKbA+t+vqcVRgRowwC6ES0A9kY5fcuURH1V41UFx/YWyHdSsHwmWlIoCCdWycF78UbNVdPg== - -"@gluestack-ui/switch@~0.1.22": - version "0.1.29" - resolved "https://registry.yarnpkg.com/@gluestack-ui/switch/-/switch-0.1.29.tgz#d2a26f66cfc3065f27ea4af0206f4cb0ef34f5ca" - integrity sha512-swx0J20ULpN+pkHg8KioWibZETWE/6XvCBckmApNZf9RO2MiHHkNMw4Txne4u1z0OyLKGzBW1dbmkhLIOObaXg== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-stately/toggle" "^3.4.4" - -"@gluestack-ui/textarea@~0.1.23": - version "0.1.25" - resolved "https://registry.yarnpkg.com/@gluestack-ui/textarea/-/textarea-0.1.25.tgz#203f3e60c0177ad3b722f0d2745db5ae7fbcd3d8" - integrity sha512-hXJx9LYSfrx7/Lrh2D2cTjr7PqvdkxJXBv2VydcR01X4WEN34DZkik+at/qk5FBw+p4t0vE+BTAK3IoMJwWvGg== - dependencies: - "@gluestack-ui/form-control" "^0.1.19" - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "^0.2.16" - -"@gluestack-ui/toast@~1.0.8": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@gluestack-ui/toast/-/toast-1.0.9.tgz#6ee18051e7af0459fbb3ede4cc4bf1a9ec1719a0" - integrity sha512-aMlPczeoH/PZTMnhV29fqqW1Xc/9QmYEsR0bU9BfLyAGM9UMjW3vGe4yZSgxX7xjQ9C7+KO5WnTH0FmPoAbVtg== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.20" - "@gluestack-ui/transitions" "^0.1.11" - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/tooltip@~0.1.32": - version "0.1.44" - resolved "https://registry.yarnpkg.com/@gluestack-ui/tooltip/-/tooltip-0.1.44.tgz#bc682f046573001a8c611f04991ab6ed3200e76f" - integrity sha512-gHVOYwjQK41ofM+D7131miNobbBAr0fuxr/52x3EJJpntqItF5u35ikj8XKy1T7nrOb0iZaYh7C0W08if1Skgg== - dependencies: - "@gluestack-ui/hooks" "0.1.13" - "@gluestack-ui/overlay" "^0.1.22" - "@gluestack-ui/utils" "^0.1.15" - "@react-native-aria/focus" "^0.2.9" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/overlays" "^0.3.15" - -"@gluestack-ui/transitions@^0.1.11": - version "0.1.11" - resolved "https://registry.yarnpkg.com/@gluestack-ui/transitions/-/transitions-0.1.11.tgz#e84a9841fabb8e67ce17cea979a1cd77b5edac56" - integrity sha512-GuCprzasuU8GKXm1GUmaVJ1Njiq72pQ4Ni0g+rg6kRQ7hyR0K4d0wdHRyFldHKmfLcQkM3i70rvYq/468hLKLw== - dependencies: - "@gluestack-ui/overlay" "^0.1.16" - "@gluestack-ui/react-native-aria" "^0.1.6" - "@gluestack-ui/utils" "^0.1.14" - "@react-native-aria/focus" "^0.2.9" - -"@gluestack-ui/utils@0.1.15", "@gluestack-ui/utils@^0.1.14", "@gluestack-ui/utils@^0.1.15": - version "0.1.15" - resolved "https://registry.yarnpkg.com/@gluestack-ui/utils/-/utils-0.1.15.tgz#55f318d81d230daca40b498039eb4680600faa90" - integrity sha512-LRBMv+tr+bV4falMYS2zSt94PxL0vpvUN6F9d6oTqOYKhkiAkU6k2ROfNlT323m0CoTQto1LkJhPxULisQ2oeQ== - dependencies: - "@react-native-aria/focus" "^0.2.9" - "@gorhom/bottom-sheet@~5.0.5": version "5.0.6" resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.0.6.tgz#f20736502399c7bcf8c73ea09e6b571dc07fe0eb" @@ -2208,32 +1989,24 @@ resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.13.tgz#ad0afd62baab1c23175115a9b62f511b6a751e45" integrity sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw== -"@internationalized/date@^3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.9.0.tgz#cf241989b5dd07a2a9f1c91aabd2ad93968a0cc3" - integrity sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg== - dependencies: - "@swc/helpers" "^0.5.0" - -"@internationalized/message@^3.1.8": - version "3.1.8" - resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.1.8.tgz#7181e8178f0868535f4507a573bf285e925832cb" - integrity sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA== +"@internationalized/date@^3.12.2": + version "3.12.2" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.12.2.tgz#08a65edd2a29775e22c168ddc029fb54bf9b8a85" + integrity sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw== dependencies: "@swc/helpers" "^0.5.0" - intl-messageformat "^10.1.0" -"@internationalized/number@^3.6.5": - version "3.6.5" - resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.5.tgz#1103f2832ca8d9dd3e4eecf95733d497791dbbbe" - integrity sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g== +"@internationalized/number@^3.6.7": + version "3.6.7" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.7.tgz#5a0a8fa413b5f8679a59dcf37e2a74dc508b8371" + integrity sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg== dependencies: "@swc/helpers" "^0.5.0" -"@internationalized/string@^3.2.7": - version "3.2.7" - resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.7.tgz#76ae10f1e6e1fdaec7d0028a3f807d37a71bd2dd" - integrity sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A== +"@internationalized/string@^3.2.9": + version "3.2.9" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.9.tgz#0e50385c411eac1275d91cdeb56dd7fbc234997f" + integrity sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg== dependencies: "@swc/helpers" "^0.5.0" @@ -2823,7 +2596,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -3058,6 +2831,13 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" +"@napi-rs/wasm-runtime@^1.1.4": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz#9657eaa103c1cedd9411012595faf8f476608f15" + integrity sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ== + dependencies: + "@tybys/wasm-util" "^0.10.3" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3347,207 +3127,137 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== -"@react-aria/checkbox@3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.2.1.tgz#493d9d584b4db474645a4565c4f899ee3a579f07" - integrity sha512-XnypnlVIfhB3CD7eSjSds8hNkzHgnhu0t48I1D0jYdL1O6tQC4UytPdIqlemRYBVHDloZkWerbjenpHnxhv8iA== - dependencies: - "@babel/runtime" "^7.6.2" - "@react-aria/label" "^3.1.1" - "@react-aria/toggle" "^3.1.1" - "@react-aria/utils" "^3.3.0" - "@react-stately/checkbox" "^3.0.1" - "@react-stately/toggle" "^3.2.1" - "@react-types/checkbox" "^3.2.1" - -"@react-aria/dialog@*": - version "3.5.30" - resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.5.30.tgz#e7e7080dec37bf4e98cb98430b931d5bd7fa7c66" - integrity sha512-fiodaeMSTiC4qKNwnCLbNykyvfcxuz/PiU/pBNhWYd4lUrX1TauBQb0++o5/K6OHt8iB+A7/LSHRbPtyOSWE9g== - dependencies: - "@react-aria/interactions" "^3.25.5" - "@react-aria/overlays" "^3.29.1" - "@react-aria/utils" "^3.30.1" - "@react-types/dialog" "^3.5.21" - "@react-types/shared" "^3.32.0" +"@react-aria/button@^3.15.0": + version "3.15.1" + resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.15.1.tgz#2ec0c6480246863cc542ae2886a6e9eb7264cdc4" + integrity sha512-SBMn8ZLvjuWCpSqi6o1hOjsqQqkdYFfzIdl/0LgNPUpTclkJuMx7gNXfM3mjgxzSCoS5CD/XdicvqJanMw6jCw== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/focus@^3.2.3", "@react-aria/focus@^3.21.1": - version "3.21.1" - resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.21.1.tgz#fad9d0803e0e4423bb6e14ed3208fffd694e5e42" - integrity sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ== +"@react-aria/checkbox@^3.16.3", "@react-aria/checkbox@^3.17.0": + version "3.17.1" + resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.17.1.tgz#ce184c1a52f4484ee509d381dbcf0c47bbcc74e0" + integrity sha512-742B+UhH87TK02SYsFJZOgQ9VZpFEythH5LXRnwxt/xShoKOp+eqXPaVah79s/+B9sxY1mF0wNORQ2RVAQFSKw== dependencies: - "@react-aria/interactions" "^3.25.5" - "@react-aria/utils" "^3.30.1" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" - clsx "^2.0.0" + react-aria "^3.48.0" -"@react-aria/form@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@react-aria/form/-/form-3.1.1.tgz#76b3eb4a6985fb27b1d437db89b5a69c54be292f" - integrity sha512-PjZC25UgH5orit9p56Ymbbo288F3eaDd3JUvD8SG+xgx302HhlFAOYsQLLAb4k4H03bp0gWtlUEkfX6KYcE1Tw== +"@react-aria/dialog@^3.5.27", "@react-aria/dialog@^3.6.0": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.6.1.tgz#8e305ca9e94f4d08826be905b5791db137209c63" + integrity sha512-Eo3Kj23TjENuERUYrGkH+VN1PJvK8/zep76pfwBg29erUVDpGdfagYRq82aGcxJ/ohG8iRxZDwp2tTrU1RG1MQ== dependencies: - "@react-aria/interactions" "^3.25.5" - "@react-aria/utils" "^3.30.1" - "@react-stately/form" "^3.2.1" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/i18n@^3.12.12": - version "3.12.12" - resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.12.12.tgz#186eadf0c5dd3c38eb31c40b7c0191df07cef185" - integrity sha512-JN6p+Xc6Pu/qddGRoeYY6ARsrk2Oz7UiQc9nLEPOt3Ch+blJZKWwDjcpo/p6/wVZdD/2BgXS7El6q6+eMg7ibw== - dependencies: - "@internationalized/date" "^3.9.0" - "@internationalized/message" "^3.1.8" - "@internationalized/number" "^3.6.5" - "@internationalized/string" "^3.2.7" - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.30.1" - "@react-types/shared" "^3.32.0" +"@react-aria/focus@^3.21.3": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.22.1.tgz#7e7ba6a2250135728eb68ffa21e422ec25a16800" + integrity sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/interactions@^3.25.5", "@react-aria/interactions@^3.3.2": - version "3.25.5" - resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.25.5.tgz#f7f69467c899f9673460c3401fcaac08d2dcac7d" - integrity sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA== +"@react-aria/interactions@^3.26.0": + version "3.28.1" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.28.1.tgz#852294ae63f6a7d8aeeba3a1e7343daa8cd8507c" + integrity sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA== dependencies: - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.30.1" - "@react-stately/flags" "^3.1.2" - "@react-types/shared" "^3.32.0" + "@react-types/shared" "^3.34.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/label@^3.1.1", "@react-aria/label@^3.7.21": - version "3.7.21" - resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.7.21.tgz#1deeb3886bc5110f76659137c3e21811fa1606d1" - integrity sha512-8G+059/GZahgQbrhMcCcVcrjm7W+pfzrypH/Qkjo7C1yqPGt6geeFwWeOIbiUZoI0HD9t9QvQPryd6m46UC7Tg== +"@react-aria/label@^3.7.23": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.8.1.tgz#8d0383100ce5736e0deccf6e9cff8f2e5283bf12" + integrity sha512-zf6hG22PnxZ9kEUwxORClBicHK5pcvucDU+lotGsI6ENbdIvJAMNn9X0oiPPae56CVf2T0KEL9NB8LKdAfzCIw== dependencies: - "@react-aria/utils" "^3.30.1" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/menu@^3.1.3", "@react-aria/menu@^3.14.1": - version "3.19.2" - resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.19.2.tgz#a506d0cd4443f4ddfb89df341937fc77ed77b47d" - integrity sha512-WzDLW2MotL0L5/LEwc5oGgISf2ODuw4FnRpF0Zk+J4tKFfC88odvKz848ubBvThRXuXEvL0BHY+WqtM+j9fn3g== - dependencies: - "@react-aria/focus" "^3.21.1" - "@react-aria/i18n" "^3.12.12" - "@react-aria/interactions" "^3.25.5" - "@react-aria/overlays" "^3.29.1" - "@react-aria/selection" "^3.25.1" - "@react-aria/utils" "^3.30.1" - "@react-stately/collections" "^3.12.7" - "@react-stately/menu" "^3.9.7" - "@react-stately/selection" "^3.20.5" - "@react-stately/tree" "^3.9.2" - "@react-types/button" "^3.14.0" - "@react-types/menu" "^3.10.4" - "@react-types/shared" "^3.32.0" +"@react-aria/menu@^3.19.4", "@react-aria/menu@^3.22.0": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.22.1.tgz#543440b952707e04d680c2ddd9eb50362b805aef" + integrity sha512-ASP2u11+jg4OQh+QaDlWb7upMgsMITBQTjPFPbUJp6eE4q/cddLPYf4/9yfxh/bwTg0iDdzoDrgdtbwNGYNSfg== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/overlays@^3.19.0", "@react-aria/overlays@^3.29.1", "@react-aria/overlays@^3.7.0": - version "3.29.1" - resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.29.1.tgz#1a43a29709ad10971116b3d77de4e9b02107193d" - integrity sha512-Yz92XNPnbrTnxrvNrY/fXJ3iWaYNrj0q24ddvZNNKDcWak0S1/mQeUwNb+PwS2AryhFU5VQqKz5rNsM96TKmPQ== - dependencies: - "@react-aria/focus" "^3.21.1" - "@react-aria/i18n" "^3.12.12" - "@react-aria/interactions" "^3.25.5" - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.30.1" - "@react-aria/visually-hidden" "^3.8.27" - "@react-stately/overlays" "^3.6.19" - "@react-types/button" "^3.14.0" - "@react-types/overlays" "^3.9.1" - "@react-types/shared" "^3.32.0" +"@react-aria/overlays@^3.31.0", "@react-aria/overlays@^3.32.0": + version "3.32.1" + resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.32.1.tgz#7db09e6d07cb24b26c37d60dbad17233445afbd2" + integrity sha512-jjVLcEK5qaGsz3SmW+eLV3QFiJzdDFzgNofPwvzBS1KTPox0a2x5u1ITUPmHwyUNiyexy531h5eVjN3tULEzHQ== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/radio@^3.1.2": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.12.1.tgz#6330e7da6458a9b50cf1626805304a57f719c4fc" - integrity sha512-feZdMJyNp+UX03seIX0W6gdUk8xayTY+U0Ct61eci6YXzyyZoL2PVh49ojkbyZ2UZA/eXeygpdF5sgQrKILHCA== - dependencies: - "@react-aria/focus" "^3.21.1" - "@react-aria/form" "^3.1.1" - "@react-aria/i18n" "^3.12.12" - "@react-aria/interactions" "^3.25.5" - "@react-aria/label" "^3.7.21" - "@react-aria/utils" "^3.30.1" - "@react-stately/radio" "^3.11.1" - "@react-types/radio" "^3.9.1" - "@react-types/shared" "^3.32.0" +"@react-aria/radio@^3.12.3", "@react-aria/radio@^3.13.0": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.13.1.tgz#89c1cf8801e42b8e0238d4fc8af530de01c437d5" + integrity sha512-Rsl/knBduhNKQfHGO3i+Jc3P3tFo213UPD3HU3W1GjZtYlUEBYL0m5ehA6cT7alse3aCWp/icoXCcYbgBHBQHw== + dependencies: + "@react-types/shared" "^3.34.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/selection@^3.25.1", "@react-aria/selection@^3.3.1": - version "3.25.1" - resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.25.1.tgz#fdd7724bd251ee72082d3b62aa891ce8957d4e8a" - integrity sha512-HG+k3rDjuhnXPdVyv9CKiebee2XNkFYeYZBxEGlK3/pFVBzndnc8BXNVrXSgtCHLs2d090JBVKl1k912BPbj0Q== - dependencies: - "@react-aria/focus" "^3.21.1" - "@react-aria/i18n" "^3.12.12" - "@react-aria/interactions" "^3.25.5" - "@react-aria/utils" "^3.30.1" - "@react-stately/selection" "^3.20.5" - "@react-types/shared" "^3.32.0" +"@react-aria/selection@^3.27.0": + version "3.28.1" + resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.28.1.tgz#4d92807cf062240d7827ccd11a7892116752f22a" + integrity sha512-gGa9HkRnWsKxRhrtVASvecNyetMtP9fNF/Vcsy9Z+6NigjGUSN4SU0bhfglZ706B4t/NSMoVhcBJXlyFiG0hQw== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/slider@^3.0.1": - version "3.8.1" - resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.8.1.tgz#26cb2dee6c19c72e859bcd76f2e8973dcbe9a29a" - integrity sha512-uPgwZQrcuqHaLU2prJtPEPIyN9ugZ7qGgi0SB2U8tvoODNVwuPvOaSsvR98Mn6jiAzMFNoWMydeIi+J1OjvWsQ== - dependencies: - "@react-aria/i18n" "^3.12.12" - "@react-aria/interactions" "^3.25.5" - "@react-aria/label" "^3.7.21" - "@react-aria/utils" "^3.30.1" - "@react-stately/slider" "^3.7.1" - "@react-types/shared" "^3.32.0" - "@react-types/slider" "^3.8.1" +"@react-aria/slider@^3.8.3", "@react-aria/slider@^3.9.0": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.9.1.tgz#b4ec3c77b327a9d7dd46102314e256cb1ae288fc" + integrity sha512-bFWhyM6mP9TzAjCpOKybS7x0RTsFa2+49XKytKN53xS7bdHEnWGbWpsdJ09qZ6z1fnrwFcq/j/hhj/0gVO8kdQ== + dependencies: + "@react-types/shared" "^3.34.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/ssr@^3.0.1", "@react-aria/ssr@^3.9.10": - version "3.9.10" - resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.10.tgz#7fdc09e811944ce0df1d7e713de1449abd7435e6" - integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== +"@react-aria/ssr@^3.9.9": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.10.1.tgz#d082600c811e35e8ab780c55c47b5d731359aaf7" + integrity sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag== dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/toggle@^3.1.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.12.1.tgz#2a32b7e8e2cbc0a8494350ba4981000093af882e" - integrity sha512-XaFiRs1KEcIT6bTtVY/KTQxw4kinemj/UwXw2iJTu9XS43hhJ/9cvj8KzNGrKGqaxTpOYj62TnSHZbSiFViHDA== - dependencies: - "@react-aria/interactions" "^3.25.5" - "@react-aria/utils" "^3.30.1" - "@react-stately/toggle" "^3.9.1" - "@react-types/checkbox" "^3.10.1" - "@react-types/shared" "^3.32.0" +"@react-aria/switch@^3.7.10", "@react-aria/switch@^3.8.0": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-aria/switch/-/switch-3.8.1.tgz#7be09fd50096d9cbf87eb857ab06508606a8581e" + integrity sha512-WzkJG3xJnBdd9rdBxG+0RgWdopAqwI61Ni4ZQtA2rgFuG9UpZIrhsL7qgFO/+R6oEgoeGAMeUlvCf97Ppj2uTg== + dependencies: + "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" + +"@react-aria/toggle@^3.13.0": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.13.1.tgz#f16beceef0d5b3f1f3590e78fb5061844b6ed756" + integrity sha512-1SPMBSrbT94Jsy7Wuv9SH2uPv7szGP7KgO4QGIAcF3e5kHwvGQJdMHY6+xR17slk8ICXfFojBqUKgpJjyY2+KQ== + dependencies: "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@react-aria/utils@^3.3.0", "@react-aria/utils@^3.30.1", "@react-aria/utils@^3.6.0": - version "3.30.1" - resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.30.1.tgz#9eb704d4193674816e1e0eab758b12c2d69d7b0b" - integrity sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A== +"@react-aria/utils@^3.32.0": + version "3.34.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.34.1.tgz#ced8c328cf7ded923a2901961325fb397a1b53c8" + integrity sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A== dependencies: - "@react-aria/ssr" "^3.9.10" - "@react-stately/flags" "^3.1.2" - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" - clsx "^2.0.0" + react-aria "^3.48.0" + react-stately "^3.46.0" -"@react-aria/visually-hidden@^3.7.0", "@react-aria/visually-hidden@^3.8.1", "@react-aria/visually-hidden@^3.8.27", "@react-aria/visually-hidden@^3.8.6": - version "3.8.27" - resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.8.27.tgz#5e73b761b2ea932b30f818f88c9b290e6437f991" - integrity sha512-hD1DbL3WnjPnCdlQjwe19bQVRAGJyN0Aaup+s7NNtvZUn7AjoEH78jo8TE+L8yM7z/OZUQF26laCfYqeIwWn4g== +"@react-aria/visually-hidden@^3.8.29": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.9.1.tgz#d2a6c96bb9b1d7be1245377a945e2c1bbad73d5f" + integrity sha512-PWuth+NTmUiBJAIyrfk7dJ5BxOBupDt0iFGlBiYr5FElSYvwN9LAk1kgkzm6hT2qzq4FmYdQgBSPkGeaxOui6w== dependencies: - "@react-aria/interactions" "^3.25.5" - "@react-aria/utils" "^3.30.1" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" "@react-buoy/core@^1.5.23": version "1.5.23" @@ -3583,122 +3293,6 @@ resolved "https://registry.yarnpkg.com/@react-buoy/shared-ui/-/shared-ui-1.5.23.tgz#de216fb14e06d07cf13672d076eace00f4eefdf2" integrity sha512-C2UAdUkCTXQrfjiv6vAxIRU8GMKWGviuttlLVbhAQmr0ZcfoY8bKrrpONJ2lgK6sAcSidaDZsidP2mamboelPQ== -"@react-native-aria/accordion@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@react-native-aria/accordion/-/accordion-0.0.2.tgz#28e57f5c3690b2b33f36acd6aff7d91a75fbf384" - integrity sha512-2Wa/YDBc2aCunTLpqwxTfCwn1t63KSAIoXd0hqrUGJJF+N2bEs2Hqs9ZgyKJ/hzFxCknVPMqo0fEVE1H23Z5+g== - -"@react-native-aria/checkbox@^0.2.10": - version "0.2.10" - resolved "https://registry.yarnpkg.com/@react-native-aria/checkbox/-/checkbox-0.2.10.tgz#04a35ee9c969456083467d613aedead2eb6d07b8" - integrity sha512-SpjAHmu+NmoztkHggtlBsygOapszjomjo8PtbGxP7rV+3b5iWhmrD2Ztf3Vc4tZMBl/GFslLA3NXp5h47T9Utg== - dependencies: - "@react-aria/checkbox" "3.2.1" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/toggle" "^0.2.11" - "@react-native-aria/utils" "0.2.12" - "@react-stately/toggle" "^3.2.1" - -"@react-native-aria/dialog@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@react-native-aria/dialog/-/dialog-0.0.5.tgz#aa8d1901cb9e132f0c750adfa2e8091c3aefc28d" - integrity sha512-ZThiWyymf3WiA2EdjStV32pTL3RjAb7H/CL0Zsd1wKNuw1lU9HX6h4UIUpt0MZhcFCUnZjCovNtU9IRwbbdj/Q== - dependencies: - "@react-aria/dialog" "*" - "@react-native-aria/utils" "0.2.12" - "@react-types/dialog" "*" - "@react-types/shared" "*" - -"@react-native-aria/focus@^0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@react-native-aria/focus/-/focus-0.2.9.tgz#bdfa84f9711843df771877ac436ee3b4f8878b74" - integrity sha512-zVgOIzKwnsyyurUxlZnzUKB2ekK/cmK64sQJIKKUlkJKVxd2EAFf7Sjz/NVEoMhTODN3qGRASTv9bMk/pBzzVA== - dependencies: - "@react-aria/focus" "^3.2.3" - -"@react-native-aria/interactions@0.2.16", "@react-native-aria/interactions@^0.2.16": - version "0.2.16" - resolved "https://registry.yarnpkg.com/@react-native-aria/interactions/-/interactions-0.2.16.tgz#fcb5745a5f87f60089717f578dcf085655bee79a" - integrity sha512-tJ7mhG0P6cElLTcL4yfV3HSfqdD8tpAwHKjtamyeua8i6tsu7BOxGmfGmPQ5ueJl7/mHqf8xyTv1xGW288CKXQ== - dependencies: - "@react-aria/interactions" "^3.3.2" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/utils" "0.2.12" - -"@react-native-aria/menu@0.2.15": - version "0.2.15" - resolved "https://registry.yarnpkg.com/@react-native-aria/menu/-/menu-0.2.15.tgz#fa5193f180c98cfa26ea71625fbea352e786b7f4" - integrity sha512-ezuz3EAPVERL5YPut0wQmcRwIyWNoAMkA9ItVI9mFWhyvUJRt1rNNA0sG580KzTwAx/1vezJmCMBhkDLeTDYjg== - dependencies: - "@react-aria/interactions" "^3.3.2" - "@react-aria/menu" "^3.1.3" - "@react-aria/selection" "^3.3.1" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/overlays" "^0.3.15" - "@react-native-aria/utils" "0.2.12" - "@react-stately/collections" "^3.3.0" - "@react-stately/menu" "^3.2.1" - "@react-stately/tree" "^3.1.2" - "@react-types/menu" "^3.1.1" - -"@react-native-aria/overlays@0.3.15", "@react-native-aria/overlays@^0.3.15": - version "0.3.15" - resolved "https://registry.yarnpkg.com/@react-native-aria/overlays/-/overlays-0.3.15.tgz#42aab9b0475dea34c488857da257e2dc3e941893" - integrity sha512-ialIMsXmiQe6A2cWv4O62nJs8raAEkC27BYUu0Z6BEWbhYn+3MhrujxFzsozJ6bwDZgsfNOqtfwk1ScXJxXjkQ== - dependencies: - "@react-aria/interactions" "^3.3.2" - "@react-aria/overlays" "^3.7.0" - "@react-native-aria/utils" "0.2.12" - "@react-stately/overlays" "^3.1.1" - "@react-types/overlays" "^3.4.0" - dom-helpers "^5.0.0" - -"@react-native-aria/radio@^0.2.13": - version "0.2.13" - resolved "https://registry.yarnpkg.com/@react-native-aria/radio/-/radio-0.2.13.tgz#a39229bfcb5c4f19917321ae75c40c48a3f83a0b" - integrity sha512-80FQ+k10738MOnFwOyP3RvAGUAdozYY54vUZNNg2z7MH6GaKyok538cDIzVa9p4cCL58CU8Te8vnd+ItOR3nBg== - dependencies: - "@react-aria/radio" "^3.1.2" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/utils" "0.2.12" - "@react-stately/radio" "^3.2.1" - "@react-types/radio" "^3.1.1" - -"@react-native-aria/slider@^0.2.12": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@react-native-aria/slider/-/slider-0.2.12.tgz#4ab6a547149b64d53cc274efb6199c05174feddd" - integrity sha512-xh528Ddwyyl7MbTNGkTlQnzLId6xZ3mLPyT9bM/oQ2rGCFufL7cznKYk41QGBLaMBJ4n2ig0MAVIijoJU+/HkQ== - dependencies: - "@react-aria/focus" "^3.2.3" - "@react-aria/interactions" "^3.3.2" - "@react-aria/label" "^3.1.1" - "@react-aria/slider" "^3.0.1" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/utils" "0.2.12" - "@react-stately/slider" "^3.0.1" - -"@react-native-aria/toggle@^0.2.11": - version "0.2.11" - resolved "https://registry.yarnpkg.com/@react-native-aria/toggle/-/toggle-0.2.11.tgz#52dc530ac6e3b5af52f5f89bd58bf988d52b4a63" - integrity sha512-oEmcHDp9dXJ8QM8BYILmvry9m1OB9prsIJJnmRjedSVMHRgeuH7CCPcJ1PUIsy56EvtAo6JbKvLTX5HUTQ/RHg== - dependencies: - "@react-aria/focus" "^3.2.3" - "@react-aria/utils" "^3.6.0" - "@react-native-aria/interactions" "0.2.16" - "@react-native-aria/utils" "0.2.12" - "@react-stately/toggle" "^3.2.1" - "@react-types/checkbox" "^3.2.1" - -"@react-native-aria/utils@0.2.12": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@react-native-aria/utils/-/utils-0.2.12.tgz#3ee1c3bf97ff3b08d7e8f49cad2ffba3d5d7227d" - integrity sha512-752/tQ5fjDdc/3Gw8cKqml4NucfiFrO8wPmlg0bBgaCvfQDJaUMi4grme3/fBPCUoK2wk9/yJHfwkv45Gi5Eew== - dependencies: - "@react-aria/ssr" "^3.0.1" - "@react-aria/utils" "^3.3.0" - "@react-native-community/netinfo@^11.4.1": version "11.4.1" resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.4.1.tgz#a3c247aceab35f75dd0aa4bfa85d2be5a4508688" @@ -3904,442 +3498,219 @@ dependencies: nanoid "^3.3.11" -"@react-stately/calendar@^3.8.4": - version "3.8.4" - resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.8.4.tgz#c64b29010dc0aba78592cb9a85a09b3ee5a49e17" - integrity sha512-q9mq0ydOLS5vJoHLnYfSCS/vppfjbg0XHJlAoPR+w+WpYZF4wPP453SrlX9T1DbxCEYFTpcxcMk/O8SDW3miAw== - dependencies: - "@internationalized/date" "^3.9.0" - "@react-stately/utils" "^3.10.8" - "@react-types/calendar" "^3.7.4" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/checkbox@^3.0.1", "@react-stately/checkbox@^3.4.2", "@react-stately/checkbox@^3.7.1": - version "3.7.1" - resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.7.1.tgz#7a0a42f94705acc21ca702ad14b067a69cef4d47" - integrity sha512-ezfKRJsDuRCLtNoNOi9JXCp6PjffZWLZ/vENW/gbRDL8i46RKC/HpfJrJhvTPmsLYazxPC99Me9iq3v0VoNCsw== - dependencies: - "@react-stately/form" "^3.2.1" - "@react-stately/utils" "^3.10.8" - "@react-types/checkbox" "^3.10.1" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/collections@^3.12.7", "@react-stately/collections@^3.3.0": - version "3.12.7" - resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.12.7.tgz#6e01a8988696e62301690eb7cd1497568236f222" - integrity sha512-0kQc0mI986GOCQHvRy4L0JQiotIK/KmEhR9Mu/6V0GoSdqg5QeUe4kyoNWj3bl03uQXme80v0L2jLHt+fOHHjA== +"@react-spectrum/button@^3.18.0": + version "3.18.1" + resolved "https://registry.yarnpkg.com/@react-spectrum/button/-/button-3.18.1.tgz#67f63c59fa7bdddd58816ffee4dd1293733e57e7" + integrity sha512-FW+1d6zfKesMLaBrsRsnnYnsLfvf2qFKuatkCo62o1oGUBtEYI/kae+lwGwlfiX5q9P5OgR2y1IqKtxabf/JKQ== dependencies: - "@react-types/shared" "^3.32.0" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/color@^3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@react-stately/color/-/color-3.9.1.tgz#268d1ac94c0d5b67398ae62f2fefcd86846b9267" - integrity sha512-fCj7fFamyuQbL++MOcf4W4d4aFWXYWJ2UI1dKhrXdqVz/ly9CBVjy/MHKQ6xZX2tEiuoPX5NexfxzKKiozE50Q== - dependencies: - "@internationalized/number" "^3.6.5" - "@internationalized/string" "^3.2.7" - "@react-stately/form" "^3.2.1" - "@react-stately/numberfield" "^3.10.1" - "@react-stately/slider" "^3.7.1" - "@react-stately/utils" "^3.10.8" - "@react-types/color" "^3.1.1" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/combobox@^3.11.1": +"@react-spectrum/checkbox@^3.11.0": version "3.11.1" - resolved "https://registry.yarnpkg.com/@react-stately/combobox/-/combobox-3.11.1.tgz#8bc48b9e9725db3382c6d1afa3946394fc097a20" - integrity sha512-ZZh+SaAmddoY+MeJr470oDYA0nGaJm4xoHCBapaBA0JNakGC/wTzF/IRz3tKQT2VYK4rumr1BJLZQydGp7zzeg== - dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/form" "^3.2.1" - "@react-stately/list" "^3.13.0" - "@react-stately/overlays" "^3.6.19" - "@react-stately/select" "^3.7.1" - "@react-stately/utils" "^3.10.8" - "@react-types/combobox" "^3.13.8" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/data@^3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@react-stately/data/-/data-3.14.0.tgz#224d1aca726cad79b05f32ee37356ce931b1356d" - integrity sha512-3GUsOXatYohBX2wTQHnJKVQlFfYXnt7IoDDuIaUeM8kXlF+dRSFAOAfPUSGAph6lJz2ht4dq1SEl6ZL/u+dRlQ== - dependencies: - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/datepicker@^3.15.1": - version "3.15.1" - resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.15.1.tgz#ca18ba954c1c1e083fb31a00c3ea76f1ed7bafab" - integrity sha512-t64iYPms9y+MEQgOAu0XUHccbEXWVUWBHJWnYvAmILCHY8ZAOeSPAT1g4v9nzyiApcflSNXgpsvbs9BBEsrWww== - dependencies: - "@internationalized/date" "^3.9.0" - "@internationalized/string" "^3.2.7" - "@react-stately/form" "^3.2.1" - "@react-stately/overlays" "^3.6.19" - "@react-stately/utils" "^3.10.8" - "@react-types/datepicker" "^3.13.1" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/disclosure@^3.0.7": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@react-stately/disclosure/-/disclosure-3.0.7.tgz#dd1d4451fb6368cbc1d7a7f6679599f1585a518d" - integrity sha512-ogM2y02uhpGfSOaBKIDz+hEha8qBH6WIRHRkoqdF4sEaR1kfq8LvBWdP1e/OcqHAhuRr28P2Rf0TDicnAnN7uA== - dependencies: - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/dnd@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@react-stately/dnd/-/dnd-3.7.0.tgz#2607fad45e32ed40e40623b8b9cc7a2a1f265bfe" - integrity sha512-DddpCVkqt6vUPHLqe/2FHxW/gkR4tEt7W0MbFcCeCLbc9lmvzOClPwNpjmU/3UnU+vPQnwGGUeF3HvaxduUq2Q== - dependencies: - "@react-stately/selection" "^3.20.5" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/flags@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-stately/flags/-/flags-3.1.2.tgz#5c8e5ae416d37d37e2e583d2fcb3a046293504f2" - integrity sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-stately/form@^3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@react-stately/form/-/form-3.2.1.tgz#5c8caad11ea952f0fa1835ae31da67c208e96a54" - integrity sha512-btgOPXkwvd6fdWKoepy5Ue43o2932OSkQxozsR7US1ffFLcQc3SNlADHaRChIXSG8ffPo9t0/Sl4eRzaKu3RgQ== - dependencies: - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/grid@^3.11.5": - version "3.11.5" - resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.11.5.tgz#1e523cee3048cee0a812050fd5383c4eb3e0c0e3" - integrity sha512-4cNjGYaNkcVS2wZoNHUrMRICBpkHStYw57EVemP7MjiWEVu53kzPgR1Iwmti2WFCpi1Lwu0qWNeCfzKpXW4BTg== - dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/selection" "^3.20.5" - "@react-types/grid" "^3.3.5" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/list@^3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.13.0.tgz#0429c212cbb674d585ec68a3d440300f60021c73" - integrity sha512-Panv8TmaY8lAl3R7CRhyUadhf2yid6VKsRDBCBB1FHQOOeL7lqIraz/oskvpabZincuaIUWqQhqYslC4a6dvuA== - dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/selection" "^3.20.5" - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/menu@^3.2.1", "@react-stately/menu@^3.9.7": - version "3.9.7" - resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.9.7.tgz#f7bab58d8ced5a2e4d960efbaf18dd27b13ac510" - integrity sha512-mfz1YoCgtje61AGxVdQaAFLlOXt9vV5dd1lQljYUPRafA/qu5Ursz4fNVlcavWW9GscebzFQErx+y0oSP7EUtQ== + resolved "https://registry.yarnpkg.com/@react-spectrum/checkbox/-/checkbox-3.11.1.tgz#82bc5c1ef6993f165ef8f5fb4ca1ed88ffcb7bfd" + integrity sha512-SkiKMYA5VMiIC+Zsob7YnGfT+hpuWsolzi+jS/Qd78h4nMIMj/kwPN17bqsYMqb5D3IM1F4YAkNpWUFFGaAeAA== dependencies: - "@react-stately/overlays" "^3.6.19" - "@react-types/menu" "^3.10.4" - "@react-types/shared" "^3.32.0" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/numberfield@^3.10.1": +"@react-spectrum/dialog@^3.10.0": version "3.10.1" - resolved "https://registry.yarnpkg.com/@react-stately/numberfield/-/numberfield-3.10.1.tgz#df6c790e56670ede8132e194ddb26a6fc4743bfe" - integrity sha512-lXABmcTneVvXYMGTgZvTCr4E+upOi7VRLL50ZzTMJqHwB/qlEQPAam3dmddQRwIsuCM3MEnL7bSZFFlSYAtkEw== + resolved "https://registry.yarnpkg.com/@react-spectrum/dialog/-/dialog-3.10.1.tgz#db01dbd65d4c9cbf4e445994804a373460b59d2a" + integrity sha512-RMoZkKswUMFiEnAHZljEV1ybpMUjrS5RxkwWb3IFMo+ZtnLIeIT87k4DxoUJKOVrw3bnSsLRSTw1Fvf7jZ/eFw== dependencies: - "@internationalized/number" "^3.6.5" - "@react-stately/form" "^3.2.1" - "@react-stately/utils" "^3.10.8" - "@react-types/numberfield" "^3.8.14" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/overlays@^3.1.1", "@react-stately/overlays@^3.6.19": - version "3.6.19" - resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.6.19.tgz#8c5fbe20cc56c594a13ad19ae1a1ef424e67c954" - integrity sha512-swZXfDvxTYd7tKEpijEHBFFaEmbbnCvEhGlmrAz4K72cuRR9O5u+lcla8y1veGBbBSzrIdKNdBoIIJ+qQH+1TQ== +"@react-spectrum/menu@^3.23.0": + version "3.23.1" + resolved "https://registry.yarnpkg.com/@react-spectrum/menu/-/menu-3.23.1.tgz#61c44aa3b1d69875567215b3912c19e60d0167e1" + integrity sha512-5Dwoq82YYGtzYaNzf9RssrxuuDhjjvys1K6GmOaQeeIIkB76lTMsMfjsf+YLmYIF/O6flWHfeHh+yKBJlLjwWQ== dependencies: - "@react-stately/utils" "^3.10.8" - "@react-types/overlays" "^3.9.1" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-stately/radio@^3.11.1", "@react-stately/radio@^3.2.1", "@react-stately/radio@^3.8.1": - version "3.11.1" - resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.11.1.tgz#a4c2eb9296699fed7040b97ccdf088d3d6355f66" - integrity sha512-ld9KWztI64gssg7zSZi9li21sG85Exb+wFPXtCim1TtpnEpmRtB05pXDDS3xkkIU/qOL4eMEnnLO7xlNm0CRIA== +"@react-spectrum/overlays@^5.10.0": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@react-spectrum/overlays/-/overlays-5.10.1.tgz#42c46cf90b5a3d5a9226e806584e186b74d34104" + integrity sha512-VSpplRbzYA/Hmg+fR7fonj/Q3jq7mJHD6A5f189teX5/uRiTk7P4DogBYfU1a70BAq3xBEA4hV3QpA8i9pK8jQ== dependencies: - "@react-stately/form" "^3.2.1" - "@react-stately/utils" "^3.10.8" - "@react-types/radio" "^3.9.1" - "@react-types/shared" "^3.32.0" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/searchfield@^3.5.15": - version "3.5.15" - resolved "https://registry.yarnpkg.com/@react-stately/searchfield/-/searchfield-3.5.15.tgz#683b1f8a5726e0637a7c8f19fbe6c0f270bfb75e" - integrity sha512-6LVVvm6Z60fetYLLa4B2Q/BIY+fSSknLTw8sjlV+iDEPAknj7MqWtoLz2gSQRTFKvyO7ZCjJoar8ZU/JEqcm+w== +"@react-spectrum/radio@^3.8.0": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-spectrum/radio/-/radio-3.8.1.tgz#caec6a8ed20e0379806907ea0b39ce38194e5063" + integrity sha512-obI0DjW+dH/yXeO3CMgR5tzCzOSFFUItnbKOvfEQTsvIdWnnmyYcjh+W4do01nm4Q5+aiXVXEvvOuVSeY9ZiRw== dependencies: - "@react-stately/utils" "^3.10.8" - "@react-types/searchfield" "^3.6.5" - "@swc/helpers" "^0.5.0" - -"@react-stately/select@^3.7.1": - version "3.7.1" - resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.7.1.tgz#2bb1a52f345b301a3073aa62dba4934697b5bf0c" - integrity sha512-vZt4j9yVyOTWWJoP9plXmYaPZH2uMxbjcGMDbiShwsFiK8C2m9b3Cvy44TZehfzCWzpMVR/DYxEYuonEIGA82Q== - dependencies: - "@react-stately/form" "^3.2.1" - "@react-stately/list" "^3.13.0" - "@react-stately/overlays" "^3.6.19" - "@react-types/select" "^3.10.1" - "@react-types/shared" "^3.32.0" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/selection@^3.20.5": - version "3.20.5" - resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.20.5.tgz#11f4fae7b6038ca167a9bd1513a57c353d308bbd" - integrity sha512-YezWUNEn2pz5mQlbhmngiX9HqQsruLSXlkrAzB1DD6aliGrUvPKufTTGCixOaB8KVeCamdiFAgx1WomNplzdQA== +"@react-spectrum/slider@^3.9.0": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-spectrum/slider/-/slider-3.9.1.tgz#f9b29ee5e178798010eb938e8e5d48c16f5e22be" + integrity sha512-UMBZaaofQsAfH+2SPh2GyxlJjKmRkjVoY1ixybkz8Jh+pOPOFEMugAt3fuLMW22geOC5PXs6AxHYsNb0BwKVnQ== dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/slider@^3.0.1", "@react-stately/slider@^3.2.4", "@react-stately/slider@^3.7.1": +"@react-spectrum/switch@^3.7.0": version "3.7.1" - resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.7.1.tgz#14f7b995eec95c3b25baa270480547afd4d62d51" - integrity sha512-J+G18m1bZBCNQSXhxGd4GNGDUVonv4Sg7fZL+uLhXUy1x71xeJfFdKaviVvZcggtl0/q5InW41PXho7EouMDEg== + resolved "https://registry.yarnpkg.com/@react-spectrum/switch/-/switch-3.7.1.tgz#91cb6f0157b590120cdff7ad75e0afa6b1391f05" + integrity sha512-71rSqseVrieCWxQpwCBbqY4F6bBo8IK/jM+r8ounN/WIkoMGhZkVigF8tSwd+/p8ym2tmJq6MoWn0he1taBo0Q== dependencies: - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" - "@react-types/slider" "^3.8.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/table@^3.15.0": - version "3.15.0" - resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.15.0.tgz#eb84fcf530140fb2e269c22cef21bd3c3b1da1e2" - integrity sha512-KbvkrVF3sb25IPwyte9JcG5/4J7TgjHSsw7D61d/T/oUFMYPYVeolW9/2y+6u48WPkDJE8HJsurme+HbTN0FQA== - dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/flags" "^3.1.2" - "@react-stately/grid" "^3.11.5" - "@react-stately/selection" "^3.20.5" - "@react-stately/utils" "^3.10.8" - "@react-types/grid" "^3.3.5" - "@react-types/shared" "^3.32.0" - "@react-types/table" "^3.13.3" + "@adobe/react-spectrum" "^3.47.0" "@swc/helpers" "^0.5.0" -"@react-stately/tabs@^3.8.5": - version "3.8.5" - resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.8.5.tgz#2447fd3aba1ea05ce9536f3626f9bdafb97bfd67" - integrity sha512-gdeI+NUH3hfqrxkJQSZkt+Zw4G2DrYJRloq/SGxu/9Bu5QD/U0psU2uqxQNtavW5qTChFK+D30rCPXpKlslWAA== +"@react-stately/checkbox@^3.7.4", "@react-stately/checkbox@^3.8.0": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.8.1.tgz#d99c2dfee10ae7df0c0511ce8572fe6f0a76ff4b" + integrity sha512-jNYNOkro8cgLkeeea24UQdE+PWpsBz/6hGPPM9CrHBvC7x2Egh3iHy1not61Oqp7BI7Rq1K1+Q4q8J37bOTr6w== dependencies: - "@react-stately/list" "^3.13.0" - "@react-types/shared" "^3.32.0" - "@react-types/tabs" "^3.3.18" "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-stately/toast@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-stately/toast/-/toast-3.1.2.tgz#0502040b6bd57479eaba1bca2f4c66e9e957e55a" - integrity sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q== +"@react-stately/collections@^3.12.9": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.13.1.tgz#bb8ff4bbb28d16dc90d16e4126ce5c1281a4b680" + integrity sha512-o1QSrtHyR7ODTdPyna87pZlgzvxBFOR8nI8XB+tQLIW2AMhE76pLYu4TN9CrZVy6nSAtE06IntyBV4toJIhorA== dependencies: "@swc/helpers" "^0.5.0" - use-sync-external-store "^1.4.0" + react-stately "^3.46.0" -"@react-stately/toggle@^3.2.1", "@react-stately/toggle@^3.4.4", "@react-stately/toggle@^3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.9.1.tgz#ae706abfebce11989ea370cdf54e821b70b6672f" - integrity sha512-L6yUdE8xZfQhw4aEFZduF8u4v0VrpYrwWEA4Tu/4qwGIPukH0wd2W21Zpw+vAiLOaDKnxel1nXX68MWnm4QXpw== +"@react-stately/menu@^3.10.0", "@react-stately/menu@^3.9.10": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.10.1.tgz#5b9916665d61c66c6dfbd7d421bc48c33111d96f" + integrity sha512-4pQklVXmHV7EVCj3xRYqpfM8pYvIsn5jdIe1CZcvwp3XXtZ5Y6TFR82C7UnI1hAvqI0//by6HWzVOPT7AvyBzw== dependencies: - "@react-stately/utils" "^3.10.8" - "@react-types/checkbox" "^3.10.1" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-stately/tooltip@^3.5.7": - version "3.5.7" - resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.5.7.tgz#7c86942a8b5ee2fad890c09e01718a4939694e7f" - integrity sha512-GYh764BcYZz+Lclyutyir5I3elNo+vVNYzeNOKmPGZCE3p5B+/8lgZAHKxnRc9qmBlxvofnhMcuQxAPlBhoEkw== +"@react-stately/overlays@^3.6.22", "@react-stately/overlays@^3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.7.1.tgz#e00d943ef7da66be314d8be95913dbf473182ebe" + integrity sha512-vGw9f8i5kPvaQqvvQ8iIhPhJZorwtg2rXycqnUNXkNLadewh1S0ocbnRvrb4HW/GGC37rFmGcG1fYCHA/WIn6w== dependencies: - "@react-stately/overlays" "^3.6.19" - "@react-types/tooltip" "^3.4.20" "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-stately/tree@^3.1.2", "@react-stately/tree@^3.9.2": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.9.2.tgz#f9960d7e6a5418a637ad9ea17e607308fcd393e5" - integrity sha512-jsT1WZZhb7GRmg1iqoib9bULsilIK5KhbE8WrcfIml8NYr4usP4DJMcIYfRuiRtPLhKtUvHSoZ5CMbinPp8PUQ== +"@react-stately/radio@^3.11.4", "@react-stately/radio@^3.12.0": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.12.1.tgz#ca6107d8000be610c47417e8f2d859bb3cf5666c" + integrity sha512-sx83ffwQMRhUuMbbRIS4O/FMDyp9DfY1P3bbcSdSgOfglCb6aILJj3ExRJcXSUZdzm79g7cKADNtqzIlW5n6pg== dependencies: - "@react-stately/collections" "^3.12.7" - "@react-stately/selection" "^3.20.5" - "@react-stately/utils" "^3.10.8" - "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-stately/utils@^3.10.8", "@react-stately/utils@^3.6.0": - version "3.10.8" - resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.8.tgz#fdb9d172f7bbc2d083e69190f5ef0edfa4b4392f" - integrity sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g== +"@react-stately/slider@^3.7.4", "@react-stately/slider@^3.8.0": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.8.1.tgz#cfbb2b8eaffc073f6fd2a1b1eda9659ed2a2302f" + integrity sha512-MSfiRJakgdp2FSPtDeViEgXbYjRBGXXSTvExUvDDZx2xiEmSgefEQ4WWwCWPN4tdWbRISZoCYrmG8mY04Gevpw== dependencies: "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-types/button@^3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.14.0.tgz#8b7d1387960bd81ff2c0aa5565d3fb407f6a59b2" - integrity sha512-pXt1a+ElxiZyWpX0uznyjy5Z6EHhYxPcaXpccZXyn6coUo9jmCbgg14xR7Odo+JcbfaaISzZTDO7oGLVTcHnpA== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/calendar@^3.7.4": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.7.4.tgz#8a630b75ce081bb656620ec64204a2b0b27ba84d" - integrity sha512-MZDyXtvdHl8CKQGYBkjYwc4ABBq6Mb4Fu7k/4boQAmMQ5Rtz29ouBCJrAs0BpR14B8ZMGzoNIolxS5RLKBmFSA== - dependencies: - "@internationalized/date" "^3.9.0" - "@react-types/shared" "^3.32.0" - -"@react-types/checkbox@^3.10.1", "@react-types/checkbox@^3.2.1": +"@react-stately/toggle@^3.10.0", "@react-stately/toggle@^3.9.4": version "3.10.1" - resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.10.1.tgz#6cd2e7dd4e27ba017593b8140d0571afcf2b7114" - integrity sha512-8ZqBoGBxtn6U/znpmyutGtBBaafUzcZnbuvYjwyRSONTrqQ0IhUq6jI/jbnE9r9SslIkbMB8IS1xRh2e63qmEQ== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/color@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@react-types/color/-/color-3.1.1.tgz#18b4d809703bb37efbf96f0b11b2335a3c252edd" - integrity sha512-zBF1Op4AO3mlygUq2gFhEoK3gZp2HgwCMUKkCzoDbrvcaahhVbDbfhRxgXKM/2dg7WkgsqhokdkjYV2mGQadRQ== - dependencies: - "@react-types/shared" "^3.32.0" - "@react-types/slider" "^3.8.1" - -"@react-types/combobox@^3.13.8": - version "3.13.8" - resolved "https://registry.yarnpkg.com/@react-types/combobox/-/combobox-3.13.8.tgz#de88e8eb51ec057f5004630668f910e1afd11c96" - integrity sha512-HGC3X9hmDRsjSZcFiflvJ7vbIgQ2gX/ZDxo1HVtvQqUDbgQCVakCcCdrB44aYgHFnyDiO6hyp7Y7jXtDBaEIIA== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/datepicker@^3.13.1": - version "3.13.1" - resolved "https://registry.yarnpkg.com/@react-types/datepicker/-/datepicker-3.13.1.tgz#488989d4ff7605cf80af6fb0d31cb227456201d2" - integrity sha512-ub+g5pS3WOo5P/3FRNsQSwvlb9CuLl2m6v6KBkRXc5xqKhFd7UjvVpL6Oi/1zwwfow4itvD1t7l1XxgCo7wZ6Q== - dependencies: - "@internationalized/date" "^3.9.0" - "@react-types/calendar" "^3.7.4" - "@react-types/overlays" "^3.9.1" - "@react-types/shared" "^3.32.0" - -"@react-types/dialog@*", "@react-types/dialog@^3.5.21": - version "3.5.21" - resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.5.21.tgz#f47302e0a15e75dfe57e868f3496f5c0a0cecd7b" - integrity sha512-jF1gN4bvwYamsLjefaFDnaSKxTa3Wtvn5f7WLjNVZ8ICVoiMBMdUJXTlPQHAL4YWqtCj4hK/3uimR1E+Pwd7Xw== - dependencies: - "@react-types/overlays" "^3.9.1" - "@react-types/shared" "^3.32.0" - -"@react-types/grid@^3.3.5": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.3.5.tgz#5764d4fed4a7f2a721bd77d1043e27686c5dbeee" - integrity sha512-hG6J2KDfmOHitkWoCa/9DvY1nTO2wgMIApcFoqLv7AWJr9CzvVqo5tIhZZCXiT1AvU2kafJxu9e7sr5GxAT2YA== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/menu@^3.1.1", "@react-types/menu@^3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.10.4.tgz#57c0259b149f80fde9230cc1a494d6ac581d09b4" - integrity sha512-jCFVShLq3eASiuznenjoKBv3j0Jy2KQilAjBxdEp56WkZ5D338y/oY5zR6d25u9M0QslpI0DgwC8BwU7MCsPnw== - dependencies: - "@react-types/overlays" "^3.9.1" - "@react-types/shared" "^3.32.0" - -"@react-types/numberfield@^3.8.14": - version "3.8.14" - resolved "https://registry.yarnpkg.com/@react-types/numberfield/-/numberfield-3.8.14.tgz#5a71fed846853176b262c4192025fd18157257dc" - integrity sha512-tlGEHJyeQSMlUoO4g9ekoELGJcqsjc/+/FAxo6YQMhQSkuIdkUKZg3UEBKzif4hLw787u80e1D0SxPUi3KO2oA== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/overlays@^3.4.0", "@react-types/overlays@^3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.9.1.tgz#b6e750d6e4bcf3aaf38fa8a5f69a817f37840934" - integrity sha512-UCG3TOu8FLk4j0Pr1nlhv0opcwMoqbGEOUvsSr6ITN6Qs2y0j+KYSYQ7a4+04m3dN//8+9Wjkkid8k+V1dV2CA== + resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.10.1.tgz#2e8d55cad838579334a93d0800e6a41683bcbc7f" + integrity sha512-oj4goolQJWdeQxRp8a1nZdsUjC779nvAU7pyT3oyWMWrxxrk0heW96TLWhdPjksvre8pLvjbRBbI2kgs8RxmOQ== dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/radio@^3.1.1", "@react-types/radio@^3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.9.1.tgz#4d37e935cd3cc482a5dfaf92cf51068f07e70750" - integrity sha512-DUCN3msm8QZ0MJrP55FmqMONaadYq6JTxihYFGMLP+NoKRnkxvXqNZ2PlkAOLGy3y4RHOnOF8O1LuJqFCCuxDw== - dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/searchfield@^3.6.5": - version "3.6.5" - resolved "https://registry.yarnpkg.com/@react-types/searchfield/-/searchfield-3.6.5.tgz#de43905d990104d640cc507c728e5e12cd1013cf" - integrity sha512-5hI+Hb1U0bSxrJLvEwFEQfk7n3S+GO4c5W/0WZBG00YlYDY9asr1V0oU1WRmKPJJlRpyfG6PkMHDC3jhdj89ew== - dependencies: - "@react-types/shared" "^3.32.0" - "@react-types/textfield" "^3.12.5" + "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-types/select@^3.10.1": +"@react-stately/tree@^3.9.5": version "3.10.1" - resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.10.1.tgz#4c19deb68250233d1acffc929b0beb1b04de2dd0" - integrity sha512-teANUr1byOzGsS/r2j7PatV470JrOhKP8En9lscfnqW5CeUghr+0NxkALnPkiEhCObi/Vu8GIcPareD0HNhtFA== + resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.10.1.tgz#a70794fb7b394ff77c154136c43f2e67344c9a97" + integrity sha512-npdJ+hQGR1J+yi+LOWX7zGS0B9U2SYVbvS31ZCwSMfVUUo4eV+bvLtsiocYUn3bX/EgowFI/ZYJChrIKBpROJA== dependencies: - "@react-types/shared" "^3.32.0" - -"@react-types/shared@*", "@react-types/shared@^3.32.0": - version "3.32.0" - resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.32.0.tgz#6c105ef05e1bd84ab04531e707074dc2a0b3ce07" - integrity sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ== + "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-types/slider@^3.8.1": - version "3.8.1" - resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.8.1.tgz#e2b1a11491cb6957584dc725513e574eddde4105" - integrity sha512-WxiQWj6iQr5Uft0/KcB9XSr361XnyTmL6eREZZacngA9CjPhRWYP3BRDPcCTuP7fj9Yi4QKMrryyjHqMHP8OKQ== +"@react-stately/utils@^3.11.0": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.12.1.tgz#1c7f3980ec00f428023a7320e8882b8449530779" + integrity sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw== dependencies: - "@react-types/shared" "^3.32.0" + "@swc/helpers" "^0.5.0" + react-stately "^3.46.0" -"@react-types/table@^3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.13.3.tgz#83fb11c63e71e726a63246dd3d12caa7c97e90d7" - integrity sha512-/kY/VlXN+8l9saySd6igcsDQ3x8pOVFJAWyMh6gOaOVN7HOJkTMIchmqS+ATa4nege8jZqcdzyGeAmv7mN655A== +"@react-types/button@^3.14.1": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.16.0.tgz#8c6945232737e1bf07036af45c475e14f9962a11" + integrity sha512-Z5///n2Y1jtF0gokBq2Y1K1cpOwsWZ24HPeAm3eEmZrbBXMrxC2oEA5ZThsSHuIGsqiyNJiQ2scsDftmr+PkZw== dependencies: - "@react-types/grid" "^3.3.5" - "@react-types/shared" "^3.32.0" + "@react-aria/button" "^3.15.0" + "@react-spectrum/button" "^3.18.0" -"@react-types/tabs@^3.3.18": - version "3.3.18" - resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.3.18.tgz#c84bf3d113d606d2af8ba52f5465e8b6bb61b707" - integrity sha512-yX/AVlGS7VXCuy2LSm8y8nxUrKVBgnLv+FrtkLqf6jUMtD4KP3k1c4+GPHeScR0HcYzCQF7gCF3Skba1RdYoug== +"@react-types/checkbox@^3.10.2": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.11.0.tgz#92c01dca762fc64f734f26041f4710be52a9354f" + integrity sha512-VXacLw/pKBcxgwejr2p4uPZtG/XXBnTb6pJCdFtUL6OuFIIOFt/9eTHD+8x2HRPLxWkmo1DD3bFBOinQ7vfu2g== dependencies: - "@react-types/shared" "^3.32.0" + "@react-aria/checkbox" "^3.17.0" + "@react-aria/toggle" "^3.13.0" + "@react-spectrum/checkbox" "^3.11.0" + "@react-stately/checkbox" "^3.8.0" + "@react-stately/toggle" "^3.10.0" -"@react-types/textfield@^3.12.5": - version "3.12.5" - resolved "https://registry.yarnpkg.com/@react-types/textfield/-/textfield-3.12.5.tgz#b9b4cd1b8f8b907d756c8323fb7fc2ac5b39a225" - integrity sha512-VXez8KIcop87EgIy00r+tb30xokA309TfJ32Qv5qOYB5SMqoHnb6SYvWL8Ih2PDqCo5eBiiGesSaWYrHnRIL8Q== +"@react-types/dialog@^3.5.22": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.6.0.tgz#5c5c1b4ccb7bf4148b8a8bf1cde81605ad8d9337" + integrity sha512-vvxohmsTRZWE/saaJt6mMy3ONA4xbQTSk1okfMUK6OMSp/VpLBRLCz/2/myiMK3UIBCagUnrwzOwbk9whnFx0g== + dependencies: + "@react-aria/dialog" "^3.6.0" + "@react-spectrum/dialog" "^3.10.0" + +"@react-types/menu@^3.10.5": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.11.0.tgz#1e91f0436922726b3c3f55380bd2c9e209ed32bd" + integrity sha512-YGmzlLJngMzpr4GrrZ7cvJP5CIjPjWPEIeU7x2Q4WvVKelfvhGuOBOU67IZEN53NUC2hiE7YxE6UjodMYp+0Ww== + dependencies: + "@react-aria/menu" "^3.22.0" + "@react-spectrum/menu" "^3.23.0" + "@react-stately/menu" "^3.10.0" + +"@react-types/overlays@^3.9.2": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.10.0.tgz#e263133b214cb428c2719487b9f51c93da317b7b" + integrity sha512-cgrcOTxy6ac0kiphQOkc8mj5artZMB/XVrFgukRZ2FcbYNEERpg2VQ5ztd0+H1ER7O0kx7AmwHxdut+x1EAjrw== + dependencies: + "@react-aria/overlays" "^3.32.0" + "@react-spectrum/overlays" "^5.10.0" + "@react-stately/overlays" "^3.7.0" + "@react-types/shared" "^3.34.0" + +"@react-types/radio@^3.9.2": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.10.0.tgz#9fe302e02a0fc9f6213c3fd6eb2a6b8fdea0a7ee" + integrity sha512-o9vDmAYf8WpI7WelgU6wpX/1acrRIGdKIgeXiVokjBelpjE0pYMY7e/L6/WKB4RUtgxLuf7JavLAU/CWAD/ewQ== + dependencies: + "@react-aria/radio" "^3.13.0" + "@react-spectrum/radio" "^3.8.0" + "@react-stately/radio" "^3.12.0" + +"@react-types/shared@^3.33.0", "@react-types/shared@^3.34.0", "@react-types/shared@^3.36.0": + version "3.36.0" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.36.0.tgz#52e713c6bae8e117967bf1d19d89db3a56219037" + integrity sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ== + +"@react-types/slider@^3.8.2": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.9.0.tgz#3a078f25e3b53c28bf485bdd99fa94a70922ff5d" + integrity sha512-gbQWB8LnucC+jhDi5Imd2ZDPbVuiHt0xzObGHWq6GTZ3slLgTvGQaHNO5h9+h7dZH0iFQOVNb6+TlyTsnjFuAw== dependencies: - "@react-types/shared" "^3.32.0" + "@react-aria/slider" "^3.9.0" + "@react-spectrum/slider" "^3.9.0" + "@react-stately/slider" "^3.8.0" -"@react-types/tooltip@^3.4.20": - version "3.4.20" - resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.4.20.tgz#af71dde12afb8a33a7b5bb3d8ff0cda6c4ee3999" - integrity sha512-tF1yThwvgSgW8Gu/CLL0p92AUldHR6szlwhwW+ewT318sQlfabMGO4xlCNFdxJYtqTpEXk2rlaVrBuaC//du0w== +"@react-types/switch@^3.5.15": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.6.0.tgz#a37365222ec26c0a03b7b99ede02e1fc06fcc5ec" + integrity sha512-6pjDO5a35ovAAw5xsSJoftmUP1BhFzKazB5IuVBNTbeEvJjz/LC1NGkVV3sQX68ZDSZ0UyYLS7ENIVHKU+hjzA== dependencies: - "@react-types/overlays" "^3.9.1" - "@react-types/shared" "^3.32.0" + "@react-aria/switch" "^3.8.0" + "@react-spectrum/switch" "^3.7.0" "@rnmapbox/maps@10.1.42-rc.0": version "10.1.42-rc.0" @@ -4589,6 +3960,23 @@ resolved "https://registry.yarnpkg.com/@solid-primitives/utils/-/utils-6.3.2.tgz#13d6126fc5a498965d7c45dd41c052e42dcfd7e1" integrity sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ== +"@spectrum-icons/ui@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@spectrum-icons/ui/-/ui-3.7.1.tgz#196095fba6da1952eab5d1ce2edc2f9e629f47b0" + integrity sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w== + dependencies: + "@adobe/react-spectrum-ui" "1.2.1" + "@babel/runtime" "^7.24.4" + "@swc/helpers" "^0.5.0" + +"@spectrum-icons/workflow@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@spectrum-icons/workflow/-/workflow-4.3.1.tgz#c05f6aba6c3c13bc7e3c906521b3c7944dac1b23" + integrity sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA== + dependencies: + "@adobe/react-spectrum-workflow" "2.3.5" + "@swc/helpers" "^0.5.0" + "@standard-schema/spec@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" @@ -4700,6 +4088,115 @@ dependencies: defer-to-connect "^2.0.0" +"@tailwindcss/node@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.3.tgz#38ff04309ff036ea3589a7bad9069c44ec9d3883" + integrity sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg== + dependencies: + "@jridgewell/remapping" "^2.3.5" + enhanced-resolve "^5.24.1" + jiti "^2.7.0" + lightningcss "1.32.0" + magic-string "^0.30.21" + source-map-js "^1.2.1" + tailwindcss "4.3.3" + +"@tailwindcss/oxide-android-arm64@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz#848f93034155daf7892185028dfb18143bfc7d07" + integrity sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw== + +"@tailwindcss/oxide-darwin-arm64@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz#5c779e32c1c361beb75136fd8e2c627fcb9a5b28" + integrity sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw== + +"@tailwindcss/oxide-darwin-x64@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz#ba292ff52fa3264139f7fe7874719ce0a4666875" + integrity sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw== + +"@tailwindcss/oxide-freebsd-x64@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz#07e589487b9a636235a4ade48cefc1f9eeeec57f" + integrity sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz#0c8abc228d9e19e0065ddb707eba52e21855b3ff" + integrity sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ== + +"@tailwindcss/oxide-linux-arm64-gnu@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz#5067dc7afd15d2b97adc77a91177dc4bec8d1b8b" + integrity sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w== + +"@tailwindcss/oxide-linux-arm64-musl@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz#29ae5f279be2ce64db368711985b6ad8e4562e57" + integrity sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA== + +"@tailwindcss/oxide-linux-x64-gnu@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz#7d693b40f69875744b499481359b227fd3ac3a3c" + integrity sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w== + +"@tailwindcss/oxide-linux-x64-musl@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz#6a55d664f47c5ff9ad3f46bf05fe07d410b8cfd8" + integrity sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img== + +"@tailwindcss/oxide-wasm32-wasi@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz#408a9bc620e68f1aaf0dfea33da7bd3b65f9e2ef" + integrity sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ== + dependencies: + "@emnapi/core" "^1.11.1" + "@emnapi/runtime" "^1.11.1" + "@emnapi/wasi-threads" "^1.2.2" + "@napi-rs/wasm-runtime" "^1.1.4" + "@tybys/wasm-util" "^0.10.2" + tslib "^2.8.1" + +"@tailwindcss/oxide-win32-arm64-msvc@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz#3d582b00180fa4680e0b6c90be69fa5f4a209092" + integrity sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ== + +"@tailwindcss/oxide-win32-x64-msvc@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz#ce4c663fef3b4fde67611a4c1391866f8c0aa79b" + integrity sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw== + +"@tailwindcss/oxide@4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.3.tgz#6266109d025cfcb04f8e4c58954a6f630a415b7f" + integrity sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA== + optionalDependencies: + "@tailwindcss/oxide-android-arm64" "4.3.3" + "@tailwindcss/oxide-darwin-arm64" "4.3.3" + "@tailwindcss/oxide-darwin-x64" "4.3.3" + "@tailwindcss/oxide-freebsd-x64" "4.3.3" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.3" + "@tailwindcss/oxide-linux-arm64-gnu" "4.3.3" + "@tailwindcss/oxide-linux-arm64-musl" "4.3.3" + "@tailwindcss/oxide-linux-x64-gnu" "4.3.3" + "@tailwindcss/oxide-linux-x64-musl" "4.3.3" + "@tailwindcss/oxide-wasm32-wasi" "4.3.3" + "@tailwindcss/oxide-win32-arm64-msvc" "4.3.3" + "@tailwindcss/oxide-win32-x64-msvc" "4.3.3" + +"@tailwindcss/postcss@^4.2.0": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.3.3.tgz#dc14df6477edc16087df1663617ee23bc1042610" + integrity sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg== + dependencies: + "@alloc/quick-lru" "^5.2.0" + "@tailwindcss/node" "4.3.3" + "@tailwindcss/oxide" "4.3.3" + postcss "^8.5.16" + tailwindcss "4.3.3" + "@tanstack/query-core@5.52.3": version "5.52.3" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.52.3.tgz#4d66b3e7a6d77513cb4d3cb661b2c03009103f5a" @@ -4898,6 +4395,13 @@ dependencies: tslib "^2.4.0" +"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== + dependencies: + tslib "^2.4.0" + "@types/babel__core@^7.1.14", "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -4948,6 +4452,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.12": + version "4.1.13" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.13.tgz#22d1cc9d542d3593caea764f974306ab36286ee7" + integrity sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw== + dependencies: + "@types/ms" "*" + "@types/debug@^4.1.6": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -5147,16 +4658,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/strip-bom@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" - integrity sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ== - -"@types/strip-json-comments@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" - integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== - "@types/supercluster@^7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/supercluster/-/supercluster-7.1.3.tgz#1a1bc2401b09174d9c9e44124931ec7874a72b27" @@ -5722,7 +5223,7 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@^3.0.3, anymatch@^3.1.3, anymatch@~3.1.2: +anymatch@^3.0.3, anymatch@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -5809,7 +5310,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.2.4: +aria-hidden@^1.2.3, aria-hidden@^1.2.4: version "1.2.6" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== @@ -6149,6 +5650,13 @@ babel-plugin-react-compiler@^1.0.0: dependencies: "@babel/types" "^7.26.0" +babel-plugin-react-compiler@^19.1.0-rc.2: + version "19.1.0-rc.1-rc-af1b7da-20250421" + resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.1-rc-af1b7da-20250421.tgz#d52096e058c63293ced8b57d3f9b0395d1894ede" + integrity sha512-E3kaokBhWDLf7ZD8fuYjYn0ZJHYZ+3EHtAWCdX2hl4lpu1z9S/Xr99sxhx2bTCVB41oIesz9FtM8f4INsrZaOw== + dependencies: + "@babel/types" "^7.26.0" + babel-plugin-react-native-web@~0.21.0: version "0.21.2" resolved "https://registry.yarnpkg.com/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz#d2f7fd673278da82577aa583457edb55d9cccbe0" @@ -6275,11 +5783,6 @@ big-integer@1.6.x: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -6361,7 +5864,7 @@ brace-expansion@^5.0.5: dependencies: balanced-match "^4.0.2" -braces@^3.0.3, braces@~3.0.2: +braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -6549,11 +6052,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -6664,21 +6162,6 @@ cheap-ruler@^4.0.0: resolved "https://registry.yarnpkg.com/cheap-ruler/-/cheap-ruler-4.0.0.tgz#bdc984de7e0e3f748bdfd2dbe23ec6b9dc820a09" integrity sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw== -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" @@ -6923,6 +6406,11 @@ colorette@^2.0.20: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +colorjs.io@0.6.0-alpha.1: + version "0.6.0-alpha.1" + resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.6.0-alpha.1.tgz#c1c382d0f4f34ae8b1d82db73a233a69b7c36f98" + integrity sha512-c/h/8uAmPydQcriRdX8UTAFHj6SpSHFHBA8LvMikvYWAVApPTwg/pyOXNsGmaCBd6L/EeDlRHSNhTtnIFp/qsg== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -7230,11 +6718,6 @@ csscolorparser@~1.0.3: resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - csso@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" @@ -7327,7 +6810,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1, debug@~4.4.0: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1, debug@~4.4.0: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -7504,11 +6987,6 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -7539,11 +7017,6 @@ dir-glob@^3.0.0, dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - dmg-builder@26.7.0: version "26.7.0" resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.7.0.tgz#3f1d2ef65c4a6087daf63a9b94621bd465993297" @@ -7590,7 +7063,7 @@ dom-accessibility-api@^0.6.3: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== -dom-helpers@^5.0.0: +dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== @@ -7598,6 +7071,14 @@ dom-helpers@^5.0.0: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-helpers@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-6.0.1.tgz#f56400eb45770e3dd4fa014513739e6b670e6362" + integrity sha512-IKySryuFwseGkrCA/pIqlwUPOD50w1Lj/B2Yief3vBOP18k5y4t+hTqKh55gULDVeJMRitcozve+g/wVFf4sFQ== + dependencies: + "@babel/runtime" "^7.27.1" + csstype "^3.1.3" + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -7844,6 +7325,14 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.24.1: + version "5.24.5" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz#b4dad3255b7545f07ba5535189868e9f85f47573" + integrity sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.3" + entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -9220,7 +8709,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@^2.3.3, fsevents@~2.3.2: +fsevents@^2.3.2, fsevents@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -9387,7 +8876,7 @@ gl-matrix@^3.4.4: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.4.tgz#7789ee4982f62c7a7af447ee488f3bd6b0c77003" integrity sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -9413,18 +8902,6 @@ glob@^10.2.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^10.3.10: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - glob@^13.0.0: version "13.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d" @@ -10010,16 +9487,6 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" -intl-messageformat@^10.1.0: - version "10.7.16" - resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.16.tgz#d909f9f9f4ab857fbe681d559b958dd4dd9f665a" - integrity sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug== - dependencies: - "@formatjs/ecma402-abstract" "2.3.4" - "@formatjs/fast-memoize" "2.2.7" - "@formatjs/icu-messageformat-parser" "2.11.2" - tslib "^2.8.0" - invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -10082,13 +9549,6 @@ is-bigint@^1.1.0: dependencies: has-bigints "^1.0.2" -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" @@ -10219,7 +9679,7 @@ is-generator-function@^1.0.10, is-generator-function@^1.0.7: has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -11087,11 +10547,6 @@ jimp@^0.22.7: "@jimp/types" "^0.22.12" regenerator-runtime "^0.13.3" -jiti@^1.21.0: - version "1.21.7" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" - integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== - jiti@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.0.tgz#b831fdc4440c0a4944c34456643c555afe09d36d" @@ -11102,6 +10557,11 @@ jiti@^2.4.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== +jiti@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" + integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== + joi@^18.0.1: version "18.0.2" resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.2.tgz#30ced6aed00a7848cc11f92859515258301dc3a4" @@ -11348,160 +10808,75 @@ lighthouse-logger@^1.0.0: debug "^2.6.9" marky "^1.2.2" -lightningcss-android-arm64@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307" - integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A== - -lightningcss-android-arm64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" - integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== - -lightningcss-darwin-arm64@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz#a5fa946d27c029e48c7ff929e6e724a7de46eb2c" - integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA== - -lightningcss-darwin-arm64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" - integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== - -lightningcss-darwin-x64@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd" - integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ== - -lightningcss-darwin-x64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" - integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== - -lightningcss-freebsd-x64@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5" - integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA== - -lightningcss-freebsd-x64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" - integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== - -lightningcss-linux-arm-gnueabihf@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb" - integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA== - -lightningcss-linux-arm-gnueabihf@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" - integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== - -lightningcss-linux-arm64-gnu@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298" - integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A== - -lightningcss-linux-arm64-gnu@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" - integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== - -lightningcss-linux-arm64-musl@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b" - integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA== - -lightningcss-linux-arm64-musl@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" - integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== - -lightningcss-linux-x64-gnu@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a" - integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w== - -lightningcss-linux-x64-gnu@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6" - integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== - -lightningcss-linux-x64-musl@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728" - integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA== - -lightningcss-linux-x64-musl@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b" - integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== - -lightningcss-win32-arm64-msvc@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a" - integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ== - -lightningcss-win32-arm64-msvc@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" - integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== - -lightningcss-win32-x64-msvc@1.30.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz#f01f382c8e0a27e1c018b0bee316d210eac43b6e" - integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw== - -lightningcss-win32-x64-msvc@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" - integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== - -lightningcss@^1.27.0: - version "1.30.2" - resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.30.2.tgz#4ade295f25d140f487d37256f4cd40dc607696d0" - integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ== - dependencies: - detect-libc "^2.0.3" - optionalDependencies: - lightningcss-android-arm64 "1.30.2" - lightningcss-darwin-arm64 "1.30.2" - lightningcss-darwin-x64 "1.30.2" - lightningcss-freebsd-x64 "1.30.2" - lightningcss-linux-arm-gnueabihf "1.30.2" - lightningcss-linux-arm64-gnu "1.30.2" - lightningcss-linux-arm64-musl "1.30.2" - lightningcss-linux-x64-gnu "1.30.2" - lightningcss-linux-x64-musl "1.30.2" - lightningcss-win32-arm64-msvc "1.30.2" - lightningcss-win32-x64-msvc "1.30.2" - -lightningcss@^1.30.1: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" - integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + +lightningcss-win32-x64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== + +lightningcss@1.30.1, lightningcss@1.32.0, lightningcss@^1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.30.1.tgz#78e979c2d595bfcb90d2a8c0eb632fe6c5bfed5d" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== dependencies: detect-libc "^2.0.3" optionalDependencies: - lightningcss-android-arm64 "1.32.0" - lightningcss-darwin-arm64 "1.32.0" - lightningcss-darwin-x64 "1.32.0" - lightningcss-freebsd-x64 "1.32.0" - lightningcss-linux-arm-gnueabihf "1.32.0" - lightningcss-linux-arm64-gnu "1.32.0" - lightningcss-linux-arm64-musl "1.32.0" - lightningcss-linux-x64-gnu "1.32.0" - lightningcss-linux-x64-musl "1.32.0" - lightningcss-win32-arm64-msvc "1.32.0" - lightningcss-win32-x64-msvc "1.32.0" - -lilconfig@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lilconfig@^3.0.0, lilconfig@~3.1.3: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + +lilconfig@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== @@ -11834,6 +11209,13 @@ lucide-react-native@~0.475.0: resolved "https://registry.yarnpkg.com/lucide-react-native/-/lucide-react-native-0.475.0.tgz#a96bbcaa8d953c845cf3038d136aba408318c484" integrity sha512-V5tho5qQ89GD4qdzL07ZyXdrnpXZFLirGfaG6BB2vKhO6X1iA7UYYqntgBQ//ZuTUEdevskl+dVT5O4A9oOJUg== +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -12359,7 +11741,7 @@ metro@0.83.5, metro@^0.83.1: ws "^7.5.10" yargs "^17.6.2" -micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8, micromatch@~4.0.8: +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.8, micromatch@~4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -12624,19 +12006,22 @@ nanoid@^3.3.1, nanoid@^3.3.11, nanoid@^3.3.7, nanoid@^3.3.8: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.16: + version "3.3.16" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c" + integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q== + napi-postinstall@^0.3.0: version "0.3.3" resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz#93d045c6b576803ead126711d3093995198c6eb9" integrity sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow== -nativewind@~4.1.21: - version "4.1.23" - resolved "https://registry.yarnpkg.com/nativewind/-/nativewind-4.1.23.tgz#badfa94a5cd2e12ff8971dd274d234cd843ca74e" - integrity sha512-oLX3suGI6ojQqWxdQezOSM5GmJ4KvMnMtmaSMN9Ggb5j7ysFt4nHxb1xs8RDjZR7BWc+bsetNJU8IQdQMHqRpg== +nativewind@5.0.0-preview.4: + version "5.0.0-preview.4" + resolved "https://registry.yarnpkg.com/nativewind/-/nativewind-5.0.0-preview.4.tgz#60720e05a3cf5c56acd3ae8015abaad35ef41e05" + integrity sha512-eLsFGczxIsWADoD18EbEyt01aWgw4uP1/t/O7JIsp/OEbSgVYVobW97e2DDTOxxiiqSu0RexC1KSo5d7tes9uQ== dependencies: - comment-json "^4.2.5" - debug "^4.3.7" - react-native-css-interop "0.1.22" + tailwindcss-safe-area "^1.1.0" natural-compare-lite@^1.4.0: version "1.4.0" @@ -12771,7 +12156,7 @@ normalize-package-data@^6.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -12903,11 +12288,6 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" @@ -13308,27 +12688,6 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -patch-package@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" - integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^4.1.2" - ci-info "^3.7.0" - cross-spawn "^7.0.3" - find-yarn-workspace-root "^2.0.0" - fs-extra "^9.0.0" - json-stable-stringify "^1.0.2" - klaw-sync "^6.0.0" - minimist "^1.2.6" - open "^7.4.2" - rimraf "^2.6.3" - semver "^7.5.3" - slash "^2.0.0" - tmp "^0.0.33" - yaml "^2.2.2" - patch-package@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" @@ -13444,7 +12803,7 @@ picocolors@^1.0.0, picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -13464,11 +12823,6 @@ pidtree@~0.6.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" @@ -13548,51 +12902,12 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.1.0.tgz#003b63c6edde948766e40f3daf7e997ae43a5ce6" - integrity sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw== - dependencies: - camelcase-css "^2.0.1" - -postcss-load-config@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-nested@^6.0.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" - integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== - dependencies: - postcss-selector-parser "^6.1.1" - -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1: - version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.23, postcss@^8.4.4: +postcss@^8.4.4: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -13601,6 +12916,15 @@ postcss@^8.4.23, postcss@^8.4.4: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.16: + version "8.5.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.25.tgz#5012a598eaaa897f21bbe8553be3cb7bd2bd78cb" + integrity sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw== + dependencies: + nanoid "^3.3.16" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postcss@~8.4.32: version "8.4.49" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" @@ -13721,7 +13045,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -13855,6 +13179,33 @@ rc@1.2.8, rc@~1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-aria-components@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/react-aria-components/-/react-aria-components-1.19.0.tgz#202394140728e2ef0ff9652276e9e12a81cccda6" + integrity sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A== + dependencies: + "@internationalized/date" "^3.12.2" + "@react-types/shared" "^3.36.0" + "@swc/helpers" "^0.5.0" + client-only "^0.0.1" + react-aria "3.50.0" + react-stately "3.48.0" + +react-aria@3.50.0, react-aria@^3.48.0: + version "3.50.0" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.50.0.tgz#75cb002c8ff94be3bc1afb6f717b37c6d0548119" + integrity sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw== + dependencies: + "@internationalized/date" "^3.12.2" + "@internationalized/number" "^3.6.7" + "@internationalized/string" "^3.2.9" + "@react-types/shared" "^3.36.0" + "@swc/helpers" "^0.5.0" + aria-hidden "^1.2.3" + clsx "^2.0.0" + react-stately "3.48.0" + use-sync-external-store "^1.6.0" + react-devtools-core@^6.1.5: version "6.1.5" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.1.5.tgz#c5eca79209dab853a03b2158c034c5166975feee" @@ -13929,17 +13280,16 @@ react-native-ble-manager@^12.1.5: version "4.3.16" resolved "https://codeload.github.com/Irfanwani/react-native-callkeep/tar.gz/957193d0716f1c2dfdc18e627cbff0f8a0800971" -react-native-css-interop@0.1.22: - version "0.1.22" - resolved "https://registry.yarnpkg.com/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz#70cc6ca7a8f14126e123e44a19ceed74dd2a167a" - integrity sha512-Mu01e+H9G+fxSWvwtgWlF5MJBJC4VszTCBXopIpeR171lbeBInHb8aHqoqRPxmJpi3xIHryzqKFOJYAdk7PBxg== +react-native-css@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/react-native-css/-/react-native-css-3.0.7.tgz#e2e38dc434b57cd1f1b191a1b58a63e84eb5d277" + integrity sha512-xsU9T5MleHI/DDmY72A7ZikbkDfDNkMrAEichOJW2bBUMxPzAJPrFk9ys4cXYk4gLFaR11y23EccOJ+JT6Iusw== dependencies: - "@babel/helper-module-imports" "^7.22.15" - "@babel/traverse" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.3.7" - lightningcss "^1.27.0" - semver "^7.6.3" + "@types/debug" "^4.1.12" + babel-plugin-react-compiler "^19.1.0-rc.2" + colorjs.io "0.6.0-alpha.1" + comment-json "^4.2.5" + debug "^4.4.1" react-native-edge-to-edge@1.6.0: version "1.6.0" @@ -14158,37 +13508,17 @@ react-remove-scroll@^2.6.3: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-stately@^3.21.0: - version "3.41.0" - resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.41.0.tgz#e8239f520cf2cbaa037c0fd9ddf274a9a6bcb3bd" - integrity sha512-Fe8PaZPm9Ue9kDXVa8KaOz6gzbmZPuzftxeVQwKVX3u/kyFhbRkr/LeAFvgP7a+EeX+Bjmdht/9ixDsBXj4qbQ== - dependencies: - "@react-stately/calendar" "^3.8.4" - "@react-stately/checkbox" "^3.7.1" - "@react-stately/collections" "^3.12.7" - "@react-stately/color" "^3.9.1" - "@react-stately/combobox" "^3.11.1" - "@react-stately/data" "^3.14.0" - "@react-stately/datepicker" "^3.15.1" - "@react-stately/disclosure" "^3.0.7" - "@react-stately/dnd" "^3.7.0" - "@react-stately/form" "^3.2.1" - "@react-stately/list" "^3.13.0" - "@react-stately/menu" "^3.9.7" - "@react-stately/numberfield" "^3.10.1" - "@react-stately/overlays" "^3.6.19" - "@react-stately/radio" "^3.11.1" - "@react-stately/searchfield" "^3.5.15" - "@react-stately/select" "^3.7.1" - "@react-stately/selection" "^3.20.5" - "@react-stately/slider" "^3.7.1" - "@react-stately/table" "^3.15.0" - "@react-stately/tabs" "^3.8.5" - "@react-stately/toast" "^3.1.2" - "@react-stately/toggle" "^3.9.1" - "@react-stately/tooltip" "^3.5.7" - "@react-stately/tree" "^3.9.2" - "@react-types/shared" "^3.32.0" +react-stately@3.48.0, react-stately@^3.46.0: + version "3.48.0" + resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.48.0.tgz#80b658d91a20b35f9803102302268a477ba819e4" + integrity sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA== + dependencies: + "@internationalized/date" "^3.12.2" + "@internationalized/number" "^3.6.7" + "@internationalized/string" "^3.2.9" + "@react-types/shared" "^3.36.0" + "@swc/helpers" "^0.5.0" + use-sync-external-store "^1.6.0" react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" @@ -14206,6 +13536,16 @@ react-test-renderer@19.1.0: react-is "^19.1.0" scheduler "^0.26.0" +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@19.1.0: version "19.1.0" resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" @@ -14218,13 +13558,6 @@ read-binary-file-arch@^1.0.6: dependencies: debug "^4.3.4" -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - read-package-up@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-11.0.0.tgz#71fb879fdaac0e16891e6e666df22de24a48d5ba" @@ -14291,13 +13624,6 @@ readable-web-to-node-stream@^3.0.0: dependencies: readable-stream "^4.7.0" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -14479,7 +13805,7 @@ resolve.exports@^2.0.0, resolve.exports@^2.0.3: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.10, resolve@^1.22.2, resolve@^1.22.4, resolve@^1.22.8: +resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.10, resolve@^1.22.2, resolve@^1.22.4, resolve@^1.22.8: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -14550,13 +13876,6 @@ rfdc@^1.4.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -15490,16 +14809,16 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + strtok3@^6.2.4: version "6.3.0" resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" @@ -15531,19 +14850,6 @@ styleq@^0.1.3: resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71" integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA== -sucrase@^3.32.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "^10.3.10" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - sucrase@~3.35.1: version "3.35.1" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1" @@ -15674,39 +14980,26 @@ tailwind-variants@~0.2.1: dependencies: tailwind-merge "^2.2.0" -tailwindcss@3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" - integrity sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A== - dependencies: - "@alloc/quick-lru" "^5.2.0" - arg "^5.0.2" - chokidar "^3.5.3" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.3.0" - glob-parent "^6.0.2" - is-glob "^4.0.3" - jiti "^1.21.0" - lilconfig "^2.1.0" - micromatch "^4.0.5" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.0.0" - postcss "^8.4.23" - postcss-import "^15.1.0" - postcss-js "^4.0.1" - postcss-load-config "^4.0.1" - postcss-nested "^6.0.1" - postcss-selector-parser "^6.0.11" - resolve "^1.22.2" - sucrase "^3.32.0" +tailwindcss-safe-area@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tailwindcss-safe-area/-/tailwindcss-safe-area-1.3.0.tgz#59ac759ce4d3a3521d4d0acf5429be1add2c6d01" + integrity sha512-RoxnW1zAjBWC3XK+row7Qj5toRMRlKNN/p3FLXb6fTGKxDGWT6JP/mcNX1yf09xRficQ308hbwiedgniepSp1Q== + +tailwindcss@4.3.3, tailwindcss@^4.2.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.3.tgz#c006861611c213c1877893ab5b23daa16be2bb55" + integrity sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ== tapable@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b" integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg== +tapable@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" + integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== + tar@^7.4.3: version "7.5.1" resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.1.tgz#750a8bd63b7c44c1848e7bf982260a083cf747c9" @@ -16001,16 +15294,6 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig@7: - version "7.0.0" - resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" - integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== - dependencies: - "@types/strip-bom" "^3.0.0" - "@types/strip-json-comments" "0.0.30" - strip-bom "^3.0.0" - strip-json-comments "^2.0.0" - tslib@2.8.1, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" @@ -16142,11 +15425,6 @@ typed-emitter@^2.1.0: optionalDependencies: rxjs "^7.5.2" -typescript@^5.6.3: - version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== - typescript@~5.9.2: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" @@ -16353,11 +15631,16 @@ use-sidecar@^1.1.3: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0: +use-sync-external-store@^1.2.2, use-sync-external-store@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== +use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + usehooks-ts@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz#0bb7f38f36f8219ee4509cc5e944ae610fb97656" @@ -16377,7 +15660,7 @@ utif2@^4.0.1: dependencies: pako "^1.0.11" -util-deprecate@^1.0.1, util-deprecate@^1.0.2: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -16871,7 +16154,7 @@ yallist@^5.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== -yaml@^2.2.2, yaml@^2.3.4: +yaml@^2.2.2: version "2.8.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== From 3e4f3aa94a3b1450040e2321e54aeae577f69002 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 31 Jul 2026 11:59:45 -0700 Subject: [PATCH 2/2] RD-T43 PR#121 Fixes --- electron/preload.js | 7 ++- src/app/(app)/_layout.tsx | 2 + src/app/(app)/home.tsx | 18 +++++-- src/app/(app)/map.tsx | 18 ++++--- .../callVideoFeeds/video-player.tsx | 40 +++++++++----- src/components/calls/call-files-modal.tsx | 52 ++++++++++--------- .../dispatch-console/active-calls-panel.tsx | 19 ++++--- .../notifications/NotificationDetail.tsx | 48 +++++++++-------- src/stores/toast/store.ts | 13 ++++- 9 files changed, 136 insertions(+), 81 deletions(-) diff --git a/electron/preload.js b/electron/preload.js index 6a06dc6..cbaff52 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -57,15 +57,18 @@ contextBridge.exposeInMainWorld('electronDeepLinks', { * Register a callback that fires when the main process forwards a * deep-link URL to the renderer. * @param {(url: string) => void} callback + * @returns {() => void} unsubscribe function that removes the listener */ onDeepLink: (callback) => { - ipcRenderer.on('deep-link', (_event, deepLinkUrl) => { + const handler = (_event, deepLinkUrl) => { try { callback(deepLinkUrl); } catch (err) { console.error('Error in deep-link callback:', err); } - }); + }; + ipcRenderer.on('deep-link', handler); + return () => ipcRenderer.removeListener('deep-link', handler); }, }); diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index 93dd81d..7b9951e 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -218,6 +218,8 @@ export default function TabLayout() { }); // Reset initialization state on error so it can be retried hasInitialized.current = false; + // If the init promise is still hanging, clear the guard so a retry is possible + isInitializing.current = false; } }, [status]); diff --git a/src/app/(app)/home.tsx b/src/app/(app)/home.tsx index 786d504..9429530 100644 --- a/src/app/(app)/home.tsx +++ b/src/app/(app)/home.tsx @@ -440,6 +440,18 @@ export default function DispatchConsole() { } }, [selectedCallId]); + const handleWeatherAlertsPress = useCallback(() => { + router.push('/(app)/weather-alerts' as Href); + }, []); + + const handleCloseAddNoteSheet = useCallback(() => { + setIsAddNoteSheetOpen(false); + }, []); + + const handleCloseCloseCallSheet = useCallback(() => { + setIsCloseCallSheetOpen(false); + }, []); + // Handle note added from bottom sheet const handleNoteAdded = () => { fetchNotes(); @@ -947,7 +959,7 @@ export default function DispatchConsole() { weatherLongitude={mapCenterLongitude} extremeAlerts={extremeAlertCount} severeAlerts={severeAlertCount} - onWeatherAlertsPress={() => router.push('/(app)/weather-alerts' as Href)} + onWeatherAlertsPress={handleWeatherAlertsPress} /> {/* Active Call Filter Banner */} @@ -960,10 +972,10 @@ export default function DispatchConsole() { {/* Add Note Bottom Sheet */} - setIsAddNoteSheetOpen(false)} onNoteAdded={handleNoteAdded} /> + {/* Close Call Bottom Sheet */} - {selectedCallId && setIsCloseCallSheetOpen(false)} callId={selectedCallId} />} + {selectedCallId && } ); } diff --git a/src/app/(app)/map.tsx b/src/app/(app)/map.tsx index 2390786..d4231d6 100644 --- a/src/app/(app)/map.tsx +++ b/src/app/(app)/map.tsx @@ -153,9 +153,11 @@ export default function Map() { logger.info({ message: 'Map focused, resetting camera to current location', context: { - latitude: location.latitude, - longitude: location.longitude, + // GDPR: log bucketed (~11km) coordinates instead of verbatim position + latitudeBucket: Math.round(location.latitude * 10) / 10, + longitudeBucket: Math.round(location.longitude * 10) / 10, isMapLocked: location.isMapLocked, + gdpr: { purpose: 'map_tracking', lawful_basis: 'consent' }, }, }); } @@ -170,10 +172,12 @@ export default function Map() { logger.info({ message: 'Location updated and map is ready', context: { - latitude: location.latitude, - longitude: location.longitude, + // GDPR: log bucketed (~11km) coordinates instead of verbatim position + latitudeBucket: Math.round(location.latitude * 10) / 10, + longitudeBucket: Math.round(location.longitude * 10) / 10, heading: location.heading, isMapLocked: location.isMapLocked, + gdpr: { purpose: 'map_tracking', lawful_basis: 'consent' }, }, }); @@ -213,8 +217,10 @@ export default function Map() { logger.info({ message: 'Map unlocked, resetting camera to normal view and user interaction state', context: { - latitude: location.latitude, - longitude: location.longitude, + // GDPR: log bucketed (~11km) coordinates instead of verbatim position + latitudeBucket: Math.round(location.latitude * 10) / 10, + longitudeBucket: Math.round(location.longitude * 10) / 10, + gdpr: { purpose: 'map_tracking', lawful_basis: 'consent' }, }, }); } diff --git a/src/components/callVideoFeeds/video-player.tsx b/src/components/callVideoFeeds/video-player.tsx index 2916a87..ed3cb06 100644 --- a/src/components/callVideoFeeds/video-player.tsx +++ b/src/components/callVideoFeeds/video-player.tsx @@ -44,25 +44,41 @@ function getPlayerHtml(feed: CallVideoFeedResultData, isDark: boolean): string | switch (feed.FeedFormat) { case CallVideoFeedFormat.HLS: case CallVideoFeedFormat.DASH: + // hls.js is pinned to an exact version and loaded with an SRI integrity hash so a + // compromised CDN cannot inject a different script. Player init runs from the + // script's onLoad callback (and onerror fallback) instead of assuming the global exists. return ` - - - - + + + + `; diff --git a/src/components/calls/call-files-modal.tsx b/src/components/calls/call-files-modal.tsx index cc9fc8c..9702469 100644 --- a/src/components/calls/call-files-modal.tsx +++ b/src/components/calls/call-files-modal.tsx @@ -93,6 +93,14 @@ export const CallFilesModal: React.FC = ({ isOpen, onClose, const handleDownloadFile = async (file: CallFileResultData) => { if (!file.Url || downloadingFiles[file.Id]) return; + const clearDownloadingState = () => { + setDownloadingFiles((prev) => { + const newState = { ...prev }; + delete newState[file.Id]; + return newState; + }); + }; + try { setDownloadingFiles((prev) => ({ ...prev, [file.Id]: 0 })); @@ -108,20 +116,23 @@ export const CallFilesModal: React.FC = ({ isOpen, onClose, const fileName = file.FileName || file.Name || `file_${file.Id}`; if (Platform.OS === 'web') { - const objectUrl = URL.createObjectURL(fileData); - const link = document.createElement('a'); - link.href = objectUrl; - link.download = fileName; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(objectUrl); - - setDownloadingFiles((prev) => { - const newState = { ...prev }; - delete newState[file.Id]; - return newState; - }); + // DOM APIs (createObjectURL/appendChild/click) can throw - guard them and + // always revoke the object URL so we don't leak it on failure + let objectUrl: string | null = null; + let link: HTMLAnchorElement | null = null; + try { + objectUrl = URL.createObjectURL(fileData); + link = document.createElement('a'); + link.href = objectUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + } finally { + link?.remove(); + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + } return; } @@ -154,20 +165,11 @@ export const CallFilesModal: React.FC = ({ isOpen, onClose, } else { Alert.alert(t('calls.files.share_error'), 'Sharing is not available on this device'); } - - setDownloadingFiles((prev) => { - const newState = { ...prev }; - delete newState[file.Id]; - return newState; - }); } catch (error) { console.error('Error downloading file:', error); Alert.alert(t('calls.files.open_error'), error instanceof Error ? error.message : 'Unknown error occurred'); - setDownloadingFiles((prev) => { - const newState = { ...prev }; - delete newState[file.Id]; - return newState; - }); + } finally { + clearDownloadingState(); } }; diff --git a/src/components/dispatch-console/active-calls-panel.tsx b/src/components/dispatch-console/active-calls-panel.tsx index d8b001b..6107d9a 100644 --- a/src/components/dispatch-console/active-calls-panel.tsx +++ b/src/components/dispatch-console/active-calls-panel.tsx @@ -411,22 +411,21 @@ export const ActiveCallsPanel: React.FC = ({ selectedCall const CONCURRENCY = 4; for (let i = 0; i < toFetch.length; i += CONCURRENCY) { const chunk = toFetch.slice(i, i + CONCURRENCY); - const results = await Promise.all( - chunk.map((callId) => - getCallExtraData(callId) - .then((res) => ({ callId, dispatches: res?.Data?.Dispatches ?? ([] as DispatchedEventResultData[]) })) - .catch(() => ({ callId, dispatches: null as DispatchedEventResultData[] | null })) - ) - ); + const results = await Promise.allSettled(chunk.map((callId) => getCallExtraData(callId))); setCallDispatchesMap((prev) => { const next = { ...prev }; - results.forEach(({ callId, dispatches }) => { + results.forEach((result, index) => { + const callId = chunk[index]; // Skip update if a newer request superseded this one for this callId. if (callFetchEpochsRef.current.get(callId) !== batchEpochs.get(callId)) return; - if (dispatches !== null) { - next[callId] = dispatches; + if (result.status === 'fulfilled') { + next[callId] = result.value?.Data?.Dispatches ?? []; } else { // Fetch failed – remove from the fetched set so the next refresh can retry + logger.warn({ + message: 'Failed to fetch dispatches for call; will retry on next refresh', + context: { callId, reason: result.reason instanceof Error ? result.reason.message : String(result.reason) }, + }); fetchedCallIdsRef.current.delete(callId); } }); diff --git a/src/components/notifications/NotificationDetail.tsx b/src/components/notifications/NotificationDetail.tsx index b80aa37..5654e94 100644 --- a/src/components/notifications/NotificationDetail.tsx +++ b/src/components/notifications/NotificationDetail.tsx @@ -192,6 +192,10 @@ const getTypeTagStyle = (type: string): any => { } }; +// Styles are computed once at module load, so resolve the color scheme a single +// time here instead of calling Appearance.getColorScheme() in every style rule. +const isDark = Appearance.getColorScheme() === 'dark'; + const styles = StyleSheet.create({ backdrop: { backgroundColor: 'rgba(0, 0, 0, 0.5)', @@ -207,8 +211,8 @@ const styles = StyleSheet.create({ right: 0, width: SIDEBAR_WIDTH, height: '100%', - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#171717' : '#fff', - shadowColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#e5e5e5', + backgroundColor: isDark ? '#171717' : '#fff', + shadowColor: isDark ? '#262626' : '#e5e5e5', shadowOffset: { width: -2, height: 0, @@ -228,14 +232,14 @@ const styles = StyleSheet.create({ padding: 16, paddingTop: Platform.OS === 'android' ? STATUS_BAR_HEIGHT + 16 : 16, borderBottomWidth: 1, - borderBottomColor: Appearance.getColorScheme() === 'dark' ? '#333333' : '#e5e5e5', + borderBottomColor: isDark ? '#333333' : '#e5e5e5', }, headerTitle: { fontSize: 18, fontWeight: 'bold', flex: 1, textAlign: 'center', - color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', + color: isDark ? '#f3f4f6' : '#111827', }, backButton: { padding: 8, @@ -258,12 +262,12 @@ const styles = StyleSheet.create({ }, dateText: { fontSize: 14, - color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', + color: isDark ? '#9ca3af' : '#6b7280', marginLeft: 6, }, timeText: { fontSize: 14, - color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', + color: isDark ? '#9ca3af' : '#6b7280', }, typeTag: { alignSelf: 'flex-start', @@ -278,28 +282,28 @@ const styles = StyleSheet.create({ textTransform: 'uppercase', }, typeTagDefault: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#374151' : '#e5e7eb', + backgroundColor: isDark ? '#374151' : '#e5e7eb', }, typeTagInfo: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#1e40af' : '#dbeafe', + backgroundColor: isDark ? '#1e40af' : '#dbeafe', }, typeTagSuccess: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#065f46' : '#d1fae5', + backgroundColor: isDark ? '#065f46' : '#d1fae5', }, typeTagWarning: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#92400e' : '#fef3c7', + backgroundColor: isDark ? '#92400e' : '#fef3c7', }, typeTagAlert: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#991b1b' : '#fee2e2', + backgroundColor: isDark ? '#991b1b' : '#fee2e2', }, title: { fontSize: 20, fontWeight: 'bold', marginBottom: 12, - color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', + color: isDark ? '#f3f4f6' : '#111827', }, bodyContainer: { - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#f9fafb', + backgroundColor: isDark ? '#262626' : '#f9fafb', padding: 16, borderRadius: 8, marginBottom: 20, @@ -307,19 +311,19 @@ const styles = StyleSheet.create({ body: { fontSize: 16, lineHeight: 24, - color: Appearance.getColorScheme() === 'dark' ? '#e5e5e5' : '#374151', + color: isDark ? '#e5e5e5' : '#374151', }, metadataDetailsContainer: { marginTop: 10, padding: 16, - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#262626' : '#f9fafb', + backgroundColor: isDark ? '#262626' : '#f9fafb', borderRadius: 8, }, metadataTitle: { fontSize: 16, fontWeight: '600', marginBottom: 10, - color: Appearance.getColorScheme() === 'dark' ? '#f3f4f6' : '#111827', + color: isDark ? '#f3f4f6' : '#111827', }, metadataItem: { flexDirection: 'row', @@ -329,31 +333,31 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '500', marginRight: 8, - color: Appearance.getColorScheme() === 'dark' ? '#9ca3af' : '#6b7280', + color: isDark ? '#9ca3af' : '#6b7280', }, metadataValue: { fontSize: 14, flex: 1, - color: Appearance.getColorScheme() === 'dark' ? '#e5e5e5' : '#111827', + color: isDark ? '#e5e5e5' : '#111827', }, buttonText: { fontSize: 16, fontWeight: '500', - color: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#2563eb', + color: isDark ? '#3b82f6' : '#2563eb', }, referenceButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 24, - backgroundColor: Appearance.getColorScheme() === 'dark' ? '#1e3a8a' : '#dbeafe', + backgroundColor: isDark ? '#1e3a8a' : '#dbeafe', padding: 12, borderRadius: 8, borderWidth: 1, - borderColor: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#60a5fa', + borderColor: isDark ? '#3b82f6' : '#60a5fa', }, referenceButtonIcon: { marginRight: 8, - color: Appearance.getColorScheme() === 'dark' ? '#3b82f6' : '#2563eb', + color: isDark ? '#3b82f6' : '#2563eb', }, }); diff --git a/src/stores/toast/store.ts b/src/stores/toast/store.ts index e3a0f7f..51776c6 100644 --- a/src/stores/toast/store.ts +++ b/src/stores/toast/store.ts @@ -24,6 +24,10 @@ interface ToastStore { removeToast: (id: string) => void; } +// Timer handles keyed by toast id so manual dismissal can cancel the pending +// auto-dismiss timeout (prevents state updates firing for already-removed toasts). +const toastTimers = new Map>(); + export const useToastStore = create((set) => ({ toasts: [], showToast: (type, message, title, options) => { @@ -32,13 +36,20 @@ export const useToastStore = create((set) => ({ toasts: [...state.toasts, { id, type, message, title, onPress: options?.onPress, duration: options?.duration }], })); // Auto remove toast after the requested duration (default 3 seconds) - setTimeout(() => { + const timerId = setTimeout(() => { + toastTimers.delete(id); set((state) => ({ toasts: state.toasts.filter((toast) => toast.id !== id), })); }, options?.duration ?? 3000); + toastTimers.set(id, timerId); }, removeToast: (id) => { + const timerId = toastTimers.get(id); + if (timerId !== undefined) { + clearTimeout(timerId); + toastTimers.delete(id); + } set((state) => ({ toasts: state.toasts.filter((toast) => toast.id !== id), }));