From 4023cb703d63c8a8197b9889141acb9b2fe597a4 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Mon, 13 Apr 2026 14:23:17 -0700 Subject: [PATCH 01/10] Added a VirtualList component for fast virtualized lists --- .vscode/extensions.json | 4 +- .vscode/settings.json | 30 +- apps/react-lightning-example/src/index.tsx | 11 +- .../src/pages/VirtualListPage.tsx | 120 ++++ .../src/index.tsx | 39 +- .../src/pages/LibraryTest.tsx | 6 +- .../src/pages/VirtualizedListTest.tsx | 24 +- apps/storybook/src/components/ScrollItem.tsx | 38 +- .../lists/VirtualList.stories.tsx | 542 ++++++++++++++++++ .../lists/FlashList.stories.tsx | 83 --- .../lists/VirtualizedList.stories.tsx | 65 +-- .../react-lightning-components/package.json | 25 +- .../VirtualList/AverageWindow.spec.ts | 56 ++ .../components/VirtualList/AverageWindow.ts | 43 ++ .../VirtualList/LayoutManager.spec.ts | 369 ++++++++++++ .../components/VirtualList/LayoutManager.ts | 391 +++++++++++++ .../VirtualList/RecyclerPool.spec.ts | 100 ++++ .../components/VirtualList/RecyclerPool.ts | 93 +++ .../VirtualList/ViewabilityTracker.spec.ts | 274 +++++++++ .../VirtualList/ViewabilityTracker.ts | 249 ++++++++ .../components/VirtualList/VirtualList.tsx | 524 +++++++++++++++++ .../VirtualList/VirtualListCell.tsx | 152 +++++ .../VirtualList/VirtualListContent.tsx | 26 + .../VirtualList/VirtualListTypes.ts | 145 +++++ .../src/components/VirtualList/index.ts | 12 + .../VirtualList/parseContentStyle.ts | 27 + .../VirtualList/useScrollHandler.ts | 258 +++++++++ .../components/VirtualList/useViewability.ts | 65 +++ .../src/exports/lists/VirtualList.tsx | 11 + .../react-lightning-components/src/index.ts | 6 + .../package.json | 36 +- .../src/exports/lists/CellContainer.tsx | 56 -- .../src/exports/lists/FlashList.tsx | 60 -- .../src/index.ts | 2 - packages/react-native-lightning/package.json | 1 + .../src/exports/VirtualizedList.tsx | 38 +- pnpm-lock.yaml | 3 + 37 files changed, 3573 insertions(+), 411 deletions(-) create mode 100644 apps/react-lightning-example/src/pages/VirtualListPage.tsx create mode 100644 apps/storybook/src/react-lightning-components/lists/VirtualList.stories.tsx delete mode 100644 apps/storybook/src/react-native-lightning-components/lists/FlashList.stories.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/AverageWindow.spec.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/AverageWindow.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualListContent.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/index.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/parseContentStyle.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts create mode 100644 packages/react-lightning-components/src/components/VirtualList/useViewability.ts create mode 100644 packages/react-lightning-components/src/exports/lists/VirtualList.tsx delete mode 100644 packages/react-native-lightning-components/src/exports/lists/CellContainer.tsx delete mode 100644 packages/react-native-lightning-components/src/exports/lists/FlashList.tsx diff --git a/.vscode/extensions.json b/.vscode/extensions.json index de51190..99e2f7d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,3 @@ { - "recommendations": [ - "biomejs.biome" - ] + "recommendations": ["oxc.oxc-vscode"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 604f553..8ce25ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,38 +1,30 @@ { "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit", - "source.fixAll.biome": "explicit" + "source.fixAll.oxc": "explicit" }, - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.words": [ - "lightningjs", - "threadx" - ], - "biome.enabled": true, - "biome.lsp.bin": "./node_modules/.bin/biome", + "js/ts.tsdk.path": "node_modules/typescript/lib", + "oxc.enable.oxfmt": true, + "oxc.enable.oxlint": true, "json.schemas": [ { - "fileMatch": [ - "manifest.json" - ], + "fileMatch": ["manifest.json"], "url": "https://json.schemastore.org/chrome-manifest.json" } ], "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[json]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "vitest.disableWorkspaceWarning": true -} \ No newline at end of file +} diff --git a/apps/react-lightning-example/src/index.tsx b/apps/react-lightning-example/src/index.tsx index f4e012c..1712a40 100644 --- a/apps/react-lightning-example/src/index.tsx +++ b/apps/react-lightning-example/src/index.tsx @@ -1,8 +1,10 @@ +import { createRoot } from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + import { Canvas, type RenderOptions } from '@plextv/react-lightning'; import { plugin as cssTransformPlugin } from '@plextv/react-lightning-plugin-css-transform'; import { plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox'; -import { createRoot } from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + import { keyMap } from './keyMap'; import { AnimationPage } from './pages/AnimationPage'; import { BrowsePage } from './pages/BrowsePage'; @@ -12,6 +14,7 @@ import { PosterPage } from './pages/PosterPage'; import { ShaderPage } from './pages/ShaderPage'; import { TexturePage } from './pages/TexturePage'; import { TransformsPage } from './pages/TransformsPage'; +import { VirtualListPage } from './pages/VirtualListPage'; import { MyCustomShader } from './shaders/MyCustomShader'; import { MyCustomTexture } from './shaders/MyCustomTexture'; @@ -24,6 +27,10 @@ const router = createBrowserRouter([ path: '/flex-test', element: , }, + { + path: '/virtual-list', + element: , + }, { path: '/poster', element: , diff --git a/apps/react-lightning-example/src/pages/VirtualListPage.tsx b/apps/react-lightning-example/src/pages/VirtualListPage.tsx new file mode 100644 index 0000000..c848552 --- /dev/null +++ b/apps/react-lightning-example/src/pages/VirtualListPage.tsx @@ -0,0 +1,120 @@ +import { focusable } from '@plextv/react-lightning'; +import { Column } from '@plextv/react-lightning-components'; +import VirtualList from '@plextv/react-lightning-components/lists/VirtualList'; + +import { ScrollItem, type ScrollItemProps } from '../components/ScrollItem'; + +const FlexItem = focusable((props) => { + return ( + + + + + ); +}); + +const Header = () => ( + + VirtualList Header + +); + +const HorizontalHeader = () => ( + + + Header + + +); + +const HorizontalFooter = () => ( + + + Footer + + +); + +const Footer = () => ( + + End of List + +); + +const Separator = () => ; + +const Separator2 = () => ; + +export const VirtualListPage = () => { + const items = Array.from({ length: 40 }, (_, i) => `Item ${i}`); + const horizontalItems = Array.from({ length: 40 }, (_, i) => `H-${i}`); + + return ( + + ( + + {item} + + )} + /> + + ( + + {item} + + )} + /> + + ); +}; diff --git a/apps/react-native-lightning-example/src/index.tsx b/apps/react-native-lightning-example/src/index.tsx index 3b8d157..da1d2b9 100644 --- a/apps/react-native-lightning-example/src/index.tsx +++ b/apps/react-native-lightning-example/src/index.tsx @@ -1,7 +1,3 @@ -import { Canvas } from '@plextv/react-lightning'; -import { Column, Row } from '@plextv/react-lightning-components'; -import '@plextv/react-lightning-plugin-flexbox/jsx'; -import { getReactNativePlugins } from '@plextv/react-native-lightning'; import type { LinkingOptions } from '@react-navigation/native'; import { createNavigatorFactory, @@ -11,24 +7,27 @@ import { useNavigation, useNavigationBuilder, } from '@react-navigation/native'; + +import '@plextv/react-lightning-plugin-flexbox/jsx'; import { createRoot } from 'react-dom/client'; import { Button } from 'react-native'; + +import { Canvas } from '@plextv/react-lightning'; +import { Column, Row } from '@plextv/react-lightning-components'; +import { getReactNativePlugins } from '@plextv/react-native-lightning'; + import { ErrorBoundary } from './ErrorBoundary'; import { keyMap } from './keyMap'; import { AnimationBuilderTest } from './pages/AnimationBuilderTest'; import { AnimationTest } from './pages/AnimationTest'; import { ComponentTest } from './pages/ComponentTest'; -import { FlashListTest } from './pages/FlashListTest'; import { LayoutTest } from './pages/LayoutTest'; import { LibraryTest } from './pages/LibraryTest'; import { SimpleTest } from './pages/SimpleTest'; import { VirtualizedListTest } from './pages/VirtualizedListTest'; function CustomNavigator(props: Parameters[1]) { - const { state, descriptors, NavigationContent } = useNavigationBuilder( - StackRouter, - props, - ); + const { state, descriptors, NavigationContent } = useNavigationBuilder(StackRouter, props); const focusedRoute = state.routes[state.index]; @@ -60,7 +59,6 @@ const screens = { Components: 'components', NestedLayouts: 'nestedLayouts', VirtualizedList: 'virtualizedList', - FlashList: 'flashList', }; const linking: LinkingOptions = { @@ -122,17 +120,9 @@ const MainApp = () => { color={'rgba(55, 55, 22, 1)'} onPress={() => nav.navigate('VirtualizedList')} /> - diff --git a/apps/react-lightning-example/src/pages/BrowsePage.tsx b/apps/react-lightning-example/src/pages/BrowsePage.tsx index d00c2a2..d947b47 100644 --- a/apps/react-lightning-example/src/pages/BrowsePage.tsx +++ b/apps/react-lightning-example/src/pages/BrowsePage.tsx @@ -1,6 +1,8 @@ +import { type FC, useCallback, useState } from 'react'; + import type { LightningElement } from '@plextv/react-lightning'; import { Column } from '@plextv/react-lightning-components'; -import { type FC, useCallback, useState } from 'react'; + import { useHubsData } from '../api/useHubsData'; import { HubRow } from '../components/HubRow'; @@ -9,9 +11,7 @@ export const BrowsePage: FC = () => { const [verticalOffset, setVerticalOffset] = useState(0); const handleFocus = useCallback((element: LightningElement) => { - setVerticalOffset( - Math.min(0, -element.node.y - element.node.h / 2 + 1080 / 2), - ); + setVerticalOffset(Math.min(0, -element.node.y - element.node.h / 2 + 1080 / 2)); }, []); if (isLoading) { diff --git a/apps/react-lightning-example/src/pages/LayoutPage.tsx b/apps/react-lightning-example/src/pages/LayoutPage.tsx index 471455e..07cc3c6 100644 --- a/apps/react-lightning-example/src/pages/LayoutPage.tsx +++ b/apps/react-lightning-example/src/pages/LayoutPage.tsx @@ -1,8 +1,9 @@ -import { focusable, type LightningImageElement } from '@plextv/react-lightning'; -import { Column, Row } from '@plextv/react-lightning-components'; import type { FC, ForwardedRef } from 'react'; import { useEffect, useMemo } from 'react'; +import { focusable, type LightningImageElement } from '@plextv/react-lightning'; +import { Column, Row } from '@plextv/react-lightning-components'; + const RandomImage = focusable<{ autoFocus?: boolean }>(({ focused }, ref) => { const seed = useMemo(() => Math.random() * 10000, []); diff --git a/apps/react-lightning-example/src/pages/Page60.tsx b/apps/react-lightning-example/src/pages/Page60.tsx index b1f77cb..cefe213 100644 --- a/apps/react-lightning-example/src/pages/Page60.tsx +++ b/apps/react-lightning-example/src/pages/Page60.tsx @@ -1,6 +1,8 @@ +import { type FC, useCallback, useState } from 'react'; + import { type LightningElement, useFocus } from '@plextv/react-lightning'; import { Column, Row } from '@plextv/react-lightning-components'; -import { type FC, useCallback, useState } from 'react'; + import Button from '../components/Button'; const TEXT_WIDTH = 1076; diff --git a/apps/react-lightning-example/src/pages/PosterPage.tsx b/apps/react-lightning-example/src/pages/PosterPage.tsx index 955294d..fe15f7e 100644 --- a/apps/react-lightning-example/src/pages/PosterPage.tsx +++ b/apps/react-lightning-example/src/pages/PosterPage.tsx @@ -1,5 +1,7 @@ -import { Column, Row } from '@plextv/react-lightning-components'; import type { FC } from 'react'; + +import { Column, Row } from '@plextv/react-lightning-components'; + import Button from '../components/Button'; export const PosterPage: FC = () => { diff --git a/apps/react-lightning-example/src/pages/ShaderPage.tsx b/apps/react-lightning-example/src/pages/ShaderPage.tsx index fa9ebcd..9f77184 100644 --- a/apps/react-lightning-example/src/pages/ShaderPage.tsx +++ b/apps/react-lightning-example/src/pages/ShaderPage.tsx @@ -1,6 +1,7 @@ -import { Column } from '@plextv/react-lightning-components'; import type { FC } from 'react'; +import { Column } from '@plextv/react-lightning-components'; + export const ShaderPage: FC = () => { return ( { return ( { { + static override resolveDefaults(props: MyCustomTextureProps): Required { return { percent: props.percent ?? 20, w: props.w, diff --git a/apps/react-lightning-example/vite.config.mjs b/apps/react-lightning-example/vite.config.mjs index 8a4cd7d..19f09b3 100644 --- a/apps/react-lightning-example/vite.config.mjs +++ b/apps/react-lightning-example/vite.config.mjs @@ -1,8 +1,9 @@ -import fontGen from '@plextv/vite-plugin-msdf-fontgen'; import legacy from '@vitejs/plugin-legacy'; import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; +import fontGen from '@plextv/vite-plugin-msdf-fontgen'; + /** * @type {import('vite').InlineConfig} */ @@ -11,7 +12,11 @@ const config = { tsconfigPaths({ skip: (dir) => dir.includes('app-template'), }), - react(), + react({ + babel: { + plugins: ['babel-plugin-react-compiler'], + }, + }), fontGen({ inputs: [ { diff --git a/apps/react-native-lightning-example/package.json b/apps/react-native-lightning-example/package.json index 4439a15..488a762 100644 --- a/apps/react-native-lightning-example/package.json +++ b/apps/react-native-lightning-example/package.json @@ -1,19 +1,18 @@ { "name": "@plextv/react-native-lightning-example", - "description": "Sample implementation of @plextv/react-native-lightning in a React Native app", "version": "0.4.0", - "author": "Plex Inc.", + "private": true, + "description": "Sample implementation of @plextv/react-native-lightning in a React Native app", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, - "packageManager": "pnpm@10.12.3", "type": "module", - "private": true, "scripts": { "build": "vite build", "clean": "del ./dist", @@ -41,15 +40,16 @@ "@plextv/vite-plugin-react-native-lightning": "workspace:*", "@plextv/vite-plugin-react-reanimated-lightning": "workspace:*", "@repo/configs": "workspace:*", - "@shopify/flash-list": "2.2.0", "@types/react": "19.2.8", "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "7.2.1", + "babel-plugin-react-compiler": "1.0.0", "typescript": "5.9.3" }, "volta": { "extends": "../../package.json" }, + "packageManager": "pnpm@10.12.3", "//": "unsure why react-dom needed to be ignored...", "depcheck": { "ignoreMatches": [ diff --git a/apps/react-native-lightning-example/src/ErrorBoundary.tsx b/apps/react-native-lightning-example/src/ErrorBoundary.tsx index 1a26c9f..f5991c7 100644 --- a/apps/react-native-lightning-example/src/ErrorBoundary.tsx +++ b/apps/react-native-lightning-example/src/ErrorBoundary.tsx @@ -1,3 +1,4 @@ +import type { ErrorInfo } from 'react'; import { Component, type ReactNode } from 'react'; interface Props { @@ -20,7 +21,7 @@ class ErrorBoundary extends Component { return { hasError: true }; } - public componentDidCatch(error: Error, info: React.ErrorInfo): void { + public componentDidCatch(error: Error, info: ErrorInfo): void { console.error(error, info.componentStack); } diff --git a/apps/react-native-lightning-example/src/components/ScrollItem.tsx b/apps/react-native-lightning-example/src/components/ScrollItem.tsx index a955236..2a325da 100644 --- a/apps/react-native-lightning-example/src/components/ScrollItem.tsx +++ b/apps/react-native-lightning-example/src/components/ScrollItem.tsx @@ -1,8 +1,9 @@ -import { focusable } from '@plextv/react-lightning'; -import { View } from '@plextv/react-native-lightning'; import { type ReactNode, useEffect, useMemo } from 'react'; import { type ColorValue, Text } from 'react-native'; +import { focusable } from '@plextv/react-lightning'; +import { View } from '@plextv/react-native-lightning'; + export type ScrollItemProps = { children: ReactNode; index: number; @@ -14,10 +15,7 @@ export type ScrollItemProps = { }; const ScrollItem = focusable( - ( - { color, altColor, image, index, horizontal, focused, children, onFocused }, - ref, - ) => { + ({ color, altColor, image, index, horizontal, focused, children, onFocused }, ref) => { useEffect(() => { if (focused) { onFocused(index); diff --git a/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx b/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx index 49c08df..51c287f 100644 --- a/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx +++ b/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx @@ -1,5 +1,3 @@ -import { Button } from '@plextv/react-native-lightning'; -import { Column, Row } from '@plextv/react-native-lightning-components'; import { type FC, useMemo, useState } from 'react'; import Animated, { type EntryOrExitLayoutType, @@ -23,6 +21,9 @@ import Animated, { SlideOutUp as SlideOutUpBuilder, } from 'react-native-reanimated'; +import { Button } from '@plextv/react-native-lightning'; +import { Column, Row } from '@plextv/react-native-lightning-components'; + type AnimatedProps = { visible: boolean; entering?: EntryOrExitLayoutType; diff --git a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx index 64f511c..2a9635c 100644 --- a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx +++ b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx @@ -55,11 +55,7 @@ const AnimationTest: FC = () => { /> - - {modalVisible ? ( - setModalVisible(false)} /> - ) : null} + {modalVisible ? setModalVisible(false)} /> : null} ); diff --git a/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx index ce550e9..a22684c 100644 --- a/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx +++ b/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx @@ -1,11 +1,13 @@ +import type { Meta, StoryFn } from '@storybook/react-vite'; +import { forwardRef, type ReactNode, useState } from 'react'; + import { type LightningElement, type LightningElementStyle, useCombinedRef, useFocus, } from '@plextv/react-lightning'; -import type { Meta, StoryFn } from '@storybook/react-vite'; -import { forwardRef, type ReactNode, useState } from 'react'; + import { FocusableImage } from '../../../components/FocusableImage'; export default { @@ -52,37 +54,19 @@ export const FocusRedirect: StoryFn = () => { return ( <> - - + + - + - + - - + + { FocusRedirect.args = { label: 'Focus Redirection Example', - description: - 'This example demonstrates how to redirect focus between elements.', + description: 'This example demonstrates how to redirect focus between elements.', }; diff --git a/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx index ef08809..3495078 100644 --- a/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx +++ b/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx @@ -1,5 +1,7 @@ -import { Column, Row } from '@plextv/react-lightning-components'; import type { Meta } from '@storybook/react-vite'; + +import { Column, Row } from '@plextv/react-lightning-components'; + import Button from '../../../components/Button'; export default { @@ -23,17 +25,9 @@ export const SimpleFocusTree = () => { return ( - + - + Top-B @@ -42,11 +36,7 @@ export const SimpleFocusTree = () => { - + diff --git a/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx index 7204228..068f951 100644 --- a/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx +++ b/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx @@ -1,10 +1,9 @@ +import type { Meta } from '@storybook/react-vite'; + import { FocusGroup } from '@plextv/react-lightning'; import { Column, Row } from '@plextv/react-lightning-components'; -import type { Meta } from '@storybook/react-vite'; -import { - FocusableImage, - type FocusableImageProps, -} from '../../../components/FocusableImage'; + +import { FocusableImage, type FocusableImageProps } from '../../../components/FocusableImage'; export default { title: 'react-lightning/Examples/Focus/TrapFocus', @@ -66,14 +65,7 @@ export const TrapFocus = () => { <> - + diff --git a/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx b/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx index 3083866..84f1dd5 100644 --- a/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx +++ b/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx @@ -40,10 +40,7 @@ const content = ( ); export const WithRtt = ({ rtt }: Props) => ( - + {content} ); @@ -53,10 +50,7 @@ WithRtt.args = { }; export const WithoutRtt = ({ rtt }: Props) => ( - + {content} ); diff --git a/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx b/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx index cfc7054..2ab2e25 100644 --- a/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx +++ b/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx @@ -1,10 +1,9 @@ import type { ITextNode } from '@lightningjs/renderer'; -import type { LightningTextElementProps } from '@plextv/react-lightning'; import type { Meta } from '@storybook/react-vite'; -import { - DefaultStoryHeight, - DefaultStoryWidth, -} from '../../../helpers/constants'; + +import type { LightningTextElementProps } from '@plextv/react-lightning'; + +import { DefaultStoryHeight, DefaultStoryWidth } from '../../../helpers/constants'; type Props = { text: string; diff --git a/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx b/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx index c607646..a210f14 100644 --- a/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx +++ b/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx @@ -1,5 +1,7 @@ -import { Column } from '@plextv/react-native-lightning-components'; import type { Meta } from '@storybook/react-vite'; + +import { Column } from '@plextv/react-native-lightning-components'; + import { ColorPalette, DefaultStoryHeight, @@ -50,9 +52,7 @@ export default { tags: ['reactNative'], } as Meta; -export const FlexStart = ({ - justifyContent = FlexTypes.FLEX_START, -}: ColumnLayoutProps) => { +export const FlexStart = ({ justifyContent = FlexTypes.FLEX_START }: ColumnLayoutProps) => { return ( { +export const FlexEnd = ({ justifyContent = FlexTypes.FLEX_END }: ColumnLayoutProps) => { return ( { +export const SpaceEvenly = ({ justifyContent = FlexTypes.SPACE_EVENLY }: ColumnLayoutProps) => { return ( { +export const SpaceBetween = ({ justifyContent = FlexTypes.SPACE_BETWEEN }: ColumnLayoutProps) => { return ( { +export const SpaceAround = ({ justifyContent = FlexTypes.SPACE_AROUND }: ColumnLayoutProps) => { return ( ; // The rest of the story definitions -export const FlexStart = ({ - justifyContent = FlexTypes.FLEX_START, -}: RowLayoutProps) => { +export const FlexStart = ({ justifyContent = FlexTypes.FLEX_START }: RowLayoutProps) => { return ( { +export const FlexEnd = ({ justifyContent = FlexTypes.FLEX_END }: RowLayoutProps) => { return ( { +export const SpaceEvenly = ({ justifyContent = FlexTypes.SPACE_EVENLY }: RowLayoutProps) => { return ( { +export const SpaceBetween = ({ justifyContent = FlexTypes.SPACE_BETWEEN }: RowLayoutProps) => { return ( { +export const SpaceAround = ({ justifyContent = FlexTypes.SPACE_AROUND }: RowLayoutProps) => { return ( ({ base: './', define: { - __DEV__: JSON.stringify( - (env.mode ?? process.env.NODE_ENV) !== 'production', - ), + __DEV__: JSON.stringify((env.mode ?? process.env.NODE_ENV) !== 'production'), 'process.env.NODE_ENV': JSON.stringify(env.mode), }, plugins: [ - reactNativeLightningPlugin(), + reactNativeLightningPlugin({ + reactOptions: { + babel: { + plugins: ['babel-plugin-react-compiler'], + }, + }, + }), reactReanimatedLightningPlugin(), fontGen({ inputs: [ diff --git a/biome.json b/biome.json deleted file mode 100644 index 998503f..0000000 --- a/biome.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "root": true, - "files": { - "includes": [ - "**/*.ts", - "**/*.tsx", - "**/*.js", - "**/*.jsx", - "**/*.json", - "!.vscode", - "!.changeset", - "!**/public" - ], - "ignoreUnknown": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "correctness": { - "noUnusedImports": { - "level": "error", - "fix": "safe" - } - }, - "a11y": { - "noStaticElementInteractions": "off", - "useAltText": "off", - "useKeyWithMouseEvents": "off", - "useKeyWithClickEvents": "off" - } - } - }, - "assist": { - "enabled": true - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "formatWithErrors": true - }, - "javascript": { - "linter": { - "enabled": true - }, - "formatter": { - "indentStyle": "space", - "quoteStyle": "single" - } - }, - "vcs": { - "enabled": true, - "clientKind": "git", - "defaultBranch": "main", - "useIgnoreFile": true - } -} diff --git a/package.json b/package.json index 2e9ef03..4e906b8 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "react-lightning", + "private": true, "description": "React reconciler for rendering React apps with Lightning.js", - "author": "Plex Inc.", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, - "private": true, "type": "module", "scripts": { "build": "turbo build", @@ -23,8 +23,8 @@ "ci:publish": "pnpm run build && pnpm exec changeset publish", "ci:version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile", "dev": "turbo dev", - "lint": "biome check", - "lint:format": "biome check --write", + "lint": "oxlint", + "lint:format": "oxlint --fix --fix-suggestions && oxfmt", "nuke": "pnpm run clean && pnpx npkill -x -y -D && pnpm install", "test": "pnpm run test:unit", "test:unit": "turbo test:unit", @@ -32,7 +32,6 @@ "prepare": "husky" }, "devDependencies": { - "@biomejs/biome": "2.3.11", "@changesets/cli": "2.29.8", "@repo/configs": "workspace:*", "@types/node": "25.0.9", @@ -41,6 +40,9 @@ "glob": "13.0.0", "husky": "9.1.7", "listr2": "10.0.0", + "oxfmt": "0.35.0", + "oxlint": "1.50.0", + "oxlint-tsgolint": "0.15.0", "tsdown": "0.19.0", "tsx": "4.21.0", "turbo": "2.7.5", @@ -51,7 +53,6 @@ "vitest": "4.0.17", "yaml": "2.8.2" }, - "packageManager": "pnpm@10.12.3", "engines": { "node": ">=22" }, @@ -59,6 +60,7 @@ "node": "24.11.1", "pnpm": "10.12.3" }, + "packageManager": "pnpm@10.12.3", "depcheck": { "ignoreMatches": [ "del-cli", diff --git a/packages/configs/package.json b/packages/configs/package.json index 67f1c61..0500888 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -2,8 +2,8 @@ "name": "@repo/configs", "version": "0.0.2", "private": true, - "author": "Plex Inc.", "license": "MIT", + "author": "Plex Inc.", "type": "module", "exports": { "./tsconfig.json": "./tsconfig.json", @@ -15,6 +15,10 @@ "./vite.config": "./vite.config.mjs" }, "scripts": {}, + "devDependencies": { + "@rollup/plugin-babel": "7.0.0", + "babel-plugin-react-compiler": "1.0.0" + }, "volta": { "extends": "../../package.json" } diff --git a/packages/configs/tsdown.config.ts b/packages/configs/tsdown.config.ts index 55169b3..fb1cd54 100644 --- a/packages/configs/tsdown.config.ts +++ b/packages/configs/tsdown.config.ts @@ -1,3 +1,4 @@ +import pluginBabel from '@rollup/plugin-babel'; import { defineConfig, type UserConfig } from 'tsdown'; const config: UserConfig = defineConfig({ @@ -20,6 +21,17 @@ const config: UserConfig = defineConfig({ resolveNewUrlToAsset: true, }, }, + plugins: [ + pluginBabel({ + babelHelpers: 'bundled', + parserOpts: { + sourceType: 'module', + plugins: ['jsx', 'typescript'], + }, + plugins: ['babel-plugin-react-compiler'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }), + ], }); export default config; diff --git a/packages/configs/tsdown.node.config.ts b/packages/configs/tsdown.node.config.ts index b618c35..884f448 100644 --- a/packages/configs/tsdown.node.config.ts +++ b/packages/configs/tsdown.node.config.ts @@ -1,4 +1,5 @@ import { defineConfig, type UserConfig } from 'tsdown'; + // @ts-expect-error: Needed for unrun to resolve this module correctly import baseConfig from './tsdown.config.ts'; diff --git a/packages/configs/tsdown.withExports.config.ts b/packages/configs/tsdown.withExports.config.ts index 66d8c91..bb40d6e 100644 --- a/packages/configs/tsdown.withExports.config.ts +++ b/packages/configs/tsdown.withExports.config.ts @@ -1,4 +1,5 @@ import { defineConfig, type UserConfig } from 'tsdown'; + // @ts-expect-error: Needed for unrun to resolve this module correctly import baseConfig from './tsdown.config.ts'; @@ -11,9 +12,7 @@ const config: UserConfig = defineConfig({ // Remove 'exports/' prefix from export paths return Object.entries(pkg).reduce( (acc, [key, value]) => { - const newKey = key.startsWith('./exports/') - ? key.replace('./exports/', './') - : key; + const newKey = key.startsWith('./exports/') ? key.replace('./exports/', './') : key; acc[newKey] = value; diff --git a/packages/plugin-css-transform/package.json b/packages/plugin-css-transform/package.json index a827314..cc8c061 100644 --- a/packages/plugin-css-transform/package.json +++ b/packages/plugin-css-transform/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning-plugin-css-transform", - "description": "Transforms CSS properties to lightning properties. Requires @plextv/react-lightning-plugin-flexbox", "version": "0.4.0", - "author": "Plex Inc.", + "description": "Transforms CSS properties to lightning properties. Requires @plextv/react-lightning-plugin-flexbox", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -21,7 +24,6 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -30,7 +32,8 @@ }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,9 +42,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { "@repo/configs": "workspace:*", "@types/react": "19.2.8", diff --git a/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts b/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts index 27c79e6..c4f2557 100644 --- a/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts +++ b/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts @@ -1,7 +1,5 @@ -import type { - LightningElementStyle, - LightningTextElementStyle, -} from '@plextv/react-lightning'; +import type { LightningElementStyle, LightningTextElementStyle } from '@plextv/react-lightning'; + import type { AllStyleProps } from './types/ReactStyle'; import { flattenStyles } from './utils/flattenStyles'; import { htmlColorToLightningColor } from './utils/htmlColorToLightningColor'; @@ -49,8 +47,7 @@ export function convertCSSStyleToLightning( } if (shadowColor != null) { - (finalStyle as LightningTextElementStyle).shadowColor = - htmlColorToLightningColor(shadowColor); + (finalStyle as LightningTextElementStyle).shadowColor = htmlColorToLightningColor(shadowColor); } if (border != null || borderWidth != null || borderColor != null) { @@ -103,9 +100,7 @@ export function convertCSSStyleToLightning( if (otherStyles.top != null) { finalStyle.y = - typeof otherStyles.top === 'number' - ? otherStyles.top - : Number.parseInt(otherStyles.top, 10); + typeof otherStyles.top === 'number' ? otherStyles.top : Number.parseInt(otherStyles.top, 10); } if (fontWeight != null) { @@ -116,8 +111,7 @@ export function convertCSSStyleToLightning( } if (transform != null) { - const { scaleX, scaleY, rotation, ...translateTransforms } = - parseTransform(transform); + const { scaleX, scaleY, rotation, ...translateTransforms } = parseTransform(transform); if (scaleX != null) { finalStyle.scaleX = scaleX; @@ -135,11 +129,7 @@ export function convertCSSStyleToLightning( } // Disabled for now as some components set overflow to hidden while not having their size correctly calculated - if ( - overflow === 'hidden' || - overflowX === 'hidden' || - overflowY === 'hidden' - ) { + if (overflow === 'hidden' || overflowX === 'hidden' || overflowY === 'hidden') { finalStyle.clipping = true; } diff --git a/packages/plugin-css-transform/src/index.ts b/packages/plugin-css-transform/src/index.ts index 9c3941b..9d545c2 100644 --- a/packages/plugin-css-transform/src/index.ts +++ b/packages/plugin-css-transform/src/index.ts @@ -1,4 +1,5 @@ import type { Plugin } from '@plextv/react-lightning'; + import { convertCSSStyleToLightning } from './convertCSSStyleToLightning'; import type { AllStyleProps } from './types/ReactStyle'; @@ -9,8 +10,31 @@ export { flattenStyles } from './utils/flattenStyles'; export { htmlColorToLightningColor } from './utils/htmlColorToLightningColor'; export { parseTransform } from './utils/parseTransform'; +const CSS_HANDLED_STYLE_PROPS: ReadonlySet = new Set([ + 'backgroundColor', + 'color', + 'border', + 'borderWidth', + 'borderColor', + 'shadowColor', + 'opacity', + 'overflow', + 'overflowX', + 'overflowY', + 'tintColor', + 'fontWeight', + 'transform', + 'width', + 'height', + 'left', + 'top', + 'display', +]); + export function plugin(): Plugin { return { + handledStyleProps: CSS_HANDLED_STYLE_PROPS, + transformProps(_instance, props) { if (!('style' in props)) { return props; diff --git a/packages/plugin-css-transform/src/types/Node.ts b/packages/plugin-css-transform/src/types/Node.ts index 31bad68..f4fa833 100644 --- a/packages/plugin-css-transform/src/types/Node.ts +++ b/packages/plugin-css-transform/src/types/Node.ts @@ -1,4 +1,5 @@ import type { INodeProps } from '@lightningjs/renderer'; + import type { LightningElement, RendererNode } from '@plextv/react-lightning'; export type RendererNodeWithCore = RendererNode & { diff --git a/packages/plugin-css-transform/src/types/ReactStyle.ts b/packages/plugin-css-transform/src/types/ReactStyle.ts index eef916d..b88f0ba 100644 --- a/packages/plugin-css-transform/src/types/ReactStyle.ts +++ b/packages/plugin-css-transform/src/types/ReactStyle.ts @@ -1,10 +1,11 @@ +import type { StandardProperties as CSSProperties } from 'csstype'; +import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'; + import type { LightningImageElementStyle, LightningTextElementStyle, LightningViewElementStyle, } from '@plextv/react-lightning'; -import type { StandardProperties as CSSProperties } from 'csstype'; -import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'; export type AllStyles = Partial< ViewStyle & diff --git a/packages/plugin-css-transform/src/types/jsx.d.ts b/packages/plugin-css-transform/src/types/jsx.d.ts index 31a86e5..df207c9 100644 --- a/packages/plugin-css-transform/src/types/jsx.d.ts +++ b/packages/plugin-css-transform/src/types/jsx.d.ts @@ -3,6 +3,7 @@ import type { LightningTextElementProps, LightningViewElementProps, } from '@plextv/react-lightning'; + import type { AllStyleProps } from './ReactStyle'; declare module 'react' { diff --git a/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts b/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts index 6f7b526..dc7cbfc 100644 --- a/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts +++ b/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts @@ -1,4 +1,5 @@ import type { Transform } from '@plextv/react-lightning-plugin-flexbox'; + import { convertRotationValue } from './convertRotationValue'; function getValue( @@ -42,19 +43,13 @@ function getXYValue( export function convertCSSTransformToLightning( transformType: string, - transformValue: - | string - | number - | number[] - | Record, + transformValue: string | number | number[] | Record, ): Transform { const transformResult: Transform = {}; if (typeof transformValue === 'object') { for (const key in transformValue) { - const value = ( - transformValue as Record - )[key]; + const value = (transformValue as Record)[key]; if (value) { const result = convertCSSTransformToLightning(key, value); @@ -97,11 +92,7 @@ export function convertCSSTransformToLightning( break; case 'rotate': case 'rotation': - transformResult.rotation = getValue( - transformValue, - 0, - convertRotationValue, - ); + transformResult.rotation = getValue(transformValue, 0, convertRotationValue); break; } diff --git a/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts b/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts index f6497c1..f4aa208 100644 --- a/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts +++ b/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; + import { convertRotationValue } from './convertRotationValue'; describe('convertRotationValue', () => { diff --git a/packages/plugin-css-transform/src/utils/flattenStyles.ts b/packages/plugin-css-transform/src/utils/flattenStyles.ts index 51649ce..4d8c0e4 100644 --- a/packages/plugin-css-transform/src/utils/flattenStyles.ts +++ b/packages/plugin-css-transform/src/utils/flattenStyles.ts @@ -1,4 +1,5 @@ import type { StyleProp } from 'react-native'; + import type { AllStyleProps, AllStyles } from '../types/ReactStyle'; export function flattenStyles(styles: AllStyleProps): T { diff --git a/packages/plugin-css-transform/src/utils/fromCssUnit.ts b/packages/plugin-css-transform/src/utils/fromCssUnit.ts index 6c97f1d..6fa3917 100644 --- a/packages/plugin-css-transform/src/utils/fromCssUnit.ts +++ b/packages/plugin-css-transform/src/utils/fromCssUnit.ts @@ -1,6 +1,7 @@ -import type { DimensionValue } from '@plextv/react-lightning-plugin-flexbox'; import type { Animated } from 'react-native'; +import type { DimensionValue } from '@plextv/react-lightning-plugin-flexbox'; + const unitRegex = /^(\d+)(px|vw|vh|%)?$/i; /** diff --git a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts index e2e9b71..53865b1 100644 --- a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts +++ b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts @@ -1,5 +1,6 @@ import type { ColorValue } from 'react-native'; import { describe, expect, it } from 'vitest'; + import { htmlColorToLightningColor } from './htmlColorToLightningColor'; describe('htmlColorToLightningColor', () => { diff --git a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts index e83ce71..59e97d3 100644 --- a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts +++ b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts @@ -1,23 +1,18 @@ import type { ColorValue } from 'react-native'; + import { htmlColorCodes } from './htmlColorCodes'; const hexRgbRegex = /^#?([a-f0-9]{6})$/i; const hexShortRgbRegex = /^#?([a-f0-9]{3})$/i; -const rgbRegex = - /^rgba?\(([0-9.]+)[, ]+([0-9.]+)[, ]+([0-9.]+)[, ]*([0-9.]+)?\)$/i; +const rgbRegex = /^rgba?\(([0-9.]+)[, ]+([0-9.]+)[, ]+([0-9.]+)[, ]*([0-9.]+)?\)$/i; -function withAlphaOverride( - color: number, - overrideAlpha?: number | string, -): number { +function withAlphaOverride(color: number, overrideAlpha?: number | string): number { if (overrideAlpha == null) { return color; } const alphaInt = - typeof overrideAlpha === 'string' - ? Number.parseInt(overrideAlpha, 16) - : overrideAlpha; + typeof overrideAlpha === 'string' ? Number.parseInt(overrideAlpha, 16) : overrideAlpha; // Create a bitmask for the alpha value const alphaMask = 0xffffff00 | alphaInt; @@ -48,13 +43,7 @@ export function htmlColorToLightningColor( const rgbResult = rgbRegex.exec(colorLower); if (rgbResult) { - const parts = rgbResult.slice() as [ - string, - string, - string, - string, - string?, - ]; + const parts = rgbResult.slice() as [string, string, string, string, string?]; const rgbColor = ((Number.parseInt(parts[1], 10) << 24) >>> 0) + @@ -68,10 +57,7 @@ export function htmlColorToLightningColor( const hexRgbResult = hexRgbRegex.exec(colorLower); if (hexRgbResult?.[1]) { - return withAlphaOverride( - Number.parseInt(`${hexRgbResult[1]}ff`, 16), - overrideAlpha, - ); + return withAlphaOverride(Number.parseInt(`${hexRgbResult[1]}ff`, 16), overrideAlpha); } const hexShortRgbResult = hexShortRgbRegex.exec(colorLower); @@ -83,7 +69,5 @@ export function htmlColorToLightningColor( return withAlphaOverride(Number.parseInt(rgbText, 16), overrideAlpha); } - throw new Error( - `Invalid hex value specified for conversion: ${color.toString()}`, - ); + throw new Error(`Invalid hex value specified for conversion: ${color.toString()}`); } diff --git a/packages/plugin-css-transform/src/utils/parseTransform.ts b/packages/plugin-css-transform/src/utils/parseTransform.ts index 5bbc972..e4b0d25 100644 --- a/packages/plugin-css-transform/src/utils/parseTransform.ts +++ b/packages/plugin-css-transform/src/utils/parseTransform.ts @@ -1,11 +1,10 @@ import type { Transform } from '@plextv/react-lightning-plugin-flexbox'; + import { convertCSSTransformToLightning } from './convertCSSTransformToLightning'; const transformPartRegex = /(\w+)\(([^)]+)\)/g; -export function parseTransform( - transform?: string | object | Array, -): Transform { +export function parseTransform(transform?: string | object | Array): Transform { if (!transform) { return {}; } @@ -22,20 +21,14 @@ export function parseTransform( if (typeof transform === 'object') { const safeTransform: Transform = {}; - const originalTranform = transform as Record< - string, - string | number | number[] - >; + const originalTranform = transform as Record; for (const t of Object.keys(originalTranform)) { if (originalTranform[t] == null) { continue; } - Object.assign( - safeTransform, - convertCSSTransformToLightning(t, originalTranform[t]), - ); + Object.assign(safeTransform, convertCSSTransformToLightning(t, originalTranform[t])); } return safeTransform; @@ -56,10 +49,7 @@ export function parseTransform( continue; } - Object.assign( - transformResult, - convertCSSTransformToLightning(transformType, transformValue), - ); + Object.assign(transformResult, convertCSSTransformToLightning(transformType, transformValue)); } return transformResult; diff --git a/packages/plugin-css-transform/tsdown.config.ts b/packages/plugin-css-transform/tsdown.config.ts index 1958bda..f146390 100644 --- a/packages/plugin-css-transform/tsdown.config.ts +++ b/packages/plugin-css-transform/tsdown.config.ts @@ -1,6 +1,7 @@ -import baseConfig from '@repo/configs/tsdown.config'; import { defineConfig, type UserConfig } from 'tsdown'; +import baseConfig from '@repo/configs/tsdown.config'; + const config: UserConfig = defineConfig({ ...baseConfig, entry: ['src/index.ts', 'src/types/jsx.d.ts'], diff --git a/packages/plugin-flexbox-lite/package.json b/packages/plugin-flexbox-lite/package.json index 05b8377..8e81e14 100644 --- a/packages/plugin-flexbox-lite/package.json +++ b/packages/plugin-flexbox-lite/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning-plugin-flexbox-lite", - "description": "A less featured but more efficient flex layout support to @plextv/react-lightning", "version": "0.4.0", - "author": "Plex Inc.", + "description": "A less featured but more efficient flex layout support to @plextv/react-lightning", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -21,7 +24,6 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -30,7 +32,8 @@ }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,9 +42,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { "@repo/configs": "workspace:*" }, diff --git a/packages/plugin-flexbox-lite/src/index.ts b/packages/plugin-flexbox-lite/src/index.ts index 3147315..2ff780f 100644 --- a/packages/plugin-flexbox-lite/src/index.ts +++ b/packages/plugin-flexbox-lite/src/index.ts @@ -1,10 +1,8 @@ -import type { LightningElement, Plugin } from '@plextv/react-lightning'; import type { SetOptional } from 'type-fest'; -function getChildrenSize( - children: LightningElement[], - dimensionProperty: 'w' | 'h', -) { +import type { LightningElement, Plugin } from '@plextv/react-lightning'; + +function getChildrenSize(children: LightningElement[], dimensionProperty: 'w' | 'h') { let totalSize = 0; for (let i = 0; i < children.length; i++) { @@ -22,10 +20,7 @@ function getChildrenSize( return totalSize; } -function getAvailableSize( - instance: LightningElement, - dimensionProperty: 'w' | 'h', -) { +function getAvailableSize(instance: LightningElement, dimensionProperty: 'w' | 'h') { let size = instance.node[dimensionProperty]; if (size === 0) { @@ -59,8 +54,7 @@ function applyFlexLayout(instance: LightningElement) { const flexDirection = style.flexDirection || 'row'; const justifyContent = style.justifyContent || 'flex-start'; - const isHorizontal = - flexDirection === 'row' || flexDirection === 'row-reverse'; + const isHorizontal = flexDirection === 'row' || flexDirection === 'row-reverse'; const dimensionProperty = isHorizontal ? 'w' : 'h'; const axisProperty = isHorizontal ? 'x' : 'y'; @@ -111,8 +105,7 @@ function applyFlexLayout(instance: LightningElement) { if (justifyContent === 'space-around') { const aroundStartSpace = - (availableSize - childrenSize) / (children.length + 1) / children.length + - 1; + (availableSize - childrenSize) / (children.length + 1) / children.length + 1; currentPosition += aroundStartSpace; availableSize -= aroundStartSpace * 2; @@ -155,11 +148,7 @@ function canFlexCanBeApplied( instance: SetOptional | null, isChild = false, ): instance is LightningElement { - if ( - instance === null || - instance === undefined || - 'node' in instance === false - ) { + if (instance === null || instance === undefined || 'node' in instance === false) { return false; } @@ -177,19 +166,11 @@ function canFlexCanBeApplied( return false; } - if ( - (style?.flexDirection === 'row' || - style?.flexDirection === 'row-reverse') && - style.w - ) { + if ((style?.flexDirection === 'row' || style?.flexDirection === 'row-reverse') && style.w) { return true; } - if ( - (style?.flexDirection === 'column' || - style?.flexDirection === 'column-reverse') && - style.h - ) { + if ((style?.flexDirection === 'column' || style?.flexDirection === 'column-reverse') && style.h) { return true; } @@ -226,11 +207,7 @@ export default function flexPlugin(): Plugin { return { onCreateInstance(instance, props) { - if ( - props === undefined || - props.style == null || - props.style === undefined - ) { + if (props === undefined || props.style == null || props.style === undefined) { return; } @@ -256,18 +233,12 @@ export default function flexPlugin(): Plugin { } }), instance.on('childInserted', (child) => { - if ( - canFlexCanBeApplied(child, true) && - canFlexCanBeApplied(child.parent) - ) { + if (canFlexCanBeApplied(child, true) && canFlexCanBeApplied(child.parent)) { queueUpdate(child.parent); } }), instance.on('childRemoved', (child) => { - if ( - canFlexCanBeApplied(child, true) && - canFlexCanBeApplied(child.parent) - ) { + if (canFlexCanBeApplied(child, true) && canFlexCanBeApplied(child.parent)) { queueUpdate(child.parent); } }), diff --git a/packages/plugin-flexbox-lite/src/types/FlexStyles.ts b/packages/plugin-flexbox-lite/src/types/FlexStyles.ts index dbbfed4..9de5139 100644 --- a/packages/plugin-flexbox-lite/src/types/FlexStyles.ts +++ b/packages/plugin-flexbox-lite/src/types/FlexStyles.ts @@ -10,12 +10,7 @@ export type AlignContent = | 'space-evenly' | 'stretch'; -export type AlignItems = - | 'baseline' - | 'center' - | 'flex-end' - | 'flex-start' - | 'stretch'; +export type AlignItems = 'baseline' | 'center' | 'flex-end' | 'flex-start' | 'stretch'; export type JustifyContent = | 'center' diff --git a/packages/plugin-flexbox-lite/src/types/jsx.d.ts b/packages/plugin-flexbox-lite/src/types/jsx.d.ts index 402450e..7fc04b0 100644 --- a/packages/plugin-flexbox-lite/src/types/jsx.d.ts +++ b/packages/plugin-flexbox-lite/src/types/jsx.d.ts @@ -1,20 +1,10 @@ -import type { - FlexContainer, - FlexItem, - FlexLightningBaseElementStyle, -} from './FlexStyles'; +import type { FlexContainer, FlexItem, FlexLightningBaseElementStyle } from './FlexStyles'; declare module '@plextv/react-lightning' { interface LightningViewElementStyle - extends FlexLightningBaseElementStyle, - FlexItem, - FlexContainer {} + extends FlexLightningBaseElementStyle, FlexItem, FlexContainer {} - interface LightningTextElementStyle - extends FlexLightningBaseElementStyle, - FlexItem {} + interface LightningTextElementStyle extends FlexLightningBaseElementStyle, FlexItem {} - interface LightningImageElementStyle - extends FlexLightningBaseElementStyle, - FlexItem {} + interface LightningImageElementStyle extends FlexLightningBaseElementStyle, FlexItem {} } diff --git a/packages/plugin-flexbox-lite/tsdown.config.ts b/packages/plugin-flexbox-lite/tsdown.config.ts index c1f7ea6..99cc7a8 100644 --- a/packages/plugin-flexbox-lite/tsdown.config.ts +++ b/packages/plugin-flexbox-lite/tsdown.config.ts @@ -1,6 +1,7 @@ -import baseConfig from '@repo/configs/tsdown.config'; import { defineConfig, type UserConfig } from 'tsdown'; +import baseConfig from '@repo/configs/tsdown.config'; + const config: UserConfig = defineConfig({ ...baseConfig, entry: ['./src/index.ts', './src/types/jsx.d.ts'], diff --git a/packages/plugin-flexbox/package.json b/packages/plugin-flexbox/package.json index fa7c001..95da74b 100644 --- a/packages/plugin-flexbox/package.json +++ b/packages/plugin-flexbox/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning-plugin-flexbox", - "description": "Adds FlexBox layout support to @plextv/react-lightning using yoga", "version": "0.4.0", - "author": "Plex Inc.", + "description": "Adds FlexBox layout support to @plextv/react-lightning using yoga", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/cjs/index.cjs", "module": "./dist/es/index.production.js", @@ -21,7 +24,6 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -31,7 +33,8 @@ }, "./package.json": "./package.json", "./jsx": "./dist/types/types/jsx.d.ts" - } + }, + "provenance": true }, "scripts": { "build": "pnpm run build:vite", @@ -45,9 +48,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "tseep": "1.3.1", "yoga-layout": "3.2.1" diff --git a/packages/plugin-flexbox/src/LightningManager.ts b/packages/plugin-flexbox/src/LightningManager.ts index 5c2cc02..57d50c8 100644 --- a/packages/plugin-flexbox/src/LightningManager.ts +++ b/packages/plugin-flexbox/src/LightningManager.ts @@ -5,11 +5,12 @@ import type { RendererNode, TextRendererNode, } from '@plextv/react-lightning'; + import type { YogaOptions } from './types/YogaOptions'; import { SimpleDataView } from './util/SimpleDataView'; +import loadYoga from './yoga'; import type { YogaManager } from './YogaManager'; import type { Workerized } from './YogaManagerWorker'; -import loadYoga from './yoga'; /** * Manages the lifecycle of Yoga nodes for Lightning elements. This can only be @@ -31,9 +32,7 @@ export class LightningManager { } if (!this._yogaManager) { - throw new Error( - 'YogaManager is not initialized. Make sure to call init() first.', - ); + throw new Error('YogaManager is not initialized. Make sure to call init() first.'); } this._elements.set(element.id, element); @@ -46,14 +45,14 @@ export class LightningManager { } this._elements.delete(element.id); - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. But avoiding the nullish operator for perf reasons + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. But avoiding the nullish operator for perf reasons this._yogaManager!.applyStyle(element.id, null, true); - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.removeNode(element.id); }), element.on('childAdded', (child, index) => { - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.addChildNode(element.id, child.id, index); this.applyStyle(element.id, element.style); }), @@ -61,9 +60,9 @@ export class LightningManager { element.on('childRemoved', (child) => { // This will remove any pending worker style updates that haven't been sent - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.applyStyle(child.id, null, true); - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.removeNode(child.id); }), @@ -80,9 +79,7 @@ export class LightningManager { element.on( 'textureLoaded', ( - node: - | RendererNode - | TextRendererNode, + node: RendererNode | TextRendererNode, event: { type: string; dimensions: { w: number; h: number } }, ) => { if (element.isTextElement) { @@ -111,7 +108,7 @@ export class LightningManager { } if (style) { - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.applyStyle(elementId, style, skipRender); } } diff --git a/packages/plugin-flexbox/src/YogaManager.spec.ts b/packages/plugin-flexbox/src/YogaManager.spec.ts index b430299..d66f9e1 100644 --- a/packages/plugin-flexbox/src/YogaManager.spec.ts +++ b/packages/plugin-flexbox/src/YogaManager.spec.ts @@ -1,5 +1,7 @@ -import type { LightningElementStyle } from '@plextv/react-lightning'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { LightningElementStyle } from '@plextv/react-lightning'; + import type { YogaOptions } from './types/YogaOptions'; import { SimpleDataView } from './util/SimpleDataView'; import { YogaManager } from './YogaManager'; @@ -124,8 +126,7 @@ describe('YogaManager', () => { }, { errata: 'absolute-position-without-insets' as const, - expected: - mockYoga.ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING, + expected: mockYoga.ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING, }, { errata: 'none' as const, expected: mockYoga.ERRATA_NONE }, ]; @@ -258,11 +259,7 @@ describe('YogaManager', () => { yogaManager.on('render', (buffer) => { expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(mockNode.calculateLayout).toHaveBeenCalledWith( - 1920, - 1080, - mockYoga.DIRECTION_LTR, - ); + expect(mockNode.calculateLayout).toHaveBeenCalledWith(1920, 1080, mockYoga.DIRECTION_LTR); resolve(); }); @@ -360,9 +357,7 @@ describe('YogaManager', () => { yogaManager.applyStyle(elementId, style); // applyReactPropsToYoga should be called with the mocked function - const { default: applyReactPropsToYoga } = await import( - './util/applyReactPropsToYoga' - ); + const { default: applyReactPropsToYoga } = await import('./util/applyReactPropsToYoga'); expect(applyReactPropsToYoga).toHaveBeenCalledWith( mockYoga, mockYogaOptions, @@ -377,15 +372,11 @@ describe('YogaManager', () => { }); it('should handle style application to non-existent node', () => { - const consoleWarnSpy = vi - .spyOn(console, 'warn') - .mockImplementation(() => {}); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); yogaManager.applyStyle(999, {}); - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Yoga node with ID 999 not found.', - ); + expect(consoleWarnSpy).toHaveBeenCalledWith('Yoga node with ID 999 not found.'); consoleWarnSpy.mockRestore(); }); @@ -403,9 +394,7 @@ describe('YogaManager', () => { yogaManager.addNode(elementId); yogaManager.applyStyle(elementId, style); - const { applyFlexPropToYoga } = await import( - './util/applyReactPropsToYoga' - ); + const { applyFlexPropToYoga } = await import('./util/applyReactPropsToYoga'); expect(applyFlexPropToYoga).toHaveBeenCalledWith( mockYoga, mockYogaOptions, @@ -432,9 +421,7 @@ describe('YogaManager', () => { yogaManager.addNode(456); yogaManager.applyStyles(styles); - const { default: applyReactPropsToYoga } = await import( - './util/applyReactPropsToYoga' - ); + const { default: applyReactPropsToYoga } = await import('./util/applyReactPropsToYoga'); expect(applyReactPropsToYoga).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/plugin-flexbox/src/YogaManager.ts b/packages/plugin-flexbox/src/YogaManager.ts index 049d806..06d6fa8 100644 --- a/packages/plugin-flexbox/src/YogaManager.ts +++ b/packages/plugin-flexbox/src/YogaManager.ts @@ -1,11 +1,11 @@ -import type { LightningElementStyle, Rect } from '@plextv/react-lightning'; import { EventEmitter } from 'tseep'; import { type Config, loadYoga, type Yoga } from 'yoga-layout/load'; + +import type { LightningElementStyle, Rect } from '@plextv/react-lightning'; + import type { ManagerNode } from './types/ManagerNode'; import type { YogaOptions } from './types/YogaOptions'; -import applyReactPropsToYoga, { - applyFlexPropToYoga, -} from './util/applyReactPropsToYoga'; +import applyReactPropsToYoga, { applyFlexPropToYoga } from './util/applyReactPropsToYoga'; import { SimpleDataView } from './util/SimpleDataView'; export type BatchedUpdate = Record>; @@ -48,22 +48,17 @@ export class YogaManager { private _eventEmitter: EventEmitter = new EventEmitter(); private _dataView: SimpleDataView; - public on: EventEmitter['on'] = this._eventEmitter.on.bind( + public on: EventEmitter['on'] = this._eventEmitter.on.bind(this._eventEmitter); + public off: EventEmitter['off'] = this._eventEmitter.off.bind( this._eventEmitter, ); - public off: EventEmitter['off'] = - this._eventEmitter.off.bind(this._eventEmitter); public get initialized(): boolean { return this._initialized; } public constructor() { - this._dataView = new SimpleDataView( - MAX_SIZEOF_UPDATE, - true, - this._flushArrayBuffer, - ); + this._dataView = new SimpleDataView(MAX_SIZEOF_UPDATE, true, this._flushArrayBuffer); } public async init(yogaOptions?: YogaOptions): Promise { @@ -84,14 +79,10 @@ export class YogaManager { this._config.setErrata(this._yoga.ERRATA_STRETCH_FLEX_BASIS); break; case 'absolute-percent-against-inner': - this._config.setErrata( - this._yoga.ERRATA_ABSOLUTE_PERCENT_AGAINST_INNER_SIZE, - ); + this._config.setErrata(this._yoga.ERRATA_ABSOLUTE_PERCENT_AGAINST_INNER_SIZE); break; case 'absolute-position-without-insets': - this._config.setErrata( - this._yoga.ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING, - ); + this._config.setErrata(this._yoga.ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING); break; default: this._config.setErrata(this._yoga.ERRATA_NONE); @@ -103,7 +94,7 @@ export class YogaManager { public addNode(elementId: number): ManagerNode { if (this._elementMap.has(elementId)) { - // biome-ignore lint/style/noNonNullAssertion: Already checked + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already checked return this._elementMap.get(elementId)!; } @@ -138,9 +129,7 @@ export class YogaManager { const childYogaNode = this._elementMap.get(childId); if (!parentYogaNode || !childYogaNode) { - throw new Error( - `Parent or child node not found for IDs ${parentId} and ${childId}.`, - ); + throw new Error(`Parent or child node not found for IDs ${parentId} and ${childId}.`); } index ??= childYogaNode.children.length; @@ -182,7 +171,7 @@ export class YogaManager { this._rootNode = root; } - // biome-ignore lint/style/noNonNullAssertion: Already checked this._yoga above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already checked this._yoga above root.node.calculateLayout(1920, 1080, this._yoga!.DIRECTION_LTR); this._initializeArrayBuffer(); @@ -284,10 +273,7 @@ export class YogaManager { const maxWidth = yogaNode.node.getMaxWidth(); - if ( - !Number.isNaN(maxWidth.value) && - maxWidth.unit !== this._yoga.UNIT_UNDEFINED - ) { + if (!Number.isNaN(maxWidth.value) && maxWidth.unit !== this._yoga.UNIT_UNDEFINED) { // If there is a max width specified, the width on the yogaNode will // be the computed width let computedWidth = yogaNode.node.getComputedWidth(); @@ -297,9 +283,7 @@ export class YogaManager { const parentWidth = yogaNode.node.getParent()?.getComputedWidth(); if (parentWidth) { - computedWidth = isPercentage - ? parentWidth * (maxWidth.value / 100) - : parentWidth; + computedWidth = isPercentage ? parentWidth * (maxWidth.value / 100) : parentWidth; } else if (maxWidth.unit === this._yoga.UNIT_POINT) { computedWidth = maxWidth.value; } @@ -342,8 +326,7 @@ export class YogaManager { // returns the new offset in the dataView private _getUpdatedStyles(yogaNode: ManagerNode, force = false) { const skipHiddenNode = - !this._yogaOptions.processHiddenNodes && - this._hiddenElements.has(yogaNode.id); + !this._yogaOptions.processHiddenNodes && this._hiddenElements.has(yogaNode.id); if (!force && (skipHiddenNode || !yogaNode.node.hasNewLayout())) { return; diff --git a/packages/plugin-flexbox/src/YogaManagerWorker.ts b/packages/plugin-flexbox/src/YogaManagerWorker.ts index 83793e5..1dd2136 100644 --- a/packages/plugin-flexbox/src/YogaManagerWorker.ts +++ b/packages/plugin-flexbox/src/YogaManagerWorker.ts @@ -1,5 +1,7 @@ -import type { LightningElementStyle } from '@plextv/react-lightning'; import { EventEmitter } from 'tseep'; + +import type { LightningElementStyle } from '@plextv/react-lightning'; + import { NodeOperations } from './types/NodeOperations'; import { SimpleDataView } from './util/SimpleDataView'; import { toSerializableValue } from './util/toSerializableValue'; @@ -8,12 +10,10 @@ import type { YogaManager, YogaManagerEvents } from './YogaManager'; const DELAY_DURATION = 1; -// biome-ignore lint/suspicious/noExplicitAny: Basic type for function signatures +// oxlint-disable-next-line typescript/no-explicit-any -- Basic type for function signatures type AnyFunc = (...args: any[]) => any; -// biome-ignore lint/suspicious/noExplicitAny: We don't care about the first parameter type here -type ParametersExceptFirst = T extends (first: any, ...args: infer U) => any - ? U - : never; +// oxlint-disable-next-line typescript/no-explicit-any -- We don't care about the first parameter type here +type ParametersExceptFirst = T extends (first: any, ...args: infer U) => any ? U : never; export type Workerized = { [K in keyof T]: T[K] extends AnyFunc @@ -23,10 +23,7 @@ export type Workerized = { : never; }; -function delay void | Promise>( - fn: T, - delay: number, -): T { +function delay void | Promise>(fn: T, delay: number): T { let timeout: ReturnType | null = null; let latestArgs: unknown[]; @@ -52,16 +49,8 @@ function wrapWorker(worker: Worker): Workerized { let _stylesToSend: Record> = {}; let _numStylesToSend = 0; let _needsRender = false; - const _childOperations = new SimpleDataView( - undefined, - undefined, - flushChildOperations, - ); - const _sizeRequests = new SimpleDataView( - undefined, - undefined, - flushSizeRequests, - ); + const _childOperations = new SimpleDataView(undefined, undefined, flushChildOperations); + const _sizeRequests = new SimpleDataView(undefined, undefined, flushSizeRequests); let _sizeRequestPromise: Promise | null = null; function flushSendStyles() { @@ -161,15 +150,11 @@ function wrapWorker(worker: Worker): Workerized { break; case 'addChildNode': if (childId === undefined) { - throw new Error( - 'Child ID must be provided for addChildNode operation', - ); + throw new Error('Child ID must be provided for addChildNode operation'); } _childOperations.writeUint8( - index === undefined - ? NodeOperations.AddChildNode - : NodeOperations.AddChildNodeAtIndex, + index === undefined ? NodeOperations.AddChildNode : NodeOperations.AddChildNodeAtIndex, ); _childOperations.writeUint32(elementOrParentId); _childOperations.writeUint32(childId); @@ -210,9 +195,7 @@ function wrapWorker(worker: Worker): Workerized { const callee = _callees[callbackId]; if (!callee) { - console.error( - `No handler found for size request id: ${callbackId}`, - ); + console.error(`No handler found for size request id: ${callbackId}`); continue; } @@ -264,9 +247,7 @@ function wrapWorker(worker: Worker): Workerized { }); } - worker.onmessage = ( - event: MessageEvent<{ id: string; result?: unknown; error?: string }>, - ) => { + worker.onmessage = (event: MessageEvent<{ id: string; result?: unknown; error?: string }>) => { const { id, result, error } = event.data; if (id === 'render') { @@ -308,11 +289,7 @@ function wrapWorker(worker: Worker): Workerized { } else if (prop === 'applyStyle') { // Special case for applyStyle return applyStyle; - } else if ( - prop === 'addNode' || - prop === 'removeNode' || - prop === 'addChildNode' - ) { + } else if (prop === 'addNode' || prop === 'removeNode' || prop === 'addChildNode') { return (...args: ParametersExceptFirst) => nodeOperation(prop, ...args); } else if (prop === 'getClampedSize') { @@ -341,5 +318,4 @@ function getId(): number { return ++count; } -export default (): Workerized => - wrapWorker(new Worker()); +export default (): Workerized => wrapWorker(new Worker()); diff --git a/packages/plugin-flexbox/src/index.ts b/packages/plugin-flexbox/src/index.ts index 1092c58..0823682 100644 --- a/packages/plugin-flexbox/src/index.ts +++ b/packages/plugin-flexbox/src/index.ts @@ -1,16 +1,15 @@ -import type { - LightningElement, - LightningElementStyle, - Plugin, -} from '@plextv/react-lightning'; +import type { LightningElement, LightningElementStyle, Plugin } from '@plextv/react-lightning'; + import { LightningManager } from './LightningManager'; import type { YogaOptions } from './types/YogaOptions'; -import { isFlexStyleProp } from './util/isFlexStyleProp'; +import { flexProps, isFlexStyleProp } from './util/isFlexStyleProp'; export function plugin(yogaOptions?: YogaOptions): Plugin { const lightningManager = new LightningManager(); return { + handledStyleProps: new Set(Object.keys(flexProps)), + init() { return lightningManager.init(yogaOptions); }, @@ -34,12 +33,7 @@ export function plugin(yogaOptions?: YogaOptions): Plugin { for (const key in styles) { const value = styles[key as keyof LightningElementStyle]; - if ( - key === 'w' || - key === 'h' || - key === 'maxWidth' || - key === 'maxHeight' - ) { + if (key === 'w' || key === 'h' || key === 'maxWidth' || key === 'maxHeight') { // Width and height go to both flex and remaining styles flexStyles[key] = value; remainingStyles[key] = value; diff --git a/packages/plugin-flexbox/src/types/FlexStyles.ts b/packages/plugin-flexbox/src/types/FlexStyles.ts index ff89061..3bc5ebb 100644 --- a/packages/plugin-flexbox/src/types/FlexStyles.ts +++ b/packages/plugin-flexbox/src/types/FlexStyles.ts @@ -10,12 +10,7 @@ export type AlignContent = | 'space-evenly' | 'stretch'; -export type AlignItems = - | 'baseline' - | 'center' - | 'flex-end' - | 'flex-start' - | 'stretch'; +export type AlignItems = 'baseline' | 'center' | 'flex-end' | 'flex-start' | 'stretch'; export type JustifyContent = | 'center' diff --git a/packages/plugin-flexbox/src/types/jsx.d.ts b/packages/plugin-flexbox/src/types/jsx.d.ts index 402450e..7fc04b0 100644 --- a/packages/plugin-flexbox/src/types/jsx.d.ts +++ b/packages/plugin-flexbox/src/types/jsx.d.ts @@ -1,20 +1,10 @@ -import type { - FlexContainer, - FlexItem, - FlexLightningBaseElementStyle, -} from './FlexStyles'; +import type { FlexContainer, FlexItem, FlexLightningBaseElementStyle } from './FlexStyles'; declare module '@plextv/react-lightning' { interface LightningViewElementStyle - extends FlexLightningBaseElementStyle, - FlexItem, - FlexContainer {} + extends FlexLightningBaseElementStyle, FlexItem, FlexContainer {} - interface LightningTextElementStyle - extends FlexLightningBaseElementStyle, - FlexItem {} + interface LightningTextElementStyle extends FlexLightningBaseElementStyle, FlexItem {} - interface LightningImageElementStyle - extends FlexLightningBaseElementStyle, - FlexItem {} + interface LightningImageElementStyle extends FlexLightningBaseElementStyle, FlexItem {} } diff --git a/packages/plugin-flexbox/src/util/SimpleDataView.spec.ts b/packages/plugin-flexbox/src/util/SimpleDataView.spec.ts index 083f05a..7971572 100644 --- a/packages/plugin-flexbox/src/util/SimpleDataView.spec.ts +++ b/packages/plugin-flexbox/src/util/SimpleDataView.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; + import { SimpleDataView } from './SimpleDataView'; describe('SimpleDataView', () => { @@ -275,17 +276,13 @@ describe('SimpleDataView', () => { const dataView = new SimpleDataView(); dataView.writeUint8(42); - expect(() => dataView.shift(2)).toThrow( - 'Cannot shift more than current offset', - ); + expect(() => dataView.shift(2)).toThrow('Cannot shift more than current offset'); }); it('should throw error when shifting from zero offset', () => { const dataView = new SimpleDataView(); - expect(() => dataView.shift(1)).toThrow( - 'Cannot shift more than current offset', - ); + expect(() => dataView.shift(1)).toThrow('Cannot shift more than current offset'); }); }); diff --git a/packages/plugin-flexbox/src/util/SimpleDataView.ts b/packages/plugin-flexbox/src/util/SimpleDataView.ts index ee7795d..4ce1e82 100644 --- a/packages/plugin-flexbox/src/util/SimpleDataView.ts +++ b/packages/plugin-flexbox/src/util/SimpleDataView.ts @@ -24,15 +24,10 @@ export class SimpleDataView { overflowHandler?: (filledBuffer: ArrayBuffer) => void, ) { this._buffer = - typeof maxSizeOrBuffer === 'number' - ? new ArrayBuffer(maxSizeOrBuffer) - : maxSizeOrBuffer; + typeof maxSizeOrBuffer === 'number' ? new ArrayBuffer(maxSizeOrBuffer) : maxSizeOrBuffer; this._view = new DataView(this._buffer); this._offset = 0; - this._maxSize = - typeof maxSizeOrBuffer === 'number' - ? maxSizeOrBuffer - : this._buffer.byteLength; + this._maxSize = typeof maxSizeOrBuffer === 'number' ? maxSizeOrBuffer : this._buffer.byteLength; this._littleEndian = littleEndian; this._overflowHandler = overflowHandler; } @@ -91,11 +86,9 @@ export class SimpleDataView { } public readUint8 = (): number => this._readInt(1, true); - public readUint8At = (offset: number): number => - this._readIntAt(offset, 1, true); + public readUint8At = (offset: number): number => this._readIntAt(offset, 1, true); public readInt8 = (): number => this._readInt(1, false); - public readInt8At = (offset: number): number => - this._readIntAt(offset, 1, false); + public readInt8At = (offset: number): number => this._readIntAt(offset, 1, false); public writeUint8 = (value: number): void => this._writeInt(1, value, true); public writeUint8At = (offset: number, value: number): void => this._writeIntAt(offset, value, 1, true); @@ -104,11 +97,9 @@ export class SimpleDataView { this._writeIntAt(offset, value, 1, false); public readUint16 = (): number => this._readInt(2, true); - public readUint16At = (offset: number): number => - this._readIntAt(offset, 2, true); + public readUint16At = (offset: number): number => this._readIntAt(offset, 2, true); public readInt16 = (): number => this._readInt(2, false); - public readInt16At = (offset: number): number => - this._readIntAt(offset, 2, false); + public readInt16At = (offset: number): number => this._readIntAt(offset, 2, false); public writeUint16 = (value: number): void => this._writeInt(2, value, true); public writeUint16At = (offset: number, value: number): void => this._writeIntAt(offset, value, 2, true); @@ -117,11 +108,9 @@ export class SimpleDataView { this._writeIntAt(offset, value, 2, false); public readUint32 = (): number => this._readInt(4, true); - public readUint32At = (offset: number): number => - this._readIntAt(offset, 4, true); + public readUint32At = (offset: number): number => this._readIntAt(offset, 4, true); public readInt32 = (): number => this._readInt(4, false); - public readInt32At = (offset: number): number => - this._readIntAt(offset, 4, false); + public readInt32At = (offset: number): number => this._readIntAt(offset, 4, false); public writeUint32 = (value: number): void => this._writeInt(4, value, true); public writeUint32At = (offset: number, value: number): void => this._writeIntAt(offset, value, 4, true); @@ -130,26 +119,20 @@ export class SimpleDataView { this._writeIntAt(offset, value, 4, false); public readBigUint64 = (): bigint => this._readInt(8, true); - public readBigUint64At = (offset: number): bigint => - this._readIntAt(offset, 8, true); + public readBigUint64At = (offset: number): bigint => this._readIntAt(offset, 8, true); public readBigInt64 = (): bigint => this._readInt(8, false); - public readBigInt64At = (offset: number): bigint => - this._readIntAt(offset, 8, false); - public writeBigUint64 = (value: bigint): void => - this._writeInt(8, value, true); + public readBigInt64At = (offset: number): bigint => this._readIntAt(offset, 8, false); + public writeBigUint64 = (value: bigint): void => this._writeInt(8, value, true); public writeBigUint64At = (offset: number, value: bigint): void => this._writeIntAt(offset, value, 8, true); - public writeBigInt64 = (value: bigint): void => - this._writeInt(8, value, false); + public writeBigInt64 = (value: bigint): void => this._writeInt(8, value, false); public writeBigInt64At = (offset: number, value: bigint): void => this._writeIntAt(offset, value, 8, false); public readFloat32 = (): number => this._readFloat(4); - public readFloat32At = (offset: number): number => - this._readFloatAt(offset, 4); + public readFloat32At = (offset: number): number => this._readFloatAt(offset, 4); public readFloat64 = (): number => this._readFloat(8); - public readFloat64At = (offset: number): number => - this._readFloatAt(offset, 8); + public readFloat64At = (offset: number): number => this._readFloatAt(offset, 8); public writeFloat32 = (value: number): void => this._writeFloat(4, value); public writeFloat32At = (offset: number, value: number): void => this._writeFloatAt(offset, value, 4); @@ -168,31 +151,17 @@ export class SimpleDataView { } } - private _readIntAt( - offset: number, - bytes: 1 | 2 | 4, - unsigned: boolean, - ): number; + private _readIntAt(offset: number, bytes: 1 | 2 | 4, unsigned: boolean): number; private _readIntAt(offset: number, bytes: 8, unsigned: boolean): bigint; - private _readIntAt( - offset: number, - bytes: 1 | 2 | 4 | 8, - unsigned: boolean, - ): number | bigint; - private _readIntAt( - offset: number, - bytes: 1 | 2 | 4 | 8, - unsigned: boolean, - ): number | bigint { + private _readIntAt(offset: number, bytes: 1 | 2 | 4 | 8, unsigned: boolean): number | bigint; + private _readIntAt(offset: number, bytes: 1 | 2 | 4 | 8, unsigned: boolean): number | bigint { if (offset < 0 || offset + bytes > this._maxSize) { throw new Error('Offset out of bounds'); } switch (bytes) { case 1: - return unsigned - ? this._view.getUint8(offset) - : this._view.getInt8(offset); + return unsigned ? this._view.getUint8(offset) : this._view.getInt8(offset); case 2: return unsigned ? this._view.getUint16(offset, this._littleEndian) @@ -219,18 +188,8 @@ export class SimpleDataView { return value; } - private _writeIntAt( - offset: number, - value: bigint, - size: 8, - unsigned: boolean, - ): void; - private _writeIntAt( - offset: number, - value: number, - size: 1 | 2 | 4, - unsigned: boolean, - ): void; + private _writeIntAt(offset: number, value: bigint, size: 8, unsigned: boolean): void; + private _writeIntAt(offset: number, value: number, size: 1 | 2 | 4, unsigned: boolean): void; private _writeIntAt( offset: number, value: number | bigint, @@ -283,11 +242,7 @@ export class SimpleDataView { private _writeInt(size: 1 | 2 | 4, value: number, unsigned: boolean): void; private _writeInt(size: 8, value: bigint, unsigned: boolean): void; - private _writeInt( - size: 1 | 2 | 4 | 8, - value: number | bigint, - unsigned: boolean, - ) { + private _writeInt(size: 1 | 2 | 4 | 8, value: number | bigint, unsigned: boolean) { this._checkOverflow(size); this._writeIntAt(this._offset, value, size, unsigned); this._offset += size; diff --git a/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts b/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts index e972817..62e791e 100644 --- a/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts +++ b/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts @@ -1,4 +1,3 @@ -import type { LightningViewElementStyle } from '@plextv/react-lightning'; import type { Align, Display, @@ -9,6 +8,9 @@ import type { FlexDirection as YogaFlexDirection, } from 'yoga-layout'; import type { Yoga } from 'yoga-layout/load'; + +import type { LightningViewElementStyle } from '@plextv/react-lightning'; + import type { YogaOptions } from '../types'; import type { AutoDimensionValue, Transform } from '../types/FlexStyles'; import type { ManagerNode } from '../types/ManagerNode'; @@ -139,11 +141,7 @@ function applyFlexBasis(node: Node, value?: AutoDimensionValue | string) { } } -function applyFlex( - node: Node, - value?: string | number, - expandToAutoFlexBasis = false, -) { +function applyFlex(node: Node, value?: string | number, expandToAutoFlexBasis = false) { if (value == null) { return; } @@ -190,16 +188,11 @@ export function applyFlexPropToYoga( } try { - const value = styleValue as Exclude< - LightningViewElementStyle[K], - Transform - >; + const value = styleValue as Exclude; switch (key) { case 'display': - node.setDisplay( - mapDisplay(yoga, value as LightningViewElementStyle['display']), - ); + node.setDisplay(mapDisplay(yoga, value as LightningViewElementStyle['display'])); return true; case 'w': node.setWidth(value as LightningViewElementStyle['w']); @@ -223,116 +216,62 @@ export function applyFlexPropToYoga( node.setAspectRatio(value as LightningViewElementStyle['aspectRatio']); return true; case 'margin': - node.setMargin( - yoga.EDGE_ALL, - value as LightningViewElementStyle['margin'], - ); + node.setMargin(yoga.EDGE_ALL, value as LightningViewElementStyle['margin']); return true; case 'marginBottom': - node.setMargin( - yoga.EDGE_BOTTOM, - value as LightningViewElementStyle['marginBottom'], - ); + node.setMargin(yoga.EDGE_BOTTOM, value as LightningViewElementStyle['marginBottom']); return true; case 'marginEnd': - node.setMargin( - yoga.EDGE_END, - value as LightningViewElementStyle['marginEnd'], - ); + node.setMargin(yoga.EDGE_END, value as LightningViewElementStyle['marginEnd']); return true; case 'marginLeft': - node.setMargin( - yoga.EDGE_LEFT, - value as LightningViewElementStyle['marginLeft'], - ); + node.setMargin(yoga.EDGE_LEFT, value as LightningViewElementStyle['marginLeft']); return true; case 'marginRight': - node.setMargin( - yoga.EDGE_RIGHT, - value as LightningViewElementStyle['marginRight'], - ); + node.setMargin(yoga.EDGE_RIGHT, value as LightningViewElementStyle['marginRight']); return true; case 'marginStart': - node.setMargin( - yoga.EDGE_START, - value as LightningViewElementStyle['marginStart'], - ); + node.setMargin(yoga.EDGE_START, value as LightningViewElementStyle['marginStart']); return true; case 'marginTop': - node.setMargin( - yoga.EDGE_TOP, - value as LightningViewElementStyle['marginTop'], - ); + node.setMargin(yoga.EDGE_TOP, value as LightningViewElementStyle['marginTop']); return true; case 'marginHorizontal': case 'marginInline': - node.setMargin( - yoga.EDGE_HORIZONTAL, - value as LightningViewElementStyle['marginInline'], - ); + node.setMargin(yoga.EDGE_HORIZONTAL, value as LightningViewElementStyle['marginInline']); return true; case 'marginVertical': case 'marginBlock': - node.setMargin( - yoga.EDGE_VERTICAL, - value as LightningViewElementStyle['marginBlock'], - ); + node.setMargin(yoga.EDGE_VERTICAL, value as LightningViewElementStyle['marginBlock']); return true; case 'padding': - node.setPadding( - yoga.EDGE_ALL, - value as LightningViewElementStyle['padding'], - ); + node.setPadding(yoga.EDGE_ALL, value as LightningViewElementStyle['padding']); return true; case 'paddingBottom': - node.setPadding( - yoga.EDGE_BOTTOM, - value as LightningViewElementStyle['paddingBottom'], - ); + node.setPadding(yoga.EDGE_BOTTOM, value as LightningViewElementStyle['paddingBottom']); return true; case 'paddingEnd': - node.setPadding( - yoga.EDGE_END, - value as LightningViewElementStyle['paddingEnd'], - ); + node.setPadding(yoga.EDGE_END, value as LightningViewElementStyle['paddingEnd']); return true; case 'paddingLeft': - node.setPadding( - yoga.EDGE_LEFT, - value as LightningViewElementStyle['paddingLeft'], - ); + node.setPadding(yoga.EDGE_LEFT, value as LightningViewElementStyle['paddingLeft']); return true; case 'paddingRight': - node.setPadding( - yoga.EDGE_RIGHT, - value as LightningViewElementStyle['paddingRight'], - ); + node.setPadding(yoga.EDGE_RIGHT, value as LightningViewElementStyle['paddingRight']); return true; case 'paddingStart': - node.setPadding( - yoga.EDGE_START, - value as LightningViewElementStyle['paddingStart'], - ); + node.setPadding(yoga.EDGE_START, value as LightningViewElementStyle['paddingStart']); return true; case 'paddingTop': - node.setPadding( - yoga.EDGE_TOP, - value as LightningViewElementStyle['paddingTop'], - ); + node.setPadding(yoga.EDGE_TOP, value as LightningViewElementStyle['paddingTop']); return true; case 'paddingHorizontal': case 'paddingInline': - node.setPadding( - yoga.EDGE_HORIZONTAL, - value as LightningViewElementStyle['paddingInline'], - ); + node.setPadding(yoga.EDGE_HORIZONTAL, value as LightningViewElementStyle['paddingInline']); return true; case 'paddingVertical': case 'paddingBlock': - node.setPadding( - yoga.EDGE_VERTICAL, - value as LightningViewElementStyle['paddingBlock'], - ); + node.setPadding(yoga.EDGE_VERTICAL, value as LightningViewElementStyle['paddingBlock']); return true; case 'flex': applyFlex(node, value, config.expandToAutoFlexBasis); @@ -362,54 +301,31 @@ export function applyFlexPropToYoga( node.setFlexGrow((value as LightningViewElementStyle['flexGrow']) ?? 1); return true; case 'flexShrink': - node.setFlexShrink( - (value as LightningViewElementStyle['flexShrink']) ?? 0, - ); + node.setFlexShrink((value as LightningViewElementStyle['flexShrink']) ?? 0); return true; case 'gap': - node.setGap( - yoga.GUTTER_ALL, - (value as LightningViewElementStyle['gap']) ?? 0, - ); + node.setGap(yoga.GUTTER_ALL, (value as LightningViewElementStyle['gap']) ?? 0); return true; case 'columnGap': - node.setGap( - yoga.GUTTER_COLUMN, - (value as LightningViewElementStyle['columnGap']) ?? 0, - ); + node.setGap(yoga.GUTTER_COLUMN, (value as LightningViewElementStyle['columnGap']) ?? 0); return true; case 'rowGap': - node.setGap( - yoga.GUTTER_ROW, - (value as LightningViewElementStyle['rowGap']) ?? 0, - ); + node.setGap(yoga.GUTTER_ROW, (value as LightningViewElementStyle['rowGap']) ?? 0); return true; case 'position': node.setPositionType(mapPosition(yoga, value)); return true; case 'right': - node.setPosition( - yoga.EDGE_RIGHT, - (value as LightningViewElementStyle['right']) ?? 0, - ); + node.setPosition(yoga.EDGE_RIGHT, (value as LightningViewElementStyle['right']) ?? 0); return true; case 'bottom': - node.setPosition( - yoga.EDGE_BOTTOM, - (value as LightningViewElementStyle['bottom']) ?? 0, - ); + node.setPosition(yoga.EDGE_BOTTOM, (value as LightningViewElementStyle['bottom']) ?? 0); return true; case 'left': - node.setPosition( - yoga.EDGE_LEFT, - (value as LightningViewElementStyle['left']) ?? 0, - ); + node.setPosition(yoga.EDGE_LEFT, (value as LightningViewElementStyle['left']) ?? 0); return true; case 'top': - node.setPosition( - yoga.EDGE_TOP, - (value as LightningViewElementStyle['top']) ?? 0, - ); + node.setPosition(yoga.EDGE_TOP, (value as LightningViewElementStyle['top']) ?? 0); return true; } } catch (err) { diff --git a/packages/plugin-flexbox/src/util/getYogaStyle.ts b/packages/plugin-flexbox/src/util/getYogaStyle.ts index 784216e..4c0e452 100644 --- a/packages/plugin-flexbox/src/util/getYogaStyle.ts +++ b/packages/plugin-flexbox/src/util/getYogaStyle.ts @@ -1,4 +1,5 @@ import type { LightningViewElementStyle } from '@plextv/react-lightning'; + import { isFlexStyleProp } from './isFlexStyleProp'; export function getYogaStyle( @@ -11,7 +12,7 @@ export function getYogaStyle( const value = style[key]; if (value != null) { - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO (yogaStyles as any)[key] = value; } } diff --git a/packages/plugin-flexbox/src/util/isFlexStyleProp.ts b/packages/plugin-flexbox/src/util/isFlexStyleProp.ts index 49f3801..8d11d61 100644 --- a/packages/plugin-flexbox/src/util/isFlexStyleProp.ts +++ b/packages/plugin-flexbox/src/util/isFlexStyleProp.ts @@ -59,8 +59,6 @@ flexProps satisfies Partial>; export type FlexProps = keyof typeof flexProps; -export function isFlexStyleProp( - prop: number | string | symbol, -): prop is FlexProps { +export function isFlexStyleProp(prop: number | string | symbol): prop is FlexProps { return prop in flexProps; } diff --git a/packages/plugin-flexbox/src/util/parseFlexValue.spec.ts b/packages/plugin-flexbox/src/util/parseFlexValue.spec.ts index 1fae0d3..0668816 100644 --- a/packages/plugin-flexbox/src/util/parseFlexValue.spec.ts +++ b/packages/plugin-flexbox/src/util/parseFlexValue.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; + import { parseFlexValue } from './parseFlexValue'; describe('parseFlexValue', () => { diff --git a/packages/plugin-flexbox/src/util/toSerializableValue.ts b/packages/plugin-flexbox/src/util/toSerializableValue.ts index 6413ae4..2e1b9f2 100644 --- a/packages/plugin-flexbox/src/util/toSerializableValue.ts +++ b/packages/plugin-flexbox/src/util/toSerializableValue.ts @@ -17,7 +17,7 @@ function isPrimitiveValue( ); } -// biome-ignore lint/suspicious/noExplicitAny: Any value can be passed in +// oxlint-disable-next-line typescript/no-explicit-any -- Any value can be passed in export function toSerializableValue(key: string, value: any): T | null { const valueType = typeof value; @@ -27,11 +27,7 @@ export function toSerializableValue(key: string, value: any): T | null { // Only transforms can be objects if (key === 'transform') { return value as T; - } else if ( - value != null && - 'current' in value && - isPrimitiveValue(typeof value.current) - ) { + } else if (value != null && 'current' in value && isPrimitiveValue(typeof value.current)) { // ...unless it's a reanimated animation. Unfortunately, there's no real // good way to serialize animations. There's also no real good way to // keep this logic in the reanimated plugin, so we'll just have to do it diff --git a/packages/plugin-flexbox/src/worker.ts b/packages/plugin-flexbox/src/worker.ts index cf3aa1c..f4bd278 100644 --- a/packages/plugin-flexbox/src/worker.ts +++ b/packages/plugin-flexbox/src/worker.ts @@ -30,9 +30,7 @@ function applyNodeOperations(buffer: ArrayBuffer) { const parentId = dataView.readUint32(); const childId = dataView.readUint32(); const index = - method === NodeOperations.AddChildNodeAtIndex - ? dataView.readUint32() - : undefined; + method === NodeOperations.AddChildNodeAtIndex ? dataView.readUint32() : undefined; manager.addChildNode(parentId, childId, index); break; @@ -83,16 +81,9 @@ self.onmessage = async ( try { if (typeof manager[method] === 'function') { // @ts-expect-error Dynamic method call - let result: Promise | null | unknown = manager[method].apply( - manager, - args, - ); - - if ( - result != null && - typeof result === 'object' && - 'then' in result - ) { + let result: Promise | null | unknown = manager[method].apply(manager, args); + + if (result != null && typeof result === 'object' && 'then' in result) { result = await (result as Promise); } @@ -103,8 +94,7 @@ self.onmessage = async ( self.postMessage({ id, error: `Method ${method} is not a function` }); } } catch (error) { - const message = - typeof error === 'string' ? error : (error as Error).message; + const message = typeof error === 'string' ? error : (error as Error).message; self.postMessage({ id, error: message }); } diff --git a/packages/plugin-flexbox/src/yoga.ts b/packages/plugin-flexbox/src/yoga.ts index 11c1860..083332d 100644 --- a/packages/plugin-flexbox/src/yoga.ts +++ b/packages/plugin-flexbox/src/yoga.ts @@ -2,13 +2,9 @@ import type { YogaOptions } from './types/YogaOptions'; import type { YogaManager } from './YogaManager'; import type { Workerized } from './YogaManagerWorker'; -async function load( - yogaOptions: YogaOptions = {}, -): Promise> { +async function load(yogaOptions: YogaOptions = {}): Promise> { if (yogaOptions.useWebWorker) { - const { default: createWorkerManager } = await import( - './YogaManagerWorker' - ); + const { default: createWorkerManager } = await import('./YogaManagerWorker'); const workerManager = createWorkerManager(); await workerManager.init(yogaOptions); diff --git a/packages/plugin-flexbox/tsdown.lib.ts b/packages/plugin-flexbox/tsdown.lib.ts index 6abab80..6e0ef37 100644 --- a/packages/plugin-flexbox/tsdown.lib.ts +++ b/packages/plugin-flexbox/tsdown.lib.ts @@ -1,6 +1,7 @@ -import baseConfig from '@repo/configs/tsdown.config.json'; import { defineConfig, type UserConfig } from 'tsdown'; +import baseConfig from '@repo/configs/tsdown.config.json'; + const config: UserConfig = defineConfig({ ...baseConfig, entry: ['src/index.ts', 'src/types/jsx.d.ts'], diff --git a/packages/plugin-flexbox/vite.config.ts b/packages/plugin-flexbox/vite.config.ts index 29c7297..5d0415b 100644 --- a/packages/plugin-flexbox/vite.config.ts +++ b/packages/plugin-flexbox/vite.config.ts @@ -1,7 +1,8 @@ -import config from '@repo/configs/vite.config'; import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + const buildTarget = 'chrome56'; export default defineConfig((env) => diff --git a/packages/plugin-reanimated/README.md b/packages/plugin-reanimated/README.md index f0179d5..8e07035 100644 --- a/packages/plugin-reanimated/README.md +++ b/packages/plugin-reanimated/README.md @@ -1,3 +1,3 @@ ## @plextv/react-lightning-plugin-reanimated -A bare bones drop-in replacement for react-native-reanimated to support animations. \ No newline at end of file +A bare bones drop-in replacement for react-native-reanimated to support animations. diff --git a/packages/plugin-reanimated/package.json b/packages/plugin-reanimated/package.json index 845866b..58b85a9 100644 --- a/packages/plugin-reanimated/package.json +++ b/packages/plugin-reanimated/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning-plugin-reanimated", - "description": "Reanimated plugin for @plextv/react-native-lightning", "version": "0.4.0", - "author": "Plex Inc.", + "description": "Reanimated plugin for @plextv/react-native-lightning", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -20,7 +23,6 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -28,7 +30,8 @@ "import": "./dist/index.js" }, "./package.json": "./package.json" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -37,9 +40,6 @@ "check:types": "tsc --noEmit -p tsconfig.json", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { "@repo/configs": "workspace:*", "@types/react": "19.2.8" diff --git a/packages/plugin-reanimated/src/animation/AnimatedValue.ts b/packages/plugin-reanimated/src/animation/AnimatedValue.ts index 3445806..1678c99 100644 --- a/packages/plugin-reanimated/src/animation/AnimatedValue.ts +++ b/packages/plugin-reanimated/src/animation/AnimatedValue.ts @@ -5,6 +5,7 @@ import type { WithSpringConfig, WithTimingConfig, } from 'react-native-reanimated-original'; + import { AnimationType } from '../types/AnimationType'; import { createSpringAnimation } from './spring'; import { createTimingAnimation } from './timing'; @@ -32,9 +33,7 @@ export class AnimatedValue { this.callback = callback; } - private _getLightningAnimationSettings( - config?: AnimationConfigType[TType], - ): AnimationSettings { + private _getLightningAnimationSettings(config?: AnimationConfigType[TType]): AnimationSettings { switch (this.type) { case AnimationType.Spring: return createSpringAnimation(config); diff --git a/packages/plugin-reanimated/src/animation/spring.ts b/packages/plugin-reanimated/src/animation/spring.ts index d89433c..fbdb702 100644 --- a/packages/plugin-reanimated/src/animation/spring.ts +++ b/packages/plugin-reanimated/src/animation/spring.ts @@ -1,10 +1,8 @@ // Extracted and adapted from Reanimated's spring implementation // See: https://github.com/software-mansion/react-native-reanimated/tree/main/packages/react-native-reanimated/src/animation/spring import type { AnimationSettings } from '@lightningjs/renderer'; -import { - ReduceMotion, - type WithSpringConfig, -} from 'react-native-reanimated-original'; +import { ReduceMotion, type WithSpringConfig } from 'react-native-reanimated-original'; + import { calculateNewStiffnessToMatchDuration, checkIfConfigIsValid, @@ -33,9 +31,7 @@ const DefaultConfig: DefaultSpringConfig = { clamp: undefined, }; -export function createSpringAnimation( - userConfig?: WithSpringConfig, -): AnimationSettings { +export function createSpringAnimation(userConfig?: WithSpringConfig): AnimationSettings { const key = JSON.stringify(userConfig); const cached = cache.get(key); @@ -70,8 +66,7 @@ export function createSpringAnimation( const startValue = 0; const toValue = 1; const x0 = startValue - toValue; - const useDuration = - userConfig?.dampingRatio != null || userConfig?.duration != null; + const useDuration = userConfig?.dampingRatio != null || userConfig?.duration != null; let current = startValue; let velocity = config.velocity; @@ -84,12 +79,7 @@ export function createSpringAnimation( config.duration = getEstimatedDuration(zeta, omega0); } - const initialEnergy = getEnergy( - x0, - config.velocity, - config.stiffness, - config.mass, - ); + const initialEnergy = getEnergy(x0, config.velocity, config.stiffness, config.mass); function easing(progress: number): number { const t = (progress * config.duration) / 1000; diff --git a/packages/plugin-reanimated/src/animation/springUtils.ts b/packages/plugin-reanimated/src/animation/springUtils.ts index 39cc9e1..13cfebd 100644 --- a/packages/plugin-reanimated/src/animation/springUtils.ts +++ b/packages/plugin-reanimated/src/animation/springUtils.ts @@ -10,9 +10,7 @@ export type DefaultSpringConfig = { export function checkIfConfigIsValid(config: DefaultSpringConfig): boolean { let errorMessage = ''; - ( - ['stiffness', 'damping', 'dampingRatio', 'mass', 'energyThreshold'] as const - ).forEach((prop) => { + (['stiffness', 'damping', 'dampingRatio', 'mass', 'energyThreshold'] as const).forEach((prop) => { const value = config[prop]; if (value <= 0) { errorMessage += `, ${prop} must be grater than zero but got ${value}`; @@ -23,11 +21,7 @@ export function checkIfConfigIsValid(config: DefaultSpringConfig): boolean { errorMessage += `, duration can't be negative, got ${config.duration}`; } - if ( - config.clamp?.min && - config.clamp?.max && - config.clamp.min > config.clamp.max - ) { + if (config.clamp?.min && config.clamp?.max && config.clamp.min > config.clamp.max) { errorMessage += `, clamp.min should be lower than clamp.max, got clamp: {min: ${config.clamp.min}, max: ${config.clamp.max}} `; } @@ -143,13 +137,10 @@ export function calculateNewStiffnessToMatchDuration( const perceptualCoefficient = 1.5; const MILLISECONDS_IN_SECOND = 1000; - const settlingDuration = - (targetDuration * perceptualCoefficient) / MILLISECONDS_IN_SECOND; + const settlingDuration = (targetDuration * perceptualCoefficient) / MILLISECONDS_IN_SECOND; const omega0 = Math.sqrt(stiffness / m) * zeta; - const xtk = - (x0 + (v0 + x0 * omega0) * settlingDuration) * - Math.exp(-omega0 * settlingDuration); + const xtk = (x0 + (v0 + x0 * omega0) * settlingDuration) * Math.exp(-omega0 * settlingDuration); const vtk = (x0 + (v0 + x0 * omega0) * settlingDuration) * @@ -187,8 +178,7 @@ export function criticallyDampedSpringCalculations(precalculatedValues: { const { v0, x0, omega0, t } = precalculatedValues; const criticallyDampedEnvelope = Math.exp(-omega0 * t); - const criticallyDampedPosition = - 1 + criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t); + const criticallyDampedPosition = 1 + criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t); const criticallyDampedVelocity = criticallyDampedEnvelope * -omega0 * (x0 + (v0 + omega0 * x0) * t) + @@ -216,15 +206,13 @@ export function underDampedSpringCalculations(precalculatedValues: { // under damped const underDampedEnvelope = Math.exp(-zeta * omega0 * t); const underDampedFrag1 = - underDampedEnvelope * - (sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1); + underDampedEnvelope * (sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1); const underDampedPosition = 1 + underDampedFrag1; // This looks crazy -- it's actually just the derivative of the oscillation function const underDampedVelocity = -zeta * omega0 * underDampedFrag1 + - underDampedEnvelope * - (cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1); + underDampedEnvelope * (cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1); return { position: underDampedPosition, velocity: underDampedVelocity }; } @@ -246,17 +234,9 @@ export function isAnimationTerminatingCalculation( } } - const currentEnergy = getEnergy( - toValue - current, - velocity, - config.stiffness, - config.mass, - ); - - return ( - initialEnergy === 0 || - currentEnergy / initialEnergy <= config.energyThreshold - ); + const currentEnergy = getEnergy(toValue - current, velocity, config.stiffness, config.mass); + + return initialEnergy === 0 || currentEnergy / initialEnergy <= config.energyThreshold; } /** diff --git a/packages/plugin-reanimated/src/animation/timing.ts b/packages/plugin-reanimated/src/animation/timing.ts index 96789ee..cabfcb1 100644 --- a/packages/plugin-reanimated/src/animation/timing.ts +++ b/packages/plugin-reanimated/src/animation/timing.ts @@ -8,9 +8,7 @@ const DefaultTimingConfig = { reduceMotion: ReduceMotion.System, }; -export function createTimingAnimation( - config?: WithTimingConfig, -): AnimationSettings { +export function createTimingAnimation(config?: WithTimingConfig): AnimationSettings { return { duration: config?.duration ?? DefaultTimingConfig.duration, easing: 'linear', diff --git a/packages/plugin-reanimated/src/builders/Fade.ts b/packages/plugin-reanimated/src/builders/Fade.ts index 9ac05ba..4bcaa64 100644 --- a/packages/plugin-reanimated/src/builders/Fade.ts +++ b/packages/plugin-reanimated/src/builders/Fade.ts @@ -13,6 +13,7 @@ import { FadeOutUp as ReanimatedFadeOutUp, } from 'react-native-reanimated-original'; import type { Class } from 'type-fest'; + import { withDelay } from '../exports/withDelay'; import { withTiming } from '../exports/withTiming'; import { createBuilderWrapper } from './createBuilderWrapper'; diff --git a/packages/plugin-reanimated/src/builders/LinearTransition.ts b/packages/plugin-reanimated/src/builders/LinearTransition.ts index 02f6d0f..1612f4e 100644 --- a/packages/plugin-reanimated/src/builders/LinearTransition.ts +++ b/packages/plugin-reanimated/src/builders/LinearTransition.ts @@ -4,54 +4,54 @@ import { LinearTransition as ReanimatedLinearTransition, } from 'react-native-reanimated-original'; import type { Class } from 'type-fest'; + import { withDelay } from '../exports/withDelay'; import { withTiming } from '../exports/withTiming'; import { createBuilderWrapper } from './createBuilderWrapper'; -export const LinearTransition: Class = - createBuilderWrapper( - ReanimatedLinearTransition, - function (this: ReanimatedLinearTransition) { - const delay = this.getDelay(); +export const LinearTransition: Class = createBuilderWrapper( + ReanimatedLinearTransition, + function (this: ReanimatedLinearTransition) { + const delay = this.getDelay(); - return (values: ExitAnimationsValues & EntryAnimationsValues) => ({ - animations: { - originX: withDelay( - delay, - withTiming(values.targetOriginX, { - duration: this.durationV, - }), - // biome-ignore lint/suspicious/noExplicitAny: Reanimated typings are wrong - ) as any, - originY: withDelay( - delay, - withTiming(values.targetOriginY, { - duration: this.durationV, - }), - // biome-ignore lint/suspicious/noExplicitAny: See above - ) as any, - width: withDelay( - delay, - withTiming(values.targetWidth, { - duration: this.durationV, - }), - // biome-ignore lint/suspicious/noExplicitAny: See above - ) as any, - height: withDelay( - delay, - withTiming(values.targetHeight, { - duration: this.durationV, - }), - // biome-ignore lint/suspicious/noExplicitAny: See above - ) as any, - }, - initialValues: { - originX: values.currentOriginX, - originY: values.currentOriginY, - width: values.currentWidth, - height: values.currentHeight, - }, - callback: this.callbackV, - }); - }, - ); + return (values: ExitAnimationsValues & EntryAnimationsValues) => ({ + animations: { + originX: withDelay( + delay, + withTiming(values.targetOriginX, { + duration: this.durationV, + }), + // oxlint-disable-next-line typescript/no-explicit-any -- Reanimated typings are wrong + ) as any, + originY: withDelay( + delay, + withTiming(values.targetOriginY, { + duration: this.durationV, + }), + // oxlint-disable-next-line typescript/no-explicit-any -- See above + ) as any, + width: withDelay( + delay, + withTiming(values.targetWidth, { + duration: this.durationV, + }), + // oxlint-disable-next-line typescript/no-explicit-any -- See above + ) as any, + height: withDelay( + delay, + withTiming(values.targetHeight, { + duration: this.durationV, + }), + // oxlint-disable-next-line typescript/no-explicit-any -- See above + ) as any, + }, + initialValues: { + originX: values.currentOriginX, + originY: values.currentOriginY, + width: values.currentWidth, + height: values.currentHeight, + }, + callback: this.callbackV, + }); + }, +); diff --git a/packages/plugin-reanimated/src/builders/Slide.ts b/packages/plugin-reanimated/src/builders/Slide.ts index d792e2e..a13a1b9 100644 --- a/packages/plugin-reanimated/src/builders/Slide.ts +++ b/packages/plugin-reanimated/src/builders/Slide.ts @@ -11,6 +11,7 @@ import { SlideOutUp as ReanimatedSlideOutUp, } from 'react-native-reanimated-original'; import type { Class } from 'type-fest'; + import { withDelay } from '../exports/withDelay'; import { withTiming } from '../exports/withTiming'; import { createBuilderWrapper } from './createBuilderWrapper'; @@ -107,35 +108,28 @@ export const SlideInDown: Class = createBuilderWrapper( }, ); -export const SlideOutRight: Class = - createBuilderWrapper( - ReanimatedSlideOutRight, - function (this: ReanimatedSlideOutRight) { - const delay = this.getDelay(); - - return (values: ExitAnimationsValues) => ({ - animations: { - x: withDelay( - delay, - withTiming( - Math.max( - values.currentOriginX + values.windowWidth, - values.windowWidth, - ), - { - duration: this.durationV, - easing: this.easingV, - }, - ), - ), - }, - initialValues: { - x: values.currentOriginX, - }, - callback: this.callbackV, - }); - }, - ); +export const SlideOutRight: Class = createBuilderWrapper( + ReanimatedSlideOutRight, + function (this: ReanimatedSlideOutRight) { + const delay = this.getDelay(); + + return (values: ExitAnimationsValues) => ({ + animations: { + x: withDelay( + delay, + withTiming(Math.max(values.currentOriginX + values.windowWidth, values.windowWidth), { + duration: this.durationV, + easing: this.easingV, + }), + ), + }, + initialValues: { + x: values.currentOriginX, + }, + callback: this.callbackV, + }); + }, +); export const SlideOutLeft: Class = createBuilderWrapper( ReanimatedSlideOutLeft, @@ -146,16 +140,10 @@ export const SlideOutLeft: Class = createBuilderWrapper( animations: { x: withDelay( delay, - withTiming( - Math.min( - values.currentOriginX - values.windowWidth, - -values.windowWidth, - ), - { - duration: this.durationV, - easing: this.easingV, - }, - ), + withTiming(Math.min(values.currentOriginX - values.windowWidth, -values.windowWidth), { + duration: this.durationV, + easing: this.easingV, + }), ), }, initialValues: { @@ -175,16 +163,10 @@ export const SlideOutUp: Class = createBuilderWrapper( animations: { y: withDelay( delay, - withTiming( - Math.min( - values.currentOriginY - values.windowHeight, - -values.windowHeight, - ), - { - duration: this.durationV, - easing: this.easingV, - }, - ), + withTiming(Math.min(values.currentOriginY - values.windowHeight, -values.windowHeight), { + duration: this.durationV, + easing: this.easingV, + }), ), }, initialValues: { @@ -204,16 +186,10 @@ export const SlideOutDown: Class = createBuilderWrapper( animations: { y: withDelay( delay, - withTiming( - Math.max( - values.currentOriginY + values.windowHeight, - values.windowHeight, - ), - { - duration: this.durationV, - easing: this.easingV, - }, - ), + withTiming(Math.max(values.currentOriginY + values.windowHeight, values.windowHeight), { + duration: this.durationV, + easing: this.easingV, + }), ), }, initialValues: { diff --git a/packages/plugin-reanimated/src/exports/FlatList.tsx b/packages/plugin-reanimated/src/exports/FlatList.tsx index ab6cef2..d3a3422 100644 --- a/packages/plugin-reanimated/src/exports/FlatList.tsx +++ b/packages/plugin-reanimated/src/exports/FlatList.tsx @@ -1,8 +1,6 @@ import { type FlatListProps, FlatList as RNFlatList } from 'react-native'; -import { - type AnimatedComponent, - createAnimatedComponent, -} from './createAnimatedComponent'; + +import { type AnimatedComponent, createAnimatedComponent } from './createAnimatedComponent'; export const FlatList: AnimatedComponent> = createAnimatedComponent(RNFlatList); diff --git a/packages/plugin-reanimated/src/exports/Image.tsx b/packages/plugin-reanimated/src/exports/Image.tsx index 5a0769a..01d7dde 100644 --- a/packages/plugin-reanimated/src/exports/Image.tsx +++ b/packages/plugin-reanimated/src/exports/Image.tsx @@ -1,8 +1,5 @@ import { type ImageProps, Image as RNImage } from 'react-native'; -import { - type AnimatedComponent, - createAnimatedComponent, -} from './createAnimatedComponent'; -export const Image: AnimatedComponent = - createAnimatedComponent(RNImage); +import { type AnimatedComponent, createAnimatedComponent } from './createAnimatedComponent'; + +export const Image: AnimatedComponent = createAnimatedComponent(RNImage); diff --git a/packages/plugin-reanimated/src/exports/ScrollView.tsx b/packages/plugin-reanimated/src/exports/ScrollView.tsx index 9cd3a5a..c319a00 100644 --- a/packages/plugin-reanimated/src/exports/ScrollView.tsx +++ b/packages/plugin-reanimated/src/exports/ScrollView.tsx @@ -1,8 +1,5 @@ import { ScrollView as RNScrollView, type ScrollViewProps } from 'react-native'; -import { - type AnimatedComponent, - createAnimatedComponent, -} from './createAnimatedComponent'; -export const ScrollView: AnimatedComponent = - createAnimatedComponent(RNScrollView); +import { type AnimatedComponent, createAnimatedComponent } from './createAnimatedComponent'; + +export const ScrollView: AnimatedComponent = createAnimatedComponent(RNScrollView); diff --git a/packages/plugin-reanimated/src/exports/Text.tsx b/packages/plugin-reanimated/src/exports/Text.tsx index 99ed6f0..2ae3629 100644 --- a/packages/plugin-reanimated/src/exports/Text.tsx +++ b/packages/plugin-reanimated/src/exports/Text.tsx @@ -1,8 +1,5 @@ import { Text as RNText, type TextProps } from 'react-native'; -import { - type AnimatedComponent, - createAnimatedComponent, -} from './createAnimatedComponent'; -export const Text: AnimatedComponent = - createAnimatedComponent(RNText); +import { type AnimatedComponent, createAnimatedComponent } from './createAnimatedComponent'; + +export const Text: AnimatedComponent = createAnimatedComponent(RNText); diff --git a/packages/plugin-reanimated/src/exports/View.tsx b/packages/plugin-reanimated/src/exports/View.tsx index 667d487..db269aa 100644 --- a/packages/plugin-reanimated/src/exports/View.tsx +++ b/packages/plugin-reanimated/src/exports/View.tsx @@ -1,8 +1,5 @@ import { View as RNView, type ViewProps } from 'react-native'; -import { - type AnimatedComponent, - createAnimatedComponent, -} from './createAnimatedComponent'; -export const View: AnimatedComponent = - createAnimatedComponent(RNView); +import { type AnimatedComponent, createAnimatedComponent } from './createAnimatedComponent'; + +export const View: AnimatedComponent = createAnimatedComponent(RNView); diff --git a/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx b/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx index 5eb15c3..df21569 100644 --- a/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx +++ b/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx @@ -1,10 +1,3 @@ -import type { - LightningElement, - LightningElementProps, - LightningViewElementStyle, - Rect, - RendererNode, -} from '@plextv/react-lightning'; import { Component, type ComponentType, @@ -19,6 +12,15 @@ import type { BaseAnimationBuilder, LayoutAnimationFunction, } from 'react-native-reanimated-original'; + +import type { + LightningElement, + LightningElementProps, + LightningViewElementStyle, + Rect, + RendererNode, +} from '@plextv/react-lightning'; + import { isAnimatedStyle } from '../isAnimatedStyle'; import type { AnimatedStyle } from '../types/AnimatedStyle'; import type { ReanimatedAnimation } from '../types/ReanimatedAnimation'; @@ -40,9 +42,7 @@ function flattenStyles( animatedStyles: Set, flattenedStyles: Partial, ): void; -function flattenStyles( - style: StyleProp, -): [Set, Partial]; +function flattenStyles(style: StyleProp): [Set, Partial]; function flattenStyles( style: StyleProp, animatedStyles: Set = new Set(), @@ -89,10 +89,7 @@ function getBuilder(layoutAnimationOrBuilder?: ReanimatedAnimation) { } else if (typeof layoutAnimationOrBuilder === 'function') { return layoutAnimationOrBuilder; } else if (import.meta.env.DEV) { - console.warn( - 'This animation is not supported in React Lightning: ', - layoutAnimationOrBuilder, - ); + console.warn('This animation is not supported in React Lightning: ', layoutAnimationOrBuilder); } return null; @@ -150,16 +147,12 @@ export function createAnimatedComponent( ComponentToAnimate: ComponentType>, ): AnimatedComponent { class AnimatedComponentInternal extends Component> { - static displayName = - `LightningAnimated(${ComponentToAnimate.displayName || ComponentToAnimate.name || 'Component'})`; + static displayName = `LightningAnimated(${ComponentToAnimate.displayName || ComponentToAnimate.name || 'Component'})`; private _ref: NativeLightningElement | null = null; private _animatedStyles: Set = new Set(); private _styles: Partial | null = null; - private _cachedBuilders = new WeakMap< - ReanimatedAnimation, - LayoutAnimationFunction | null - >(); + private _cachedBuilders = new WeakMap(); constructor(props: AnimatedProps) { super(props); @@ -207,13 +200,7 @@ export function createAnimatedComponent( } render() { - return ( - - ); + return ; } _resolveComponentRef = (ref: NativeLightningElement | null) => { @@ -259,9 +246,7 @@ export function createAnimatedComponent( }; _transformStyles() { - const [newAnimatedStyles, flattenedStyles] = flattenStyles( - this.props.style, - ); + const [newAnimatedStyles, flattenedStyles] = flattenStyles(this.props.style); if (this._ref) { // Remove refs for any animated styles that were removed @@ -278,10 +263,7 @@ export function createAnimatedComponent( this._styles = flattenedStyles; } - private _runAnimation( - builder: LayoutAnimationFunction | null, - callback?: () => void, - ) { + private _runAnimation(builder: LayoutAnimationFunction | null, callback?: () => void) { if (!this._ref || !builder) { callback?.(); return; @@ -306,9 +288,7 @@ export function createAnimatedComponent( return; } - const lightningAnimation = toLightningAnimationAndStyles( - layoutAnimation.animations, - ); + const lightningAnimation = toLightningAnimationAndStyles(layoutAnimation.animations); el.once('animationFinished', () => { if (layoutAnimation.callback) { @@ -317,14 +297,8 @@ export function createAnimatedComponent( callback?.(); }); - for (const [key, value] of Object.entries( - layoutAnimation.initialValues, - )) { - el.setNodeProp( - key as keyof RendererNode, - value, - false, - ); + for (const [key, value] of Object.entries(layoutAnimation.initialValues)) { + el.setNodeProp(key as keyof RendererNode, value, false); } el?.setProps({ @@ -335,14 +309,12 @@ export function createAnimatedComponent( } } - return forwardRef>( - (props, forwardedRef) => { - return ( - )} - forwardedRef={forwardedRef} - /> - ); - }, - ); + return forwardRef>((props, forwardedRef) => { + return ( + )} + forwardedRef={forwardedRef} + /> + ); + }); } diff --git a/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx b/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx index accf188..c3cbfe9 100644 --- a/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx +++ b/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx @@ -12,31 +12,29 @@ export const useAnimatedScrollHandler: UseAnimatedScrollHandlerFn = ( scrollHandlers, dependencies, ) => { + 'use no memo'; + const inputs: DependencyList = dependencies ?? []; // We want to persist context between scroll events // The caller should use it, we won't do any assignment in // this function. const contextRef = useRef({}); - return useCallback( - (event) => { - const context = contextRef.current; - // Only allow onScroll event - const reanimatedEvent = { - eventName: 'onScroll', - ...event.nativeEvent, - }; + return useCallback((event) => { + const context = contextRef.current; + // Only allow onScroll event + const reanimatedEvent = { + eventName: 'onScroll', + ...event.nativeEvent, + }; - if (typeof scrollHandlers === 'function') { - scrollHandlers(reanimatedEvent, context); - return; - } + if (typeof scrollHandlers === 'function') { + scrollHandlers(reanimatedEvent, context); + return; + } - if (scrollHandlers && typeof scrollHandlers.onScroll === 'function') { - scrollHandlers.onScroll(reanimatedEvent, context); - } - }, - // biome-ignore lint/correctness/useExhaustiveDependencies: We're passing a dependencies array from the props - inputs, - ); + if (scrollHandlers && typeof scrollHandlers.onScroll === 'function') { + scrollHandlers.onScroll(reanimatedEvent, context); + } + }, inputs); }; diff --git a/packages/plugin-reanimated/src/exports/useAnimatedStyle.ts b/packages/plugin-reanimated/src/exports/useAnimatedStyle.ts index 8ccb72a..08bf728 100644 --- a/packages/plugin-reanimated/src/exports/useAnimatedStyle.ts +++ b/packages/plugin-reanimated/src/exports/useAnimatedStyle.ts @@ -1,19 +1,16 @@ -import type { - LightningElement, - LightningElementStyle, -} from '@plextv/react-lightning'; import type { DependencyList } from 'react'; -import { useCallback, useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import type { useAnimatedStyle as useAnimatedStyleRN } from 'react-native-reanimated-original'; import type { Mutable } from 'react-native-reanimated/lib/typescript/commonTypes'; import type { DefaultStyle } from 'react-native-reanimated/lib/typescript/hook/commonTypes'; -import type { useAnimatedStyle as useAnimatedStyleRN } from 'react-native-reanimated-original'; + +import type { LightningElement, LightningElementStyle } from '@plextv/react-lightning'; + import type { AnimatedObject } from '../types/AnimatedObject'; import type { AnimatedStyle } from '../types/AnimatedStyle'; import { toLightningAnimationAndStyles } from '../utils/toLightningAnimationAndStyles'; -type UseAnimatedStyleFn = ( - ...args: Parameters -) => AnimatedStyle; +type UseAnimatedStyleFn = (...args: Parameters) => AnimatedStyle; function computeAndSetStyles( updater: () => AnimatedObject, @@ -36,25 +33,26 @@ function computeAndSetStyles( let idCount = 0; export const useAnimatedStyle: UseAnimatedStyleFn = (updater, dependencies) => { - const viewsRef = useRef(new Set()); + const [views] = useState(() => new Set()); const inputs: DependencyList = dependencies ?? []; const timerRef = useRef(0); // Debounce this call so we don't end up calculating the styles multiple times // when updating multiple properties in the same hook - const applyStyles = useCallback(() => { + const applyStyles = () => { if (timerRef.current) { window.clearTimeout(timerRef.current); } timerRef.current = window.setTimeout(() => { - computeAndSetStyles(updater, viewsRef.current); + computeAndSetStyles(updater, views); timerRef.current = 0; }, 2); - }, [updater]); + }; useEffect(() => { - const id = idCount++; + const id = idCount; + idCount += 1; for (const dep of inputs) { if (dep && typeof dep === 'object' && 'addListener' in dep) { @@ -75,6 +73,6 @@ export const useAnimatedStyle: UseAnimatedStyleFn = (updater, dependencies) => { }, [inputs, applyStyles]); return { - viewsRef: viewsRef.current, + viewsRef: views, }; }; diff --git a/packages/plugin-reanimated/src/exports/useComposedEventHandler.ts b/packages/plugin-reanimated/src/exports/useComposedEventHandler.ts index c8cbdfb..b760eaa 100644 --- a/packages/plugin-reanimated/src/exports/useComposedEventHandler.ts +++ b/packages/plugin-reanimated/src/exports/useComposedEventHandler.ts @@ -1,4 +1,4 @@ -// biome-ignore-all lint/suspicious/noExplicitAny: Valid use of any here +// oxlint-disable typescript/no-explicit-any -- Valid use of any here type EventHandler = (...args: any[]) => void; export function useComposedEventHandler(...handlers: EventHandler[]) { diff --git a/packages/plugin-reanimated/src/exports/withSequence.ts b/packages/plugin-reanimated/src/exports/withSequence.ts index ed6db2a..0712d98 100644 --- a/packages/plugin-reanimated/src/exports/withSequence.ts +++ b/packages/plugin-reanimated/src/exports/withSequence.ts @@ -15,9 +15,7 @@ export function withSequence( const returnAnimation = animations[0]; if (!returnAnimation) { - throw new Error( - '[Reanimated] withSequence requires at least one animation.', - ); + throw new Error('[Reanimated] withSequence requires at least one animation.'); } return typeof returnAnimation === 'function' diff --git a/packages/plugin-reanimated/src/exports/withSpring.ts b/packages/plugin-reanimated/src/exports/withSpring.ts index 1f011c1..6b3ae8a 100644 --- a/packages/plugin-reanimated/src/exports/withSpring.ts +++ b/packages/plugin-reanimated/src/exports/withSpring.ts @@ -1,4 +1,5 @@ import type Animated from 'react-native-reanimated-original'; + import { AnimatedValue } from '../animation/AnimatedValue'; import { AnimationType } from '../types/AnimationType'; diff --git a/packages/plugin-reanimated/src/exports/withTiming.ts b/packages/plugin-reanimated/src/exports/withTiming.ts index 06f4686..5c016af 100644 --- a/packages/plugin-reanimated/src/exports/withTiming.ts +++ b/packages/plugin-reanimated/src/exports/withTiming.ts @@ -1,4 +1,5 @@ import type Animated from 'react-native-reanimated-original'; + import { AnimatedValue } from '../animation/AnimatedValue'; import { AnimationType } from '../types/AnimationType'; diff --git a/packages/plugin-reanimated/src/index.ts b/packages/plugin-reanimated/src/index.ts index 479239b..49aa0fd 100644 --- a/packages/plugin-reanimated/src/index.ts +++ b/packages/plugin-reanimated/src/index.ts @@ -12,8 +12,7 @@ export * from 'react-native-reanimated-original'; // Overrides export default { - createAnimatedComponent: - createAnimatedComponent as typeof createAnimatedComponent, + createAnimatedComponent: createAnimatedComponent as typeof createAnimatedComponent, addWhitelistedUIProps: Noop as () => null, addWhitelistedNativeProps: Noop as () => null, Image: Image as typeof Image, diff --git a/packages/plugin-reanimated/src/isAnimatedStyle.ts b/packages/plugin-reanimated/src/isAnimatedStyle.ts index da074cc..94bca3b 100644 --- a/packages/plugin-reanimated/src/isAnimatedStyle.ts +++ b/packages/plugin-reanimated/src/isAnimatedStyle.ts @@ -1,6 +1,6 @@ import type { AnimatedStyle } from './types/AnimatedStyle'; -// biome-ignore lint/suspicious/noExplicitAny: Valid use of any here +// oxlint-disable-next-line typescript/no-explicit-any -- Valid use of any here export function isAnimatedStyle(style: any): style is AnimatedStyle { return style != null && typeof style === 'object' && 'viewsRef' in style; } diff --git a/packages/plugin-reanimated/src/mergeRefs.ts b/packages/plugin-reanimated/src/mergeRefs.ts index 9849597..c74d409 100644 --- a/packages/plugin-reanimated/src/mergeRefs.ts +++ b/packages/plugin-reanimated/src/mergeRefs.ts @@ -6,9 +6,7 @@ * * */ -/** biome-ignore-all lint/suspicious/noExplicitAny: Valid use of any */ - -import * as React from 'react'; +/* oxlint-disable typescript/no-explicit-any -- Valid use of any */ export default function mergeRefs(...refs: any[]) { const _len = refs.length; @@ -39,10 +37,5 @@ export default function mergeRefs(...refs: any[]) { } export function useMergeRefs(...refs: any[]): (node: any) => void { - const _len = refs.length; - const args = Array.from({ length: _len }); - for (let _key = 0; _key < _len; _key++) { - args[_key] = refs[_key]; - } - return React.useMemo(() => mergeRefs(...args), [args]); + return mergeRefs(...refs); } diff --git a/packages/plugin-reanimated/src/types/ReanimatedAnimation.ts b/packages/plugin-reanimated/src/types/ReanimatedAnimation.ts index 6c161f6..959036c 100644 --- a/packages/plugin-reanimated/src/types/ReanimatedAnimation.ts +++ b/packages/plugin-reanimated/src/types/ReanimatedAnimation.ts @@ -4,7 +4,4 @@ import type { LayoutAnimationFunction, } from 'react-native-reanimated-original'; -export type ReanimatedAnimation = - | ILayoutAnimationBuilder - | LayoutAnimationFunction - | Keyframe; +export type ReanimatedAnimation = ILayoutAnimationBuilder | LayoutAnimationFunction | Keyframe; diff --git a/packages/plugin-reanimated/src/utils/getTransitionProperty.ts b/packages/plugin-reanimated/src/utils/getTransitionProperty.ts index 89e9eff..c734c2f 100644 --- a/packages/plugin-reanimated/src/utils/getTransitionProperty.ts +++ b/packages/plugin-reanimated/src/utils/getTransitionProperty.ts @@ -1,6 +1,7 @@ -import type { LightningElementStyle } from '@plextv/react-lightning'; import type { DefaultStyle } from 'react-native-reanimated/lib/typescript/hook/commonTypes'; +import type { LightningElementStyle } from '@plextv/react-lightning'; + export function getTransitionProperty( prop: K, ): keyof LightningElementStyle { diff --git a/packages/plugin-reanimated/src/utils/toLightningAnimationAndStyles.ts b/packages/plugin-reanimated/src/utils/toLightningAnimationAndStyles.ts index 13873cf..1cc998d 100644 --- a/packages/plugin-reanimated/src/utils/toLightningAnimationAndStyles.ts +++ b/packages/plugin-reanimated/src/utils/toLightningAnimationAndStyles.ts @@ -1,22 +1,16 @@ -import type { - Animatable, - LightningElementStyle, -} from '@plextv/react-lightning'; +import type { DefaultStyle } from 'react-native-reanimated/lib/typescript/hook/commonTypes'; + +import type { Animatable, LightningElementStyle } from '@plextv/react-lightning'; import { convertCSSTransformToLightning } from '@plextv/react-lightning-plugin-css-transform'; import type { Transform } from '@plextv/react-lightning-plugin-flexbox'; -import type { DefaultStyle } from 'react-native-reanimated/lib/typescript/hook/commonTypes'; + import { AnimatedValue } from '../animation/AnimatedValue'; import type { AnimatedObject } from '../types/AnimatedObject'; import { getTransitionProperty } from '../utils/getTransitionProperty'; -type AnimatableTransform = Record< - keyof Transform, - number | string | AnimatedValue ->; +type AnimatableTransform = Record; -type LightningTransition = NonNullable< - Animatable['transition'] ->; +type LightningTransition = NonNullable['transition']>; type DefaultStyleWithLightningTransform = Omit & { transform?: Transform; @@ -50,7 +44,7 @@ function applyTransform( case 'translateY': // Using our lightning style transform instead of RN style.transform = { - ...(style.transform ?? {}), + ...style.transform, ...convertCSSTransformToLightning(key, actualValue), }; @@ -95,18 +89,16 @@ function applyStyle( if (value instanceof AnimatedValue) { const transitionProp = getTransitionProperty(prop as keyof DefaultStyle); - // biome-ignore lint/suspicious/noExplicitAny: Just passing through + // oxlint-disable-next-line typescript/no-explicit-any -- Just passing through (style as any)[transitionProp] = value.value as T[K]; transition[transitionProp] = value.lngAnimation; } else { - // biome-ignore lint/suspicious/noExplicitAny: Just passing through + // oxlint-disable-next-line typescript/no-explicit-any -- Just passing through (style as any)[prop] = value; } } -export function toLightningAnimationAndStyles( - computedStyle: AnimatedObject, -): { +export function toLightningAnimationAndStyles(computedStyle: AnimatedObject): { transition: LightningTransition; style: DefaultStyleWithLightningTransform; } { diff --git a/packages/plugin-reanimated/tsdown.config.ts b/packages/plugin-reanimated/tsdown.config.ts index 9db0041..b09c784 100644 --- a/packages/plugin-reanimated/tsdown.config.ts +++ b/packages/plugin-reanimated/tsdown.config.ts @@ -1,6 +1,7 @@ -import baseConfig from '@repo/configs/tsdown.config'; import { defineConfig, type UserConfig } from 'tsdown'; +import baseConfig from '@repo/configs/tsdown.config'; + const config: UserConfig = defineConfig({ ...baseConfig, external: ['react-native-reanimated-original'], diff --git a/packages/plugin-reanimated/vite.config.ts b/packages/plugin-reanimated/vite.config.ts index 3700ab1..8582877 100644 --- a/packages/plugin-reanimated/vite.config.ts +++ b/packages/plugin-reanimated/vite.config.ts @@ -1,7 +1,8 @@ -import config from '@repo/configs/vite.config'; import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + export default defineConfig((env) => mergeConfig(config(env), { plugins: [ diff --git a/packages/react-lightning-components/src/exports/layout/Column.tsx b/packages/react-lightning-components/src/exports/layout/Column.tsx index 33c0e86..351ad5a 100644 --- a/packages/react-lightning-components/src/exports/layout/Column.tsx +++ b/packages/react-lightning-components/src/exports/layout/Column.tsx @@ -1,10 +1,11 @@ +import { type ForwardRefExoticComponent, forwardRef } from 'react'; + import { FocusGroup, type LightningViewElement, type LightningViewElementProps, type LightningViewElementStyle, } from '@plextv/react-lightning'; -import { type ForwardRefExoticComponent, forwardRef } from 'react'; export interface ColumnProps extends LightningViewElementProps { focusable?: boolean; @@ -50,9 +51,7 @@ const Column: ForwardRefExoticComponent = forwardRef< trapFocusRight={trapFocusRight} trapFocusDown={trapFocusDown} trapFocusLeft={trapFocusLeft} - style={(focused) => - focused ? finalStyle : { ...finalStyle, ...focusedStyle } - } + style={(focused) => (focused ? finalStyle : { ...finalStyle, ...focusedStyle })} /> ); } diff --git a/packages/react-lightning-components/src/exports/layout/Row.tsx b/packages/react-lightning-components/src/exports/layout/Row.tsx index d22b04f..03af9dc 100644 --- a/packages/react-lightning-components/src/exports/layout/Row.tsx +++ b/packages/react-lightning-components/src/exports/layout/Row.tsx @@ -1,10 +1,11 @@ +import { type ForwardRefExoticComponent, forwardRef } from 'react'; + import { FocusGroup, type LightningViewElement, type LightningViewElementProps, type LightningViewElementStyle, } from '@plextv/react-lightning'; -import { type ForwardRefExoticComponent, forwardRef } from 'react'; export interface RowProps extends LightningViewElementProps { focusable?: boolean; @@ -16,10 +17,7 @@ export interface RowProps extends LightningViewElementProps { trapFocusLeft?: boolean; } -const Row: ForwardRefExoticComponent = forwardRef< - LightningViewElement, - RowProps ->( +const Row: ForwardRefExoticComponent = forwardRef( ( { style, @@ -50,9 +48,7 @@ const Row: ForwardRefExoticComponent = forwardRef< trapFocusRight={trapFocusRight} trapFocusDown={trapFocusDown} trapFocusLeft={trapFocusLeft} - style={(focused) => - focused ? finalStyle : { ...finalStyle, ...focusedStyle } - } + style={(focused) => (focused ? finalStyle : { ...finalStyle, ...focusedStyle })} /> ); } diff --git a/packages/react-lightning-components/src/exports/lists/VirtualList.tsx b/packages/react-lightning-components/src/exports/lists/VirtualList.tsx index 8d6f301..6a06546 100644 --- a/packages/react-lightning-components/src/exports/lists/VirtualList.tsx +++ b/packages/react-lightning-components/src/exports/lists/VirtualList.tsx @@ -1,5 +1,6 @@ export type { ContentStyle, + OverrideItemLayout, OverrideItemLayoutFn, ScrollEvent, ViewabilityConfig, diff --git a/packages/react-lightning-components/src/exports/text/StyledText.tsx b/packages/react-lightning-components/src/exports/text/StyledText.tsx index 381654a..33a2ccf 100644 --- a/packages/react-lightning-components/src/exports/text/StyledText.tsx +++ b/packages/react-lightning-components/src/exports/text/StyledText.tsx @@ -1,11 +1,12 @@ +import type { FC, ReactNode } from 'react'; +import { useEffect, useRef, useState } from 'react'; + import type { LightningTextElementStyle, LightningViewElement, LightningViewElementProps, LightningViewElementStyle, } from '@plextv/react-lightning'; -import type React from 'react'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; type Part = { content?: string | Part[]; @@ -49,39 +50,25 @@ type TextSegmentProps = { handleTextureLoaded: () => void; }; -const TextSegment: React.FC = ({ - part, - tagStyles, - style, - handleTextureLoaded, -}) => { +const TextSegment: FC = ({ part, tagStyles, style, handleTextureLoaded }) => { if (typeof part.content === 'string') { const lastCharEmpty = part.content?.slice(-1) === ' '; return ( - + {`${part.content}${lastCharEmpty ? ' ' : ''}`} ); } if (Array.isArray(part.content)) { - return ( - <>{applyTags(part.content, tagStyles, style, handleTextureLoaded)} - ); + return <>{applyTags(part.content, tagStyles, style, handleTextureLoaded)}; } return null; }; -const parseText = ( - inputText: string, - values: { [key: string]: string }, -): Part[] => { +const parseText = (inputText: string, values: { [key: string]: string }): Part[] => { const parts: Part[] = []; const regex = /<(\w+)>(.*?)<\/\1>|{(\w+)}|([^<{]+)/g; // Match ..., {placeholder}, or plain text let match = regex.exec(inputText); @@ -125,7 +112,7 @@ const applyTags = ( tagStyles: Record, textStyle: LightningTextElementStyle, onLoad: () => void, -): React.ReactNode => { +): ReactNode => { return textParts.map((part: Part) => { // Determine if the part is a placeholder or a custom tag, and apply relevant styles let combinedStyle = { ...textStyle }; @@ -155,7 +142,7 @@ const applyTags = ( }); }; -const StyledText: React.FC = ({ +const StyledText: FC = ({ text, values = {}, tagStyles = {}, @@ -163,10 +150,18 @@ const StyledText: React.FC = ({ style = {}, }) => { const containerRef = useRef(null); + const [positionVersion, setPositionVersion] = useState(0); + + const handleTextureLoaded = () => { + setPositionVersion((v) => v + 1); + }; - const setPositions = useCallback(() => { + // oxlint-disable-next-line react-hooks/exhaustive-deps -- text/values/tagStyles force re-layout when content changes + useEffect(() => { const container = containerRef.current; - if (!container) return; + if (!container) { + return; + } let cumulativeWidth = 0; @@ -175,38 +170,21 @@ const StyledText: React.FC = ({ childNode.x = cumulativeWidth; cumulativeWidth += childNode.w || 0; } - }, []); - - const handleTextureLoaded = useCallback(() => { - setPositions(); - }, [setPositions]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: The extra dependencies are added to force re-render when those values change - useEffect(() => { - if (containerRef.current) { - setPositions(); - } - }, [text, values, tagStyles, setPositions]); + }, [text, values, tagStyles, positionVersion]); // Parse the text into parts (plain text, placeholders, and custom tags) - const parts = useMemo(() => parseText(text, values), [text, values]); - - const combinedTextStyle = useMemo( - () => ({ - ...BASE_STYLE, - ...textStyle, - }), - [textStyle], - ); - - const containerFinalStyle: LightningViewElementStyle = useMemo( - () => ({ - ...style, - // In case flexbox plugin is included, use row flow - flexDirection: 'row', - }), - [style], - ); + const parts = parseText(text, values); + + const combinedTextStyle = { + ...BASE_STYLE, + ...textStyle, + }; + + const containerFinalStyle: LightningViewElementStyle = { + ...style, + // In case flexbox plugin is included, use row flow + flexDirection: 'row', + }; return ( diff --git a/packages/react-lightning-components/src/exports/util/FPSMonitor.tsx b/packages/react-lightning-components/src/exports/util/FPSMonitor.tsx index fa3bdb3..e7de184 100644 --- a/packages/react-lightning-components/src/exports/util/FPSMonitor.tsx +++ b/packages/react-lightning-components/src/exports/util/FPSMonitor.tsx @@ -1,8 +1,6 @@ -import { - LightningRootContext, - type LightningTextElementStyle, -} from '@plextv/react-lightning'; -import { type FC, useCallback, useContext, useEffect, useState } from 'react'; +import { type FC, useContext, useEffect, useState } from 'react'; + +import { LightningRootContext, type LightningTextElementStyle } from '@plextv/react-lightning'; export interface FPSMonitorProps { prefix?: string; @@ -27,21 +25,19 @@ const FPSMonitor: FC = ({ const [fps, setFps] = useState(0); const [color, setColor] = useState(0); - const updateFps = useCallback( - (_: unknown, { fps: value }: { fps: number }) => { - setFps(value); + const updateFps = (_: unknown, { fps: value }: { fps: number }) => { + setFps(value); - if (value > mediumCutoff) { - setColor(highColor); - } else if (value > lowCutoff) { - setColor(mediumColor); - } else { - setColor(lowColor); - } - }, - [highColor, mediumColor, mediumCutoff, lowColor, lowCutoff], - ); + if (value > mediumCutoff) { + setColor(highColor); + } else if (value > lowCutoff) { + setColor(mediumColor); + } else { + setColor(lowColor); + } + }; + // oxlint-disable-next-line react-hooks/exhaustive-deps -- React Compiler auto-memoizes updateFps useEffect(() => { lngContext?.renderer.on('fpsUpdate', updateFps); diff --git a/packages/react-lightning-components/tsconfig.json b/packages/react-lightning-components/tsconfig.json index 9e82ff4..73fca59 100644 --- a/packages/react-lightning-components/tsconfig.json +++ b/packages/react-lightning-components/tsconfig.json @@ -1,11 +1,7 @@ { "extends": "@repo/configs/tsconfig.react-library.json", "compilerOptions": { - "types": [ - "node", - "@plextv/react-lightning-plugin-flexbox/jsx", - "vite/client" - ] + "types": ["node", "@plextv/react-lightning-plugin-flexbox/jsx", "vite/client"] }, "include": ["src", "../../types/*.d.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/react-lightning-components/vite.config.ts b/packages/react-lightning-components/vite.config.ts index 9cdbacc..e1d0a1a 100644 --- a/packages/react-lightning-components/vite.config.ts +++ b/packages/react-lightning-components/vite.config.ts @@ -1,8 +1,10 @@ import path from 'node:path'; -import config from '@repo/configs/vite.config'; + import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + export default defineConfig((env) => mergeConfig(config(env), { plugins: [externalizeDeps()], diff --git a/packages/react-lightning/package.json b/packages/react-lightning/package.json index 296dd0e..961872e 100644 --- a/packages/react-lightning/package.json +++ b/packages/react-lightning/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning", - "description": "React renderer for rendering React apps with Lightning.js", "version": "0.4.0", - "author": "Plex Inc.", + "description": "React renderer for rendering React apps with Lightning.js", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -21,7 +24,6 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -30,7 +32,8 @@ }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,9 +42,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "react-reconciler": "0.33.0", "tseep": "1.3.1" diff --git a/packages/react-lightning/src/components/Canvas/CanvasBridge.tsx b/packages/react-lightning/src/components/Canvas/CanvasBridge.tsx index a102560..9c0b37f 100644 --- a/packages/react-lightning/src/components/Canvas/CanvasBridge.tsx +++ b/packages/react-lightning/src/components/Canvas/CanvasBridge.tsx @@ -1,4 +1,5 @@ import { type FC, useEffect, useRef, useState } from 'react'; + import { createRoot, type LightningRoot } from '../../render'; import type { CanvasProps } from './CanvasProps'; diff --git a/packages/react-lightning/src/components/Canvas/CanvasProps.ts b/packages/react-lightning/src/components/Canvas/CanvasProps.ts index 9ceb8bd..ad778d3 100644 --- a/packages/react-lightning/src/components/Canvas/CanvasProps.ts +++ b/packages/react-lightning/src/components/Canvas/CanvasProps.ts @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; + import type { KeyMap } from '../../input/KeyMapContext'; import type { RenderOptions } from '../../render'; diff --git a/packages/react-lightning/src/components/Canvas/CanvasRoot.tsx b/packages/react-lightning/src/components/Canvas/CanvasRoot.tsx index e7bd651..ef3c05c 100644 --- a/packages/react-lightning/src/components/Canvas/CanvasRoot.tsx +++ b/packages/react-lightning/src/components/Canvas/CanvasRoot.tsx @@ -1,10 +1,12 @@ -import { type FC, useRef } from 'react'; +import { type FC, useContext, useEffect, useRef, useState } from 'react'; + import { FocusGroup } from '../../focus/FocusGroup'; import { FocusKeyManager } from '../../focus/FocusKeyManager'; import { FocusManager } from '../../focus/FocusManager'; import { FocusManagerContext } from '../../focus/FocusManagerContext'; import { KeyMapContext } from '../../input/KeyMapContext'; import { KeyPressHandler } from '../../input/KeyPressHandler'; +import { LightningRootContext } from '../../render'; import type { LightningElement } from '../../types'; import type { CanvasProps } from './CanvasProps'; @@ -12,19 +14,38 @@ type Props = Omit & { width?: number; height?: number }; export const CanvasRoot: FC = ({ width, height, children, keyMap }) => { const ref = useRef(null); - const focusManager = useRef>( - new FocusManager(), - ); - const focusKeyManager = useRef>( - new FocusKeyManager(focusManager.current), - ); + const [focusManager] = useState(() => new FocusManager()); + const [focusKeyManager] = useState(() => new FocusKeyManager(focusManager)); + const rootContext = useContext(LightningRootContext); + + useEffect(() => { + if (!import.meta.env.DEV || !rootContext) { + return; + } + + let hook = window.__LIGHTNINGJS_DEVTOOLS_HOOK__; + + if (!hook) { + hook = window.__LIGHTNINGJS_DEVTOOLS_HOOK__ = {}; + } + + hook.config = { + renderer: rootContext.renderer, + focusManager, + features: ['react-lightning'], + }; + + if (hook?.inject) { + hook.inject(); + } + }); return ( diff --git a/packages/react-lightning/src/components/Canvas/index.tsx b/packages/react-lightning/src/components/Canvas/index.tsx index 9aa092f..a3c74ff 100644 --- a/packages/react-lightning/src/components/Canvas/index.tsx +++ b/packages/react-lightning/src/components/Canvas/index.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react'; + import { CanvasBridge } from './CanvasBridge'; import type { CanvasProps } from './CanvasProps'; import { CanvasRoot } from './CanvasRoot'; @@ -6,11 +7,7 @@ import { CanvasRoot } from './CanvasRoot'; export const Canvas: FC = ({ options, ...props }) => { return ( - + ); }; diff --git a/packages/react-lightning/src/element/LightningImageElement.ts b/packages/react-lightning/src/element/LightningImageElement.ts index bc1488a..b67ed71 100644 --- a/packages/react-lightning/src/element/LightningImageElement.ts +++ b/packages/react-lightning/src/element/LightningImageElement.ts @@ -1,10 +1,6 @@ -import type { - INode, - INodeProps, - NodeLoadedPayload, - RendererMain, -} from '@lightningjs/renderer'; +import type { INode, INodeProps, NodeLoadedPayload, RendererMain } from '@lightningjs/renderer'; import type { Fiber } from 'react-reconciler'; + import type { Plugin } from '../render/Plugin'; import { type LightningElement, @@ -21,22 +17,20 @@ function getImageType(src?: string | null): INode['imageType'] { } const srcLower = src.toLowerCase(); - const isSvg = - srcLower.endsWith('.svg') || srcLower.startsWith('data:image/svg+xml,'); + const isSvg = srcLower.endsWith('.svg') || srcLower.startsWith('data:image/svg+xml,'); return isSvg ? 'svg' : null; } export class LightningImageElement< TStyleProps extends LightningImageElementStyle = LightningImageElementStyle, - TProps extends - LightningImageElementProps = LightningImageElementProps, + TProps extends LightningImageElementProps = LightningImageElementProps, > extends LightningViewElement { public override get type(): LightningElementType { return LightningElementType.Image; } - public declare node: RendererNode; + declare public node: RendererNode; public get isImageElement() { return true; diff --git a/packages/react-lightning/src/element/LightningTextElement.ts b/packages/react-lightning/src/element/LightningTextElement.ts index 426ef34..cd70289 100644 --- a/packages/react-lightning/src/element/LightningTextElement.ts +++ b/packages/react-lightning/src/element/LightningTextElement.ts @@ -1,4 +1,5 @@ import type { INodeProps } from '@lightningjs/renderer'; + import { LightningElementType, type LightningTextElementProps, @@ -16,7 +17,7 @@ export class LightningTextElement extends LightningViewElement< return LightningElementType.Text; } - public declare node: TextRendererNode; + declare public node: TextRendererNode; public override get isTextElement() { return true; diff --git a/packages/react-lightning/src/element/LightningViewElement.ts b/packages/react-lightning/src/element/LightningViewElement.ts index 841f6df..8244faf 100644 --- a/packages/react-lightning/src/element/LightningViewElement.ts +++ b/packages/react-lightning/src/element/LightningViewElement.ts @@ -14,6 +14,7 @@ import type { } from '@lightningjs/renderer'; import type { Fiber } from 'react-reconciler'; import { EventEmitter, type IEventEmitter } from 'tseep'; + import type { Plugin } from '../render/Plugin'; import { type Focusable, @@ -60,10 +61,7 @@ function __checkProps(props: string[]) { } } -function createTexture( - renderer: RendererMain, - textureDef: TextureDef, -): Texture { +function createTexture(renderer: RendererMain, textureDef: TextureDef): Texture { return renderer.createTexture(textureDef.type, textureDef.props); } @@ -71,10 +69,8 @@ let idCounter = 0; export class LightningViewElement< TStyleProps extends LightningViewElementStyle = LightningViewElementStyle, - TProps extends - LightningViewElementProps = LightningViewElementProps, -> implements Focusable -{ + TProps extends LightningViewElementProps = LightningViewElementProps, +> implements Focusable { public static allElements: Record = {}; public readonly id: number; @@ -98,12 +94,12 @@ export class LightningViewElement< private _focused = false; private _focusable = false; private _visible = true; + private _recycled = false; private _hasStagedUpdates = false; private _hasLayout = false; private _eventEmitter = new EventEmitter(); private _deferTarget: LightningElement | null = null; - private _deferNodeRemovalHandler: ((destroy: () => void) => void) | null = - null; + private _deferNodeRemovalHandler: ((destroy: () => void) => void) | null = null; public get visible(): boolean { return this._visible; @@ -113,6 +109,27 @@ export class LightningViewElement< return this._focusable && this.visible; } + /** + * Returns the raw focusable flag without visibility checks. + * Used by focus navigation when `allowOffscreen` is enabled to allow + * focusing elements that are clipped or not yet visible (e.g. in virtualized lists). + */ + public get focusableIntent(): boolean { + return this._focusable; + } + + /** + * Whether this element is a recycled cell from a virtualized list. + * Used by devtools to display a ♻ indicator in the focus graph and elements tree. + */ + public get recycled(): boolean { + return this._recycled; + } + + public set recycled(value: boolean) { + this._recycled = value; + } + public set focusable(value: boolean) { if (this._focusable === value) { return; @@ -153,11 +170,7 @@ export class LightningViewElement< } public set parent(parent) { - if ( - parent && - this._parent === parent && - this._parent.node === parent.node - ) { + if (parent && this._parent === parent && this._parent.node === parent.node) { return; } @@ -226,6 +239,7 @@ export class LightningViewElement< } public get rootElement(): LightningElement { + // oxlint-disable-next-line typescript/no-this-alias - Required for loop optimization let root: LightningElement = this; while (root.parent) { @@ -253,6 +267,12 @@ export class LightningViewElement< this._plugins = plugins ?? []; if (import.meta.env.DEV) { + if (!fiber._debugInfo) { + fiber._debugInfo = {}; + } + + fiber._debugInfo.isLngNode = true; + if (!__bannedPropsInitialized) { __getBannedProps(renderer.createNode({})); } @@ -271,10 +291,7 @@ export class LightningViewElement< const lngProps = this._toLightningNodeProps(this.props, true); - this._styleProxy = new Proxy( - this.props.style ?? {}, - this._styleProxyHandler, - ); + this._styleProxy = new Proxy(this.props.style ?? {}, this._styleProxyHandler); if (import.meta.env.DEV) { __checkProps(Object.keys(lngProps)); @@ -321,9 +338,7 @@ export class LightningViewElement< this._eventEmitter.emit('destroy'); } - public on = ( - ...args: Parameters['on']> - ): (() => void) => { + public on = (...args: Parameters['on']>): (() => void) => { this._eventEmitter.on(...args); return () => this._eventEmitter.off(...args); }; @@ -364,17 +379,12 @@ export class LightningViewElement< this.recalculateVisibility(); } - public insertChild( - child: LightningElement, - beforeChild?: LightningElement | null, - ): void { + public insertChild(child: LightningElement, beforeChild?: LightningElement | null): void { if (child.parent === this && child.parent.node === this.node) { return; } - const index = beforeChild - ? this.children.indexOf(beforeChild) - : this.children.length; + const index = beforeChild ? this.children.indexOf(beforeChild) : this.children.length; if (beforeChild) { this.children.splice(index, 0, child); @@ -433,6 +443,7 @@ export class LightningViewElement< let totalX = 0; let totalY = 0; + // oxlint-disable-next-line typescript/no-this-alias - Required for loop optimization let curr: LightningElement | null = this; while (curr && curr !== ancestor) { @@ -563,8 +574,7 @@ export class LightningViewElement< const prevFocusable = this.focusable; const prevVisible = this._visible; - this._visible = - this.node.alpha > 0 && (!this.parent || this.parent.visible); + this._visible = this.node.alpha > 0 && (!this.parent || this.parent.visible); if (this._visible !== prevVisible) { this._eventEmitter.emit('visibilityChanged', this._visible); @@ -594,9 +604,7 @@ export class LightningViewElement< ).start(); } - public animateShader( - props: Partial, - ): IAnimationController { + public animateShader(props: Partial): IAnimationController { return this._createAnimation( { shaderProps: props, @@ -606,7 +614,7 @@ export class LightningViewElement< } public toString(expanded?: boolean) { - return `${this.constructor.name} id=${this.id}${this.focusable ? ' focusable' : ''}${this.visible ? ' visible' : ''}${expanded ? ` props=${JSON.stringify(this.props)}` : ''}`; + return `${this._recycled ? '\u267B ' : ''}${this.constructor.name} id=${this.id}${this.focusable ? ' focusable' : ''}${this.visible ? ' visible' : ''}${expanded ? ` props=${JSON.stringify(this.props)}` : ''}`; } private _destroyFinalize = () => { @@ -616,10 +624,7 @@ export class LightningViewElement< }; // Don't pass down the `data` prop to the lightning node. - private _createNode({ - data: _data, - ...props - }: Partial): RendererNode { + private _createNode({ data: _data, ...props }: Partial): RendererNode { const node = this.isTextElement ? this._renderer.createTextNode(props) : this._renderer.createNode(props); @@ -649,6 +654,11 @@ export class LightningViewElement< this._stagedUpdates = {}; this._hasStagedUpdates = false; + // Fast path: style-only updates where no plugin handles the changed properties + if (this._canFastPathStyle(payload)) { + return this._applyStyleFastPath(payload); + } + const transformedProps = this._transformProps(payload) ?? ({} as TProps); const previousOpacity = this.node.alpha; @@ -671,26 +681,147 @@ export class LightningViewElement< Object.assign(this.rawProps, payload); } + // Prevent non-numeric width and height values from being set on the + // lightning node, as this can cause it to disappear. Instead, we'll set + // them on the style proxy so they can be applied once they're valid. This + // allows for things like setting width/height to '100%' in JSX without + // breaking the node, even though it's not a valid value for the lightning + // node itself. Once the layout system calculates the actual pixel values, + // it will update the lightning node and remove the string values from the + // style proxy. + if (typeof lngProps.w !== 'number') { + delete lngProps.w; + } + + if (typeof lngProps.h !== 'number') { + delete lngProps.h; + } + Object.assign(this.node, lngProps); - // biome-ignore lint/suspicious/noExplicitAny: Required for accessing AllStyleProps symbol + // oxlint-disable-next-line typescript/no-explicit-any -- Required for accessing AllStyleProps symbol Object.assign((this.style as any)[AllStyleProps], this.props.style); if (previousOpacity !== this.node.alpha) { this.recalculateVisibility(); } - // Check for style changes before computing Object.keys() - const hasStyleChanges = - payload.style && Object.keys(payload.style).length > 0; + // Check for style changes without allocating an array + let hasStyleChanges = false; + if (payload.style) { + for (const _ in payload.style) { + hasStyleChanges = true; + break; + } + } if (hasStyleChanges) { - this._eventEmitter.emit( - 'stylesChanged', - this.props.style as Partial, - ); + this._eventEmitter.emit('stylesChanged', this.props.style as Partial); + } + + this._isUpdateQueued = false; + + return changed; + } + + /** Style properties that may trigger shader creation — must use the slow path. */ + private static readonly _shaderStyleProps = new Set([ + 'borderRadius', + 'borderTop', + 'borderLeft', + 'borderRight', + 'borderBottom', + ]); + + /** + * Checks whether a staged update can skip the plugin transform pipeline. + * Returns true when the payload only contains style properties that no + * plugin declares as handled and that don't require shader creation. + */ + private _canFastPathStyle(payload: Partial): boolean { + if (!('style' in payload) || !payload.style) { + return false; + } + + for (const key in payload) { + if (key !== 'style') { + return false; + } + } + + const style = payload.style; + + for (const key in style) { + if (LightningViewElement._shaderStyleProps.has(key)) { + return false; + } + + for (const plugin of this._plugins) { + if (!plugin.transformProps) { + continue; + } + + const handled = plugin.handledStyleProps; + + if (!handled || handled.has(key)) { + return false; + } + } + } + + return true; + } + + /** + * Applies a style-only update directly to the Lightning node, bypassing + * the plugin transform pipeline and full props-merge iteration. + */ + private _applyStyleFastPath(payload: Partial): boolean { + const style = payload.style as Partial; + const previousOpacity = this.node.alpha; + let changed = false; + + if (this.props.style !== style) { + // oxlint-disable-next-line typescript/no-explicit-any -- matching slow-path behaviour + (this.props as any).style = style; + changed = true; + } + + if (import.meta.env.DEV) { + Object.assign(this.rawProps, payload); + } + + const transition = this.props.transition; + + for (const key in style) { + const typedKey = key as string & keyof TStyleProps; + const value = style[typedKey]; + + if (value == null) { + continue; + } + + if ((key === 'w' || key === 'h') && typeof value !== 'number') { + continue; + } + + if (transition?.[typedKey]) { + this.animateStyle(typedKey, value as TStyleProps[typeof typedKey]); + } else { + // oxlint-disable-next-line typescript/no-explicit-any -- direct node property assignment + (this.node as any)[key] = value; + } } + // oxlint-disable-next-line typescript/no-explicit-any -- Required for accessing AllStyleProps symbol + Object.assign((this.style as any)[AllStyleProps], style); + + if (previousOpacity !== this.node.alpha) { + this.recalculateVisibility(); + } + + this._eventEmitter.emit('stylesChanged', this.props.style as Partial); + this._isUpdateQueued = false; return changed; @@ -736,9 +867,7 @@ export class LightningViewElement< return animation; } - private _getShaderFromStyle( - style: TStyleProps | undefined | null, - ): ShaderDef | undefined { + private _getShaderFromStyle(style: TStyleProps | undefined | null): ShaderDef | undefined { if (!style) { return; } @@ -747,15 +876,8 @@ export class LightningViewElement< let type: ShaderDef['type'] | undefined; let hasRounded = false; - const { - border, - borderColor, - borderTop, - borderLeft, - borderRight, - borderBottom, - borderRadius, - } = style; + const { border, borderColor, borderTop, borderLeft, borderRight, borderBottom, borderRadius } = + style; if (borderRadius) { type = 'Rounded'; @@ -763,14 +885,7 @@ export class LightningViewElement< hasRounded = true; } - if ( - border || - borderColor || - borderTop || - borderLeft || - borderRight || - borderBottom - ) { + if (border || borderColor || borderTop || borderLeft || borderRight || borderBottom) { if (type && type === 'Rounded') { type = 'RoundedWithBorder'; } else { @@ -807,7 +922,7 @@ export class LightningViewElement< } public _toLightningNodeProps( - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO props: TProps & Record, initial = false, ): Partial { @@ -827,10 +942,12 @@ export class LightningViewElement< const finalStyle: Partial = {}; if (style !== undefined && style !== null) { - // Optimize by using for...in loop instead of Object.entries() + const transition = !initial ? this.props.transition : undefined; + const isText = this.isTextElement; + for (const key in style) { // Lightning doesn't allow setting w/h on text nodes - if (this.isTextElement && (key === 'w' || key === 'h')) { + if (isText && (key === 'w' || key === 'h')) { continue; } @@ -840,7 +957,7 @@ export class LightningViewElement< continue; } - if (!initial && this.props.transition?.[key as keyof TStyleProps]) { + if (transition?.[key as keyof TStyleProps]) { this.animateStyle(key as keyof TStyleProps, value); } else if (initial && key === 'initialDimensions') { const rect = value as NonNullable; @@ -858,7 +975,7 @@ export class LightningViewElement< finalStyle.y = rect.y; } } else { - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO (finalStyle as any)[key] = value; } } @@ -886,10 +1003,7 @@ export class LightningViewElement< this._shaderDef.props ) { this.animateShader(this._shaderDef.props); - } else if ( - this._shaderDef.type === oldShader?.type && - this.shader.props - ) { + } else if (this._shaderDef.type === oldShader?.type && this.shader.props) { for (const [key, value] of Object.entries(this._shaderDef.props)) { if (this.shader.props[key]) { this.shader.props[key] = value; @@ -910,11 +1024,7 @@ export class LightningViewElement< const finalProps = Object.assign(otherProps, finalStyle); - if ( - initial === true && - this.isImageElement === false && - finalProps.color === undefined - ) { + if (initial === true && this.isImageElement === false && finalProps.color === undefined) { // set default color to 0 for all elements except image elements finalProps.color = 0; } diff --git a/packages/react-lightning/src/element/createLightningElement.ts b/packages/react-lightning/src/element/createLightningElement.ts index 682f6a2..02eff00 100644 --- a/packages/react-lightning/src/element/createLightningElement.ts +++ b/packages/react-lightning/src/element/createLightningElement.ts @@ -1,5 +1,6 @@ import type { RendererMain } from '@lightningjs/renderer'; import type { Fiber } from 'react-reconciler'; + import type { Plugin } from '../render/Plugin'; import { type LightningElement, diff --git a/packages/react-lightning/src/focus/FocusGroup.tsx b/packages/react-lightning/src/focus/FocusGroup.tsx index e8696a6..645e502 100644 --- a/packages/react-lightning/src/focus/FocusGroup.tsx +++ b/packages/react-lightning/src/focus/FocusGroup.tsx @@ -1,25 +1,13 @@ -import { - type ForwardRefExoticComponent, - forwardRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { type ForwardRefExoticComponent, forwardRef, useEffect, useRef, useState } from 'react'; + import { useCombinedRef } from '../hooks/useCombinedRef'; -import type { - KeyEvent, - LightningElement, - LightningViewElementProps, -} from '../types'; +import type { KeyEvent, LightningElement, LightningViewElementProps } from '../types'; import { FocusGroupContext } from './FocusGroupContext'; import { useFocus } from './useFocus'; import { useFocusKeyManager } from './useFocusKeyManager'; import { useFocusManager } from './useFocusManager'; -export interface FocusGroupProps - extends Omit { +export interface FocusGroupProps extends Omit { autoFocus?: boolean; disable?: boolean; focusRedirect?: boolean; @@ -28,105 +16,100 @@ export interface FocusGroupProps trapFocusRight?: boolean; trapFocusDown?: boolean; trapFocusLeft?: boolean; + /** When true, focus navigation can target non-visible children (e.g. clipped items in a virtualized list). Defaults to false. */ + allowOffscreen?: boolean; style?: | LightningViewElementProps['style'] | ((focused: boolean) => LightningViewElementProps['style']); onChildFocused?: (child: LightningElement) => void; } -export const FocusGroup: ForwardRefExoticComponent = - forwardRef( - ( - { - autoFocus = false, - disable, - focusRedirect, - destinations, - trapFocusUp, - trapFocusRight, - trapFocusDown, - trapFocusLeft, - style, - onKeyDown, - onChildFocused, - ...otherProps - }, - ref, - ) => { - const focusManager = useFocusManager(); - const focusKeyManager = useFocusKeyManager(); - const { ref: focusRef, focused } = useFocus({ - autoFocus, - active: !disable, - focusRedirect, - destinations, - onChildFocused, - }); - const [viewElement, setViewElement] = useState( - null, - ); - const viewRef = useRef(null); - const combinedRef = useCombinedRef(ref, focusRef, viewRef); +export const FocusGroup: ForwardRefExoticComponent = forwardRef< + LightningElement, + FocusGroupProps +>( + ( + { + autoFocus = false, + disable, + focusRedirect, + destinations, + trapFocusUp, + trapFocusRight, + trapFocusDown, + trapFocusLeft, + allowOffscreen, + style, + onKeyDown, + onChildFocused, + ...otherProps + }, + ref, + ) => { + const focusManager = useFocusManager(); + const focusKeyManager = useFocusKeyManager(); + const { ref: focusRef, focused } = useFocus({ + autoFocus, + active: !disable, + focusRedirect, + destinations, + onChildFocused, + allowOffscreen, + }); + const [viewElement, setViewElement] = useState(null); + const viewRef = useRef(null); + const combinedRef = useCombinedRef(ref, focusRef, viewRef); + + const traps = { + up: trapFocusUp ?? false, + right: trapFocusRight ?? false, + down: trapFocusDown ?? false, + left: trapFocusLeft ?? false, + }; - const traps = useMemo( - () => ({ - up: trapFocusUp ?? false, - right: trapFocusRight ?? false, - down: trapFocusDown ?? false, - left: trapFocusLeft ?? false, - }), - [trapFocusUp, trapFocusRight, trapFocusDown, trapFocusLeft], - ); + const handleFocusKeyDown = (event: KeyEvent) => { + if (!viewRef.current) { + return onKeyDown?.(event); + } - const handleFocusKeyDown = useCallback( - (event: KeyEvent) => { - if (!viewRef.current) { - return onKeyDown?.(event); - } + const result = focusKeyManager.handleKeyDown(viewRef.current, event); - const result = focusKeyManager.handleKeyDown(viewRef.current, event); + return result === false ? false : onKeyDown?.(event); + }; - return result === false ? false : onKeyDown?.(event); - }, - [focusKeyManager, onKeyDown], - ); + const finalStyle = typeof style === 'function' ? style(focused) : style; - const finalStyle = useMemo( - () => (typeof style === 'function' ? style(focused) : style), - [style, focused], - ); + useEffect(() => { + if (viewElement) { + focusManager.setTraps(viewElement, traps); + } + }, [focusManager, viewElement, traps]); - useEffect(() => { - if (viewElement) { - focusManager.setTraps(viewElement, traps); - } - }, [focusManager, viewElement, traps]); + useEffect(() => { + if (viewRef.current) { + viewRef.current.isFocusGroup = true; + setViewElement(viewRef.current); + } - useEffect(() => { + return () => { if (viewRef.current) { - viewRef.current.isFocusGroup = true; - setViewElement(viewRef.current); + viewRef.current.isFocusGroup = false; + setViewElement(null); } + }; + }, []); - return () => { - if (viewRef.current) { - viewRef.current.isFocusGroup = false; - setViewElement(null); - } - }; - }, []); - - return ( - - - - ); - }, - ); + return ( + + + + ); + }, +); FocusGroup.displayName = 'FocusGroup'; diff --git a/packages/react-lightning/src/focus/FocusGroupContext.tsx b/packages/react-lightning/src/focus/FocusGroupContext.tsx index 6d3f798..4ab26c6 100644 --- a/packages/react-lightning/src/focus/FocusGroupContext.tsx +++ b/packages/react-lightning/src/focus/FocusGroupContext.tsx @@ -1,4 +1,5 @@ import { type Context, createContext } from 'react'; + import type { LightningElement } from '../types'; export const FocusGroupContext: Context = diff --git a/packages/react-lightning/src/focus/FocusKeyManager.ts b/packages/react-lightning/src/focus/FocusKeyManager.ts index be8fc83..97f3158 100644 --- a/packages/react-lightning/src/focus/FocusKeyManager.ts +++ b/packages/react-lightning/src/focus/FocusKeyManager.ts @@ -2,7 +2,15 @@ import { Keys } from '../input/Keys'; import type { KeyEvent, LightningElement } from '../types'; import { findClosestElement } from '../utils/findClosestElement'; import { Direction } from './Direction'; -import type { FocusManager } from './FocusManager'; +import type { FocusManager, FocusNode } from './FocusManager'; + +/** Lazily maps FocusNode children to their elements without allocating an array */ +function* childElements(children: FocusNode[]): Iterable { + for (let i = 0; i < children.length; i++) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- bounds-checked loop + yield children[i]!.element; + } +} export class FocusKeyManager { private _focusManager: FocusManager; @@ -52,11 +60,7 @@ export class FocusKeyManager { // Returns false if focus works, to stop the propagation of the key event. // If there's nothing to navigate to, return true and let the event bubble // up to be handled by the next focus group. - private _tryFocusNext = ( - element: T, - event: KeyEvent, - direction: Direction, - ): boolean => { + private _tryFocusNext = (element: T, event: KeyEvent, direction: Direction): boolean => { const focusNode = this._focusManager.getFocusNode(element); if (!focusNode) { @@ -69,9 +73,10 @@ export class FocusKeyManager { const closestElement = findClosestElement( focusNode.focusedElement.element, - focusNode.children.map((child) => child.element), + childElements(focusNode.children), focusNode.parent.element, direction, + focusNode.allowOffscreen, ); if (closestElement) { diff --git a/packages/react-lightning/src/focus/FocusManager.spec.ts b/packages/react-lightning/src/focus/FocusManager.spec.ts index 20e0772..486c556 100644 --- a/packages/react-lightning/src/focus/FocusManager.spec.ts +++ b/packages/react-lightning/src/focus/FocusManager.spec.ts @@ -1,8 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockElement, - type MockElement, -} from '../mocks/createMockElement'; + +import { createMockElement, type MockElement } from '../mocks/createMockElement'; import { FocusManager } from './FocusManager'; describe('FocusManager', () => { @@ -287,12 +285,7 @@ describe('FocusManager', () => { focusManager.addElement(modalGrandChild, modalChild); focusManager.addElement(modalChild, modal); - expect(focusManager.focusPath).toEqual([ - parent, - modal, - modalChild, - modalGrandChild, - ]); + expect(focusManager.focusPath).toEqual([parent, modal, modalChild, modalGrandChild]); }); describe('Layer Management (Modal Support)', () => { diff --git a/packages/react-lightning/src/focus/FocusManager.ts b/packages/react-lightning/src/focus/FocusManager.ts index f0b388c..797efad 100644 --- a/packages/react-lightning/src/focus/FocusManager.ts +++ b/packages/react-lightning/src/focus/FocusManager.ts @@ -1,4 +1,5 @@ import { EventEmitter, type IEventEmitter } from 'tseep'; + import type { Focusable } from '../types'; import type { EventNotifier } from '../types/EventNotifier'; import type { Traps } from './Traps'; @@ -20,6 +21,8 @@ export type FocusNode = Omit, 'element'> & { destinations: (T | null)[] | null; traps: Traps; hasFocusableChildren: boolean; + /** When true, focus navigation can target non-visible children (e.g. clipped items in a virtualized list). */ + allowOffscreen: boolean; }; type FocusLayer = { @@ -40,9 +43,7 @@ function isRootNode(node: FocusNode | RootNode): node is RootNode { return !('parent' in node) && node.element === null; } -function hasExternalRedirect( - node: FocusNode, -): boolean { +function hasExternalRedirect(node: FocusNode): boolean { if (!node.focusRedirect || !node.destinations) { return false; } @@ -67,11 +68,9 @@ export class FocusManager< parent?: T | null; isFocusGroup?: boolean; }, -> implements EventNotifier> -{ +> implements EventNotifier> { private _disposers: Map void)[]> = new Map(); - private _childFocusEventHandlers: Map void) | undefined> = - new Map(); + private _childFocusEventHandlers: Map void) | undefined> = new Map(); private _focusStack: FocusLayer[] = []; private _eventEmitter = new EventEmitter>(); @@ -102,18 +101,15 @@ export class FocusManager< ]; } - public on = ( - ...args: Parameters>['on']> - ): (() => void) => { + public on = (...args: Parameters>['on']>): (() => void) => { this._eventEmitter.on(...args); return () => this._eventEmitter.off(...args); }; - public off: EventEmitter>['off'] = this._eventEmitter.off.bind( + public off: EventEmitter>['off'] = this._eventEmitter.off.bind(this._eventEmitter); + public emit: EventEmitter>['emit'] = this._eventEmitter.emit.bind( this._eventEmitter, ); - public emit: EventEmitter>['emit'] = - this._eventEmitter.emit.bind(this._eventEmitter); public getFocusNode(element: T): FocusNode | null { const node = this.activeLayer.elements.get(element); @@ -133,11 +129,13 @@ export class FocusManager< focusRedirect?: boolean; destinations?: (T | null)[] | null; traps?: Traps; + allowOffscreen?: boolean; }, ): void { const autoFocus = options?.autoFocus ?? false; const focusRedirect = options?.focusRedirect ?? false; const destinations = options?.destinations ?? null; + const allowOffscreen = options?.allowOffscreen ?? false; const traps = options?.traps ?? { up: false, right: false, @@ -156,18 +154,12 @@ export class FocusManager< // their hooks are run before getting attached to the tree. This causes // the component to get added to the old layer before the new layer's // created. - const parentNodeInPreviousLayer: - | RootNode - | FocusNode - | undefined = this._focusStack.at(-2)?.elements?.get?.(parent); - - if ( - parentNodeInPreviousLayer && - !isRootNode(parentNodeInPreviousLayer) - ) { - parentNode = this._addMissingParentsToCurrentLayer( - parentNodeInPreviousLayer, - ); + const parentNodeInPreviousLayer: RootNode | FocusNode | undefined = this._focusStack + .at(-2) + ?.elements?.get?.(parent); + + if (parentNodeInPreviousLayer && !isRootNode(parentNodeInPreviousLayer)) { + parentNode = this._addMissingParentsToCurrentLayer(parentNodeInPreviousLayer); } if (!parentNode) { @@ -191,16 +183,14 @@ export class FocusManager< childNode.focusRedirect = focusRedirect; childNode.destinations = destinations; childNode.traps = traps; + childNode.allowOffscreen = allowOffscreen; // If the child node already exists, we need to remove it from its current parent if (childNode.parent !== parentNode) { const index = childNode.parent.children.indexOf(childNode); if (childNode.parent.focusedElement === childNode) { - childNode.parent.focusedElement = this._findNextBestFocus( - childNode.parent, - childNode, - ); + childNode.parent.focusedElement = this._findNextBestFocus(childNode.parent, childNode); } if (index !== -1) { @@ -225,6 +215,7 @@ export class FocusManager< focusRedirect, destinations, traps, + allowOffscreen, ); } @@ -237,8 +228,7 @@ export class FocusManager< if ( child.focusable && !hasExternalRedirect(childNode) && - (!parentNode.focusedElement || - (!parentNode.focusedElement.autoFocus && autoFocus)) + (!parentNode.focusedElement || (!parentNode.focusedElement.autoFocus && autoFocus)) ) { parentNode.focusedElement = childNode; } @@ -246,13 +236,10 @@ export class FocusManager< this._recalculateFocusPath(); } - private _forAllNodes( - element: T, - callback: (node: FocusNode) => void, - ): void { + private _forAllNodes(element: T, callback: (node: FocusNode) => void): void { for (let i = this._focusStack.length - 1; i >= 0; i--) { const layer = this._focusStack[i]; - // biome-ignore lint/style/noNonNullAssertion: Already asserted layer exists + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already asserted layer exists const node = layer!.elements.get(element); if (node) { @@ -280,12 +267,9 @@ export class FocusManager< } public setFocusRedirect(element: T, focusRedirect?: boolean): void { - // Only apply redirect to the active layer, as redirects are likely to be layer-specific - const node = this.activeLayer.elements.get(element); - - if (node) { + this._forAllNodes(element, (node) => { node.focusRedirect = !!focusRedirect; - } + }); } public setDestinations(element: T, destinations?: (T | null)[]): void { @@ -294,10 +278,13 @@ export class FocusManager< }); } - public setOnChildFocused( - element: T, - onChildFocused?: (child: T) => void, - ): void { + public setAllowOffscreen(element: T, allowOffscreen?: boolean): void { + this._forAllNodes(element, (node) => { + node.allowOffscreen = !!allowOffscreen; + }); + } + + public setOnChildFocused(element: T, onChildFocused?: (child: T) => void): void { if (onChildFocused) { this._childFocusEventHandlers.set(element, onChildFocused); } else { @@ -309,8 +296,10 @@ export class FocusManager< // Store the current layer before creating new one const previousLayer = this.activeLayer; - // Blur all currently focused elements (they belong to the previous layer) - for (const element of previousLayer.focusPath) { + // Blur in reverse order (leaf-first) so children clean up before parents + for (let i = previousLayer.focusPath.length - 1; i >= 0; i--) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- bounds-checked loop + const element = previousLayer.focusPath[i]!; if (element.focused) { element.blur(); this._eventEmitter.emit('blurred', element); @@ -345,15 +334,17 @@ export class FocusManager< // Get current layer info before popping const currentLayer = this.activeLayer; - // Blur all elements in current layer - for (const element of currentLayer.focusPath) { + // Blur in reverse order (leaf-first) so children clean up before parents + for (let i = currentLayer.focusPath.length - 1; i >= 0; i--) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- bounds-checked loop + const element = currentLayer.focusPath[i]!; if (element.focused) { element.blur(); this._eventEmitter.emit('blurred', element); } } - // biome-ignore lint/style/noNonNullAssertion: Already checked above + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already checked above this._focusStack.pop()!; this._eventEmitter.emit('layerRemoved'); @@ -425,9 +416,7 @@ export class FocusManager< } } - return path - .map((id) => (id === node.element.id.toString() ? `[${id}]` : id)) - .join(' > '); + return path.map((id) => (id === node.element.id.toString() ? `[${id}]` : id)).join(' > '); } private _addMissingParentsToCurrentLayer(nodeFromAnotherLayer: FocusNode) { @@ -444,7 +433,7 @@ export class FocusManager< let prevNode: FocusNode | null = null; while (stack.length > 0) { - // biome-ignore lint/style/noNonNullAssertion: Already asserted stack is not empty + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already asserted stack is not empty const nodeToCreate = stack.pop()!; const parentNode = prevNode === null ? this.activeLayer.root : prevNode; const newNode = this._createFocusNode(nodeToCreate.element, parentNode); @@ -464,6 +453,7 @@ export class FocusManager< focusRedirect = false, destinations: (T | null)[] | null = null, traps: Traps = { up: false, right: false, down: false, left: false }, + allowOffscreen = false, ) { const node: FocusNode = { element, @@ -475,6 +465,7 @@ export class FocusManager< destinations, traps, hasFocusableChildren: false, + allowOffscreen, }; this.activeLayer.elements.set(element, node); @@ -491,21 +482,31 @@ export class FocusManager< this._disposers.set(element, [ element.on('focusableChanged', (_, isFocusable) => { - if (!node.parent.focusedElement) { - node.parent.focusedElement = this._findNextBestFocus(node.parent); - } else if (!isFocusable && node.parent.focusedElement === node) { - node.parent.focusedElement = this._findNextBestFocus( - node.parent, - node, + // Look up the current node to avoid stale closure references + // after re-parenting + const currentNode = this.activeLayer.elements.get(element); + if (!currentNode) { + return; + } + + if (!currentNode.parent.focusedElement) { + currentNode.parent.focusedElement = this._findNextBestFocus(currentNode.parent); + } else if (!isFocusable && currentNode.parent.focusedElement === currentNode) { + currentNode.parent.focusedElement = this._findNextBestFocus( + currentNode.parent, + currentNode, ); } - this._checkFocusableChildren(node.parent); + this._checkFocusableChildren(currentNode.parent); this._recalculateFocusPath(); }), element.on('focusChanged', (_, isFocused) => { if (isFocused && !element.focused) { + const currentNode = this.activeLayer.elements.get(element); this.focus(element); - this._tryEmitChildFocusedEvent(node); + if (currentNode) { + this._tryEmitChildFocusedEvent(currentNode); + } } }), ]); @@ -524,42 +525,44 @@ export class FocusManager< } } - private _focusNode(childNode: FocusNode) { + private _focusNode(childNode: FocusNode, visitedRedirects?: Set) { let currParent = childNode.parent; let currChild: FocusNode | RootNode = childNode; const elements = this.activeLayer.elements; if (currChild.children.length && !currChild.focusedElement) { - this._findNextBestFocus(currChild); + currChild.focusedElement = this._findNextBestFocus(currChild); } while (currChild && !isRootNode(currChild) && currParent) { if (currChild.focusRedirect && currChild.destinations) { // TODO: Probably something smarter here to decide which destination to focus - const destination = currChild.destinations?.find( - (child) => child?.focusable, - ); + const destination = currChild.destinations?.find((child) => child?.focusable); if (destination) { const focusNode = elements.get(destination); if (!focusNode) { - console.warn( - 'FocusManager: No focus node found for destination', - destination, - ); + console.warn('FocusManager: No focus node found for destination', destination); + return; + } + + // Detect redirect cycles + const visited = visitedRedirects ?? new Set(); + if (visited.has(destination)) { + console.warn('FocusManager: Focus redirect cycle detected, aborting'); return; } + visited.add(destination); - this._focusNode(focusNode); + this._focusNode(focusNode, visited); return; } } currParent.focusedElement = currChild as FocusNode; currChild = currParent; - currParent = - 'parent' in currChild ? currChild.parent : this.activeLayer.root; + currParent = 'parent' in currChild ? currChild.parent : this.activeLayer.root; } this._recalculateFocusPath(); @@ -571,9 +574,7 @@ export class FocusManager< return; } - const onChildFocused = this._childFocusEventHandlers.get( - node.parent.element, - ); + const onChildFocused = this._childFocusEventHandlers.get(node.parent.element); if (onChildFocused) { onChildFocused(node.element); @@ -622,7 +623,7 @@ export class FocusManager< let hasFocusableChildren = false; for (let i = 0; i < childrenLength; i++) { - // biome-ignore lint/style/noNonNullAssertion: Already asserted that child exists + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already asserted that child exists const child = children[i]!; if (child.element.focusable) { @@ -643,27 +644,20 @@ export class FocusManager< // Check each child for leaf node ancestry and update focusability for (let i = 0; i < childrenLength; i++) { - // biome-ignore lint/style/noNonNullAssertion: Already asserted that child exists + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already asserted that child exists const child = children[i]!; if (this._hasLeafParent(child.element, leafNodes, parentNode.element)) { child.element.focusable = false; if (parentNode.focusedElement === child) { - parentNode.focusedElement = this._findNextBestFocus( - parentNode, - child, - ); + parentNode.focusedElement = this._findNextBestFocus(parentNode, child); } } } } - private _hasLeafParent( - element: T, - leafNodes: Set, - parentNode: T | null, - ): boolean { + private _hasLeafParent(element: T, leafNodes: Set, parentNode: T | null): boolean { let curr: T | null = element.parent as T | null; while (curr && curr !== parentNode) { @@ -715,35 +709,53 @@ export class FocusManager< } private _recalculateFocusPath(): void { - const newPath: T[] = []; const layer = this.activeLayer; + const oldPath = layer.focusPath; + + // Quick check: walk the focused chain and compare against old path. + // If every element matches and lengths are equal, nothing changed. let curr: FocusNode | null = layer.root.focusedElement; + let newLength = 0; let divergenceIndex = 0; + let pathMatches = true; while (curr) { - newPath.push(curr.element); - - if (newPath[divergenceIndex] === layer.focusPath[divergenceIndex]) { - divergenceIndex++; + if (pathMatches && oldPath[newLength] === curr.element) { + divergenceIndex = newLength + 1; + } else { + pathMatches = false; } - + newLength++; curr = curr.focusedElement; } - // Only process elements that actually changed - let changed = false; + // If entire path matches and same length, nothing to do + if (pathMatches && newLength === oldPath.length) { + return; + } - for (let i = layer.focusPath.length - 1; i >= divergenceIndex; i--) { - const removedFocus = layer.focusPath[i]; + // Build new path only when we know it changed + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated array filled in the loop below + const newPath: T[] = new Array(newLength); + curr = layer.root.focusedElement; + for (let i = 0; i < newLength; i++) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- curr is non-null for newLength iterations + newPath[i] = curr!.element; + // oxlint-disable-next-line typescript/no-non-null-assertion -- curr is non-null for newLength iterations + curr = curr!.focusedElement; + } + + // Blur removed elements (leaf-first) + for (let i = oldPath.length - 1; i >= divergenceIndex; i--) { + const removedFocus = oldPath[i]; if (removedFocus?.focused) { removedFocus.blur(); this._eventEmitter.emit('blurred', removedFocus); } - - changed = true; } + // Focus newly added elements (root-first) for (let i = divergenceIndex; i < newPath.length; i++) { const addedFocus = newPath[i]; @@ -751,13 +763,9 @@ export class FocusManager< addedFocus.focus(); this._eventEmitter.emit('focused', addedFocus); } - - changed = true; } - if (changed) { - layer.focusPath = newPath; - this._eventEmitter.emit('focusPathChanged', newPath); - } + layer.focusPath = newPath; + this._eventEmitter.emit('focusPathChanged', newPath); } } diff --git a/packages/react-lightning/src/focus/FocusManagerContext.tsx b/packages/react-lightning/src/focus/FocusManagerContext.tsx index 5ae2237..61f92a1 100644 --- a/packages/react-lightning/src/focus/FocusManagerContext.tsx +++ b/packages/react-lightning/src/focus/FocusManagerContext.tsx @@ -1,4 +1,5 @@ import { type Context, createContext } from 'react'; + import type { LightningElement } from '../types'; import type { FocusKeyManager } from './FocusKeyManager'; import type { FocusManager } from './FocusManager'; @@ -8,5 +9,4 @@ type ContextType = { focusKeyManager: FocusKeyManager; } | null; -export const FocusManagerContext: Context = - createContext(null); +export const FocusManagerContext: Context = createContext(null); diff --git a/packages/react-lightning/src/focus/focusable.tsx b/packages/react-lightning/src/focus/focusable.tsx index bef389b..77c2c1a 100644 --- a/packages/react-lightning/src/focus/focusable.tsx +++ b/packages/react-lightning/src/focus/focusable.tsx @@ -7,14 +7,13 @@ import type { Ref, RefAttributes, } from 'react'; -import { forwardRef, memo, useMemo } from 'react'; +import { forwardRef, memo } from 'react'; + import { useCombinedRef } from '../hooks/useCombinedRef'; import type { LightningElement } from '../types'; import { type FocusOptions, useFocus } from './useFocus'; -type Focusable = P & - RefAttributes & - FocusOptions & { focused: boolean }; +type Focusable = P & RefAttributes & FocusOptions & { focused: boolean }; const ForwardRefComponent = forwardRef(() =>
); @@ -48,23 +47,15 @@ export function focusable( isForwardRef(Component) ? Component : forwardRef>( - Component as ForwardRefRenderFunction< - T, - PropsWithoutRef> - >, + Component as ForwardRefRenderFunction>>, ), ); MemoRefComponent.displayName = `Focusable${Component.displayName}`; return forwardRef>((props, forwardedRef) => { - const options = useMemo( - () => - typeof focusOptions === 'function' - ? focusOptions(props as Focusable) - : focusOptions, - [props, focusOptions], - ); + const options = + typeof focusOptions === 'function' ? focusOptions(props as Focusable) : focusOptions; const { ref, focused } = useFocus(options); const combinedRef = useCombinedRef(ref, forwardedRef); @@ -72,11 +63,6 @@ export function focusable( }); } -function isForwardRef( - Component: object, -): Component is ForwardRefExoticComponent { - return ( - '$$typeof' in Component && - Component.$$typeof === ForwardRefComponent.$$typeof - ); +function isForwardRef(Component: object): Component is ForwardRefExoticComponent { + return '$$typeof' in Component && Component.$$typeof === ForwardRefComponent.$$typeof; } diff --git a/packages/react-lightning/src/focus/useFocus.tsx b/packages/react-lightning/src/focus/useFocus.tsx index 98b1194..c6fdec0 100644 --- a/packages/react-lightning/src/focus/useFocus.tsx +++ b/packages/react-lightning/src/focus/useFocus.tsx @@ -1,10 +1,5 @@ -import { - type RefObject, - useContext, - useEffect, - useRef, - useSyncExternalStore, -} from 'react'; +import { type RefObject, useContext, useEffect, useRef, useSyncExternalStore } from 'react'; + import type { LightningElement } from '../types'; import { FocusGroupContext } from './FocusGroupContext'; import { useFocusManager } from './useFocusManager'; @@ -15,6 +10,8 @@ export type FocusOptions = { focusRedirect?: boolean; destinations?: (LightningElement | null)[]; onChildFocused?: (child: LightningElement) => void; + /** When true, focus navigation can target non-visible children (e.g. clipped items in a virtualized list). */ + allowOffscreen?: boolean; }; export function useFocus( @@ -24,6 +21,7 @@ export function useFocus( focusRedirect, destinations, onChildFocused, + allowOffscreen, }: FocusOptions = { active: true, autoFocus: false, @@ -52,7 +50,7 @@ export function useFocus( // so we can properly remove the child element. const elementRef = useRef(null); - /* biome-ignore lint/correctness/useExhaustiveDependencies: We purposely leave + /* oxlint-disable-next-line react-hooks/exhaustive-deps -- We purposely leave out the autoFocus/focusRedirect/destinations dependencies here. This will prevent unnecessary removal and re-addition of the elements to the focus manager. Those dependencies get updated below in other effects. */ @@ -63,7 +61,14 @@ export function useFocus( autoFocus, focusRedirect, destinations, + allowOffscreen, }); + } else if (import.meta.env.DEV && ref.current && !parentFocusable) { + console.warn( + 'useFocus: Element exists but no parent FocusGroup found. ' + + 'This element will not participate in focus management. ' + + 'Wrap it in a FocusGroup or ensure the parent FocusGroup has mounted.', + ); } return () => { @@ -97,6 +102,12 @@ export function useFocus( } }, [focusManager, onChildFocused]); + useEffect(() => { + if (ref.current) { + focusManager.setAllowOffscreen(ref.current, allowOffscreen); + } + }, [focusManager, allowOffscreen]); + useEffect(() => { if (ref.current) { ref.current.focusable = active !== undefined ? active : true; diff --git a/packages/react-lightning/src/focus/useFocusKeyManager.ts b/packages/react-lightning/src/focus/useFocusKeyManager.ts index 2670edf..a19e279 100644 --- a/packages/react-lightning/src/focus/useFocusKeyManager.ts +++ b/packages/react-lightning/src/focus/useFocusKeyManager.ts @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import type { LightningElement } from '../types'; import type { FocusKeyManager } from './FocusKeyManager'; import { FocusManagerContext } from './FocusManagerContext'; @@ -7,9 +8,7 @@ export const useFocusKeyManager = (): FocusKeyManager => { const focusContext = useContext(FocusManagerContext); if (!focusContext) { - throw new Error( - 'useFocusKeyManager must be used within a FocusManagerProvider', - ); + throw new Error('useFocusKeyManager must be used within a FocusManagerProvider'); } return focusContext.focusKeyManager; diff --git a/packages/react-lightning/src/focus/useFocusManager.ts b/packages/react-lightning/src/focus/useFocusManager.ts index a31adbb..96983cf 100644 --- a/packages/react-lightning/src/focus/useFocusManager.ts +++ b/packages/react-lightning/src/focus/useFocusManager.ts @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import type { LightningElement } from '../types'; import type { FocusManager } from './FocusManager'; import { FocusManagerContext } from './FocusManagerContext'; @@ -7,9 +8,7 @@ export const useFocusManager = (): FocusManager => { const focusContext = useContext(FocusManagerContext); if (!focusContext) { - throw new Error( - 'useFocusManager must be used within a FocusManagerProvider', - ); + throw new Error('useFocusManager must be used within a FocusManagerProvider'); } return focusContext.focusManager; diff --git a/packages/react-lightning/src/hooks/useCombinedRef.ts b/packages/react-lightning/src/hooks/useCombinedRef.ts index b4cf274..7fe4f19 100644 --- a/packages/react-lightning/src/hooks/useCombinedRef.ts +++ b/packages/react-lightning/src/hooks/useCombinedRef.ts @@ -1,9 +1,4 @@ -import { - type MutableRefObject, - type Ref, - type RefCallback, - useCallback, -} from 'react'; +import { type MutableRefObject, type Ref, type RefCallback } from 'react'; /** * This hook allows you to combine multiple refs into a single ref. This is useful when you need to pass a ref to a component that already has a ref. See the example below @@ -34,18 +29,13 @@ import { * ``` */ export const useCombinedRef = (...refs: Ref[]): RefCallback => { - const combinedCallback = useCallback( - (node: T) => { - for (const ref of refs) { - if (typeof ref === 'function') { - ref(node); - } else if (ref) { - (ref as MutableRefObject).current = node; - } + return (node: T) => { + for (const ref of refs) { + if (typeof ref === 'function') { + ref(node); + } else if (ref) { + (ref as MutableRefObject).current = node; } - }, - [refs], - ); - - return combinedCallback; + } + }; }; diff --git a/packages/react-lightning/src/hooks/useDebugHooks.ts b/packages/react-lightning/src/hooks/useDebugHooks.ts index e24ed8c..b73722d 100644 --- a/packages/react-lightning/src/hooks/useDebugHooks.ts +++ b/packages/react-lightning/src/hooks/useDebugHooks.ts @@ -1,12 +1,6 @@ -import { - type DependencyList, - useCallback, - useEffect, - useMemo, - useRef, -} from 'react'; - -// biome-ignore lint/suspicious/noExplicitAny: any is used so we can pass any function +import { type DependencyList, useCallback, useEffect, useMemo, useRef } from 'react'; + +// oxlint-disable-next-line typescript/no-explicit-any -- any is used so we can pass any function type DebuggableHook = (...args: any[]) => any; function wrapHook(hook: T, name: string): T { @@ -14,18 +8,19 @@ function wrapHook(hook: T, name: string): T { const dependencies = args.at(-1) as DependencyList; const prevValue = useRef([]); - const changedDeps = dependencies.reduce< - Record - >((acc, dependency, index) => { - if (dependency !== prevValue.current[index]) { - acc[index] = { - old: prevValue.current[index], - new: dependency, - }; - } + const changedDeps = dependencies.reduce>( + (acc, dependency, index) => { + if (dependency !== prevValue.current[index]) { + acc[index] = { + old: prevValue.current[index], + new: dependency, + }; + } - return acc; - }, {}); + return acc; + }, + {}, + ); if (Object.keys(changedDeps).length) { console.log(`[${name}] `, changedDeps); @@ -37,12 +32,6 @@ function wrapHook(hook: T, name: string): T { }) as T; } -export const useEffectDebug: typeof useEffect = wrapHook( - useEffect, - 'useEffectDebug', -); -export const useCallbackDebug: typeof useCallback = wrapHook( - useCallback, - 'useCallbackDebug', -); +export const useEffectDebug: typeof useEffect = wrapHook(useEffect, 'useEffectDebug'); +export const useCallbackDebug: typeof useCallback = wrapHook(useCallback, 'useCallbackDebug'); export const useMemoDebug: typeof useMemo = wrapHook(useMemo, 'useMemoDebug'); diff --git a/packages/react-lightning/src/index.ts b/packages/react-lightning/src/index.ts index 0dd643e..188cfca 100644 --- a/packages/react-lightning/src/index.ts +++ b/packages/react-lightning/src/index.ts @@ -12,12 +12,7 @@ export { useFocus } from './focus/useFocus'; export { useFocusManager } from './focus/useFocusManager'; export { useCombinedRef } from './hooks/useCombinedRef'; export { Keys } from './input/Keys'; -export { - createRoot, - type LightningRoot, - LightningRootContext, - type RenderOptions, -} from './render'; +export { createRoot, type LightningRoot, LightningRootContext, type RenderOptions } from './render'; export type { Plugin } from './render/Plugin'; export * from './types'; export { simpleDiff } from './utils/simpleDiff'; diff --git a/packages/react-lightning/src/input/KeyMapContext.ts b/packages/react-lightning/src/input/KeyMapContext.ts index 52a3d81..8ea5254 100644 --- a/packages/react-lightning/src/input/KeyMapContext.ts +++ b/packages/react-lightning/src/input/KeyMapContext.ts @@ -1,4 +1,5 @@ import { type Context, createContext } from 'react'; + import type { Keys } from './Keys'; export type KeyMap = Record; diff --git a/packages/react-lightning/src/input/KeyPressHandler.tsx b/packages/react-lightning/src/input/KeyPressHandler.tsx index f7b4b54..d5b45cb 100644 --- a/packages/react-lightning/src/input/KeyPressHandler.tsx +++ b/packages/react-lightning/src/input/KeyPressHandler.tsx @@ -1,5 +1,6 @@ import type { FC, ReactNode } from 'react'; -import { useCallback, useContext, useEffect, useRef } from 'react'; +import { useContext, useEffect, useRef } from 'react'; + import { useFocusManager } from '../focus/useFocusManager'; import { bubbleEvent } from './bubbleEvent'; import type { KeyMap } from './KeyMapContext'; @@ -13,66 +14,56 @@ export const KeyPressHandler: FC<{ children: ReactNode }> = ({ children }) => { const focusManager = useFocusManager(); const keyDownTime = useRef(0); - const createKeyHandler = useCallback( - (handler: 'onKeyDown' | 'onKeyUp', keyMap: KeyMap) => { - return (event: KeyboardEvent) => { - if (event.repeat) { - return; - } + const createKeyHandler = (handler: 'onKeyDown' | 'onKeyUp', keyMap: KeyMap) => { + return (event: KeyboardEvent) => { + if (event.repeat) { + return; + } + + const element = focusManager.focusPath.at(-1); + + if (!element) { + return; + } + + if (event instanceof KeyboardEvent) { + const remoteKey = keyMap[event.keyCode] ?? Keys.Unknown; - const element = focusManager.focusPath.at(-1); + // Build the event object once and reuse for all bubbleEvent calls + const keyEvent = { + keyCode: event.keyCode, + key: event.key, + code: event.code, + remoteKey, + repeat: event.repeat, + target: element, + currentTarget: element, + stopFocusHandling: false, + preventDefault: event.preventDefault, + }; - if (!element) { - return; + if (handler === 'onKeyDown') { + keyDownTime.current = event.timeStamp; + } else if (handler === 'onKeyUp') { + const duration = event.timeStamp - keyDownTime.current; + + keyDownTime.current = 0; + + bubbleEvent(duration > LONG_PRESS_THRESHOLD ? 'onLongPress' : 'onKeyPress', keyEvent); + + // Reset stopFocusHandling for the next bubbleEvent call + keyEvent.stopFocusHandling = false; } - if (event instanceof KeyboardEvent) { - const remoteKey = keyMap[event.keyCode] ?? Keys.Unknown; - - if (handler === 'onKeyDown') { - keyDownTime.current = event.timeStamp; - } else if (handler === 'onKeyUp') { - const duration = event.timeStamp - keyDownTime.current; - - keyDownTime.current = 0; - - bubbleEvent( - duration > LONG_PRESS_THRESHOLD ? 'onLongPress' : 'onKeyPress', - { - keyCode: event.keyCode, - key: event.key, - code: event.code, - remoteKey, - repeat: event.repeat, - target: element, - currentTarget: element, - stopFocusHandling: false, - preventDefault: event.preventDefault, - }, - ); - } - - bubbleEvent(handler, { - keyCode: event.keyCode, - key: event.key, - code: event.code, - remoteKey, - repeat: event.repeat, - target: element, - currentTarget: element, - stopFocusHandling: false, - preventDefault: event.preventDefault, - }); - - if (remoteKey !== Keys.Unknown) { - event.stopPropagation(); - event.preventDefault(); - } + bubbleEvent(handler, keyEvent); + + if (remoteKey !== Keys.Unknown) { + event.stopPropagation(); + event.preventDefault(); } - }; - }, - [focusManager], - ); + } + }; + }; useEffect(() => { const keyDownHandler = createKeyHandler('onKeyDown', keyMap); diff --git a/packages/react-lightning/src/mocks/createMockElement.ts b/packages/react-lightning/src/mocks/createMockElement.ts index c280c2c..ba12cd3 100644 --- a/packages/react-lightning/src/mocks/createMockElement.ts +++ b/packages/react-lightning/src/mocks/createMockElement.ts @@ -46,10 +46,6 @@ export class MockElement implements Focusable, EventNotifier { } } -export function createMockElement( - id: number, - name: string, - visible = true, -): MockElement { +export function createMockElement(id: number, name: string, visible = true): MockElement { return new MockElement(id, name, visible); } diff --git a/packages/react-lightning/src/render/Plugin.ts b/packages/react-lightning/src/render/Plugin.ts index 8a0d02f..4e189b8 100644 --- a/packages/react-lightning/src/render/Plugin.ts +++ b/packages/react-lightning/src/render/Plugin.ts @@ -1,6 +1,7 @@ import type { RendererMain } from '@lightningjs/renderer'; import type { Fiber, Reconciler } from 'react-reconciler'; import type { SetOptional } from 'type-fest'; + import type { LightningTextElement } from '../element/LightningTextElement'; import type { LightningElement } from '../types'; import type { ReconcilerContainer } from './createHostConfig'; @@ -11,33 +12,28 @@ export type Plugin = { */ init?( renderer: RendererMain, - reconciler: Reconciler< - ReconcilerContainer, - T, - LightningTextElement, - null, - null, - T - >, + reconciler: Reconciler, ): Promise; /** * Fires when an element is created, before it's set up and initialized. Props * passed in are the raw props before any prop transforms are run. */ - onCreateInstance?( - instance: SetOptional, - initialProps: T['props'], - fiber: Fiber, - ): void; + onCreateInstance?(instance: SetOptional, initialProps: T['props'], fiber: Fiber): void; /** * Transforms the payload that is used to update the LightningElement. Return * the value to be used in the update, or null to not update. Note that if you * return null, other plugins that may transform the props will be skipped. */ - transformProps?( - instance: SetOptional, - props: T['props'], - ): object | null; + transformProps?(instance: SetOptional, props: T['props']): object | null; + + /** + * Declares which style properties this plugin's transformProps handles. + * When set, transformProps is only called if the style update includes at + * least one property in this set. When not set, transformProps is always + * called. This enables a fast path for style-only updates that no plugin + * needs to process. + */ + handledStyleProps?: ReadonlySet; }; diff --git a/packages/react-lightning/src/render/createHostConfig.ts b/packages/react-lightning/src/render/createHostConfig.ts index 7b4801e..584dc47 100644 --- a/packages/react-lightning/src/render/createHostConfig.ts +++ b/packages/react-lightning/src/render/createHostConfig.ts @@ -1,10 +1,8 @@ import type { RendererMain } from '@lightningjs/renderer'; import { createContext } from 'react'; import type { EventPriority, HostConfig } from 'react-reconciler'; -import { - DefaultEventPriority, - NoEventPriority, -} from 'react-reconciler/constants'; +import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants'; + import { createLightningElement } from '../element/createLightningElement'; import { LightningTextElement } from '../element/LightningTextElement'; import { @@ -37,23 +35,19 @@ export type LightningHostConfig = HostConfig< unknown, unknown, unknown ->; +> & { + rendererPackageName: string; + rendererVersion: string; + extraDevToolsConfig: unknown; +}; -type LightningHostConfigOptions = Pick< - LightningHostConfig, - 'isPrimaryRenderer' ->; +type LightningHostConfigOptions = Pick; -export function createHostConfig( - options?: LightningHostConfigOptions, -): LightningHostConfig { +export function createHostConfig(options?: LightningHostConfigOptions): LightningHostConfig { const HostTransitionContext = createContext(null); let currentUpdatePriority: EventPriority = NoEventPriority; - function appendChild( - parentInstance: LightningElement, - child: LightningElement, - ) { + function appendChild(parentInstance: LightningElement, child: LightningElement) { if (child.parent !== parentInstance) { parentInstance.insertChild(child); } @@ -62,6 +56,12 @@ export function createHostConfig( return { isPrimaryRenderer: options?.isPrimaryRenderer ?? true, warnsIfNotActing: false, + + // React DevTools integration — read by react-reconciler's + // injectIntoDevTools() to identify this renderer. + rendererPackageName: '@plextv/react-lightning', + rendererVersion: '0.4.0', + extraDevToolsConfig: null, supportsMutation: true, supportsPersistence: false, supportsHydration: false, @@ -90,9 +90,9 @@ export function createHostConfig( NotPendingTransition: null, HostTransitionContext: { $$typeof: HostTransitionContext.$$typeof, - // biome-ignore lint/suspicious/noExplicitAny: Needs to be null + // oxlint-disable-next-line typescript/no-explicit-any -- Needs to be null Provider: null as any, - // biome-ignore lint/suspicious/noExplicitAny: Needs to be null + // oxlint-disable-next-line typescript/no-explicit-any -- Needs to be null Consumer: null as any, _currentValue: null, _currentValue2: null, @@ -116,12 +116,11 @@ export function createHostConfig( appendChildToContainer(container, child) { if (container.renderer.root) { - const root = container.renderer - .root as unknown as RendererNode; + const root = container.renderer.root as unknown as RendererNode; child.setLightningNode(root); - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO (window as any).rootElement = child; } }, @@ -140,12 +139,7 @@ export function createHostConfig( }, createTextInstance(text, _rootContainerInstance, container, fiber) { - return new LightningTextElement( - { text }, - container.renderer, - container.plugins, - fiber, - ); + return new LightningTextElement({ text }, container.renderer, container.plugins, fiber); }, finalizeInitialChildren() { @@ -215,10 +209,7 @@ export function createHostConfig( }, commitUpdate(instance, type, oldProps, newProps) { - const diffedProps: Partial | null = simpleDiff( - oldProps, - newProps, - ); + const diffedProps: Partial | null = simpleDiff(oldProps, newProps); if (!diffedProps) { return null; diff --git a/packages/react-lightning/src/render/index.tsx b/packages/react-lightning/src/render/index.tsx index 9699214..467fac6 100644 --- a/packages/react-lightning/src/render/index.tsx +++ b/packages/react-lightning/src/render/index.tsx @@ -5,18 +5,14 @@ import { type Stage, type TextureMap, } from '@lightningjs/renderer'; -import { - CanvasCoreRenderer, - CanvasTextRenderer, -} from '@lightningjs/renderer/canvas'; -import { - SdfTextRenderer, - WebGlCoreRenderer, -} from '@lightningjs/renderer/webgl'; +import { CanvasCoreRenderer, CanvasTextRenderer } from '@lightningjs/renderer/canvas'; +import { SdfTextRenderer, WebGlCoreRenderer } from '@lightningjs/renderer/webgl'; import type { ComponentType, Context, ReactNode } from 'react'; import { createContext, createElement } from 'react'; import createReconciler, { type Reconciler } from 'react-reconciler'; + import type { LightningTextElement } from '../element/LightningTextElement'; +import type { FocusManager } from '../focus/FocusManager'; import type { LightningElement } from '../types'; import { traceWrap } from '../utils/traceWrap'; import { createHostConfig, type ReconcilerContainer } from './createHostConfig'; @@ -25,9 +21,22 @@ import type { Plugin } from './Plugin'; // https://github.com/lightning-js/devtools/blob/main/src/types/globals.d.ts declare global { interface Window { - __LIGHTNINGJS_DEVTOOLS__?: { - renderer: RendererMain; - features?: string[]; + __LIGHTNINGJS_DEVTOOLS_HOOK__?: { + config?: { + renderer?: RendererMain; + focusManager?: FocusManager; + features?: string[]; + }; + injected?: boolean; + inject?: () => Promise; + }; + } +} + +declare module 'react-reconciler' { + interface Fiber { + _debugInfo?: { + isLngNode?: boolean; }; } } @@ -53,10 +62,7 @@ export type RenderOptions = Omit< }; export type LightningRoot = { - render( - component: ReactNode | ComponentType, - callback?: () => void, - ): void; + render(component: ReactNode | ComponentType, callback?: () => void): void; unmount(): void; configure(): void; renderer: RendererMain; @@ -99,14 +105,15 @@ export async function createRoot( }; // Don't use the lightning inspector, we have our own. - const { fonts, useCanvas, includeCanvasFontRenderer, ...finalOptions } = - allOptions; + const { fonts, useCanvas, includeCanvasFontRenderer, ...finalOptions } = allOptions; const fontEngines: RendererMainSettings['fontEngines'] = []; let renderEngine: RendererMainSettings['renderEngine']; + /* oxlint-disable typescript-eslint/consistent-type-imports -- typeof import() is the only way to type dynamic imports */ let shaders: | typeof import('@lightningjs/renderer/webgl/shaders') | typeof import('@lightningjs/renderer/canvas/shaders'); + /* oxlint-enable typescript-eslint/consistent-type-imports */ if (useCanvas) { renderEngine = CanvasCoreRenderer; @@ -134,13 +141,6 @@ export async function createRoot( target, ); - if (import.meta.env.DEV) { - window.__LIGHTNINGJS_DEVTOOLS__ = { - renderer, - features: ['react-lightning'], - }; - } - for (const font of fonts) { const { type, ...options } = font; @@ -172,10 +172,7 @@ export async function createRoot( if (finalOptions.textures) { for (const [key, textureType] of Object.entries(finalOptions.textures)) { - renderer.stage.txManager.registerTextureType( - key as keyof TextureMap, - textureType, - ); + renderer.stage.txManager.registerTextureType(key as keyof TextureMap, textureType); } } @@ -193,13 +190,17 @@ export async function createRoot( } reconciler = createReconciler(hostConfig); + + reconciler.injectIntoDevTools({ + bundleType: import.meta.env.DEV ? 1 : 0, + version: '0.4.0', + rendererPackageName: '@plextv/react-lightning', + }); } await Promise.all([ import('../shim/resizeObserverShim'), - ...(finalOptions.plugins?.map?.((plugin) => - plugin.init?.(renderer, reconciler), - ) ?? []), + ...(finalOptions.plugins?.map?.((plugin) => plugin.init?.(renderer, reconciler)) ?? []), ]); const root = reconciler.createContainer( diff --git a/packages/react-lightning/src/render/isValidTextChild.ts b/packages/react-lightning/src/render/isValidTextChild.ts index af09cd7..c2ccbce 100644 --- a/packages/react-lightning/src/render/isValidTextChild.ts +++ b/packages/react-lightning/src/render/isValidTextChild.ts @@ -1,9 +1,3 @@ -export function isValidTextChild( - text: unknown, -): text is boolean | number | string { - return ( - typeof text === 'string' || - typeof text === 'number' || - typeof text === 'boolean' - ); +export function isValidTextChild(text: unknown): text is boolean | number | string { + return typeof text === 'string' || typeof text === 'number' || typeof text === 'boolean'; } diff --git a/packages/react-lightning/src/render/mapReactPropsToLightning.ts b/packages/react-lightning/src/render/mapReactPropsToLightning.ts index 74dcb9d..5941b01 100644 --- a/packages/react-lightning/src/render/mapReactPropsToLightning.ts +++ b/packages/react-lightning/src/render/mapReactPropsToLightning.ts @@ -5,9 +5,7 @@ import { } from '../types'; import { isValidTextChild } from './isValidTextChild'; -function isIntlObject( - obj: unknown, -): obj is { props: { defaultMessage?: string } } { +function isIntlObject(obj: unknown): obj is { props: { defaultMessage?: string } } { return ( typeof obj === 'object' && obj !== null && @@ -42,14 +40,21 @@ export function mapReactPropsToLightning( if (isValidTextChild(children)) { textProps.text = String(children); - } else if ( - Array.isArray(children) && - children.every((child) => isValidTextChild(child)) - ) { - textProps.text = children.reduce( - (acc, child) => acc + String(child), - '', - ); + } else if (Array.isArray(children)) { + // Single-pass: validate and concatenate simultaneously + let text = ''; + let allValid = true; + for (let i = 0; i < children.length; i++) { + if (isValidTextChild(children[i])) { + text += String(children[i]); + } else { + allValid = false; + break; + } + } + if (allValid) { + textProps.text = text; + } } else if (isIntlObject(children)) { textProps.text = children.props.defaultMessage; } else if (children) { @@ -65,7 +70,7 @@ export function mapReactPropsToLightning( break; default: - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO mappedProps[prop] = props[prop] as any; break; } diff --git a/packages/react-lightning/src/shim/resizeObserverShim.ts b/packages/react-lightning/src/shim/resizeObserverShim.ts index 4cd61c7..c1edfa0 100644 --- a/packages/react-lightning/src/shim/resizeObserverShim.ts +++ b/packages/react-lightning/src/shim/resizeObserverShim.ts @@ -11,10 +11,7 @@ class LightningResizeObserver extends window.ResizeObserver { this._callback = callback; } - public override observe( - target: Element, - options?: ResizeObserverOptions | undefined, - ): void { + public override observe(target: Element, options?: ResizeObserverOptions | undefined): void { if (target instanceof LightningViewElement) { this._targets.add(target); target.on('layout', this._fireCallbacks); @@ -39,37 +36,30 @@ class LightningResizeObserver extends window.ResizeObserver { } private _fireCallbacks = (dimensions: Rect): void => { - const entries = Array.from(this._targets).map( - (target) => { - return { - borderBoxSize: [ - { - blockSize: dimensions.h, - inlineSize: dimensions.w, - }, - ], - contentBoxSize: [ - { - blockSize: dimensions.h, - inlineSize: dimensions.w, - }, - ], - devicePixelContentBoxSize: [ - { - blockSize: dimensions.h, - inlineSize: dimensions.w, - }, - ], - contentRect: new DOMRectReadOnly( - dimensions.x, - dimensions.y, - dimensions.w, - dimensions.h, - ), - target: target as unknown as Element, - }; - }, - ); + const entries = Array.from(this._targets).map((target) => { + return { + borderBoxSize: [ + { + blockSize: dimensions.h, + inlineSize: dimensions.w, + }, + ], + contentBoxSize: [ + { + blockSize: dimensions.h, + inlineSize: dimensions.w, + }, + ], + devicePixelContentBoxSize: [ + { + blockSize: dimensions.h, + inlineSize: dimensions.w, + }, + ], + contentRect: new DOMRectReadOnly(dimensions.x, dimensions.y, dimensions.w, dimensions.h), + target: target as unknown as Element, + }; + }); this._callback(entries, this); }; diff --git a/packages/react-lightning/src/types/Element.ts b/packages/react-lightning/src/types/Element.ts index d19802b..57148ee 100644 --- a/packages/react-lightning/src/types/Element.ts +++ b/packages/react-lightning/src/types/Element.ts @@ -2,7 +2,4 @@ import type { LightningImageElement } from '../element/LightningImageElement'; import type { LightningTextElement } from '../element/LightningTextElement'; import type { LightningViewElement } from '../element/LightningViewElement'; -export type LightningElement = - | LightningViewElement - | LightningImageElement - | LightningTextElement; +export type LightningElement = LightningViewElement | LightningImageElement | LightningTextElement; diff --git a/packages/react-lightning/src/types/Focusable.ts b/packages/react-lightning/src/types/Focusable.ts index 05f6ab2..3da05c5 100644 --- a/packages/react-lightning/src/types/Focusable.ts +++ b/packages/react-lightning/src/types/Focusable.ts @@ -13,7 +13,7 @@ export interface FocusEvents { focusChanged: (element: T, isFocused: boolean) => void; focusableChanged: (element: T, isFocusable: boolean) => void; - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO [x: string | symbol]: (...args: any[]) => void; } diff --git a/packages/react-lightning/src/types/LightningElementEvents.ts b/packages/react-lightning/src/types/LightningElementEvents.ts index 390b906..4fca165 100644 --- a/packages/react-lightning/src/types/LightningElementEvents.ts +++ b/packages/react-lightning/src/types/LightningElementEvents.ts @@ -4,6 +4,7 @@ import type { NodeLoadedEventHandler, NodeRenderStateEventHandler, } from '@lightningjs/renderer'; + import type { LightningElement } from './Element'; import type { FocusEvents } from './Focusable'; import type { Rect } from './Geometry'; @@ -30,6 +31,6 @@ export interface LightningElementEvents extends FocusEvents { propsChanged: (newProps: Partial) => void; animationFinished: (animationName: IAnimationController) => void; - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO [k: string | symbol]: (...args: any[]) => void; } diff --git a/packages/react-lightning/src/types/Props.ts b/packages/react-lightning/src/types/Props.ts index f453585..21aed36 100644 --- a/packages/react-lightning/src/types/Props.ts +++ b/packages/react-lightning/src/types/Props.ts @@ -1,10 +1,6 @@ -import type { - Dimensions, - INodeProps, - RendererMain, - TextureMap, -} from '@lightningjs/renderer'; +import type { Dimensions, INodeProps, RendererMain, TextureMap } from '@lightningjs/renderer'; import type { ReactNode, RefAttributes } from 'react'; + import type { Animatable } from './Animatable'; import type { LightningElement } from './Element'; import type { FocusableProps } from './Focusable'; @@ -35,9 +31,7 @@ export type ExtractProps = Type extends { ? Props : never; -export interface TextureDef< - textureType extends keyof TextureMap = keyof TextureMap, -> { +export interface TextureDef { type: textureType; props: ExtractProps; } @@ -49,7 +43,9 @@ export interface ShaderDef { export interface LightningViewElementProps< TStyleProps extends LightningViewElementStyle = LightningViewElementStyle, -> extends LightningElementEventProps, +> + extends + LightningElementEventProps, FocusableProps, Animatable, RefAttributes { diff --git a/packages/react-lightning/src/types/Styles.ts b/packages/react-lightning/src/types/Styles.ts index bd3f7b7..92e6371 100644 --- a/packages/react-lightning/src/types/Styles.ts +++ b/packages/react-lightning/src/types/Styles.ts @@ -1,4 +1,5 @@ import type { INodeProps, ITextNodeProps } from '@lightningjs/renderer'; + import type { Rect } from './Geometry'; interface BorderStyleObject { @@ -13,11 +14,10 @@ type RGBA = [r: number, g: number, b: number, a: number]; // list exclusions, so if new lightning props get added, we immediately get // typing errors and know if new props are available. -export interface LightningViewElementStyle - extends Omit< - Partial, - 'parent' | 'src' | 'shader' | 'data' | 'texture' - > { +export interface LightningViewElementStyle extends Omit< + Partial, + 'parent' | 'src' | 'shader' | 'data' | 'texture' +> { border?: BorderStyle; borderColor?: number; borderTop?: number; @@ -40,11 +40,9 @@ export interface LightningViewElementStyle export interface LightningImageElementStyle extends LightningViewElementStyle {} export interface LightningTextElementStyle - extends LightningViewElementStyle, - Omit< - Partial, - 'debug' | 'parent' | 'shader' | 'src' | 'text' | 'texture' - > { + extends + LightningViewElementStyle, + Omit, 'debug' | 'parent' | 'shader' | 'src' | 'text' | 'texture'> { shadow?: boolean; shadowColor?: RGBA | number; shadowOffsetX?: number; diff --git a/packages/react-lightning/src/utils/EventEmitter.ts b/packages/react-lightning/src/utils/EventEmitter.ts index 809ccb9..b444560 100644 --- a/packages/react-lightning/src/utils/EventEmitter.ts +++ b/packages/react-lightning/src/utils/EventEmitter.ts @@ -1,5 +1,5 @@ export class EventEmitter< - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO T extends Record any>, > { private _eventListeners: Partial>> = {}; @@ -73,10 +73,7 @@ export class EventEmitter< } } - public async asyncEmit( - name: K, - ...args: Parameters - ): Promise { + public async asyncEmit(name: K, ...args: Parameters): Promise { const listeners = this._eventListeners[name]; const promises: Promise[] = []; diff --git a/packages/react-lightning/src/utils/findClosestElement.spec.ts b/packages/react-lightning/src/utils/findClosestElement.spec.ts index b01429c..6d0defc 100644 --- a/packages/react-lightning/src/utils/findClosestElement.spec.ts +++ b/packages/react-lightning/src/utils/findClosestElement.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, suite } from 'vitest'; + import { Direction } from '../focus/Direction'; import type { LightningElement, Rect } from '../types'; import { findClosestElement, getOverlap } from './findClosestElement'; @@ -13,13 +14,11 @@ function createMockElement(id: number, dimensions: Rect): LightningElement { children: [], node: { ...dimensions }, focusable: true, + focusableIntent: true, insertChild(this: LightningElement, child: LightningElement) { this.children.push(child); }, - getRelativePosition( - this: LightningElement, - relativeElement?: LightningElement, - ) { + getRelativePosition(this: LightningElement, relativeElement?: LightningElement) { const relativeX = relativeElement?.node?.x ?? 0; const relativeY = relativeElement?.node?.y ?? 0; @@ -69,12 +68,7 @@ function runTestsOnElements( throw new Error(`Element ${source} not found.`); } - const closest = findClosestElement( - sourceElement, - childElements, - elements.root, - direction, - ); + const closest = findClosestElement(sourceElement, childElements, elements.root, direction); it(`the ${Direction[direction]} of element ${source} it should be ${expected}`, () => { expect(closest?.id ?? null).toBe(expected); @@ -335,4 +329,90 @@ suite('getOverlap', () => { expect(getOverlap(Direction.Down, a, b)).toEqual(25); expect(getOverlap(Direction.Left, a, b)).toEqual(0); }); + + describe('allowOffscreen', () => { + it('should skip non-visible elements by default', () => { + const elements = createLayout(400, 200, [ + { x: 0, y: 0, w: 200, h: 200 }, + { x: 200, y: 0, w: 200, h: 200 }, + ]); + + // Make element 2 non-visible (focusable returns false, but focusableIntent is true) + Object.assign(elements[2], { focusable: false, focusableIntent: true }); + + const closest = findClosestElement( + // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence + elements[1]!, + elements.root.children, + elements.root, + Direction.Right, + ); + + expect(closest).toBeNull(); + }); + + it('should allow focusing non-visible elements when allowOffscreen is true', () => { + const elements = createLayout(400, 200, [ + { x: 0, y: 0, w: 200, h: 200 }, + { x: 200, y: 0, w: 200, h: 200 }, + ]); + + // Make element 2 non-visible but with focusableIntent + Object.assign(elements[2], { focusable: false, focusableIntent: true }); + + const closest = findClosestElement( + // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence + elements[1]!, + elements.root.children, + elements.root, + Direction.Right, + true, + ); + + expect(closest?.id).toBe(2); + }); + + it('should skip elements with zero dimensions when allowOffscreen is true', () => { + const elements = createLayout(400, 200, [ + { x: 0, y: 0, w: 200, h: 200 }, + { x: 200, y: 0, w: 0, h: 0 }, + ]); + + // Zero-dimension elements should still be skipped even with allowOffscreen + // because they have no spatial position to navigate to + Object.assign(elements[2], { focusableIntent: true }); + + const closest = findClosestElement( + // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence + elements[1]!, + elements.root.children, + elements.root, + Direction.Right, + true, + ); + + expect(closest).toBeNull(); + }); + + it('should not focus elements without focusableIntent when allowOffscreen is true', () => { + const elements = createLayout(400, 200, [ + { x: 0, y: 0, w: 200, h: 200 }, + { x: 200, y: 0, w: 200, h: 200 }, + ]); + + // Element has neither focusable nor focusableIntent + Object.assign(elements[2], { focusable: false, focusableIntent: false }); + + const closest = findClosestElement( + // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence + elements[1]!, + elements.root.children, + elements.root, + Direction.Right, + true, + ); + + expect(closest).toBeNull(); + }); + }); }); diff --git a/packages/react-lightning/src/utils/findClosestElement.ts b/packages/react-lightning/src/utils/findClosestElement.ts index 28cd26e..b3e1a57 100644 --- a/packages/react-lightning/src/utils/findClosestElement.ts +++ b/packages/react-lightning/src/utils/findClosestElement.ts @@ -29,11 +29,7 @@ type Dimensions = { * we would expect (2) to be the closest element, but if we calculate using the * centers of elements of (2) and (3), (3) would be closer. */ -function getDistance( - direction: Direction, - source: Dimensions, - target: Dimensions, -): number | null { +function getDistance(direction: Direction, source: Dimensions, target: Dimensions): number | null { let targetX: number; let targetY: number; @@ -112,11 +108,7 @@ function calculateShortestDistance( return euclidean + displacement - alignment - Math.sqrt(overlap); } -function getAlignment( - direction: Direction, - source: Dimensions, - overlap: number, -): number { +function getAlignment(direction: Direction, source: Dimensions, overlap: number): number { const isHorizontal = direction & Direction.Horizontal; const bias = overlap / (isHorizontal ? source.w : source.h); @@ -161,26 +153,38 @@ export function getOverlap( return Math.abs(length); } -function isOverlap( - { x: x1, y: y1, w: w1, h: h1 }: Dimensions, - { x: x2, y: y2, w: w2, h: h2 }: Dimensions, -) { - return { - x: x1 < x2 + w2 && x1 + w1 > x2, - y: y1 < y2 + h2 && y1 + h1 > y2, - }; -} +// Scratch objects reused across calls to avoid per-element allocations +const _scratchSource: Dimensions = { + w: 0, + h: 0, + x: 0, + y: 0, + centerX: 0, + centerY: 0, +}; +const _scratchTarget: Dimensions = { + w: 0, + h: 0, + x: 0, + y: 0, + centerX: 0, + centerY: 0, +}; -function getDimensions( +function fillDimensions( + out: Dimensions, element: LightningElement, relativeElement: LightningElement | null, ): Dimensions { const { w, h } = element.node; const { x, y } = element.getRelativePosition(relativeElement); - const centerX = x + w / 2; - const centerY = y + h / 2; - - return { w, h, x, y, centerX, centerY }; + out.w = w; + out.h = h; + out.x = x; + out.y = y; + out.centerX = x + w / 2; + out.centerY = y + h / 2; + return out; } export function findClosestElement( @@ -188,49 +192,67 @@ export function findClosestElement( elementsToCheck: Iterable, parentElement: LightningElement | null, direction: Direction, + allowOffscreen = false, ): LightningElement | null { - let closest: LightningElement[] = []; + let closest: LightningElement[] | null = null; let closestDistance = Number.MAX_VALUE; - const sourceDimensions = getDimensions(sourceElement, parentElement); + const sourceDimensions = fillDimensions(_scratchSource, sourceElement, parentElement); + const isHorizontal = direction & Direction.Horizontal; for (const otherElement of elementsToCheck) { - if ( - otherElement === sourceElement || - otherElement.node.w === 0 || - otherElement.node.h === 0 || - !otherElement.focusable - ) { + if (otherElement === sourceElement) { + continue; + } + + if (allowOffscreen) { + // When allowing offscreen focus, only skip elements that were never + // marked focusable. Visibility checks are skipped so virtualized/clipped + // children can still receive focus. Zero-dimension checks are kept + // because spatial navigation requires valid dimensions. + if ( + otherElement.node.w === 0 || + otherElement.node.h === 0 || + (!otherElement.focusable && !otherElement.focusableIntent) + ) { + continue; + } + } else if (otherElement.node.w === 0 || otherElement.node.h === 0 || !otherElement.focusable) { continue; } - const otherDimensions = getDimensions(otherElement, parentElement); - const { x: isOverlappedX, y: isOverlappedY } = isOverlap( - sourceDimensions, - otherDimensions, - ); + const otherDimensions = fillDimensions(_scratchTarget, otherElement, parentElement); - const distance = calculateShortestDistance( - direction, - sourceDimensions, - otherDimensions, - ); + // Inline overlap check for the relevant axis to avoid object allocation + const isOverlapped = isHorizontal + ? sourceDimensions.y < otherDimensions.y + otherDimensions.h && + sourceDimensions.y + sourceDimensions.h > otherDimensions.y + : sourceDimensions.x < otherDimensions.x + otherDimensions.w && + sourceDimensions.x + sourceDimensions.w > otherDimensions.x; + + const distance = calculateShortestDistance(direction, sourceDimensions, otherDimensions); if (distance === null) { continue; } - const isHorizontal = direction & Direction.Horizontal; - const isOverlapped = isHorizontal ? isOverlappedY : isOverlappedX; - if (distance < closestDistance && isOverlapped) { - closest = [otherElement]; + if (closest) { + closest.length = 0; + } else { + closest = []; + } + closest.push(otherElement); closestDistance = distance; } else if (distance === closestDistance) { closest?.push(otherElement); } } + if (!closest || closest.length === 0) { + return null; + } + // If we have multiple elements with the same closeness, then try to pick the // next element in the render tree. This may not be the same order as the // `elementsToCheck` array. diff --git a/packages/react-lightning/src/utils/isTextStyleProp.ts b/packages/react-lightning/src/utils/isTextStyleProp.ts index fd13661..5a77b08 100644 --- a/packages/react-lightning/src/utils/isTextStyleProp.ts +++ b/packages/react-lightning/src/utils/isTextStyleProp.ts @@ -29,8 +29,6 @@ textProps satisfies Partial>; export type TextProps = keyof typeof textProps; -export function isTextStyleProp( - prop: number | string | symbol, -): prop is TextProps { +export function isTextStyleProp(prop: number | string | symbol): prop is TextProps { return prop in textProps; } diff --git a/packages/react-lightning/src/utils/simpleDiff.spec.ts b/packages/react-lightning/src/utils/simpleDiff.spec.ts index bb6a258..191dca4 100644 --- a/packages/react-lightning/src/utils/simpleDiff.spec.ts +++ b/packages/react-lightning/src/utils/simpleDiff.spec.ts @@ -1,5 +1,6 @@ import { createElement } from 'react'; import { describe, expect, it } from 'vitest'; + import { simpleDiff } from './simpleDiff'; describe('simpleDiff', () => { @@ -82,10 +83,7 @@ describe('simpleDiff', () => { const first = { value: null, other: undefined }; const second = { value: 'test', other: undefined }; - const result = simpleDiff<{ value: string | null; other: undefined }>( - first, - second, - ); + const result = simpleDiff<{ value: string | null; other: undefined }>(first, second); expect(result).toEqual({ value: 'test' }); }); diff --git a/packages/react-lightning/src/utils/simpleDiff.ts b/packages/react-lightning/src/utils/simpleDiff.ts index 3dd3472..8c10cf9 100644 --- a/packages/react-lightning/src/utils/simpleDiff.ts +++ b/packages/react-lightning/src/utils/simpleDiff.ts @@ -1,7 +1,5 @@ const REACT_ELEMENT_TYPE = Symbol.for('react.element'); -const REACT_TRANSITIONAL_ELEMENT_TYPE = Symbol.for( - 'react.transitional.element', -); +const REACT_TRANSITIONAL_ELEMENT_TYPE = Symbol.for('react.transitional.element'); // This is a list of properties that should be deeply compared, specifically for // React elements. @@ -64,32 +62,38 @@ function areValuesEqual(first: unknown, second: unknown): boolean { // Handle objects - reference check already done above, so do shallow comparison if (typeof first === 'object' && typeof second === 'object') { - const firstKeys = Object.keys(first); - const secondKeys = Object.keys(second); - - if (firstKeys.length !== secondKeys.length) { - return false; + if (first instanceof Date && second instanceof Date) { + return first.getTime() === second.getTime(); } - for (const key of firstKeys) { + // Count keys and compare in a single pass without allocating arrays + let firstKeyCount = 0; + for (const key in first as Record) { + firstKeyCount++; const firstValue = (first as Record)[key]; const secondValue = (second as Record)[key]; if (!(key in second) || firstValue !== secondValue) { if (DEEP_PROPS.includes(key)) { - return areValuesEqual(firstValue, secondValue); + if (!areValuesEqual(firstValue, secondValue)) { + return false; + } + } else { + return false; } - - return false; } } - if (first instanceof Date && second instanceof Date) { - // Special case for Date objects - return first.getTime() === second.getTime(); + // Ensure second doesn't have extra keys + let secondKeyCount = 0; + for (const _ in second as Record) { + secondKeyCount++; + if (secondKeyCount > firstKeyCount) { + return false; + } } - return true; + return firstKeyCount === secondKeyCount; } // For all other cases (primitives of different types), they're not equal @@ -113,10 +117,7 @@ function areValuesEqual(first: unknown, second: unknown): boolean { * * Note, Symbols as keys are not supported, and will be ignored. */ -export function simpleDiff( - first: T, - second: T, -): Partial | null { +export function simpleDiff(first: T, second: T): Partial | null { // If objects are referentially equal, return null if (first === second) { return null; @@ -124,12 +125,10 @@ export function simpleDiff( const secondCopy = { ...second }; let hasDiffs = false; + let firstKeyCount = 0; - // Get all keys from both objects - const firstKeys = Object.keys(first); - const secondKeys = Object.keys(secondCopy); - - for (const key of firstKeys) { + for (const key in first) { + firstKeyCount++; const firstValue = first[key as keyof T]; const secondHasKey = key in secondCopy; const secondValue = secondCopy[key as keyof T]; @@ -151,8 +150,15 @@ export function simpleDiff( } // Check for keys that are only in the second object - if (!hasDiffs && firstKeys.length !== secondKeys.length) { - hasDiffs = true; + if (!hasDiffs) { + let secondKeyCount = 0; + for (const _ in second) { + secondKeyCount++; + if (secondKeyCount > firstKeyCount) { + hasDiffs = true; + break; + } + } } return hasDiffs ? secondCopy : null; diff --git a/packages/react-lightning/tsdown.config.ts b/packages/react-lightning/tsdown.config.ts index 1958bda..f146390 100644 --- a/packages/react-lightning/tsdown.config.ts +++ b/packages/react-lightning/tsdown.config.ts @@ -1,6 +1,7 @@ -import baseConfig from '@repo/configs/tsdown.config'; import { defineConfig, type UserConfig } from 'tsdown'; +import baseConfig from '@repo/configs/tsdown.config'; + const config: UserConfig = defineConfig({ ...baseConfig, entry: ['src/index.ts', 'src/types/jsx.d.ts'], diff --git a/packages/react-lightning/vite.config.ts b/packages/react-lightning/vite.config.ts index 6a7a0eb..221a29f 100644 --- a/packages/react-lightning/vite.config.ts +++ b/packages/react-lightning/vite.config.ts @@ -1,7 +1,8 @@ -import config from '@repo/configs/vite.config'; import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + export default defineConfig((env) => mergeConfig(config(env), { plugins: [externalizeDeps()], diff --git a/packages/react-native-lightning-components/src/exports/layout/Column.tsx b/packages/react-native-lightning-components/src/exports/layout/Column.tsx index bfc3822..a99d934 100644 --- a/packages/react-native-lightning-components/src/exports/layout/Column.tsx +++ b/packages/react-native-lightning-components/src/exports/layout/Column.tsx @@ -1,10 +1,11 @@ +import { type ForwardRefExoticComponent, forwardRef } from 'react'; +import type { ViewProps } from 'react-native'; + import type { LightningViewElement, Rect } from '@plextv/react-lightning'; import RLColumn, { type ColumnProps as RLColumnProps, } from '@plextv/react-lightning-components/layout/Column'; import { createLayoutEvent } from '@plextv/react-native-lightning'; -import { type ForwardRefExoticComponent, forwardRef, useCallback } from 'react'; -import type { ViewProps } from 'react-native'; export interface ColumnProps extends Omit { onLayout?: ViewProps['onLayout']; @@ -14,12 +15,9 @@ const Column: ForwardRefExoticComponent = forwardRef< LightningViewElement, ColumnProps >(({ onLayout, ...props }, ref) => { - const handleLayout = useCallback( - (rect: Rect) => { - onLayout?.(createLayoutEvent(rect)); - }, - [onLayout], - ); + const handleLayout = (rect: Rect) => { + onLayout?.(createLayoutEvent(rect)); + }; return ; }); diff --git a/packages/react-native-lightning-components/src/exports/layout/Row.tsx b/packages/react-native-lightning-components/src/exports/layout/Row.tsx index 7b87a36..bdc393d 100644 --- a/packages/react-native-lightning-components/src/exports/layout/Row.tsx +++ b/packages/react-native-lightning-components/src/exports/layout/Row.tsx @@ -1,28 +1,23 @@ +import { type ForwardRefExoticComponent, forwardRef } from 'react'; +import type { ViewProps } from 'react-native'; + import type { LightningViewElement, Rect } from '@plextv/react-lightning'; -import RLRow, { - type RowProps as RLRowProps, -} from '@plextv/react-lightning-components/layout/Row'; +import RLRow, { type RowProps as RLRowProps } from '@plextv/react-lightning-components/layout/Row'; import { createLayoutEvent } from '@plextv/react-native-lightning'; -import { type ForwardRefExoticComponent, forwardRef, useCallback } from 'react'; -import type { ViewProps } from 'react-native'; export interface RowProps extends Omit { onLayout?: ViewProps['onLayout']; } -const Row: ForwardRefExoticComponent = forwardRef< - LightningViewElement, - RowProps ->(({ onLayout, ...props }, ref) => { - const handleLayout = useCallback( - (rect: Rect) => { +const Row: ForwardRefExoticComponent = forwardRef( + ({ onLayout, ...props }, ref) => { + const handleLayout = (rect: Rect) => { onLayout?.(createLayoutEvent(rect)); - }, - [onLayout], - ); + }; - return ; -}); + return ; + }, +); Row.displayName = 'Row'; diff --git a/packages/react-native-lightning-components/tsconfig.json b/packages/react-native-lightning-components/tsconfig.json index 9e82ff4..73fca59 100644 --- a/packages/react-native-lightning-components/tsconfig.json +++ b/packages/react-native-lightning-components/tsconfig.json @@ -1,11 +1,7 @@ { "extends": "@repo/configs/tsconfig.react-library.json", "compilerOptions": { - "types": [ - "node", - "@plextv/react-lightning-plugin-flexbox/jsx", - "vite/client" - ] + "types": ["node", "@plextv/react-lightning-plugin-flexbox/jsx", "vite/client"] }, "include": ["src", "../../types/*.d.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/react-native-lightning-components/vite.config.ts b/packages/react-native-lightning-components/vite.config.ts index 9cdbacc..e1d0a1a 100644 --- a/packages/react-native-lightning-components/vite.config.ts +++ b/packages/react-native-lightning-components/vite.config.ts @@ -1,8 +1,10 @@ import path from 'node:path'; -import config from '@repo/configs/vite.config'; + import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + export default defineConfig((env) => mergeConfig(config(env), { plugins: [externalizeDeps()], diff --git a/packages/react-native-lightning/package.json b/packages/react-native-lightning/package.json index 56ca056..b9a9ad5 100644 --- a/packages/react-native-lightning/package.json +++ b/packages/react-native-lightning/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-native-lightning", - "description": "@plextv/react-lightning implementation for react-native", "version": "0.4.0", - "author": "Plex Inc.", + "description": "@plextv/react-lightning implementation for react-native", + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, + "files": [ + "dist" + ], "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -20,7 +23,6 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { @@ -28,7 +30,8 @@ "import": "./dist/index.js" }, "./package.json": "./package.json" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -37,9 +40,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "@plextv/react-lightning-components": "workspace:*", "@plextv/react-lightning-plugin-css-transform": "workspace:*", @@ -59,12 +59,12 @@ "react": "^19.2.3", "react-native": "^0.82.1" }, + "volta": { + "extends": "../../package.json" + }, "peerDependencyRules": { "allowedVersions": { "react": "^19" } - }, - "volta": { - "extends": "../../package.json" } } diff --git a/packages/react-native-lightning/src/exports/ActivityIndicator.tsx b/packages/react-native-lightning/src/exports/ActivityIndicator.tsx index 93361b2..5be777e 100644 --- a/packages/react-native-lightning/src/exports/ActivityIndicator.tsx +++ b/packages/react-native-lightning/src/exports/ActivityIndicator.tsx @@ -1,63 +1,58 @@ -import type { LightningViewElement } from '@plextv/react-lightning'; -import { htmlColorToLightningColor } from '@plextv/react-lightning-plugin-css-transform'; -import type { - ForwardRefExoticComponent, - RefAttributes, - // useCallback, -} from 'react'; +import type { ForwardRefExoticComponent, RefAttributes } from 'react'; import { forwardRef, useEffect, useState } from 'react'; import type { ActivityIndicatorProps as RNActivityIndicatorProps } from 'react-native'; + +import type { LightningViewElement } from '@plextv/react-lightning'; +import { htmlColorToLightningColor } from '@plextv/react-lightning-plugin-css-transform'; + import activityImage from '../../assets/activity.png'; -export type ActivityIndicatorProps = RNActivityIndicatorProps & - RefAttributes; - -export const ActivityIndicator: ForwardRefExoticComponent = - forwardRef( - ({ color, size }, ref) => { - const duration = 1200; - const [rotation, setRotation] = useState(0); - const actualColor = htmlColorToLightningColor( - (color as string) || 'lightblue', - ); - - let actualSize = 30; - - if (typeof size === 'number') { - actualSize = size; - } else if (size === 'large') { - actualSize = 80; - } - - useEffect(() => { - setRotation(Math.PI * 2); - }); - - return ( - - - - ); - }, +export type ActivityIndicatorProps = RNActivityIndicatorProps & RefAttributes; + +export const ActivityIndicator: ForwardRefExoticComponent = forwardRef< + LightningViewElement, + ActivityIndicatorProps +>(({ color, size }, ref) => { + const duration = 1200; + const [rotation, setRotation] = useState(0); + const actualColor = htmlColorToLightningColor((color as string) || 'lightblue'); + + let actualSize = 30; + + if (typeof size === 'number') { + actualSize = size; + } else if (size === 'large') { + actualSize = 80; + } + + useEffect(() => { + setRotation(Math.PI * 2); + }); + + return ( + + + ); +}); ActivityIndicator.displayName = 'ActivityIndicator'; diff --git a/packages/react-native-lightning/src/exports/Button.tsx b/packages/react-native-lightning/src/exports/Button.tsx index c44297e..1a1f800 100644 --- a/packages/react-native-lightning/src/exports/Button.tsx +++ b/packages/react-native-lightning/src/exports/Button.tsx @@ -1,26 +1,14 @@ -import { - type LightningViewElement, - useCombinedRef, - useFocus, -} from '@plextv/react-lightning'; -import { - type ForwardRefExoticComponent, - forwardRef, - type RefAttributes, -} from 'react'; -import type { - ButtonProps as RNButtonProps, - StyleProp, - ViewStyle, -} from 'react-native'; +import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from 'react'; +import type { ButtonProps as RNButtonProps, StyleProp, ViewStyle } from 'react-native'; + +import { type LightningViewElement, useCombinedRef, useFocus } from '@plextv/react-lightning'; + import { Pressable } from './Pressable'; import { Text } from './Text'; export type ButtonProps = RNButtonProps & RefAttributes & { - style?: - | StyleProp - | ((props: { pressed: boolean }) => StyleProp); + style?: StyleProp | ((props: { pressed: boolean }) => StyleProp); }; export const Button: ForwardRefExoticComponent = forwardRef< diff --git a/packages/react-native-lightning/src/exports/FocusGroup.tsx b/packages/react-native-lightning/src/exports/FocusGroup.tsx index 64f1247..2ccd370 100644 --- a/packages/react-native-lightning/src/exports/FocusGroup.tsx +++ b/packages/react-native-lightning/src/exports/FocusGroup.tsx @@ -1,16 +1,14 @@ +import { type ForwardRefExoticComponent, forwardRef } from 'react'; +import type { TargetedEvent } from 'react-native'; + import { type FocusableProps, type LightningElement, FocusGroup as RLFocusGroup, type FocusGroupProps as RLFocusGroupProps, } from '@plextv/react-lightning'; -import { type ForwardRefExoticComponent, forwardRef } from 'react'; -import type { TargetedEvent } from 'react-native'; -import { - type FocusHandler, - useBlurHandler, - useFocusHandler, -} from '../hooks/useFocusHandler'; + +import { type FocusHandler, useBlurHandler, useFocusHandler } from '../hooks/useFocusHandler'; import { useLayoutHandler } from '../hooks/useLayoutHandler'; import type { AddMissingProps } from '../types/AddMissingProps'; import type { ViewProps } from './View'; @@ -43,4 +41,6 @@ const FocusGroup: ForwardRefExoticComponent = forwardRef< ); }); +FocusGroup.displayName = 'RNLFocusGroup'; + export { FocusGroup }; diff --git a/packages/react-native-lightning/src/exports/Image.tsx b/packages/react-native-lightning/src/exports/Image.tsx index c83ec46..4fa9cfb 100644 --- a/packages/react-native-lightning/src/exports/Image.tsx +++ b/packages/react-native-lightning/src/exports/Image.tsx @@ -1,7 +1,3 @@ -import type { - LightningElementStyle, - LightningImageElement, -} from '@plextv/react-lightning'; import { type ForwardRefExoticComponent, forwardRef } from 'react'; import type { ImageSourcePropType, @@ -9,14 +5,15 @@ import type { Image as RNImage, ImageProps as RNImageProps, } from 'react-native'; + +import type { LightningElementStyle, LightningImageElement } from '@plextv/react-lightning'; + import { useImageLoadedHandler } from '../hooks/useImageLoadedHandler'; import { useLayoutHandler } from '../hooks/useLayoutHandler'; export type ImageProps = RNImageProps; -function isImageURISource( - source: ImageSourcePropType, -): source is ImageURISource { +function isImageURISource(source: ImageSourcePropType): source is ImageURISource { return !Array.isArray(source); } @@ -25,47 +22,40 @@ export type Image = RNImage & LightningImageElement; export const Image: ForwardRefExoticComponent = forwardRef< LightningImageElement, RNImageProps ->( - ( - { onLoad, onLayout, width, height, src, source, style, ...otherProps }, - ref, - ) => { - const handleImageLayout = useLayoutHandler(onLayout); - const handleImageLoaded = useImageLoadedHandler(src as string, onLoad); +>(({ onLoad, onLayout, width, height, src, source, style, ...otherProps }, ref) => { + const handleImageLayout = useLayoutHandler(onLayout); + const handleImageLoaded = useImageLoadedHandler(src as string, onLoad); - let finalSource: string | undefined; + let finalSource: string | undefined; - if (typeof source === 'object') { - if (!isImageURISource(source)) { - console.error( - '[Image] Lightning images only support ImageURISource as a source', - ); - } else { - finalSource = source.uri; - } - } else if (typeof source === 'number') { - console.error('[Image] Lightning images do not support numeric sources'); - } else if (source || src) { - finalSource = source ?? src; + if (typeof source === 'object') { + if (!isImageURISource(source)) { + console.error('[Image] Lightning images only support ImageURISource as a source'); } else { - return null; + finalSource = source.uri; } - - return ( - - ); - }, -); + } else if (typeof source === 'number') { + console.error('[Image] Lightning images do not support numeric sources'); + } else if (source || src) { + finalSource = source ?? src; + } else { + return null; + } + + return ( + + ); +}); Image.displayName = 'Image'; diff --git a/packages/react-native-lightning/src/exports/Platform.ts b/packages/react-native-lightning/src/exports/Platform.ts index dc04159..c36931d 100644 --- a/packages/react-native-lightning/src/exports/Platform.ts +++ b/packages/react-native-lightning/src/exports/Platform.ts @@ -23,11 +23,9 @@ function select( | { [platform in PlatformOSType | 'lightning']: T } | ({ [platform in PlatformOSType | 'lightning']?: T } & { default: T }), ): T; -function select( - specifics: { - [platform in PlatformOSType | 'lightning' | 'default']?: T; - }, -): T | undefined { +function select(specifics: { + [platform in PlatformOSType | 'lightning' | 'default']?: T; +}): T | undefined { return specifics.lightning ?? specifics.default; } diff --git a/packages/react-native-lightning/src/exports/Pressable.tsx b/packages/react-native-lightning/src/exports/Pressable.tsx index 39a7713..498a73c 100644 --- a/packages/react-native-lightning/src/exports/Pressable.tsx +++ b/packages/react-native-lightning/src/exports/Pressable.tsx @@ -1,39 +1,26 @@ -import type { KeyEvent } from '@plextv/react-lightning'; -import { - focusable, - Keys, - type LightningViewElement, -} from '@plextv/react-lightning'; -import type { - DependencyList, - ForwardRefExoticComponent, - RefAttributes, -} from 'react'; -import { useCallback, useState } from 'react'; +import type { ForwardRefExoticComponent, RefAttributes } from 'react'; +import { useState } from 'react'; import type { PressableProps as RNPressableProps } from 'react-native'; + +import type { KeyEvent } from '@plextv/react-lightning'; +import { focusable, Keys, type LightningViewElement } from '@plextv/react-lightning'; + import { useBlurHandler, useFocusHandler } from '../hooks/useFocusHandler'; import { useLayoutHandler } from '../hooks/useLayoutHandler'; import { createGestureResponderEvent } from '../utils/createGestureResponderEvent'; import { View, type ViewProps } from './View'; -export type PressableProps = RNPressableProps & - RefAttributes; +export type PressableProps = RNPressableProps & RefAttributes; -function useEnterKeyHandler( - handler: (e: KeyEvent) => void, - dependencies: DependencyList, -): (e: KeyEvent) => boolean { - return useCallback( - (e) => { - if (e.remoteKey === Keys.Enter) { - handler(e); - return false; - } +function useEnterKeyHandler(handler: (e: KeyEvent) => void): (e: KeyEvent) => boolean { + return (e) => { + if (e.remoteKey === Keys.Enter) { + handler(e); + return false; + } - return true; - }, - [...dependencies, handler], - ); + return true; + }; } export const Pressable: ForwardRefExoticComponent = focusable< @@ -62,35 +49,23 @@ export const Pressable: ForwardRefExoticComponent = focusable< const handleBlur = useBlurHandler(onBlur); const handleLayout = useLayoutHandler(onLayout); - const handleKeyDown = useEnterKeyHandler( - (e) => { - onPressIn?.(createGestureResponderEvent(e, ref)); - setState({ pressed: true }); - }, - [onPressIn, setState], - ); + const handleKeyDown = useEnterKeyHandler((e) => { + onPressIn?.(createGestureResponderEvent(e, ref)); + setState({ pressed: true }); + }); - const handleKeyUp = useEnterKeyHandler( - (e) => { - onPressOut?.(createGestureResponderEvent(e, ref)); - setState({ pressed: false }); - }, - [onPressOut, setState], - ); + const handleKeyUp = useEnterKeyHandler((e) => { + onPressOut?.(createGestureResponderEvent(e, ref)); + setState({ pressed: false }); + }); - const handleKeyPress = useEnterKeyHandler( - (e) => { - onPress?.(createGestureResponderEvent(e, ref)); - }, - [onPress], - ); + const handleKeyPress = useEnterKeyHandler((e) => { + onPress?.(createGestureResponderEvent(e, ref)); + }); - const handleLongPress = useEnterKeyHandler( - (e) => { - onLongPress?.(createGestureResponderEvent(e, ref)); - }, - [onLongPress], - ); + const handleLongPress = useEnterKeyHandler((e) => { + onLongPress?.(createGestureResponderEvent(e, ref)); + }); const finalStyle = typeof style === 'function' ? style(state) : style; diff --git a/packages/react-native-lightning/src/exports/ScrollView.tsx b/packages/react-native-lightning/src/exports/ScrollView.tsx index e01e73e..fb23ad2 100644 --- a/packages/react-native-lightning/src/exports/ScrollView.tsx +++ b/packages/react-native-lightning/src/exports/ScrollView.tsx @@ -1,15 +1,17 @@ -import { - type LightningElement, - LightningViewElement, - type LightningViewElementProps, -} from '@plextv/react-lightning'; import { createRef, PureComponent } from 'react'; -import type { JSX } from 'react/jsx-runtime'; import type { NativeScrollEvent, ScrollView as RNScrollView, ScrollViewProps as RNScrollViewProps, } from 'react-native'; +import type { JSX } from 'react/jsx-runtime'; + +import { + type LightningElement, + LightningViewElement, + type LightningViewElementProps, +} from '@plextv/react-lightning'; + import type { LightningViewElementStyle } from '../../../react-lightning/src/types'; import { createHandler } from '../hooks/useFocusHandler'; import type { NativeLightningViewElement } from '../types/NativeLightningViewElement'; @@ -51,17 +53,11 @@ function getAxisOffset( const itemMidPoint = childOffset + childSize / 2; const halfViewportSize = viewportSize / 2; - offset = Math.min( - Math.max(itemMidPoint - halfViewportSize, 0), - scrollableSize, - ); + offset = Math.min(Math.max(itemMidPoint - halfViewportSize, 0), scrollableSize); break; } case 'end': - offset = Math.max( - Math.min(childOffset + childSize - viewportSize, scrollableSize), - 0, - ); + offset = Math.max(Math.min(childOffset + childSize - viewportSize, scrollableSize), 0); break; } @@ -80,22 +76,10 @@ function getScrollInfo( } const x = horizontal - ? getAxisOffset( - viewport.w, - container.w, - child.x, - child.w, - snapToAlignment ?? 'start', - ) + ? getAxisOffset(viewport.w, container.w, child.x, child.w, snapToAlignment ?? 'start') : container.x; const y = !horizontal - ? getAxisOffset( - viewport.h, - container.h, - child.y, - child.h, - snapToAlignment ?? 'start', - ) + ? getAxisOffset(viewport.h, container.h, child.y, child.h, snapToAlignment ?? 'start') : container.y; return { @@ -107,10 +91,7 @@ function getScrollInfo( }; } -export class ScrollView extends PureComponent< - ScrollViewProps, - ScrollViewState -> { +export class ScrollView extends PureComponent { private _containerRef = createRef(); private _viewportRef = createRef(); @@ -192,8 +173,7 @@ export class ScrollView extends PureComponent< } public render(): JSX.Element { - const { children, style, contentContainerStyle, horizontal, ...props } = - this.props; + const { children, style, contentContainerStyle, horizontal, ...props } = this.props; const flexDirection = horizontal ? 'row' : 'column'; return ( @@ -233,15 +213,11 @@ export class ScrollView extends PureComponent< private _getChildOffset = (child?: LightningElement | null | Rect) => { const isElement = child instanceof LightningViewElement; - const rect = isElement - ? child.getBoundingClientRect(this._containerRef.current) - : child; + const rect = isElement ? child.getBoundingClientRect(this._containerRef.current) : child; return getScrollInfo( this._viewportRef.current?.getBoundingClientRect(), - this._containerRef.current?.getBoundingClientRect( - this._viewportRef.current, - ), + this._containerRef.current?.getBoundingClientRect(this._viewportRef.current), rect, // If we're getting offset via a positional value, we make sure we don't // use the snapToAlignment to calculate the offset since the offset should diff --git a/packages/react-native-lightning/src/exports/StyleSheet.ts b/packages/react-native-lightning/src/exports/StyleSheet.ts index 519aae4..8ebe89e 100644 --- a/packages/react-native-lightning/src/exports/StyleSheet.ts +++ b/packages/react-native-lightning/src/exports/StyleSheet.ts @@ -7,9 +7,7 @@ export function create( export function flatten(...args: T[]): Exclude { return Object.assign( {}, - ...args - .filter((obj) => obj != null && obj !== false) - .flat(Number.POSITIVE_INFINITY), + ...args.filter((obj) => obj != null && obj !== false).flat(Number.POSITIVE_INFINITY), ); } @@ -20,8 +18,8 @@ export function compose(style1: T, style2: T): T | NonNullable[] { return style1 || style2; } -export function setStyleAttributePreprocessor(...args: unknown[]): void { - console.log('>> setStyleAttributePreprocessor', args); +export function setStyleAttributePreprocessor(): void { + // no-op } export const hairlineWidth = 1; diff --git a/packages/react-native-lightning/src/exports/Text.tsx b/packages/react-native-lightning/src/exports/Text.tsx index 257e6ca..e26ce53 100644 --- a/packages/react-native-lightning/src/exports/Text.tsx +++ b/packages/react-native-lightning/src/exports/Text.tsx @@ -1,9 +1,8 @@ -import type { - LightningTextElement, - LightningTextElementStyle, -} from '@plextv/react-lightning'; -import { type ForwardRefExoticComponent, forwardRef, useMemo } from 'react'; +import { type ForwardRefExoticComponent, forwardRef } from 'react'; import type { Text as RNText, TextProps as RNTextProps } from 'react-native'; + +import type { LightningTextElement, LightningTextElementStyle } from '@plextv/react-lightning'; + import { useLayoutHandler } from '../hooks/useLayoutHandler'; import { useTextLayoutHandler } from '../hooks/useTextLayoutHandler'; @@ -24,10 +23,10 @@ export const Text: ForwardRefExoticComponent = forwardRef< onLayout, onTextLayout, // Press events ignored on purpose in lightning - onLongPress, - onPress, - onPressIn, - onPressOut, + onLongPress: _onLongPress, + onPress: _onPress, + onPressIn: _onPressIn, + onPressOut: _onPressOut, children, ellipsizeMode, numberOfLines, @@ -39,29 +38,21 @@ export const Text: ForwardRefExoticComponent = forwardRef< const handleTextLayout = useTextLayoutHandler(onTextLayout); const handleLayout = useLayoutHandler(onLayout); - const overflowStyle = useMemo(() => { - const overflow: LightningTextElementStyle = { - maxLines: numberOfLines, - }; - - if (ellipsizeMode === 'clip') { - overflow.textOverflow = 'clip'; - } else if (ellipsizeMode === 'tail') { - overflow.textOverflow = 'ellipsis'; - } + const overflowStyle: LightningTextElementStyle = { + maxLines: numberOfLines, + }; - return overflow; - }, [ellipsizeMode, numberOfLines]); + if (ellipsizeMode === 'clip') { + overflowStyle.textOverflow = 'clip'; + } else if (ellipsizeMode === 'tail') { + overflowStyle.textOverflow = 'ellipsis'; + } return ( diff --git a/packages/react-native-lightning/src/exports/TouchableHighlight.tsx b/packages/react-native-lightning/src/exports/TouchableHighlight.tsx index d87205f..edbbd13 100644 --- a/packages/react-native-lightning/src/exports/TouchableHighlight.tsx +++ b/packages/react-native-lightning/src/exports/TouchableHighlight.tsx @@ -1,30 +1,29 @@ -import type { LightningElement } from '@plextv/react-lightning'; -import { useCombinedRef, useFocus } from '@plextv/react-lightning'; import { type ForwardRefExoticComponent, forwardRef } from 'react'; import type { TouchableHighlightProps } from 'react-native'; + +import type { LightningElement } from '@plextv/react-lightning'; +import { useCombinedRef, useFocus } from '@plextv/react-lightning'; + import { Pressable } from './Pressable'; -export const TouchableHighlight: ForwardRefExoticComponent = - forwardRef( - ({ onLayout, activeOpacity, style, ...props }, ref) => { - const { ref: focusRef, focused } = useFocus(); - const combinedRef = useCombinedRef(ref, focusRef); +export const TouchableHighlight: ForwardRefExoticComponent = forwardRef< + LightningElement, + TouchableHighlightProps +>(({ onLayout, activeOpacity, style, ...props }, ref) => { + const { ref: focusRef, focused } = useFocus(); + const combinedRef = useCombinedRef(ref, focusRef); - const baseOpacity = focused ? 1 : 0.8; + const baseOpacity = focused ? 1 : 0.8; - return ( - [ - style, - { opacity: pressed ? (activeOpacity ?? 0.2) : baseOpacity }, - ]} - {...props} - onLayout={onLayout} - /> - ); - }, + return ( + [style, { opacity: pressed ? (activeOpacity ?? 0.2) : baseOpacity }]} + {...props} + onLayout={onLayout} + /> ); +}); TouchableHighlight.displayName = 'TouchableHighlight'; diff --git a/packages/react-native-lightning/src/exports/TouchableOpacity.tsx b/packages/react-native-lightning/src/exports/TouchableOpacity.tsx index 8ce35b6..e430ac6 100644 --- a/packages/react-native-lightning/src/exports/TouchableOpacity.tsx +++ b/packages/react-native-lightning/src/exports/TouchableOpacity.tsx @@ -1,30 +1,29 @@ -import type { LightningElement } from '@plextv/react-lightning'; -import { useCombinedRef, useFocus } from '@plextv/react-lightning'; import { type ForwardRefExoticComponent, forwardRef } from 'react'; import type { TouchableOpacityProps } from 'react-native'; + +import type { LightningElement } from '@plextv/react-lightning'; +import { useCombinedRef, useFocus } from '@plextv/react-lightning'; + import { Pressable } from './Pressable'; -export const TouchableOpacity: ForwardRefExoticComponent = - forwardRef( - ({ onLayout, activeOpacity, style, ...props }, ref) => { - const { ref: focusRef, focused } = useFocus(); - const combinedRef = useCombinedRef(ref, focusRef); +export const TouchableOpacity: ForwardRefExoticComponent = forwardRef< + LightningElement, + TouchableOpacityProps +>(({ onLayout, activeOpacity, style, ...props }, ref) => { + const { ref: focusRef, focused } = useFocus(); + const combinedRef = useCombinedRef(ref, focusRef); - const baseOpacity = focused ? 1 : 0.8; + const baseOpacity = focused ? 1 : 0.8; - return ( - [ - style, - { opacity: pressed ? (activeOpacity ?? 0.2) : baseOpacity }, - ]} - {...props} - onLayout={onLayout} - /> - ); - }, + return ( + [style, { opacity: pressed ? (activeOpacity ?? 0.2) : baseOpacity }]} + {...props} + onLayout={onLayout} + /> ); +}); TouchableOpacity.displayName = 'TouchableOpacity'; diff --git a/packages/react-native-lightning/src/exports/TouchableWithoutFeedback.tsx b/packages/react-native-lightning/src/exports/TouchableWithoutFeedback.tsx index b9b2374..d9dd932 100644 --- a/packages/react-native-lightning/src/exports/TouchableWithoutFeedback.tsx +++ b/packages/react-native-lightning/src/exports/TouchableWithoutFeedback.tsx @@ -1,21 +1,17 @@ -import { - type LightningViewElement, - useCombinedRef, - useFocus, -} from '@plextv/react-lightning'; import { type ForwardRefExoticComponent, forwardRef } from 'react'; import type { TouchableWithoutFeedbackProps } from 'react-native'; + +import { type LightningViewElement, useCombinedRef, useFocus } from '@plextv/react-lightning'; + import { Pressable } from './Pressable'; export const TouchableWithoutFeedback: ForwardRefExoticComponent = - forwardRef( - ({ onLayout, ...props }, ref) => { - const { ref: focusRef } = useFocus(); - const combinedRef = useCombinedRef(ref, focusRef); + forwardRef(({ onLayout, ...props }, ref) => { + const { ref: focusRef } = useFocus(); + const combinedRef = useCombinedRef(ref, focusRef); - return ; - }, - ); + return ; + }); TouchableWithoutFeedback.displayName = 'TouchableWithoutFeedback'; diff --git a/packages/react-native-lightning/src/exports/View.tsx b/packages/react-native-lightning/src/exports/View.tsx index fe3e28b..eca4c6c 100644 --- a/packages/react-native-lightning/src/exports/View.tsx +++ b/packages/react-native-lightning/src/exports/View.tsx @@ -1,3 +1,7 @@ +import type { ForwardRefExoticComponent, RefAttributes } from 'react'; +import { forwardRef } from 'react'; +import type { View as RNView, ViewProps as RNViewProps } from 'react-native'; + import type { FocusableProps, LightningElementEventProps, @@ -5,9 +9,7 @@ import type { LightningViewElementProps, } from '@plextv/react-lightning'; import type { AllStyleProps } from '@plextv/react-lightning-plugin-css-transform'; -import type { ForwardRefExoticComponent, RefAttributes } from 'react'; -import { forwardRef } from 'react'; -import type { View as RNView, ViewProps as RNViewProps } from 'react-native'; + import { useLayoutHandler } from '../hooks/useLayoutHandler'; import type { NativeLightningViewElement } from '../types/NativeLightningViewElement'; @@ -40,9 +42,7 @@ export const View: ForwardRefExoticComponent = forwardRef< >(({ onLayout, ...props }, ref) => { const handleLayout = useLayoutHandler(onLayout); - return ( - - ); + return ; }); View.displayName = 'View'; diff --git a/packages/react-native-lightning/src/hooks/useFocusHandler.ts b/packages/react-native-lightning/src/hooks/useFocusHandler.ts index c47cc29..ac2f1fc 100644 --- a/packages/react-native-lightning/src/hooks/useFocusHandler.ts +++ b/packages/react-native-lightning/src/hooks/useFocusHandler.ts @@ -1,8 +1,3 @@ -import { - type LightningElement, - LightningViewElement, -} from '@plextv/react-lightning'; -import { useCallback } from 'react'; import type { BlurEvent, FocusEvent, @@ -10,6 +5,9 @@ import type { TargetedEvent, ViewProps, } from 'react-native'; + +import { type LightningElement, LightningViewElement } from '@plextv/react-lightning'; + import { createNativeSyntheticEvent } from '../utils/createNativeSyntheticEvent'; export type FocusHandler = ( @@ -24,22 +22,14 @@ function handleEvent( onEvent: ViewProps[`on${Capitalize}`] | undefined, ): void { if (element instanceof LightningViewElement) { - onEvent?.( - createNativeSyntheticEvent( - { target: element.id }, - element, - ), - ); + onEvent?.(createNativeSyntheticEvent({ target: element.id }, element)); } } function useHandler( onEvent?: ViewProps[`on${Capitalize}`], ): FocusHandler | undefined { - const handler = useCallback>( - (element) => handleEvent(element, onEvent), - [onEvent], - ); + const handler: FocusHandler = (element) => handleEvent(element, onEvent); return onEvent ? handler : undefined; } diff --git a/packages/react-native-lightning/src/hooks/useImageLoadedHandler.ts b/packages/react-native-lightning/src/hooks/useImageLoadedHandler.ts index 70081c5..60b7206 100644 --- a/packages/react-native-lightning/src/hooks/useImageLoadedHandler.ts +++ b/packages/react-native-lightning/src/hooks/useImageLoadedHandler.ts @@ -3,8 +3,8 @@ */ import type { Dimensions } from '@lightningjs/renderer'; -import { useCallback } from 'react'; import type { ImageLoadEvent, ImageProps } from 'react-native'; + import { createNativeSyntheticEvent } from '../utils/createNativeSyntheticEvent'; type Handler = (event: Dimensions) => void; @@ -13,20 +13,17 @@ export function useImageLoadedHandler( src: string, rnOnLoadHandler?: ImageProps['onLoad'], ): Handler | undefined { - const handler = useCallback( - ({ w, h }) => { - rnOnLoadHandler?.( - createNativeSyntheticEvent({ - source: { - height: h, - width: w, - uri: src, - }, - }), - ); - }, - [src, rnOnLoadHandler], - ); + const handler: Handler = ({ w, h }) => { + rnOnLoadHandler?.( + createNativeSyntheticEvent({ + source: { + height: h, + width: w, + uri: src, + }, + }), + ); + }; return rnOnLoadHandler ? handler : undefined; } diff --git a/packages/react-native-lightning/src/hooks/useKeyEventHandler.ts b/packages/react-native-lightning/src/hooks/useKeyEventHandler.ts index 989bf1c..07ece5a 100644 --- a/packages/react-native-lightning/src/hooks/useKeyEventHandler.ts +++ b/packages/react-native-lightning/src/hooks/useKeyEventHandler.ts @@ -1,17 +1,15 @@ +import type { BaseSyntheticEvent, ModifierKey } from 'react'; + import type { KeyEvent, LightningElement, LightningViewElementProps, } from '@plextv/react-lightning'; -import { type BaseSyntheticEvent, type ModifierKey, useCallback } from 'react'; + import { createSyntheticEvent } from '../utils/createSyntheticEvent'; // Based on the KeyboardEvent interface from react, but extended properly for Lightning -type KeyboardEvent = BaseSyntheticEvent< - KeyEvent, - LightningElement, - LightningElement -> & { +type KeyboardEvent = BaseSyntheticEvent & { altKey: boolean; ctrlKey: boolean; code: string; @@ -34,31 +32,28 @@ export function useKeyEventHandler( eventType: 'onKeyDown' | 'onKeyUp', onKeyEvent?: (e: KeyboardEvent) => void, ): LightningViewElementProps[typeof eventType] | undefined { - const handler = useCallback( - (event: KeyEvent) => { - // Some ugly casting to force the typings to work - (onKeyEvent as unknown as (e: KeyboardEvent) => void)?.( - createSyntheticEvent(event.target, { - altKey: false, - ctrlKey: false, - code: event.code, - key: event.key, - // TODO - locale: 'en', - location: 0, - metaKey: false, - repeat: event.repeat, - shiftKey: false, - nativeEvent: event, - type: eventType, - getModifierState: () => false, - }), - ); + const handler = (event: KeyEvent) => { + // Some ugly casting to force the typings to work + (onKeyEvent as unknown as (e: KeyboardEvent) => void)?.( + createSyntheticEvent(event.target, { + altKey: false, + ctrlKey: false, + code: event.code, + key: event.key, + // TODO + locale: 'en', + location: 0, + metaKey: false, + repeat: event.repeat, + shiftKey: false, + nativeEvent: event, + type: eventType, + getModifierState: () => false, + }), + ); - return undefined; - }, - [eventType, onKeyEvent], - ); + return undefined; + }; return onKeyEvent ? handler : undefined; } diff --git a/packages/react-native-lightning/src/hooks/useLayoutHandler.ts b/packages/react-native-lightning/src/hooks/useLayoutHandler.ts index 41bd017..b7e6be4 100644 --- a/packages/react-native-lightning/src/hooks/useLayoutHandler.ts +++ b/packages/react-native-lightning/src/hooks/useLayoutHandler.ts @@ -1,23 +1,19 @@ -import type { Rect } from '@plextv/react-lightning'; -import { useCallback } from 'react'; import type { LayoutChangeEvent, ViewProps } from 'react-native'; + +import type { Rect } from '@plextv/react-lightning'; + import { createLayoutEvent } from '../utils/createLayoutEvent'; type Handler = (event: LayoutChangeEvent | Rect) => void; -export function useLayoutHandler( - onLayout?: ViewProps['onLayout'], -): Handler | undefined { - const handleLayout = useCallback( - (event) => { - if ('nativeEvent' in event) { - onLayout?.(event); - } else { - onLayout?.(createLayoutEvent(event)); - } - }, - [onLayout], - ); +export function useLayoutHandler(onLayout?: ViewProps['onLayout']): Handler | undefined { + const handleLayout: Handler = (event) => { + if ('nativeEvent' in event) { + onLayout?.(event); + } else { + onLayout?.(createLayoutEvent(event)); + } + }; return onLayout ? handleLayout : undefined; } diff --git a/packages/react-native-lightning/src/hooks/useTextLayoutHandler.ts b/packages/react-native-lightning/src/hooks/useTextLayoutHandler.ts index 37f6a77..564e9a5 100644 --- a/packages/react-native-lightning/src/hooks/useTextLayoutHandler.ts +++ b/packages/react-native-lightning/src/hooks/useTextLayoutHandler.ts @@ -3,8 +3,8 @@ */ import type { Dimensions } from '@lightningjs/renderer'; -import { useCallback } from 'react'; import type { TextLayoutEvent, TextProps } from 'react-native'; + import { createNativeSyntheticEvent } from '../utils/createNativeSyntheticEvent'; type Handler = (event: Dimensions) => void; @@ -12,7 +12,7 @@ type Handler = (event: Dimensions) => void; export function useTextLayoutHandler( onTextLayout?: TextProps['onTextLayout'], ): Handler | undefined { - const handler = useCallback(() => { + const handler: Handler = () => { onTextLayout?.( createNativeSyntheticEvent({ // TODO: Calculate lines properly @@ -20,7 +20,7 @@ export function useTextLayoutHandler( target: 0, }), ); - }, [onTextLayout]); + }; return onTextLayout ? handler : undefined; } diff --git a/packages/react-native-lightning/src/index.ts b/packages/react-native-lightning/src/index.ts index 5ebba5a..12078aa 100644 --- a/packages/react-native-lightning/src/index.ts +++ b/packages/react-native-lightning/src/index.ts @@ -8,10 +8,7 @@ export { useRef as usePlatformMethods } from 'react'; export * from 'react-native-web'; // Override RN exports with our own -export { - ActivityIndicator, - type ActivityIndicatorProps, -} from './exports/ActivityIndicator'; +export { ActivityIndicator, type ActivityIndicatorProps } from './exports/ActivityIndicator'; export { Button, type ButtonProps } from './exports/Button'; export { FocusGroup, type FocusGroupProps } from './exports/FocusGroup'; export { Image, type ImageProps } from './exports/Image'; @@ -20,14 +17,8 @@ export { Pressable, type PressableProps } from './exports/Pressable'; export { ScrollView, type ScrollViewProps } from './exports/ScrollView'; export * as StyleSheet from './exports/StyleSheet'; export { Text, type TextProps } from './exports/Text'; -export { - TouchableHighlight, - type TouchableHighlightProps, -} from './exports/TouchableHighlight'; -export { - TouchableOpacity, - type TouchableOpacityProps, -} from './exports/TouchableOpacity'; +export { TouchableHighlight, type TouchableHighlightProps } from './exports/TouchableHighlight'; +export { TouchableOpacity, type TouchableOpacityProps } from './exports/TouchableOpacity'; export { TouchableWithoutFeedback, type TouchableWithoutFeedbackProps, @@ -46,7 +37,4 @@ export { domPolyfillsPlugin } from './plugins/domPolyfillsPlugin'; export { reactNativePolyfillsPlugin } from './plugins/reactNativePolyfillsPlugin'; export { createLayoutEvent } from './utils/createLayoutEvent'; export { createSyntheticEvent } from './utils/createSyntheticEvent'; -export { - getReactNativePlugins, - type PluginOptions, -} from './utils/getReactNativePlugins'; +export { getReactNativePlugins, type PluginOptions } from './utils/getReactNativePlugins'; diff --git a/packages/react-native-lightning/src/plugins/cssClassNameTransformPlugin.ts b/packages/react-native-lightning/src/plugins/cssClassNameTransformPlugin.ts index 078f129..e035726 100644 --- a/packages/react-native-lightning/src/plugins/cssClassNameTransformPlugin.ts +++ b/packages/react-native-lightning/src/plugins/cssClassNameTransformPlugin.ts @@ -45,11 +45,7 @@ export const cssClassNameTransformPlugin = (): Plugin => { let selectedRule: CSSStyleRule | null = null; for (const rule of sheet.cssRules) { - if ( - rule && - 'selectorText' in rule && - rule.selectorText === `.${value}` - ) { + if (rule && 'selectorText' in rule && rule.selectorText === `.${value}`) { selectedRule = rule as CSSStyleRule; break; } diff --git a/packages/react-native-lightning/src/plugins/domPolyfillsPlugin.ts b/packages/react-native-lightning/src/plugins/domPolyfillsPlugin.ts index f3edfe4..97ef33f 100644 --- a/packages/react-native-lightning/src/plugins/domPolyfillsPlugin.ts +++ b/packages/react-native-lightning/src/plugins/domPolyfillsPlugin.ts @@ -4,6 +4,7 @@ import { LightningViewElement, type Plugin, } from '@plextv/react-lightning'; + import type { NativeLightningViewElement } from '../types/NativeLightningViewElement'; const ELEMENT_NODE = 1; @@ -156,16 +157,12 @@ export const domPolyfillsPlugin = (): Plugin => { }, {} as PropertyDescriptorMap), ); - Object.defineProperty( - LightningViewElement.prototype, - '__domPolyfillsAdded', - { - value: true, - configurable: false, - enumerable: false, - writable: false, - }, - ); + Object.defineProperty(LightningViewElement.prototype, '__domPolyfillsAdded', { + value: true, + configurable: false, + enumerable: false, + writable: false, + }); return Promise.resolve(); }, diff --git a/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts b/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts index 0c16831..6d1282e 100644 --- a/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts +++ b/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts @@ -1,13 +1,10 @@ -import { - type FocusManager, - LightningViewElement, - type Plugin, -} from '@plextv/react-lightning'; -import { flattenStyles } from '@plextv/react-lightning-plugin-css-transform'; import { type Fiber as ItsFineFiber, traverseFiber } from 'its-fine'; import type { NativeMethods } from 'react-native'; import type { Fiber } from 'react-reconciler'; +import { type FocusManager, LightningViewElement, type Plugin } from '@plextv/react-lightning'; +import { flattenStyles } from '@plextv/react-lightning-plugin-css-transform'; + type LightningNativeViewElement = NativeMethods & LightningViewElement & { __loaded?: boolean; @@ -19,9 +16,7 @@ type LightningNativeViewElement = NativeMethods & // Kind of hacky, but we can't use hooks here. We need to somehow traverse the // fiber tree to find the focus manager though, so we can set destinations for // focusable elements. -function tryFindFocusManager( - fiber: Fiber, -): FocusManager | null { +function tryFindFocusManager(fiber: Fiber): FocusManager | null { try { const context = traverseFiber( fiber as ItsFineFiber, @@ -33,11 +28,10 @@ function tryFindFocusManager( ); if (context) { - return context.memoizedProps.value - .focusManager as FocusManager; + return context.memoizedProps.value.focusManager as FocusManager; } } catch (error) { - console.warn('>> React fiber access failed:', error); + console.warn('[react-native-lightning] React fiber access failed:', error); } return null; @@ -74,25 +68,19 @@ export const reactNativePolyfillsPlugin = (): Plugin => { const rect = this.getBoundingClientRect(); callback(this.node.x, this.node.y, rect.w, rect.h, rect.x, rect.y); } else { - this.__loadPromise.then(() => - nativeMethods.measure.call(this, callback), - ); + this.__loadPromise.then(() => nativeMethods.measure.call(this, callback)); } }, measureInWindow(this: LightningNativeViewElement, callback) { if (this.__loaded) { callback(this.node.x, this.node.y, this.node.w, this.node.h); } else { - this.__loadPromise.then(() => - nativeMethods.measureInWindow.call(this, callback), - ); + this.__loadPromise.then(() => nativeMethods.measureInWindow.call(this, callback)); } }, measureLayout(this: LightningNativeViewElement, relative, onSuccess) { if (this.__loaded) { - const { x, y } = this.getRelativePosition( - relative as unknown as LightningViewElement, - ); + const { x, y } = this.getRelativePosition(relative as unknown as LightningViewElement); onSuccess(x, y, this.node.w, this.node.h); } else { diff --git a/packages/react-native-lightning/src/utils/createGestureResponderEvent.ts b/packages/react-native-lightning/src/utils/createGestureResponderEvent.ts index fb7e423..b844a32 100644 --- a/packages/react-native-lightning/src/utils/createGestureResponderEvent.ts +++ b/packages/react-native-lightning/src/utils/createGestureResponderEvent.ts @@ -1,11 +1,11 @@ import type { GestureResponderEvent } from 'react-native'; export function createGestureResponderEvent( - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO originalEvent?: any, - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO currentTarget?: any, - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO originalTarget?: any, ): GestureResponderEvent { return { diff --git a/packages/react-native-lightning/src/utils/createLayoutEvent.ts b/packages/react-native-lightning/src/utils/createLayoutEvent.ts index 14890fe..ff814e6 100644 --- a/packages/react-native-lightning/src/utils/createLayoutEvent.ts +++ b/packages/react-native-lightning/src/utils/createLayoutEvent.ts @@ -1,6 +1,7 @@ -import type { Rect } from '@plextv/react-lightning'; import type { LayoutChangeEvent } from 'react-native'; +import type { Rect } from '@plextv/react-lightning'; + export function createLayoutEvent({ x, y, w, h }: Rect): LayoutChangeEvent { return { // $FlowFixMe diff --git a/packages/react-native-lightning/src/utils/createNativeSyntheticEvent.ts b/packages/react-native-lightning/src/utils/createNativeSyntheticEvent.ts index fc5fe6e..f4c7522 100644 --- a/packages/react-native-lightning/src/utils/createNativeSyntheticEvent.ts +++ b/packages/react-native-lightning/src/utils/createNativeSyntheticEvent.ts @@ -1,8 +1,8 @@ -import type { LightningElement } from '@plextv/react-lightning'; import type { NativeSyntheticEvent } from 'react-native'; -type TypeFromEventOrEventHandler = - T extends NativeSyntheticEvent ? U : T; +import type { LightningElement } from '@plextv/react-lightning'; + +type TypeFromEventOrEventHandler = T extends NativeSyntheticEvent ? U : T; export function createNativeSyntheticEvent( event: TypeFromEventOrEventHandler, diff --git a/packages/react-native-lightning/src/utils/createSyntheticEvent.ts b/packages/react-native-lightning/src/utils/createSyntheticEvent.ts index 51353fd..4c2197a 100644 --- a/packages/react-native-lightning/src/utils/createSyntheticEvent.ts +++ b/packages/react-native-lightning/src/utils/createSyntheticEvent.ts @@ -1,6 +1,7 @@ -import type { LightningViewElement } from '@plextv/react-lightning'; import type { BaseSyntheticEvent } from 'react'; +import type { LightningViewElement } from '@plextv/react-lightning'; + const defaultEventProps = { bubbles: true, cancelable: false, @@ -22,15 +23,10 @@ defaultEventProps satisfies Partial< type DefaultEventProps = typeof defaultEventProps; export function createSyntheticEvent< - E extends BaseSyntheticEvent< - unknown, - LightningViewElement, - LightningViewElement - >, + E extends BaseSyntheticEvent, >( target: LightningViewElement, - props: Omit & - Partial, + props: Omit & Partial, ): E { return { currentTarget: target, diff --git a/packages/react-native-lightning/src/utils/getReactNativePlugins.ts b/packages/react-native-lightning/src/utils/getReactNativePlugins.ts index 0e1e2fe..a399f2c 100644 --- a/packages/react-native-lightning/src/utils/getReactNativePlugins.ts +++ b/packages/react-native-lightning/src/utils/getReactNativePlugins.ts @@ -1,9 +1,7 @@ import type { Plugin } from '@plextv/react-lightning'; import { plugin as cssPlugin } from '@plextv/react-lightning-plugin-css-transform'; -import { - plugin as flexboxPlugin, - type YogaOptions, -} from '@plextv/react-lightning-plugin-flexbox'; +import { plugin as flexboxPlugin, type YogaOptions } from '@plextv/react-lightning-plugin-flexbox'; + import { cssClassNameTransformPlugin } from '../plugins/cssClassNameTransformPlugin'; import { domPolyfillsPlugin } from '../plugins/domPolyfillsPlugin'; import { reactNativePolyfillsPlugin } from '../plugins/reactNativePolyfillsPlugin'; diff --git a/packages/react-native-lightning/tsconfig.json b/packages/react-native-lightning/tsconfig.json index 8f8ca2b..d8fa551 100644 --- a/packages/react-native-lightning/tsconfig.json +++ b/packages/react-native-lightning/tsconfig.json @@ -1,11 +1,7 @@ { "extends": "@repo/configs/tsconfig.react-library.json", "compilerOptions": { - "types": [ - "node", - "@plextv/react-lightning-plugin-css-transform/jsx", - "vite/client" - ] + "types": ["node", "@plextv/react-lightning-plugin-css-transform/jsx", "vite/client"] }, "include": ["src", "../../types/*.d.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/react-native-lightning/vite.config.mts b/packages/react-native-lightning/vite.config.mts index 6a7a0eb..221a29f 100644 --- a/packages/react-native-lightning/vite.config.mts +++ b/packages/react-native-lightning/vite.config.mts @@ -1,7 +1,8 @@ -import config from '@repo/configs/vite.config'; import { defineConfig, mergeConfig, type UserConfig } from 'vite'; import { externalizeDeps } from 'vite-plugin-externalize-deps'; +import config from '@repo/configs/vite.config'; + export default defineConfig((env) => mergeConfig(config(env), { plugins: [externalizeDeps()], diff --git a/packages/vite-plugin-msdf-fontgen/package.json b/packages/vite-plugin-msdf-fontgen/package.json index a9c5bf1..b8d7674 100644 --- a/packages/vite-plugin-msdf-fontgen/package.json +++ b/packages/vite-plugin-msdf-fontgen/package.json @@ -1,18 +1,21 @@ { "name": "@plextv/vite-plugin-msdf-fontgen", - "description": "Vite plugin for generating MSDF fonts for use in Lightningjs", "version": "1.3.5", - "author": "Plex Inc.", + "description": "Vite plugin for generating MSDF fonts for use in Lightningjs", + "keywords": [ + "vite-plugin" + ], + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, - "keywords": [ - "vite-plugin" + "files": [ + "dist" ], "type": "module", "types": "./dist/index.d.mts", @@ -21,8 +24,8 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, - "access": "public" + "access": "public", + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -32,9 +35,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "@lightningjs/msdf-generator": "1.2.0", "crc-32": "1.2.2", diff --git a/packages/vite-plugin-msdf-fontgen/src/checksum.ts b/packages/vite-plugin-msdf-fontgen/src/checksum.ts index 3abd9a1..893d36b 100644 --- a/packages/vite-plugin-msdf-fontgen/src/checksum.ts +++ b/packages/vite-plugin-msdf-fontgen/src/checksum.ts @@ -1,32 +1,26 @@ import { existsSync } from 'node:fs'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; + import crc32 from 'crc-32'; const CHECKSUM_FILENAME = 'hash.json'; -export async function readFileChecksum( - filePath: string, -): Promise { +export async function readFileChecksum(filePath: string): Promise { try { const fileData = await readFile(filePath); return crc32.buf(fileData, 0); - } catch (_err) { + } catch { return null; } } -export async function readChecksumCache( - checksumFolder: string, -): Promise> { +export async function readChecksumCache(checksumFolder: string): Promise> { try { console.info('Loading checksums from cache...'); - const checksumsData = await readFile( - path.join(checksumFolder, CHECKSUM_FILENAME), - 'utf8', - ); + const checksumsData = await readFile(path.join(checksumFolder, CHECKSUM_FILENAME), 'utf8'); return JSON.parse(checksumsData.toString()); } catch (err) { @@ -46,9 +40,7 @@ export async function writeChecksumCache( ): Promise { try { if (!existsSync(checksumFolder)) { - console.info( - `Cache folder doesn't exist. Creating one at ${checksumFolder}`, - ); + console.info(`Cache folder doesn't exist. Creating one at ${checksumFolder}`); await mkdir(checksumFolder, { recursive: true }); } diff --git a/packages/vite-plugin-msdf-fontgen/src/configs.ts b/packages/vite-plugin-msdf-fontgen/src/configs.ts index 2f69bb8..6625ea7 100644 --- a/packages/vite-plugin-msdf-fontgen/src/configs.ts +++ b/packages/vite-plugin-msdf-fontgen/src/configs.ts @@ -1,5 +1,6 @@ import { existsSync } from 'node:fs'; import { unlink, writeFile } from 'node:fs/promises'; + import type { OptionsInput } from './types'; export async function ensureConfigsExist({ @@ -34,9 +35,7 @@ export async function ensureConfigsExist({ await writeFile(overridesFile, JSON.stringify(overrides, null, 2)); cleanupFiles.push(overridesFile); } else { - console.info( - ' Overrides file not found and no overrides provided. Skipping.', - ); + console.info(' Overrides file not found and no overrides provided. Skipping.'); } } diff --git a/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts b/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts index b6a0dc7..e8c9d62 100644 --- a/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts +++ b/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts @@ -1,7 +1,9 @@ import { cp } from 'node:fs/promises'; import path from 'node:path'; + import { genFont, setGeneratePaths } from '@lightningjs/msdf-generator'; import { adjustFont } from '@lightningjs/msdf-generator/adjustFont'; + import { readFileChecksum, writeChecksumCache } from './checksum'; import { ensureConfigsExist } from './configs'; import getFiles from './getFiles'; @@ -25,12 +27,7 @@ export default async function generateFonts( return; } - for (const { - inputDir, - outputDir, - charsetChecksum, - files: fontFiles, - } of files) { + for (const { inputDir, outputDir, charsetChecksum, files: fontFiles } of files) { checksums[charsetFile] = charsetChecksum; setGeneratePaths(inputDir, outputDir, charsetFile); diff --git a/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts b/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts index 05c3fd4..8e9f27d 100644 --- a/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts +++ b/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts @@ -1,4 +1,5 @@ import { existsSync } from 'node:fs'; + import { readFileChecksum } from './checksum'; export async function getFileChangeInfo( diff --git a/packages/vite-plugin-msdf-fontgen/src/getFiles.ts b/packages/vite-plugin-msdf-fontgen/src/getFiles.ts index 172576b..0c9a087 100644 --- a/packages/vite-plugin-msdf-fontgen/src/getFiles.ts +++ b/packages/vite-plugin-msdf-fontgen/src/getFiles.ts @@ -1,5 +1,7 @@ import path from 'node:path'; + import { glob } from 'glob'; + import { getFileChangeInfo } from './getFileChangeInfo'; import { sortByExtension } from './sortByExtension'; import type { OptionsInput } from './types'; @@ -26,24 +28,18 @@ export default async function getFiles( checksums: Record, ): Promise { const fontFiles: Record = {}; - const extensionGlob = - extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; + const extensionGlob = extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; console.log('Looking for fonts...'); const inputFonts = await glob(`${src}/**/*.${extensionGlob}`); const charsetFileChangeInfo = await getFileChangeInfo(charsetFile, checksums); const shouldForce = force || charsetFileChangeInfo.needsUpdate; - console.info( - `Found ${inputFonts.length} files:\n ${inputFonts.join('\n ')}`, - ); + console.info(`Found ${inputFonts.length} files:\n ${inputFonts.join('\n ')}`); // Sort files by extension so it follows the order of the extensions option for (const inputFontFile of inputFonts.sort(sortByExtension(extensions))) { - const inputFileChangeInfo = await getFileChangeInfo( - inputFontFile, - checksums, - ); + const inputFileChangeInfo = await getFileChangeInfo(inputFontFile, checksums); const inputDir = path.dirname(inputFontFile); const outputDir = path.join(dest, path.relative(src, inputDir)); @@ -71,10 +67,7 @@ export default async function getFiles( for (const type of types) { const { name } = path.parse(fontFilename); const outputFile = path.join(outputDir, `${name}.${type}.png`); - const outputFileChangeInfo = await getFileChangeInfo( - outputFile, - checksums, - ); + const outputFileChangeInfo = await getFileChangeInfo(outputFile, checksums); if (shouldForce || outputFileChangeInfo.needsUpdate) { outputs.push({ diff --git a/packages/vite-plugin-msdf-fontgen/src/index.ts b/packages/vite-plugin-msdf-fontgen/src/index.ts index a2efff1..eb259a6 100644 --- a/packages/vite-plugin-msdf-fontgen/src/index.ts +++ b/packages/vite-plugin-msdf-fontgen/src/index.ts @@ -1,5 +1,7 @@ import path from 'node:path'; + import type { PluginOption } from 'vite'; + import { readChecksumCache } from './checksum'; import generateFonts from './generateFonts'; import type { OptionsArg, OptionsInput, OptionsInputArg } from './types'; @@ -21,13 +23,7 @@ export default function msdfFontGen({ for (const input of inputs) { const options = getOptions(input); - await generateFonts( - options, - force, - checksums, - cacheFolder, - copyOriginalToDestDir, - ); + await generateFonts(options, force, checksums, cacheFolder, copyOriginalToDestDir); } }, }; diff --git a/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts b/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts index cc316c2..1c5de75 100644 --- a/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts +++ b/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts @@ -1,6 +1,4 @@ -export function sortByExtension( - extensions: string[], -): (a: string, b: string) => number { +export function sortByExtension(extensions: string[]): (a: string, b: string) => number { return (a: string, b: string): number => { const extA = a.split('.').pop() as string; const extB = b.split('.').pop() as string; diff --git a/packages/vite-plugin-msdf-fontgen/src/types.ts b/packages/vite-plugin-msdf-fontgen/src/types.ts index 67f2ea8..f19450a 100644 --- a/packages/vite-plugin-msdf-fontgen/src/types.ts +++ b/packages/vite-plugin-msdf-fontgen/src/types.ts @@ -74,10 +74,7 @@ export interface Options { type RequiredProps = Omit & { [P in K]-?: T[P] }; -export type OptionsInputArg = RequiredProps< - Partial, - 'src' | 'dest' ->; +export type OptionsInputArg = RequiredProps, 'src' | 'dest'>; export type OptionsArg = Omit, 'inputs'> & { inputs: OptionsInputArg[]; diff --git a/packages/vite-plugin-react-native-lightning/package.json b/packages/vite-plugin-react-native-lightning/package.json index 3e3107d..3b4a162 100644 --- a/packages/vite-plugin-react-native-lightning/package.json +++ b/packages/vite-plugin-react-native-lightning/package.json @@ -1,20 +1,23 @@ { "name": "@plextv/vite-plugin-react-native-lightning", - "description": "Vite plugin for adding react-native-lightning support", "version": "0.4.1", - "author": "Plex Inc.", + "description": "Vite plugin for adding react-native-lightning support", + "keywords": [ + "lightning-js", + "react-native", + "vite-plugin" + ], + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, - "keywords": [ - "vite-plugin", - "react-native", - "lightning-js" + "files": [ + "dist" ], "type": "module", "types": "./dist/index.d.mts", @@ -23,8 +26,8 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, - "access": "public" + "access": "public", + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -34,9 +37,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "@vitejs/plugin-react": "5.1.2" }, diff --git a/packages/vite-plugin-react-reanimated-lightning/package.json b/packages/vite-plugin-react-reanimated-lightning/package.json index 9433902..99a728e 100644 --- a/packages/vite-plugin-react-reanimated-lightning/package.json +++ b/packages/vite-plugin-react-reanimated-lightning/package.json @@ -1,21 +1,24 @@ { "name": "@plextv/vite-plugin-react-reanimated-lightning", - "description": "Vite plugin for @plextv/react-lightning-plugin-reanimated", "version": "0.4.1", - "author": "Plex Inc.", + "description": "Vite plugin for @plextv/react-lightning-plugin-reanimated", + "keywords": [ + "lightning-js", + "react-native", + "react-reanimated", + "vite-plugin" + ], + "bugs": { + "url": "https://github.com/plexinc/react-lightning/issues/new" + }, "license": "MIT", + "author": "Plex Inc.", "repository": { "type": "git", "url": "git+https://github.com/plexinc/react-lightning.git" }, - "bugs": { - "url": "https://github.com/plexinc/react-lightning/issues/new" - }, - "keywords": [ - "vite-plugin", - "react-native", - "react-reanimated", - "lightning-js" + "files": [ + "dist" ], "type": "module", "types": "./dist/index.d.mts", @@ -24,8 +27,8 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, - "access": "public" + "access": "public", + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -35,9 +38,6 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { "@repo/configs": "workspace:*" }, diff --git a/packages/vite-plugin-react-reanimated-lightning/src/index.ts b/packages/vite-plugin-react-reanimated-lightning/src/index.ts index b1dbc48..f06b4cc 100644 --- a/packages/vite-plugin-react-reanimated-lightning/src/index.ts +++ b/packages/vite-plugin-react-reanimated-lightning/src/index.ts @@ -1,4 +1,5 @@ import { createRequire } from 'node:module'; + import type { Plugin } from 'vite'; type Options = { @@ -13,19 +14,13 @@ const plugin = (options?: Options): Plugin => { // This needs to be first try { alias['react-native-reanimated/scripts/validate-worklets-version'] = - require.resolve( - 'react-native-reanimated/scripts/validate-worklets-version', - ); + require.resolve('react-native-reanimated/scripts/validate-worklets-version'); } catch { // Do nothing } - alias['react-native-reanimated'] = require.resolve( - '@plextv/react-lightning-plugin-reanimated', - ); - alias['react-native-reanimated-original'] = require.resolve( - 'react-native-reanimated', - ); + alias['react-native-reanimated'] = require.resolve('@plextv/react-lightning-plugin-reanimated'); + alias['react-native-reanimated-original'] = require.resolve('react-native-reanimated'); return { name: 'vite-react-reanimated-lightning', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 282bab7..6cfaf50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: devDependencies: - '@biomejs/biome': - specifier: 2.3.11 - version: 2.3.11 '@changesets/cli': specifier: 2.29.8 version: 2.29.8(@types/node@25.0.9) @@ -35,6 +32,15 @@ importers: listr2: specifier: 10.0.0 version: 10.0.0 + oxfmt: + specifier: 0.35.0 + version: 0.35.0 + oxlint: + specifier: 1.50.0 + version: 1.50.0(oxlint-tsgolint@0.15.0) + oxlint-tsgolint: + specifier: 0.15.0 + version: 0.15.0 tsdown: specifier: 0.19.0 version: 0.19.0(typescript@5.9.3) @@ -111,6 +117,9 @@ importers: '@vitejs/plugin-react': specifier: 5.1.2 version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 vite-tsconfig-paths: specifier: 6.0.4 version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -166,9 +175,6 @@ importers: '@repo/configs': specifier: workspace:* version: link:../../packages/configs - '@shopify/flash-list': - specifier: 2.2.0 - version: 2.2.0(@babel/runtime@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) '@types/react': specifier: 19.2.8 version: 19.2.8 @@ -178,6 +184,9 @@ importers: '@vitejs/plugin-legacy': specifier: 7.2.1 version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 typescript: specifier: 5.9.3 version: 5.9.3 @@ -248,8 +257,18 @@ importers: '@types/react': specifier: 19.2.8 version: 19.2.8 + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 - packages/configs: {} + packages/configs: + devDependencies: + '@rollup/plugin-babel': + specifier: 7.0.0 + version: 7.0.0(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.55.2) + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 packages/plugin-css-transform: dependencies: @@ -446,9 +465,6 @@ importers: '@plextv/react-native-lightning': specifier: workspace:^ version: link:../react-native-lightning - '@shopify/flash-list': - specifier: ^2.2.0 - version: 2.2.0(@babel/runtime@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) react: specifier: ^19.2.3 version: 19.2.3 @@ -1138,59 +1154,6 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.3.11': - resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.3.11': - resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.3.11': - resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.3.11': - resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-arm64@2.3.11': - resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-x64-musl@2.3.11': - resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-linux-x64@2.3.11': - resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-win32-arm64@2.3.11': - resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.3.11': - resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} @@ -1805,6 +1768,264 @@ packages: '@oxc-project/types@0.108.0': resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==} + '@oxfmt/binding-android-arm-eabi@0.35.0': + resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.35.0': + resolution: {integrity: sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.35.0': + resolution: {integrity: sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.35.0': + resolution: {integrity: sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.35.0': + resolution: {integrity: sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': + resolution: {integrity: sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.35.0': + resolution: {integrity: sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.35.0': + resolution: {integrity: sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-arm64-musl@0.35.0': + resolution: {integrity: sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-ppc64-gnu@0.35.0': + resolution: {integrity: sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-gnu@0.35.0': + resolution: {integrity: sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-musl@0.35.0': + resolution: {integrity: sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-s390x-gnu@0.35.0': + resolution: {integrity: sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxfmt/binding-linux-x64-gnu@0.35.0': + resolution: {integrity: sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-linux-x64-musl@0.35.0': + resolution: {integrity: sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-openharmony-arm64@0.35.0': + resolution: {integrity: sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.35.0': + resolution: {integrity: sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.35.0': + resolution: {integrity: sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.35.0': + resolution: {integrity: sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.15.0': + resolution: {integrity: sha512-d7Ch+A6hic+RYrm32+Gh1o4lOrQqnFsHi721ORdHUDBiQPea+dssKUEMwIbA6MKmCy6TVJ02sQyi24OEfCiGzw==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.15.0': + resolution: {integrity: sha512-Aoai2wAkaUJqp/uEs1gml6TbaPW4YmyO5Ai/vOSkiizgHqVctjhjKqmRiWTX2xuPY94VkwOLqp+Qr3y/0qSpWQ==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.15.0': + resolution: {integrity: sha512-4og13a7ec4Vku5t2Y7s3zx6YJP6IKadb1uA9fOoRH6lm/wHWoCnxjcfJmKHXRZJII81WmbdJMSPxaBfwN/S68Q==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.15.0': + resolution: {integrity: sha512-9b9xzh/1Harn3a+XiKTK/8LrWw3VcqLfYp/vhV5/zAVR2Mt0d63WSp4FL+wG7DKnI2T/CbMFUFHwc7kCQjDMzQ==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.15.0': + resolution: {integrity: sha512-nNac5hewHdkk5mowOwTqB1ZD76zB/FsUiyUvdCyupq5cG54XyKqSLEp9QGbx7wFJkWCkeWmuwRed4sfpAlKaeA==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.15.0': + resolution: {integrity: sha512-ioAY2XLpy83E2EqOLH9p1cEgj0G2qB1lmAn0a3yFV1jHQB29LIPIKGNsu/tYCClpwmHN79pT5KZAHZOgWxxqNg==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.50.0': + resolution: {integrity: sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.50.0': + resolution: {integrity: sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.50.0': + resolution: {integrity: sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.50.0': + resolution: {integrity: sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.50.0': + resolution: {integrity: sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + resolution: {integrity: sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + resolution: {integrity: sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.50.0': + resolution: {integrity: sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.50.0': + resolution: {integrity: sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + resolution: {integrity: sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + resolution: {integrity: sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.50.0': + resolution: {integrity: sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.50.0': + resolution: {integrity: sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.50.0': + resolution: {integrity: sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.50.0': + resolution: {integrity: sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.50.0': + resolution: {integrity: sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.50.0': + resolution: {integrity: sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.50.0': + resolution: {integrity: sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.50.0': + resolution: {integrity: sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2060,6 +2281,19 @@ packages: '@rolldown/pluginutils@1.0.0-beta.60': resolution: {integrity: sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==} + '@rollup/plugin-babel@7.0.0': + resolution: {integrity: sha512-NS2+P7v80N3MQqehZEjgpaFb9UyX3URNMW/zvoECKGo4PY4DvJfQusTI7BX/Ks+CPvtTfk3TqcR6S9VYBi/C+A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + rollup: + optional: true + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -2194,13 +2428,6 @@ packages: cpu: [x64] os: [win32] - '@shopify/flash-list@2.2.0': - resolution: {integrity: sha512-mL61IofcfBNRZ/qazIf+pghGULkcZUQ7EZNldH1JBbIjtDb25ADSiQrt62ZTnRz0H5+bPFEZUmN9+WChHzX8pw==} - peerDependencies: - '@babel/runtime': '*' - react: '*' - react-native: '*' - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -2611,6 +2838,9 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + babel-plugin-syntax-hermes-parser@0.32.0: resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} @@ -3209,6 +3439,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: @@ -3217,7 +3448,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -3850,6 +4081,25 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxfmt@0.35.0: + resolution: {integrity: sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint-tsgolint@0.15.0: + resolution: {integrity: sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw==} + hasBin: true + + oxlint@1.50.0: + resolution: {integrity: sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.14.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -4589,6 +4839,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -5881,41 +6135,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.3.11': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.11 - '@biomejs/cli-darwin-x64': 2.3.11 - '@biomejs/cli-linux-arm64': 2.3.11 - '@biomejs/cli-linux-arm64-musl': 2.3.11 - '@biomejs/cli-linux-x64': 2.3.11 - '@biomejs/cli-linux-x64-musl': 2.3.11 - '@biomejs/cli-win32-arm64': 2.3.11 - '@biomejs/cli-win32-x64': 2.3.11 - - '@biomejs/cli-darwin-arm64@2.3.11': - optional: true - - '@biomejs/cli-darwin-x64@2.3.11': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.3.11': - optional: true - - '@biomejs/cli-linux-arm64@2.3.11': - optional: true - - '@biomejs/cli-linux-x64-musl@2.3.11': - optional: true - - '@biomejs/cli-linux-x64@2.3.11': - optional: true - - '@biomejs/cli-win32-arm64@2.3.11': - optional: true - - '@biomejs/cli-win32-x64@2.3.11': - optional: true - '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -6595,6 +6814,138 @@ snapshots: '@oxc-project/types@0.108.0': {} + '@oxfmt/binding-android-arm-eabi@0.35.0': + optional: true + + '@oxfmt/binding-android-arm64@0.35.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.35.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.35.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.35.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.35.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.35.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.35.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.35.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.35.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.35.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.35.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.35.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.35.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.35.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.35.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.35.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.35.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.15.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.15.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.15.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.15.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.15.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.15.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.50.0': + optional: true + + '@oxlint/binding-android-arm64@1.50.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.50.0': + optional: true + + '@oxlint/binding-darwin-x64@1.50.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.50.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.50.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.50.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.50.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.50.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.50.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.50.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.50.0': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -6797,6 +7148,17 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.60': {} + '@rollup/plugin-babel@7.0.0(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.55.2)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@rollup/pluginutils': 5.3.0(rollup@4.55.2) + optionalDependencies: + '@types/babel__core': 7.20.5 + rollup: 4.55.2 + transitivePeerDependencies: + - supports-color + '@rollup/pluginutils@5.3.0(rollup@4.55.2)': dependencies: '@types/estree': 1.0.8 @@ -6880,12 +7242,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.55.2': optional: true - '@shopify/flash-list@2.2.0(@babel/runtime@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.6 - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) - '@sinclair/typebox@0.27.8': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -7368,6 +7724,10 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.28.6 + babel-plugin-syntax-hermes-parser@0.32.0: dependencies: hermes-parser: 0.32.0 @@ -8797,6 +9157,62 @@ snapshots: outdent@0.5.0: {} + oxfmt@0.35.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.35.0 + '@oxfmt/binding-android-arm64': 0.35.0 + '@oxfmt/binding-darwin-arm64': 0.35.0 + '@oxfmt/binding-darwin-x64': 0.35.0 + '@oxfmt/binding-freebsd-x64': 0.35.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.35.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.35.0 + '@oxfmt/binding-linux-arm64-gnu': 0.35.0 + '@oxfmt/binding-linux-arm64-musl': 0.35.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.35.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.35.0 + '@oxfmt/binding-linux-riscv64-musl': 0.35.0 + '@oxfmt/binding-linux-s390x-gnu': 0.35.0 + '@oxfmt/binding-linux-x64-gnu': 0.35.0 + '@oxfmt/binding-linux-x64-musl': 0.35.0 + '@oxfmt/binding-openharmony-arm64': 0.35.0 + '@oxfmt/binding-win32-arm64-msvc': 0.35.0 + '@oxfmt/binding-win32-ia32-msvc': 0.35.0 + '@oxfmt/binding-win32-x64-msvc': 0.35.0 + + oxlint-tsgolint@0.15.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.15.0 + '@oxlint-tsgolint/darwin-x64': 0.15.0 + '@oxlint-tsgolint/linux-arm64': 0.15.0 + '@oxlint-tsgolint/linux-x64': 0.15.0 + '@oxlint-tsgolint/win32-arm64': 0.15.0 + '@oxlint-tsgolint/win32-x64': 0.15.0 + + oxlint@1.50.0(oxlint-tsgolint@0.15.0): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.50.0 + '@oxlint/binding-android-arm64': 1.50.0 + '@oxlint/binding-darwin-arm64': 1.50.0 + '@oxlint/binding-darwin-x64': 1.50.0 + '@oxlint/binding-freebsd-x64': 1.50.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.50.0 + '@oxlint/binding-linux-arm-musleabihf': 1.50.0 + '@oxlint/binding-linux-arm64-gnu': 1.50.0 + '@oxlint/binding-linux-arm64-musl': 1.50.0 + '@oxlint/binding-linux-ppc64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-musl': 1.50.0 + '@oxlint/binding-linux-s390x-gnu': 1.50.0 + '@oxlint/binding-linux-x64-gnu': 1.50.0 + '@oxlint/binding-linux-x64-musl': 1.50.0 + '@oxlint/binding-openharmony-arm64': 1.50.0 + '@oxlint/binding-win32-arm64-msvc': 1.50.0 + '@oxlint/binding-win32-ia32-msvc': 1.50.0 + '@oxlint/binding-win32-x64-msvc': 1.50.0 + oxlint-tsgolint: 0.15.0 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -9614,6 +10030,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@2.1.0: {} + tinyrainbow@2.0.0: {} tinyrainbow@3.0.3: {} diff --git a/scripts/depcheck.ts b/scripts/depcheck.ts index 4bd9e58..89f8697 100644 --- a/scripts/depcheck.ts +++ b/scripts/depcheck.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; + import depcheck, { type Options } from 'depcheck'; import { sync as globSync } from 'glob'; import { Listr, type ListrTask } from 'listr2'; @@ -10,7 +11,7 @@ const rootDependencies: string[] = []; const defaultDepcheckOptions: Options = { ignorePatterns: ['dist', 'node_modules'], - detectors: [...Object.values(depcheck.detector)], + detectors: Object.values(depcheck.detector), }; type Context = { @@ -67,9 +68,7 @@ function createCheckTask( return { title: `Checking ${title}`, task: (ctx, task) => { - const errorDependencies = isError - ? dependencies.filter(isError) - : dependencies; + const errorDependencies = isError ? dependencies.filter(isError) : dependencies; if (errorDependencies.length) { task.title = `${title[0]?.toUpperCase()}${title.slice(1)} (${errorDependencies.length})`; @@ -108,8 +107,10 @@ const listr = new Listr( // Slightly different task handling for root. We want to check // if sub-packages are using dependencies for any of the unused // root packages - const { dependencies, devDependencies, missing, using } = - await depcheck(packageJson.path, packageJson.depcheck); + const { dependencies, devDependencies, missing, using } = await depcheck( + packageJson.path, + packageJson.depcheck, + ); const missingDependencies = Object.keys(missing); if (packageJson.isRoot) { diff --git a/templates/app-template/package.json b/templates/app-template/package.json index f2c09b5..47d6ff7 100644 --- a/templates/app-template/package.json +++ b/templates/app-template/package.json @@ -1,11 +1,10 @@ { "name": "react-lightning-sample-app", - "description": "An app template using @plextv/react-lightning", "version": "0.0.1", + "private": true, + "description": "An app template using @plextv/react-lightning", "license": "MIT", - "packageManager": "pnpm@10.12.3", "type": "module", - "private": true, "scripts": { "build": "vite build", "clean": "del ./dist", @@ -30,5 +29,6 @@ "@vitejs/plugin-react": "5.1.2", "del-cli": "7.0.0", "vite": "7.3.1" - } + }, + "packageManager": "pnpm@10.12.3" } diff --git a/templates/app-template/src/App.tsx b/templates/app-template/src/App.tsx index 21a61a1..50b4f01 100644 --- a/templates/app-template/src/App.tsx +++ b/templates/app-template/src/App.tsx @@ -1,4 +1,5 @@ import { Outlet } from 'react-router-dom'; + import { Nav } from './components/Nav'; export const App = () => { diff --git a/templates/app-template/src/components/Button.tsx b/templates/app-template/src/components/Button.tsx index b442ccd..ae732a8 100644 --- a/templates/app-template/src/components/Button.tsx +++ b/templates/app-template/src/components/Button.tsx @@ -1,9 +1,7 @@ -import { - type KeyEvent, - type LightningElementStyle, - useFocus, -} from '@plextv/react-lightning'; import { useCallback, useMemo } from 'react'; + +import { type KeyEvent, type LightningElementStyle, useFocus } from '@plextv/react-lightning'; + import { Text } from './Text'; type Variants = 'accent' | 'default'; @@ -37,18 +35,9 @@ type Props = { onPress: () => void; }; -export const Button = ({ - label, - variant = 'default', - style, - autoFocus, - onPress, -}: Props) => { +export const Button = ({ label, variant = 'default', style, autoFocus, onPress }: Props) => { const { focused, ref } = useFocus({ autoFocus }); - const { active, inactive, activeText, inactiveText } = useMemo( - () => themes[variant], - [variant], - ); + const { active, inactive, activeText, inactiveText } = useMemo(() => themes[variant], [variant]); const handleOnKeyUp = useCallback( (event: KeyEvent) => { diff --git a/templates/app-template/src/components/Nav.tsx b/templates/app-template/src/components/Nav.tsx index b96e57a..bb91ac0 100644 --- a/templates/app-template/src/components/Nav.tsx +++ b/templates/app-template/src/components/Nav.tsx @@ -1,5 +1,7 @@ -import { FocusGroup } from '@plextv/react-lightning'; import { useNavigate } from 'react-router-dom'; + +import { FocusGroup } from '@plextv/react-lightning'; + import { Button } from './Button'; export const Nav = () => { diff --git a/templates/app-template/src/index.tsx b/templates/app-template/src/index.tsx index a334722..4b0c6a7 100644 --- a/templates/app-template/src/index.tsx +++ b/templates/app-template/src/index.tsx @@ -1,6 +1,8 @@ -import { Canvas, type RenderOptions } from '@plextv/react-lightning'; import { createRoot } from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + +import { Canvas, type RenderOptions } from '@plextv/react-lightning'; + import { App } from './App'; import { keyMap } from './keyMap'; import { BrowsePage } from './pages/BrowsePage'; diff --git a/templates/app-template/src/pages/BrowsePage.tsx b/templates/app-template/src/pages/BrowsePage.tsx index 0d11f04..41040ef 100644 --- a/templates/app-template/src/pages/BrowsePage.tsx +++ b/templates/app-template/src/pages/BrowsePage.tsx @@ -1,6 +1,7 @@ -import { FocusGroup, useFocus } from '@plextv/react-lightning'; import { useMemo } from 'react'; +import { FocusGroup, useFocus } from '@plextv/react-lightning'; + const WIDTH = 200; const HEIGHT = 300; @@ -11,10 +12,7 @@ type ImageProps = { }; const Image = ({ x, y, autoFocus }: ImageProps) => { - const src = useMemo( - () => `https://picsum.photos/${WIDTH}/${HEIGHT}?random=${Math.random()}`, - [], - ); + const src = useMemo(() => `https://picsum.photos/${WIDTH}/${HEIGHT}?random=${Math.random()}`, []); const { focused, ref } = useFocus({ autoFocus }); return ( diff --git a/templates/app-template/vite.config.ts b/templates/app-template/vite.config.ts index c6b1322..1628e1a 100644 --- a/templates/app-template/vite.config.ts +++ b/templates/app-template/vite.config.ts @@ -1,7 +1,8 @@ -import fontGen from '@plextv/vite-plugin-msdf-fontgen'; import react from '@vitejs/plugin-react'; import type { InlineConfig } from 'vite'; +import fontGen from '@plextv/vite-plugin-msdf-fontgen'; + const config: InlineConfig = { plugins: [ react(), From 41fe50a45fb9f32df80f8f31f546b8a8bfe17fa1 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Mon, 13 Apr 2026 17:07:43 -0700 Subject: [PATCH 03/10] Add pnpm catalogs --- apps/react-lightning-example/package.json | 16 +- .../package.json | 20 +- apps/storybook/package.json | 15 +- packages/configs/package.json | 2 +- packages/plugin-css-transform/package.json | 6 +- packages/plugin-flexbox-lite/package.json | 3 +- packages/plugin-flexbox/package.json | 5 +- packages/plugin-reanimated/package.json | 11 +- .../react-lightning-components/package.json | 4 +- packages/react-lightning/package.json | 11 +- .../package.json | 8 +- packages/react-native-lightning/package.json | 16 +- .../vite-plugin-msdf-fontgen/package.json | 3 + .../package.json | 7 +- .../package.json | 3 +- pnpm-lock.yaml | 206 ++++++++++++------ pnpm-workspace.yaml | 23 ++ 17 files changed, 232 insertions(+), 127 deletions(-) diff --git a/apps/react-lightning-example/package.json b/apps/react-lightning-example/package.json index b38b479..387b79a 100644 --- a/apps/react-lightning-example/package.json +++ b/apps/react-lightning-example/package.json @@ -22,24 +22,24 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", "@plextv/react-lightning-plugin-css-transform": "workspace:*", "@plextv/react-lightning-plugin-flexbox": "workspace:*", - "react": "19.2.3", - "react-dom": "19.2.3", + "react": "catalog:", + "react-dom": "catalog:", "react-router-dom": "7.12.0", "swr": "2.3.8" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", "@repo/configs": "workspace:*", - "@types/react": "19.2.8", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-legacy": "7.2.1", - "@vitejs/plugin-react": "5.1.2", - "babel-plugin-react-compiler": "1.0.0", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-legacy": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:", "vite-tsconfig-paths": "6.0.4" }, "volta": { diff --git a/apps/react-native-lightning-example/package.json b/apps/react-native-lightning-example/package.json index 488a762..e8101c3 100644 --- a/apps/react-native-lightning-example/package.json +++ b/apps/react-native-lightning-example/package.json @@ -22,28 +22,30 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", + "@plextv/react-lightning-plugin-css-transform": "workspace:*", "@plextv/react-lightning-plugin-flexbox": "workspace:*", "@plextv/react-lightning-plugin-reanimated": "workspace:*", "@plextv/react-native-lightning": "workspace:*", "@plextv/react-native-lightning-components": "workspace:*", "@react-navigation/native": "7.1.28", - "react": "19.2.3", - "react-dom": "19.2.3", - "react-native": "0.82.1", - "react-native-reanimated": "4.2.1" + "react": "catalog:", + "react-dom": "catalog:", + "react-native": "catalog:", + "react-native-reanimated": "catalog:" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", "@plextv/vite-plugin-react-native-lightning": "workspace:*", "@plextv/vite-plugin-react-reanimated-lightning": "workspace:*", "@repo/configs": "workspace:*", - "@types/react": "19.2.8", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-legacy": "7.2.1", - "babel-plugin-react-compiler": "1.0.0", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-legacy": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:", "typescript": "5.9.3" }, "volta": { diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 37bc72f..bd903d1 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -19,9 +19,10 @@ "dev": "storybook dev -p 6006 --no-open" }, "dependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", + "@plextv/react-lightning-plugin-css-transform": "workspace:*", "@plextv/react-lightning-plugin-flexbox": "workspace:*", "@plextv/react-lightning-plugin-flexbox-lite": "workspace:*", "@plextv/react-lightning-plugin-reanimated": "workspace:*", @@ -29,11 +30,10 @@ "@plextv/react-native-lightning-components": "workspace:*", "@storybook/addon-docs": "9.1.17", "@storybook/addon-links": "9.1.17", - "@storybook/builder-vite": "9.1.17", "@storybook/react-vite": "9.1.17", - "react": "19.2.3", - "react-native": "0.82.1", - "react-native-reanimated": "4.2.1", + "react": "catalog:", + "react-native": "catalog:", + "react-native-reanimated": "catalog:", "storybook": "9.1.17" }, "devDependencies": { @@ -41,8 +41,9 @@ "@plextv/vite-plugin-react-native-lightning": "workspace:*", "@plextv/vite-plugin-react-reanimated-lightning": "workspace:*", "@repo/configs": "workspace:*", - "@types/react": "19.2.8", - "babel-plugin-react-compiler": "1.0.0" + "@types/react": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/configs/package.json b/packages/configs/package.json index 0500888..e2bc229 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -17,7 +17,7 @@ "scripts": {}, "devDependencies": { "@rollup/plugin-babel": "7.0.0", - "babel-plugin-react-compiler": "1.0.0" + "babel-plugin-react-compiler": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-css-transform/package.json b/packages/plugin-css-transform/package.json index cc8c061..ad04668 100644 --- a/packages/plugin-css-transform/package.json +++ b/packages/plugin-css-transform/package.json @@ -44,14 +44,14 @@ }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/react": "19.2.8", + "@types/react": "catalog:", "csstype": "3.2.3" }, "peerDependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", - "react-native": "^0.82.1" + "react-native": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-flexbox-lite/package.json b/packages/plugin-flexbox-lite/package.json index 8e81e14..52106de 100644 --- a/packages/plugin-flexbox-lite/package.json +++ b/packages/plugin-flexbox-lite/package.json @@ -43,7 +43,8 @@ "test:unit": "vitest run --passWithNoTests" }, "devDependencies": { - "@repo/configs": "workspace:*" + "@repo/configs": "workspace:*", + "type-fest": "catalog:" }, "peerDependencies": { "@plextv/react-lightning": "workspace:^" diff --git a/packages/plugin-flexbox/package.json b/packages/plugin-flexbox/package.json index 95da74b..915e404 100644 --- a/packages/plugin-flexbox/package.json +++ b/packages/plugin-flexbox/package.json @@ -49,7 +49,7 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "tseep": "1.3.1", + "tseep": "catalog:", "yoga-layout": "3.2.1" }, "devDependencies": { @@ -57,8 +57,7 @@ "copyfiles": "2.4.1" }, "peerDependencies": { - "@plextv/react-lightning": "workspace:^", - "react": "^19.2.3" + "@plextv/react-lightning": "workspace:^" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-reanimated/package.json b/packages/plugin-reanimated/package.json index 58b85a9..0bd228b 100644 --- a/packages/plugin-reanimated/package.json +++ b/packages/plugin-reanimated/package.json @@ -42,17 +42,18 @@ }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/react": "19.2.8" + "@types/react": "catalog:", + "type-fest": "catalog:" }, "peerDependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-plugin-css-transform": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", "@plextv/react-native-lightning": "workspace:^", - "react": "^19.2.3", - "react-native": "^0.82.1", - "react-native-reanimated": "^4.2.1" + "react": "catalog:peers", + "react-native": "catalog:peers", + "react-native-reanimated": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning-components/package.json b/packages/react-lightning-components/package.json index fafd4f7..2daf8a6 100644 --- a/packages/react-lightning-components/package.json +++ b/packages/react-lightning-components/package.json @@ -67,12 +67,12 @@ }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/react": "19.2.8" + "@types/react": "catalog:" }, "peerDependencies": { "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", - "react": "^19.2.3" + "react": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning/package.json b/packages/react-lightning/package.json index 961872e..7e00ca8 100644 --- a/packages/react-lightning/package.json +++ b/packages/react-lightning/package.json @@ -44,16 +44,17 @@ }, "dependencies": { "react-reconciler": "0.33.0", - "tseep": "1.3.1" + "tseep": "catalog:" }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/react": "19.2.8", - "@types/react-reconciler": "0.32.3" + "@types/react": "catalog:", + "@types/react-reconciler": "catalog:", + "type-fest": "catalog:" }, "peerDependencies": { - "@lightningjs/renderer": "3.0.0-beta20", - "react": "^19.2.3" + "@lightningjs/renderer": "catalog:", + "react": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-native-lightning-components/package.json b/packages/react-native-lightning-components/package.json index 23bb9dc..a5d3230 100644 --- a/packages/react-native-lightning-components/package.json +++ b/packages/react-native-lightning-components/package.json @@ -52,16 +52,14 @@ }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/react": "19.2.8" + "@types/react": "catalog:" }, "peerDependencies": { "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-components": "workspace:^", - "@plextv/react-lightning-plugin-css-transform": "workspace:^", - "@plextv/react-lightning-plugin-flexbox": "workspace:^", "@plextv/react-native-lightning": "workspace:^", - "react": "^19.2.3", - "react-native": "^0.82.1" + "react": "catalog:peers", + "react-native": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-native-lightning/package.json b/packages/react-native-lightning/package.json index b9a9ad5..7950653 100644 --- a/packages/react-native-lightning/package.json +++ b/packages/react-native-lightning/package.json @@ -41,23 +41,23 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@plextv/react-lightning-components": "workspace:*", - "@plextv/react-lightning-plugin-css-transform": "workspace:*", - "@plextv/react-lightning-plugin-flexbox": "workspace:*", "its-fine": "2.0.0", "react-native-web": "0.21.2" }, "devDependencies": { "@repo/configs": "workspace:*", "@types/node": "25.0.9", - "@types/react": "19.2.8", - "@types/react-reconciler": "0.32.3" + "@types/react": "catalog:", + "@types/react-reconciler": "catalog:" }, "peerDependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:^", - "react": "^19.2.3", - "react-native": "^0.82.1" + "@plextv/react-lightning-components": "workspace:^", + "@plextv/react-lightning-plugin-css-transform": "workspace:^", + "@plextv/react-lightning-plugin-flexbox": "workspace:^", + "react": "catalog:peers", + "react-native": "catalog:peers" }, "volta": { "extends": "../../package.json" diff --git a/packages/vite-plugin-msdf-fontgen/package.json b/packages/vite-plugin-msdf-fontgen/package.json index b8d7674..2204b8e 100644 --- a/packages/vite-plugin-msdf-fontgen/package.json +++ b/packages/vite-plugin-msdf-fontgen/package.json @@ -42,5 +42,8 @@ }, "devDependencies": { "@repo/configs": "workspace:*" + }, + "peerDependencies": { + "vite": "catalog:peers" } } diff --git a/packages/vite-plugin-react-native-lightning/package.json b/packages/vite-plugin-react-native-lightning/package.json index 3b4a162..7a37ad2 100644 --- a/packages/vite-plugin-react-native-lightning/package.json +++ b/packages/vite-plugin-react-native-lightning/package.json @@ -37,13 +37,12 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "dependencies": { - "@vitejs/plugin-react": "5.1.2" - }, "devDependencies": { "@repo/configs": "workspace:*" }, "peerDependencies": { - "@plextv/react-native-lightning": "workspace:^" + "@plextv/react-native-lightning": "workspace:^", + "@vitejs/plugin-react": "catalog:peers", + "vite": "catalog:peers" } } diff --git a/packages/vite-plugin-react-reanimated-lightning/package.json b/packages/vite-plugin-react-reanimated-lightning/package.json index 99a728e..9514080 100644 --- a/packages/vite-plugin-react-reanimated-lightning/package.json +++ b/packages/vite-plugin-react-reanimated-lightning/package.json @@ -43,6 +43,7 @@ }, "peerDependencies": { "@plextv/react-lightning-plugin-reanimated": "workspace:^", - "react-native-reanimated": "^4.2.1" + "react-native-reanimated": "catalog:peers", + "vite": "catalog:peers" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cfaf50..f6536ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,64 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@lightningjs/renderer': + specifier: 3.0.0-beta20 + version: 3.0.0-beta20 + '@types/react': + specifier: 19.2.8 + version: 19.2.8 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3 + '@types/react-reconciler': + specifier: 0.32.3 + version: 0.32.3 + '@vitejs/plugin-legacy': + specifier: 7.2.1 + version: 7.2.1 + '@vitejs/plugin-react': + specifier: 5.1.2 + version: 5.1.2 + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3 + react-native: + specifier: 0.82.1 + version: 0.82.1 + react-native-reanimated: + specifier: 4.2.1 + version: 4.2.1 + tseep: + specifier: 1.3.1 + version: 1.3.1 + type-fest: + specifier: 5.4.1 + version: 5.4.1 + peers: + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.1.2 + react: + specifier: ^19.2.3 + version: 19.2.3 + react-native: + specifier: ^0.82.1 + version: 0.82.1 + react-native-reanimated: + specifier: ^4.2.1 + version: 4.2.1 + vite: + specifier: ^5.0.0 || ^6.0.0 || ^7.0.0 + version: 7.3.1 + importers: .: @@ -72,7 +130,7 @@ importers: apps/react-lightning-example: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:* @@ -87,10 +145,10 @@ importers: specifier: workspace:* version: link:../../packages/plugin-flexbox react: - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3 react-dom: - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3(react@19.2.3) react-router-dom: specifier: 7.12.0 @@ -106,19 +164,19 @@ importers: specifier: workspace:* version: link:../../packages/configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 '@types/react-dom': - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-legacy': - specifier: 7.2.1 + specifier: 'catalog:' version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitejs/plugin-react': - specifier: 5.1.2 + specifier: 'catalog:' version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) babel-plugin-react-compiler: - specifier: 1.0.0 + specifier: 'catalog:' version: 1.0.0 vite-tsconfig-paths: specifier: 6.0.4 @@ -127,7 +185,7 @@ importers: apps/react-native-lightning-example: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:* @@ -135,6 +193,9 @@ importers: '@plextv/react-lightning-components': specifier: workspace:* version: link:../../packages/react-lightning-components + '@plextv/react-lightning-plugin-css-transform': + specifier: workspace:* + version: link:../../packages/plugin-css-transform '@plextv/react-lightning-plugin-flexbox': specifier: workspace:* version: link:../../packages/plugin-flexbox @@ -151,16 +212,16 @@ importers: specifier: 7.1.28 version: 7.1.28(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) react: - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3 react-dom: - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3(react@19.2.3) react-native: - specifier: 0.82.1 + specifier: 'catalog:' version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) react-native-reanimated: - specifier: 4.2.1 + specifier: 'catalog:' version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) devDependencies: '@plextv/vite-plugin-msdf-fontgen': @@ -176,16 +237,19 @@ importers: specifier: workspace:* version: link:../../packages/configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 '@types/react-dom': - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-legacy': - specifier: 7.2.1 + specifier: 'catalog:' version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) babel-plugin-react-compiler: - specifier: 1.0.0 + specifier: 'catalog:' version: 1.0.0 typescript: specifier: 5.9.3 @@ -194,7 +258,7 @@ importers: apps/storybook: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:* @@ -202,6 +266,9 @@ importers: '@plextv/react-lightning-components': specifier: workspace:* version: link:../../packages/react-lightning-components + '@plextv/react-lightning-plugin-css-transform': + specifier: workspace:* + version: link:../../packages/plugin-css-transform '@plextv/react-lightning-plugin-flexbox': specifier: workspace:* version: link:../../packages/plugin-flexbox @@ -223,20 +290,17 @@ importers: '@storybook/addon-links': specifier: 9.1.17 version: 9.1.17(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) - '@storybook/builder-vite': - specifier: 9.1.17 - version: 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@storybook/react-vite': specifier: 9.1.17 version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.55.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) react: - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3 react-native: - specifier: 0.82.1 + specifier: 'catalog:' version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) react-native-reanimated: - specifier: 4.2.1 + specifier: 'catalog:' version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) storybook: specifier: 9.1.17 @@ -255,10 +319,13 @@ importers: specifier: workspace:* version: link:../../packages/configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) babel-plugin-react-compiler: - specifier: 1.0.0 + specifier: 'catalog:' version: 1.0.0 packages/configs: @@ -267,13 +334,13 @@ importers: specifier: 7.0.0 version: 7.0.0(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.55.2) babel-plugin-react-compiler: - specifier: 1.0.0 + specifier: 'catalog:' version: 1.0.0 packages/plugin-css-transform: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:^ @@ -282,14 +349,14 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react-native: - specifier: ^0.82.1 + specifier: catalog:peers version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 csstype: specifier: 3.2.3 @@ -300,11 +367,8 @@ importers: '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning - react: - specifier: ^19.2.3 - version: 19.2.3 tseep: - specifier: 1.3.1 + specifier: 'catalog:' version: 1.3.1 yoga-layout: specifier: 3.2.1 @@ -326,11 +390,14 @@ importers: '@repo/configs': specifier: workspace:* version: link:../configs + type-fest: + specifier: 'catalog:' + version: 5.4.1 packages/plugin-reanimated: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:^ @@ -345,46 +412,52 @@ importers: specifier: workspace:^ version: link:../react-native-lightning react: - specifier: ^19.2.3 + specifier: catalog:peers version: 19.2.3 react-native: - specifier: ^0.82.1 + specifier: catalog:peers version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) react-native-reanimated: - specifier: ^4.2.1 + specifier: catalog:peers version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 + type-fest: + specifier: 'catalog:' + version: 5.4.1 packages/react-lightning: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 react: - specifier: ^19.2.3 + specifier: catalog:peers version: 19.2.3 react-reconciler: specifier: 0.33.0 version: 0.33.0(react@19.2.3) tseep: - specifier: 1.3.1 + specifier: 'catalog:' version: 1.3.1 devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 '@types/react-reconciler': - specifier: 0.32.3 + specifier: 'catalog:' version: 0.32.3(@types/react@19.2.8) + type-fest: + specifier: 'catalog:' + version: 5.4.1 packages/react-lightning-components: dependencies: @@ -395,41 +468,41 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react: - specifier: ^19.2.3 + specifier: catalog:peers version: 19.2.3 devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 packages/react-native-lightning: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 + specifier: 'catalog:' version: 3.0.0-beta20 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning '@plextv/react-lightning-components': - specifier: workspace:* + specifier: workspace:^ version: link:../react-lightning-components '@plextv/react-lightning-plugin-css-transform': - specifier: workspace:* + specifier: workspace:^ version: link:../plugin-css-transform '@plextv/react-lightning-plugin-flexbox': - specifier: workspace:* + specifier: workspace:^ version: link:../plugin-flexbox its-fine: specifier: 2.0.0 version: 2.0.0(@types/react@19.2.8)(react@19.2.3) react: - specifier: ^19.2.3 + specifier: catalog:peers version: 19.2.3 react-native: - specifier: ^0.82.1 + specifier: catalog:peers version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) react-native-web: specifier: 0.21.2 @@ -442,10 +515,10 @@ importers: specifier: 25.0.9 version: 25.0.9 '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 '@types/react-reconciler': - specifier: 0.32.3 + specifier: 'catalog:' version: 0.32.3(@types/react@19.2.8) packages/react-native-lightning-components: @@ -456,27 +529,21 @@ importers: '@plextv/react-lightning-components': specifier: workspace:^ version: link:../react-lightning-components - '@plextv/react-lightning-plugin-css-transform': - specifier: workspace:^ - version: link:../plugin-css-transform - '@plextv/react-lightning-plugin-flexbox': - specifier: workspace:^ - version: link:../plugin-flexbox '@plextv/react-native-lightning': specifier: workspace:^ version: link:../react-native-lightning react: - specifier: ^19.2.3 + specifier: catalog:peers version: 19.2.3 react-native: - specifier: ^0.82.1 + specifier: catalog:peers version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 + specifier: 'catalog:' version: 19.2.8 packages/vite-plugin-msdf-fontgen: @@ -490,6 +557,9 @@ importers: glob: specifier: 13.0.0 version: 13.0.0 + vite: + specifier: catalog:peers + version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) devDependencies: '@repo/configs': specifier: workspace:* @@ -501,8 +571,11 @@ importers: specifier: workspace:^ version: link:../react-native-lightning '@vitejs/plugin-react': - specifier: 5.1.2 + specifier: catalog:peers version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vite: + specifier: catalog:peers + version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) devDependencies: '@repo/configs': specifier: workspace:* @@ -514,8 +587,11 @@ importers: specifier: workspace:^ version: link:../plugin-reanimated react-native-reanimated: - specifier: ^4.2.1 + specifier: catalog:peers version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + vite: + specifier: catalog:peers + version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) devDependencies: '@repo/configs': specifier: workspace:* diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a0d4d82..e6c6fb7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,29 @@ packages: - apps/* - packages/* +catalog: + '@lightningjs/renderer': 3.0.0-beta20 + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3 + '@types/react-reconciler': 0.32.3 + '@vitejs/plugin-legacy': 7.2.1 + '@vitejs/plugin-react': 5.1.2 + babel-plugin-react-compiler: 1.0.0 + react: 19.2.3 + react-dom: 19.2.3 + react-native: 0.82.1 + react-native-reanimated: 4.2.1 + tseep: 1.3.1 + type-fest: 5.4.1 + +catalogs: + peers: + '@vitejs/plugin-react': ^5.0.0 + react: ^19.2.3 + react-native: ^0.82.1 + react-native-reanimated: ^4.2.1 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + onlyBuiltDependencies: - core-js - esbuild From 7bf778a2e11657c4da43bc129e4fdbdd006565ba Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Mon, 13 Apr 2026 22:45:42 -0700 Subject: [PATCH 04/10] Bump packages --- .oxlintrc.json | 1 + apps/react-lightning-example/package.json | 13 +- apps/react-lightning-example/tsconfig.json | 1 - .../package.json | 10 +- .../tsconfig.json | 3 +- apps/storybook/package.json | 13 +- package.json | 28 +- packages/configs/package.json | 5 + packages/configs/tsdown.node.config.ts | 4 +- packages/plugin-css-transform/package.json | 7 +- packages/plugin-flexbox-lite/package.json | 4 +- packages/plugin-flexbox/tsconfig.json | 1 + packages/plugin-reanimated/package.json | 7 +- .../react-lightning-components/package.json | 24 +- .../VirtualList/ViewabilityTracker.spec.ts | 27 +- packages/react-lightning/package.json | 7 +- packages/react-lightning/src/render/index.tsx | 1 - .../src/utils/findClosestElement.spec.ts | 44 +- .../package.json | 13 +- packages/react-native-lightning/package.json | 10 +- .../vite-plugin-msdf-fontgen/package.json | 2 +- pnpm-lock.yaml | 4991 +++++++++-------- pnpm-workspace.yaml | 30 +- 23 files changed, 2659 insertions(+), 2587 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 1979941..ec29658 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,6 +12,7 @@ ], "rules": { "typescript/consistent-type-imports": "error", + "typescript/no-non-null-assertion": "error", "react-hooks/exhaustive-deps": "off", "jsx-a11y/no-autofocus": "off", "jsx-a11y/no-static-element-interactions": "off", diff --git a/apps/react-lightning-example/package.json b/apps/react-lightning-example/package.json index 387b79a..072e468 100644 --- a/apps/react-lightning-example/package.json +++ b/apps/react-lightning-example/package.json @@ -29,8 +29,8 @@ "@plextv/react-lightning-plugin-flexbox": "workspace:*", "react": "catalog:", "react-dom": "catalog:", - "react-router-dom": "7.12.0", - "swr": "2.3.8" + "react-router-dom": "7.14.1", + "swr": "2.4.1" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", @@ -40,10 +40,15 @@ "@vitejs/plugin-legacy": "catalog:", "@vitejs/plugin-react": "catalog:", "babel-plugin-react-compiler": "catalog:", - "vite-tsconfig-paths": "6.0.4" + "vite-tsconfig-paths": "6.1.1" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@10.12.3" + "packageManager": "pnpm@10.12.3", + "depcheck": { + "ignoreMatches": [ + "babel-plugin-react-compiler" + ] + } } diff --git a/apps/react-lightning-example/tsconfig.json b/apps/react-lightning-example/tsconfig.json index 5001905..fea3f73 100644 --- a/apps/react-lightning-example/tsconfig.json +++ b/apps/react-lightning-example/tsconfig.json @@ -2,7 +2,6 @@ "extends": "@repo/configs/tsconfig.react-library.json", "compilerOptions": { "isolatedDeclarations": false, - "outDir": "dist", "types": ["node", "vite/client"] }, "include": ["src"], diff --git a/apps/react-native-lightning-example/package.json b/apps/react-native-lightning-example/package.json index e8101c3..bc13397 100644 --- a/apps/react-native-lightning-example/package.json +++ b/apps/react-native-lightning-example/package.json @@ -30,11 +30,12 @@ "@plextv/react-lightning-plugin-reanimated": "workspace:*", "@plextv/react-native-lightning": "workspace:*", "@plextv/react-native-lightning-components": "workspace:*", - "@react-navigation/native": "7.1.28", + "@react-navigation/native": "7.2.2", "react": "catalog:", "react-dom": "catalog:", "react-native": "catalog:", - "react-native-reanimated": "catalog:" + "react-native-reanimated": "catalog:", + "react-native-worklets": "0.8.1" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", @@ -46,7 +47,7 @@ "@vitejs/plugin-legacy": "catalog:", "@vitejs/plugin-react": "catalog:", "babel-plugin-react-compiler": "catalog:", - "typescript": "5.9.3" + "typescript": "6.0.2" }, "volta": { "extends": "../../package.json" @@ -56,7 +57,8 @@ "depcheck": { "ignoreMatches": [ "typescript", - "react-dom" + "react-dom", + "babel-plugin-react-compiler" ] } } diff --git a/apps/react-native-lightning-example/tsconfig.json b/apps/react-native-lightning-example/tsconfig.json index 996e397..9677c64 100644 --- a/apps/react-native-lightning-example/tsconfig.json +++ b/apps/react-native-lightning-example/tsconfig.json @@ -2,8 +2,7 @@ "extends": "@repo/configs/tsconfig.react-native-library.json", "compilerOptions": { "jsx": "react-native", - "isolatedDeclarations": false, - "outDir": "dist" + "isolatedDeclarations": false }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/apps/storybook/package.json b/apps/storybook/package.json index bd903d1..cf9b622 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -28,13 +28,15 @@ "@plextv/react-lightning-plugin-reanimated": "workspace:*", "@plextv/react-native-lightning": "workspace:*", "@plextv/react-native-lightning-components": "workspace:*", - "@storybook/addon-docs": "9.1.17", - "@storybook/addon-links": "9.1.17", - "@storybook/react-vite": "9.1.17", + "@storybook/addon-docs": "10.3.5", + "@storybook/addon-links": "10.3.5", + "@storybook/react-vite": "10.3.5", "react": "catalog:", + "react-dom": "catalog:", "react-native": "catalog:", "react-native-reanimated": "catalog:", - "storybook": "9.1.17" + "react-native-worklets": "0.8.1", + "storybook": "10.3.5" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", @@ -52,7 +54,8 @@ "depcheck": { "ignoreMatches": [ "@storybook/**", - "**/storybook" + "**/storybook", + "babel-plugin-react-compiler" ] } } diff --git a/package.json b/package.json index 4e906b8..fa1dc5e 100644 --- a/package.json +++ b/package.json @@ -32,26 +32,26 @@ "prepare": "husky" }, "devDependencies": { - "@changesets/cli": "2.29.8", + "@changesets/cli": "2.30.0", "@repo/configs": "workspace:*", - "@types/node": "25.0.9", + "@types/node": "25.6.0", "del-cli": "7.0.0", "depcheck": "1.4.7", - "glob": "13.0.0", + "glob": "13.0.6", "husky": "9.1.7", - "listr2": "10.0.0", - "oxfmt": "0.35.0", - "oxlint": "1.50.0", - "oxlint-tsgolint": "0.15.0", - "tsdown": "0.19.0", + "listr2": "10.2.1", + "oxfmt": "0.45.0", + "oxlint": "1.60.0", + "oxlint-tsgolint": "0.20.0", + "tsdown": "0.21.8", "tsx": "4.21.0", - "turbo": "2.7.5", - "type-fest": "5.4.1", - "typescript": "5.9.3", - "vite": "7.3.1", + "turbo": "2.9.6", + "type-fest": "5.5.0", + "typescript": "6.0.2", + "vite": "8.0.8", "vite-plugin-externalize-deps": "0.10.0", - "vitest": "4.0.17", - "yaml": "2.8.2" + "vitest": "4.1.4", + "yaml": "2.8.3" }, "engines": { "node": ">=22" diff --git a/packages/configs/package.json b/packages/configs/package.json index e2bc229..7138774 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -21,5 +21,10 @@ }, "volta": { "extends": "../../package.json" + }, + "depcheck": { + "ignoreMatches": [ + "babel-plugin-react-compiler" + ] } } diff --git a/packages/configs/tsdown.node.config.ts b/packages/configs/tsdown.node.config.ts index 884f448..06bafb0 100644 --- a/packages/configs/tsdown.node.config.ts +++ b/packages/configs/tsdown.node.config.ts @@ -8,7 +8,9 @@ const config: UserConfig = defineConfig({ format: 'esm', target: 'node22', platform: 'node', - external: [/^node:.*/], + deps: { + neverBundle: [/^node:.*/], + }, exports: { devExports: false, }, diff --git a/packages/plugin-css-transform/package.json b/packages/plugin-css-transform/package.json index ad04668..b313ef2 100644 --- a/packages/plugin-css-transform/package.json +++ b/packages/plugin-css-transform/package.json @@ -27,8 +27,8 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" @@ -55,5 +55,8 @@ }, "volta": { "extends": "../../package.json" + }, + "inlinedDependencies": { + "csstype": "3.2.3" } } diff --git a/packages/plugin-flexbox-lite/package.json b/packages/plugin-flexbox-lite/package.json index 52106de..f0eb957 100644 --- a/packages/plugin-flexbox-lite/package.json +++ b/packages/plugin-flexbox-lite/package.json @@ -27,8 +27,8 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" diff --git a/packages/plugin-flexbox/tsconfig.json b/packages/plugin-flexbox/tsconfig.json index 9315187..d670838 100644 --- a/packages/plugin-flexbox/tsconfig.json +++ b/packages/plugin-flexbox/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@repo/configs/tsconfig.react-library.json", "compilerOptions": { + "rootDir": "./src", "lib": ["es2022", "DOM", "DOM.Iterable", "WebWorker"] }, "include": ["src", "../../types/*.d.ts"], diff --git a/packages/plugin-reanimated/package.json b/packages/plugin-reanimated/package.json index 0bd228b..5ad6e22 100644 --- a/packages/plugin-reanimated/package.json +++ b/packages/plugin-reanimated/package.json @@ -26,8 +26,8 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json" }, @@ -62,5 +62,8 @@ "ignoreMatches": [ "react-native-reanimated-original" ] + }, + "inlinedDependencies": { + "type-fest": "5.5.0" } } diff --git a/packages/react-lightning-components/package.json b/packages/react-lightning-components/package.json index 2daf8a6..4699915 100644 --- a/packages/react-lightning-components/package.json +++ b/packages/react-lightning-components/package.json @@ -31,28 +31,28 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./layout/Column": { - "require": "./dist/exports/layout/Column.cjs", - "import": "./dist/exports/layout/Column.js" + "import": "./dist/exports/layout/Column.js", + "require": "./dist/exports/layout/Column.cjs" }, "./layout/Row": { - "require": "./dist/exports/layout/Row.cjs", - "import": "./dist/exports/layout/Row.js" + "import": "./dist/exports/layout/Row.js", + "require": "./dist/exports/layout/Row.cjs" }, "./lists/VirtualList": { - "require": "./dist/exports/lists/VirtualList.cjs", - "import": "./dist/exports/lists/VirtualList.js" + "import": "./dist/exports/lists/VirtualList.js", + "require": "./dist/exports/lists/VirtualList.cjs" }, "./text/StyledText": { - "require": "./dist/exports/text/StyledText.cjs", - "import": "./dist/exports/text/StyledText.js" + "import": "./dist/exports/text/StyledText.js", + "require": "./dist/exports/text/StyledText.cjs" }, "./util/FPSMonitor": { - "require": "./dist/exports/util/FPSMonitor.cjs", - "import": "./dist/exports/util/FPSMonitor.js" + "import": "./dist/exports/util/FPSMonitor.js", + "require": "./dist/exports/util/FPSMonitor.cjs" }, "./package.json": "./package.json" }, diff --git a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts index 9296787..a4010be 100644 --- a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts @@ -11,6 +11,14 @@ const makeLayout = (offset: number, size: number): ComputedLayout => ({ crossSize: 100, }); +function getCallArgs(fn: ReturnType, index: number) { + const call = fn.mock.calls[index]; + if (!call) { + throw new Error(`Expected mock call at index ${index}`); + } + return call[0]; +} + describe('ViewabilityTracker', () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); @@ -29,7 +37,7 @@ describe('ViewabilityTracker', () => { tracker.update([0, 1, 2], 0, 250, false); expect(onChange).toHaveBeenCalledTimes(1); - const { viewableItems, changed } = onChange.mock.calls[0][0]; + const { viewableItems, changed } = getCallArgs(onChange, 0); expect(viewableItems).toHaveLength(3); expect(changed).toHaveLength(3); }); @@ -67,7 +75,7 @@ describe('ViewabilityTracker', () => { // Viewport 0–120: item 0 fully visible, item 1 only 20% visible tracker.update([0, 1], 0, 120, false); - const items = onChange.mock.calls[0][0].viewableItems; + const items = getCallArgs(onChange, 0).viewableItems; expect(items).toHaveLength(1); expect(items[0].index).toBe(0); }); @@ -86,7 +94,7 @@ describe('ViewabilityTracker', () => { // Viewport 0–100, both items are 50px → each covers 50% tracker.update([0, 1], 0, 100, false); - expect(onChange.mock.calls[0][0].viewableItems).toHaveLength(2); + expect(getCallArgs(onChange, 0).viewableItems).toHaveLength(2); }); it('respects waitForInteraction', () => { @@ -164,7 +172,7 @@ describe('ViewabilityTracker', () => { // Only item 1 visible now tracker.update([1], 100, 100, false); - const { changed } = onChange.mock.calls[0][0]; + const { changed } = getCallArgs(onChange, 0); const left = changed.find( (t: { index: number; isViewable: boolean }) => t.index === 0 && !t.isViewable, ); @@ -191,7 +199,7 @@ describe('ViewabilityTracker', () => { vi.advanceTimersByTime(500); expect(onChange).toHaveBeenCalledTimes(2); // Final state includes both items - const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]; + const lastCall = getCallArgs(onChange, onChange.mock.calls.length - 1); expect(lastCall.viewableItems).toHaveLength(2); }); @@ -217,13 +225,14 @@ describe('ViewabilityTracker', () => { // At t=500, only T0 fires (item 0 visible 500ms, item 1 only 200ms) vi.advanceTimersByTime(200); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange.mock.calls[0][0].viewableItems).toHaveLength(1); - expect(onChange.mock.calls[0][0].viewableItems[0].index).toBe(0); + const firstCall = getCallArgs(onChange, 0); + expect(firstCall.viewableItems).toHaveLength(1); + expect(firstCall.viewableItems[0].index).toBe(0); // At t=800, T1 fires (item 1 visible 500ms) vi.advanceTimersByTime(300); expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange.mock.calls[1][0].viewableItems).toHaveLength(2); + expect(getCallArgs(onChange, 1).viewableItems).toHaveLength(2); }); it('immediately reports committed items leaving with minimumViewTime', () => { @@ -246,7 +255,7 @@ describe('ViewabilityTracker', () => { // Item 0 leaves — should be reported immediately tracker.update([], 200, 200, false); expect(onChange).toHaveBeenCalledTimes(1); - const { changed, viewableItems } = onChange.mock.calls[0][0]; + const { changed, viewableItems } = getCallArgs(onChange, 0); expect(viewableItems).toHaveLength(0); expect(changed).toHaveLength(1); expect(changed[0].index).toBe(0); diff --git a/packages/react-lightning/package.json b/packages/react-lightning/package.json index 7e00ca8..d63e824 100644 --- a/packages/react-lightning/package.json +++ b/packages/react-lightning/package.json @@ -27,8 +27,8 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json", "./jsx": "./dist/types/jsx.d.ts" @@ -58,5 +58,8 @@ }, "volta": { "extends": "../../package.json" + }, + "inlinedDependencies": { + "type-fest": "5.5.0" } } diff --git a/packages/react-lightning/src/render/index.tsx b/packages/react-lightning/src/render/index.tsx index 467fac6..e1d0d71 100644 --- a/packages/react-lightning/src/render/index.tsx +++ b/packages/react-lightning/src/render/index.tsx @@ -217,7 +217,6 @@ export async function createRoot( (error) => console.error(error), (error) => console.error(error), () => {}, - null, ); const lngRoot: LightningRoot = { diff --git a/packages/react-lightning/src/utils/findClosestElement.spec.ts b/packages/react-lightning/src/utils/findClosestElement.spec.ts index 6d0defc..baa6454 100644 --- a/packages/react-lightning/src/utils/findClosestElement.spec.ts +++ b/packages/react-lightning/src/utils/findClosestElement.spec.ts @@ -337,12 +337,17 @@ suite('getOverlap', () => { { x: 200, y: 0, w: 200, h: 200 }, ]); + const source = elements[1]; + const target = elements[2]; + if (!source || !target) { + throw new Error('Expected elements at indices 1 and 2'); + } + // Make element 2 non-visible (focusable returns false, but focusableIntent is true) - Object.assign(elements[2], { focusable: false, focusableIntent: true }); + Object.assign(target, { focusable: false, focusableIntent: true }); const closest = findClosestElement( - // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence - elements[1]!, + source, elements.root.children, elements.root, Direction.Right, @@ -357,12 +362,17 @@ suite('getOverlap', () => { { x: 200, y: 0, w: 200, h: 200 }, ]); + const source = elements[1]; + const target = elements[2]; + if (!source || !target) { + throw new Error('Expected elements at indices 1 and 2'); + } + // Make element 2 non-visible but with focusableIntent - Object.assign(elements[2], { focusable: false, focusableIntent: true }); + Object.assign(target, { focusable: false, focusableIntent: true }); const closest = findClosestElement( - // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence - elements[1]!, + source, elements.root.children, elements.root, Direction.Right, @@ -378,13 +388,18 @@ suite('getOverlap', () => { { x: 200, y: 0, w: 0, h: 0 }, ]); + const source = elements[1]; + const target = elements[2]; + if (!source || !target) { + throw new Error('Expected elements at indices 1 and 2'); + } + // Zero-dimension elements should still be skipped even with allowOffscreen // because they have no spatial position to navigate to - Object.assign(elements[2], { focusableIntent: true }); + Object.assign(target, { focusableIntent: true }); const closest = findClosestElement( - // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence - elements[1]!, + source, elements.root.children, elements.root, Direction.Right, @@ -400,12 +415,17 @@ suite('getOverlap', () => { { x: 200, y: 0, w: 200, h: 200 }, ]); + const source = elements[1]; + const target = elements[2]; + if (!source || !target) { + throw new Error('Expected elements at indices 1 and 2'); + } + // Element has neither focusable nor focusableIntent - Object.assign(elements[2], { focusable: false, focusableIntent: false }); + Object.assign(target, { focusable: false, focusableIntent: false }); const closest = findClosestElement( - // oxlint-disable-next-line typescript/no-non-null-assertion -- test setup guarantees existence - elements[1]!, + source, elements.root.children, elements.root, Direction.Right, diff --git a/packages/react-native-lightning-components/package.json b/packages/react-native-lightning-components/package.json index a5d3230..7adde5f 100644 --- a/packages/react-native-lightning-components/package.json +++ b/packages/react-native-lightning-components/package.json @@ -28,16 +28,16 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./layout/Column": { - "require": "./dist/exports/layout/Column.cjs", - "import": "./dist/exports/layout/Column.js" + "import": "./dist/exports/layout/Column.js", + "require": "./dist/exports/layout/Column.cjs" }, "./layout/Row": { - "require": "./dist/exports/layout/Row.cjs", - "import": "./dist/exports/layout/Row.js" + "import": "./dist/exports/layout/Row.js", + "require": "./dist/exports/layout/Row.cjs" }, "./package.json": "./package.json" }, @@ -57,6 +57,7 @@ "peerDependencies": { "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-components": "workspace:^", + "@plextv/react-lightning-plugin-flexbox": "workspace:^", "@plextv/react-native-lightning": "workspace:^", "react": "catalog:peers", "react-native": "catalog:peers" diff --git a/packages/react-native-lightning/package.json b/packages/react-native-lightning/package.json index 7950653..d91937c 100644 --- a/packages/react-native-lightning/package.json +++ b/packages/react-native-lightning/package.json @@ -26,8 +26,8 @@ "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json" }, @@ -46,7 +46,7 @@ }, "devDependencies": { "@repo/configs": "workspace:*", - "@types/node": "25.0.9", + "@types/node": "25.6.0", "@types/react": "catalog:", "@types/react-reconciler": "catalog:" }, @@ -66,5 +66,9 @@ "allowedVersions": { "react": "^19" } + }, + "inlinedDependencies": { + "tseep": "1.3.1", + "type-fest": "5.5.0" } } diff --git a/packages/vite-plugin-msdf-fontgen/package.json b/packages/vite-plugin-msdf-fontgen/package.json index 2204b8e..112d92f 100644 --- a/packages/vite-plugin-msdf-fontgen/package.json +++ b/packages/vite-plugin-msdf-fontgen/package.json @@ -38,7 +38,7 @@ "dependencies": { "@lightningjs/msdf-generator": "1.2.0", "crc-32": "1.2.2", - "glob": "13.0.0" + "glob": "13.0.6" }, "devDependencies": { "@repo/configs": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6536ec..db1be29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,74 +7,74 @@ settings: catalogs: default: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: 3.0.1 + version: 3.0.1 '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 19.2.14 + version: 19.2.14 '@types/react-dom': specifier: 19.2.3 version: 19.2.3 '@types/react-reconciler': - specifier: 0.32.3 - version: 0.32.3 + specifier: 0.33.0 + version: 0.33.0 '@vitejs/plugin-legacy': - specifier: 7.2.1 - version: 7.2.1 + specifier: 8.0.1 + version: 8.0.1 '@vitejs/plugin-react': - specifier: 5.1.2 - version: 5.1.2 + specifier: 6.0.1 + version: 6.0.1 babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 react: - specifier: 19.2.3 - version: 19.2.3 + specifier: 19.2.5 + version: 19.2.5 react-dom: - specifier: 19.2.3 - version: 19.2.3 + specifier: 19.2.5 + version: 19.2.5 react-native: - specifier: 0.82.1 - version: 0.82.1 + specifier: 0.85.1 + version: 0.85.1 react-native-reanimated: - specifier: 4.2.1 - version: 4.2.1 + specifier: 4.3.0 + version: 4.3.0 tseep: specifier: 1.3.1 version: 1.3.1 type-fest: - specifier: 5.4.1 - version: 5.4.1 + specifier: 5.5.0 + version: 5.5.0 peers: '@vitejs/plugin-react': - specifier: ^5.0.0 - version: 5.1.2 + specifier: ^6.0.0 + version: 6.0.1 react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: ^19.2.5 + version: 19.2.5 react-native: - specifier: ^0.82.1 - version: 0.82.1 + specifier: ^0.85.1 + version: 0.85.1 react-native-reanimated: - specifier: ^4.2.1 - version: 4.2.1 + specifier: ^4.3.0 + version: 4.3.0 vite: - specifier: ^5.0.0 || ^6.0.0 || ^7.0.0 - version: 7.3.1 + specifier: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + version: 8.0.8 importers: .: devDependencies: '@changesets/cli': - specifier: 2.29.8 - version: 2.29.8(@types/node@25.0.9) + specifier: 2.30.0 + version: 2.30.0(@types/node@25.6.0) '@repo/configs': specifier: workspace:* version: link:packages/configs '@types/node': - specifier: 25.0.9 - version: 25.0.9 + specifier: 25.6.0 + version: 25.6.0 del-cli: specifier: 7.0.0 version: 7.0.0 @@ -82,56 +82,56 @@ importers: specifier: 1.4.7 version: 1.4.7 glob: - specifier: 13.0.0 - version: 13.0.0 + specifier: 13.0.6 + version: 13.0.6 husky: specifier: 9.1.7 version: 9.1.7 listr2: - specifier: 10.0.0 - version: 10.0.0 + specifier: 10.2.1 + version: 10.2.1 oxfmt: - specifier: 0.35.0 - version: 0.35.0 + specifier: 0.45.0 + version: 0.45.0 oxlint: - specifier: 1.50.0 - version: 1.50.0(oxlint-tsgolint@0.15.0) + specifier: 1.60.0 + version: 1.60.0(oxlint-tsgolint@0.20.0) oxlint-tsgolint: - specifier: 0.15.0 - version: 0.15.0 + specifier: 0.20.0 + version: 0.20.0 tsdown: - specifier: 0.19.0 - version: 0.19.0(typescript@5.9.3) + specifier: 0.21.8 + version: 0.21.8(typescript@6.0.2) tsx: specifier: 4.21.0 version: 4.21.0 turbo: - specifier: 2.7.5 - version: 2.7.5 + specifier: 2.9.6 + version: 2.9.6 type-fest: - specifier: 5.4.1 - version: 5.4.1 + specifier: 5.5.0 + version: 5.5.0 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.2 + version: 6.0.2 vite: - specifier: 7.3.1 - version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + specifier: 8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-externalize-deps: specifier: 0.10.0 - version: 0.10.0(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.10.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.0.17 - version: 4.0.17(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + specifier: 4.1.4 + version: 4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) yaml: - specifier: 2.8.2 - version: 2.8.2 + specifier: 2.8.3 + version: 2.8.3 apps/react-lightning-example: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -146,16 +146,16 @@ importers: version: link:../../packages/plugin-flexbox react: specifier: 'catalog:' - version: 19.2.3 + version: 19.2.5 react-dom: specifier: 'catalog:' - version: 19.2.3(react@19.2.3) + version: 19.2.5(react@19.2.5) react-router-dom: - specifier: 7.12.0 - version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 7.14.1 + version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) swr: - specifier: 2.3.8 - version: 2.3.8(react@19.2.3) + specifier: 2.4.1 + version: 2.4.1(react@19.2.5) devDependencies: '@plextv/vite-plugin-msdf-fontgen': specifier: workspace:* @@ -165,28 +165,28 @@ importers: version: link:../../packages/configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 '@types/react-dom': specifier: 'catalog:' - version: 19.2.3(@types/react@19.2.8) + version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-legacy': specifier: 'catalog:' - version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 8.0.1(terser@5.46.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 vite-tsconfig-paths: - specifier: 6.0.4 - version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 6.1.1 + version: 6.1.1(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) apps/react-native-lightning-example: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -209,20 +209,23 @@ importers: specifier: workspace:* version: link:../../packages/react-native-lightning-components '@react-navigation/native': - specifier: 7.1.28 - version: 7.1.28(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + specifier: 7.2.2 + version: 7.2.2(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react: specifier: 'catalog:' - version: 19.2.3 + version: 19.2.5 react-dom: specifier: 'catalog:' - version: 19.2.3(react@19.2.3) + version: 19.2.5(react@19.2.5) react-native: specifier: 'catalog:' - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: specifier: 'catalog:' - version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + react-native-worklets: + specifier: 0.8.1 + version: 0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) devDependencies: '@plextv/vite-plugin-msdf-fontgen': specifier: workspace:* @@ -238,28 +241,28 @@ importers: version: link:../../packages/configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 '@types/react-dom': specifier: 'catalog:' - version: 19.2.3(@types/react@19.2.8) + version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-legacy': specifier: 'catalog:' - version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 8.0.1(terser@5.46.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.2 + version: 6.0.2 apps/storybook: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -285,26 +288,32 @@ importers: specifier: workspace:* version: link:../../packages/react-native-lightning-components '@storybook/addon-docs': - specifier: 9.1.17 - version: 9.1.17(@types/react@19.2.8)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) + specifier: 10.3.5 + version: 10.3.5(@types/react@19.2.14)(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/addon-links': - specifier: 9.1.17 - version: 9.1.17(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) + specifier: 10.3.5 + version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/react-vite': - specifier: 9.1.17 - version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.55.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 10.3.5 + version: 10.3.5(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) react: specifier: 'catalog:' - version: 19.2.3 + version: 19.2.5 + react-dom: + specifier: 'catalog:' + version: 19.2.5(react@19.2.5) react-native: specifier: 'catalog:' - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: specifier: 'catalog:' - version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + react-native-worklets: + specifier: 0.8.1 + version: 0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) storybook: - specifier: 9.1.17 - version: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 10.3.5 + version: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) devDependencies: '@plextv/vite-plugin-msdf-fontgen': specifier: workspace:* @@ -320,10 +329,10 @@ importers: version: link:../../packages/configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 @@ -332,7 +341,7 @@ importers: devDependencies: '@rollup/plugin-babel': specifier: 7.0.0 - version: 7.0.0(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.55.2) + version: 7.0.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.55.2) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 @@ -341,7 +350,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -350,14 +359,14 @@ importers: version: link:../plugin-flexbox react-native: specifier: catalog:peers - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 csstype: specifier: 3.2.3 version: 3.2.3 @@ -392,13 +401,13 @@ importers: version: link:../configs type-fest: specifier: 'catalog:' - version: 5.4.1 + version: 5.5.0 packages/plugin-reanimated: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -413,35 +422,35 @@ importers: version: link:../react-native-lightning react: specifier: catalog:peers - version: 19.2.3 + version: 19.2.5 react-native: specifier: catalog:peers - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: specifier: catalog:peers - version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 type-fest: specifier: 'catalog:' - version: 5.4.1 + version: 5.5.0 packages/react-lightning: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 react: specifier: catalog:peers - version: 19.2.3 + version: 19.2.5 react-reconciler: specifier: 0.33.0 - version: 0.33.0(react@19.2.3) + version: 0.33.0(react@19.2.5) tseep: specifier: 'catalog:' version: 1.3.1 @@ -451,13 +460,13 @@ importers: version: link:../configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 '@types/react-reconciler': specifier: 'catalog:' - version: 0.32.3(@types/react@19.2.8) + version: 0.33.0(@types/react@19.2.14) type-fest: specifier: 'catalog:' - version: 5.4.1 + version: 5.5.0 packages/react-lightning-components: dependencies: @@ -469,20 +478,20 @@ importers: version: link:../plugin-flexbox react: specifier: catalog:peers - version: 19.2.3 + version: 19.2.5 devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 packages/react-native-lightning: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.0.0-beta20 + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -497,29 +506,29 @@ importers: version: link:../plugin-flexbox its-fine: specifier: 2.0.0 - version: 2.0.0(@types/react@19.2.8)(react@19.2.3) + version: 2.0.0(@types/react@19.2.14)(react@19.2.5) react: specifier: catalog:peers - version: 19.2.3 + version: 19.2.5 react-native: specifier: catalog:peers - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-web: specifier: 0.21.2 - version: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.21.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/node': - specifier: 25.0.9 - version: 25.0.9 + specifier: 25.6.0 + version: 25.6.0 '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 '@types/react-reconciler': specifier: 'catalog:' - version: 0.32.3(@types/react@19.2.8) + version: 0.33.0(@types/react@19.2.14) packages/react-native-lightning-components: dependencies: @@ -529,22 +538,25 @@ importers: '@plextv/react-lightning-components': specifier: workspace:^ version: link:../react-lightning-components + '@plextv/react-lightning-plugin-flexbox': + specifier: workspace:^ + version: link:../plugin-flexbox '@plextv/react-native-lightning': specifier: workspace:^ version: link:../react-native-lightning react: specifier: catalog:peers - version: 19.2.3 + version: 19.2.5 react-native: specifier: catalog:peers - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': specifier: 'catalog:' - version: 19.2.8 + version: 19.2.14 packages/vite-plugin-msdf-fontgen: dependencies: @@ -555,11 +567,11 @@ importers: specifier: 1.2.2 version: 1.2.2 glob: - specifier: 13.0.0 - version: 13.0.0 + specifier: 13.0.6 + version: 13.0.6 vite: specifier: catalog:peers - version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': specifier: workspace:* @@ -572,10 +584,10 @@ importers: version: link:../react-native-lightning '@vitejs/plugin-react': specifier: catalog:peers - version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) vite: specifier: catalog:peers - version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': specifier: workspace:* @@ -588,10 +600,10 @@ importers: version: link:../plugin-reanimated react-native-reanimated: specifier: catalog:peers - version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) vite: specifier: catalog:peers - version: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': specifier: workspace:* @@ -606,18 +618,38 @@ packages: resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.6': resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.6': resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.28.6': resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -638,8 +670,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-define-polyfill-provider@0.6.5': - resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -689,10 +721,18 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -710,6 +750,16 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} @@ -740,29 +790,31 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + '@babel/plugin-proposal-export-default-from@7.27.1': + resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + '@babel/plugin-syntax-export-default-from@7.28.6': + resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + '@babel/plugin-syntax-flow@7.28.6': + resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -779,64 +831,22 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-optional-chaining@7.8.3': resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.28.6': resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} @@ -855,8 +865,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.28.6': - resolution: {integrity: sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==} + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -879,12 +889,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.27.1': - resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.28.6': resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} engines: {node: '>=6.9.0'} @@ -897,12 +901,6 @@ packages: peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.4': - resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-classes@7.28.6': resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} engines: {node: '>=6.9.0'} @@ -933,8 +931,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.28.6': - resolution: {integrity: sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -963,6 +961,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-flow-strip-types@7.27.1': + resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-for-of@7.27.1': resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} engines: {node: '>=6.9.0'} @@ -1011,8 +1015,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.28.5': - resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} + '@babel/plugin-transform-modules-systemjs@7.29.0': + resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1023,8 +1027,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': - resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1035,12 +1039,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': - resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} engines: {node: '>=6.9.0'} @@ -1071,12 +1069,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.27.1': - resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.28.6': resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} engines: {node: '>=6.9.0'} @@ -1107,6 +1099,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -1119,8 +1117,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.6': - resolution: {integrity: sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==} + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1137,6 +1141,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-shorthand-properties@7.27.1': resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} engines: {node: '>=6.9.0'} @@ -1197,8 +1207,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.28.6': - resolution: {integrity: sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==} + '@babel/preset-env@7.29.2': + resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1226,12 +1236,24 @@ packages: resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.6': resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} - '@changesets/apply-release-plan@7.0.14': - resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@changesets/apply-release-plan@7.1.0': + resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} '@changesets/assemble-release-plan@6.0.9': resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} @@ -1239,12 +1261,12 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.8': - resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==} + '@changesets/cli@2.30.0': + resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} hasBin: true - '@changesets/config@3.1.2': - resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==} + '@changesets/config@3.1.3': + resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} @@ -1252,8 +1274,8 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.14': - resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==} + '@changesets/get-release-plan@4.0.15': + resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -1264,14 +1286,14 @@ packages: '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - '@changesets/parse@0.4.2': - resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==} + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} '@changesets/pre@2.0.2': resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - '@changesets/read@0.6.6': - resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==} + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} '@changesets/should-skip-package@0.1.2': resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} @@ -1285,20 +1307,14 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} @@ -1306,300 +1322,150 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.27.2': resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.27.2': resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.27.2': resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.27.2': resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.27.2': resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.27.2': resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.27.2': resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.27.2': resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.27.2': resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.27.2': resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.27.2': resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.27.2': resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.27.2': resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.27.2': resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.27.2': resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.27.2': resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.27.2': resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.27.2': resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.27.2': resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.27.2': resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.27.2': resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.27.2': resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} @@ -1615,50 +1481,14 @@ packages: '@types/node': optional: true - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/create-cache-key-function@29.7.0': - resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/types@29.6.3': resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1775,11 +1605,11 @@ packages: resolution: {integrity: sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==} engines: {node: '>=18'} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1': - resolution: {integrity: sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true @@ -1807,8 +1637,8 @@ packages: resolution: {integrity: sha512-FHIgj5rkOQPd9/wDXaiR0GOoWDEj7BytIzvYq5K8/wAh3z2bbW8gTNN+0J5kc1KXtqPrbyg1i87ksUVLrLEr1g==} engines: {node: '>=18.0.0'} - '@lightningjs/renderer@3.0.0-beta20': - resolution: {integrity: sha512-JsalgPEKvxc6q8cs9m2QW+aIwx9aET52nvU1OUhkO573Ae5WFJFM3MR/zTc2cPM3AJsa0alie98pKoGWrerO9g==} + '@lightningjs/renderer@3.0.1': + resolution: {integrity: sha512-xAn5eVtYdAmpqA8rN5/f4LnF/48cqrC204s1Mv51KL6GylYHCdhzdGqX480Apgiq3ub+DzNDgNs/xhTASzi7hQ==} engines: {node: '>= 18.0.0', npm: '>= 10.0.0', pnpm: '>= 10.17.0'} '@manypkg/find-root@1.1.0': @@ -1823,8 +1653,11 @@ packages: '@types/react': '>=16' react: '>=16' - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1838,274 +1671,267 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.107.0': - resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - '@oxc-project/types@0.108.0': - resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==} - - '@oxfmt/binding-android-arm-eabi@0.35.0': - resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} + '@oxfmt/binding-android-arm-eabi@0.45.0': + resolution: {integrity: sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.35.0': - resolution: {integrity: sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ==} + '@oxfmt/binding-android-arm64@0.45.0': + resolution: {integrity: sha512-L63z4uZmHjgvvqvMJD7mwff8aSBkM0+X4uFr6l6U5t6+Qc9DCLVZWIunJ7Gm4fn4zHPdSq6FFQnhu9yqqobxIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.35.0': - resolution: {integrity: sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA==} + '@oxfmt/binding-darwin-arm64@0.45.0': + resolution: {integrity: sha512-UV34dd623FzqT+outIGndsCA/RBB+qgB3XVQhgmmJ9PJwa37NzPC9qzgKeOhPKxVk2HW+JKldQrVL54zs4Noww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.35.0': - resolution: {integrity: sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw==} + '@oxfmt/binding-darwin-x64@0.45.0': + resolution: {integrity: sha512-pMNJv0CMa1pDefVPeNbuQxibh8ITpWDFEhMC/IBB9Zlu76EbgzYwrzI4Cb11mqX2+rIYN70UTrh3z06TM59ptQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.35.0': - resolution: {integrity: sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ==} + '@oxfmt/binding-freebsd-x64@0.45.0': + resolution: {integrity: sha512-xTcRoxbbo61sW2+ZRPeH+vp/o9G8gkdhiVumFU+TpneiPm14c79l6GFlxPXlCE9bNWikigbsrvJw46zCVAQFfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': - resolution: {integrity: sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.45.0': + resolution: {integrity: sha512-hWL8Hdni+3U1mPFx1UtWeGp3tNb6EhBAUHRMbKUxVkOp3WwoJbpVO2bfUVbS4PfpledviXXNHSTl1veTa6FhkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.35.0': - resolution: {integrity: sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A==} + '@oxfmt/binding-linux-arm-musleabihf@0.45.0': + resolution: {integrity: sha512-6Blt/0OBT7vvfQpqYuYbpbFLPqSiaYpEJzUUWhinPEuADypDbtV1+LdjM0vYBNGPvnj85ex7lTerEX6JGcPt9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.35.0': - resolution: {integrity: sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA==} + '@oxfmt/binding-linux-arm64-gnu@0.45.0': + resolution: {integrity: sha512-jLjoLfe+hGfjhA8hNBSdw85yCA8ePKq7ME4T+g6P9caQXvmt6IhE2X7iVjnVdkmYUWEzZrxlh4p6RkDmAMJY/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxfmt/binding-linux-arm64-musl@0.35.0': - resolution: {integrity: sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q==} + '@oxfmt/binding-linux-arm64-musl@0.45.0': + resolution: {integrity: sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxfmt/binding-linux-ppc64-gnu@0.35.0': - resolution: {integrity: sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw==} + '@oxfmt/binding-linux-ppc64-gnu@0.45.0': + resolution: {integrity: sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxfmt/binding-linux-riscv64-gnu@0.35.0': - resolution: {integrity: sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.45.0': + resolution: {integrity: sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxfmt/binding-linux-riscv64-musl@0.35.0': - resolution: {integrity: sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w==} + '@oxfmt/binding-linux-riscv64-musl@0.45.0': + resolution: {integrity: sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxfmt/binding-linux-s390x-gnu@0.35.0': - resolution: {integrity: sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g==} + '@oxfmt/binding-linux-s390x-gnu@0.45.0': + resolution: {integrity: sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxfmt/binding-linux-x64-gnu@0.35.0': - resolution: {integrity: sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w==} + '@oxfmt/binding-linux-x64-gnu@0.45.0': + resolution: {integrity: sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxfmt/binding-linux-x64-musl@0.35.0': - resolution: {integrity: sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA==} + '@oxfmt/binding-linux-x64-musl@0.45.0': + resolution: {integrity: sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxfmt/binding-openharmony-arm64@0.35.0': - resolution: {integrity: sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA==} + '@oxfmt/binding-openharmony-arm64@0.45.0': + resolution: {integrity: sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.35.0': - resolution: {integrity: sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg==} + '@oxfmt/binding-win32-arm64-msvc@0.45.0': + resolution: {integrity: sha512-v3Vj7iKKsUFwt9w5hsqIIoErKVoENC6LoqfDlteOQ5QMDCXihlqLoxpmviUhXnNncg4zV6U9BPwlBbwa+qm4wg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.35.0': - resolution: {integrity: sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g==} + '@oxfmt/binding-win32-ia32-msvc@0.45.0': + resolution: {integrity: sha512-N8yotPBX6ph0H3toF4AEpdCeVPrdcSetj+8eGiZGsrLsng3bs/Q5HPu4bbSxip5GBPx5hGbGHrZwH4+rcrjhHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.35.0': - resolution: {integrity: sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ==} + '@oxfmt/binding-win32-x64-msvc@0.45.0': + resolution: {integrity: sha512-w5MMTRCK1dpQeRA+HHqXQXyN33DlG/N2LOYxJmaT4fJjcmZrbNnqw7SmIk7I2/a2493PPLZ+2E/Ar6t2iKVMug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.15.0': - resolution: {integrity: sha512-d7Ch+A6hic+RYrm32+Gh1o4lOrQqnFsHi721ORdHUDBiQPea+dssKUEMwIbA6MKmCy6TVJ02sQyi24OEfCiGzw==} + '@oxlint-tsgolint/darwin-arm64@0.20.0': + resolution: {integrity: sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.15.0': - resolution: {integrity: sha512-Aoai2wAkaUJqp/uEs1gml6TbaPW4YmyO5Ai/vOSkiizgHqVctjhjKqmRiWTX2xuPY94VkwOLqp+Qr3y/0qSpWQ==} + '@oxlint-tsgolint/darwin-x64@0.20.0': + resolution: {integrity: sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.15.0': - resolution: {integrity: sha512-4og13a7ec4Vku5t2Y7s3zx6YJP6IKadb1uA9fOoRH6lm/wHWoCnxjcfJmKHXRZJII81WmbdJMSPxaBfwN/S68Q==} + '@oxlint-tsgolint/linux-arm64@0.20.0': + resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.15.0': - resolution: {integrity: sha512-9b9xzh/1Harn3a+XiKTK/8LrWw3VcqLfYp/vhV5/zAVR2Mt0d63WSp4FL+wG7DKnI2T/CbMFUFHwc7kCQjDMzQ==} + '@oxlint-tsgolint/linux-x64@0.20.0': + resolution: {integrity: sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.15.0': - resolution: {integrity: sha512-nNac5hewHdkk5mowOwTqB1ZD76zB/FsUiyUvdCyupq5cG54XyKqSLEp9QGbx7wFJkWCkeWmuwRed4sfpAlKaeA==} + '@oxlint-tsgolint/win32-arm64@0.20.0': + resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.15.0': - resolution: {integrity: sha512-ioAY2XLpy83E2EqOLH9p1cEgj0G2qB1lmAn0a3yFV1jHQB29LIPIKGNsu/tYCClpwmHN79pT5KZAHZOgWxxqNg==} + '@oxlint-tsgolint/win32-x64@0.20.0': + resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.50.0': - resolution: {integrity: sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg==} + '@oxlint/binding-android-arm-eabi@1.60.0': + resolution: {integrity: sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.50.0': - resolution: {integrity: sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg==} + '@oxlint/binding-android-arm64@1.60.0': + resolution: {integrity: sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.50.0': - resolution: {integrity: sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw==} + '@oxlint/binding-darwin-arm64@1.60.0': + resolution: {integrity: sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.50.0': - resolution: {integrity: sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw==} + '@oxlint/binding-darwin-x64@1.60.0': + resolution: {integrity: sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.50.0': - resolution: {integrity: sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw==} + '@oxlint/binding-freebsd-x64@1.60.0': + resolution: {integrity: sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.50.0': - resolution: {integrity: sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ==} + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': + resolution: {integrity: sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.50.0': - resolution: {integrity: sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg==} + '@oxlint/binding-linux-arm-musleabihf@1.60.0': + resolution: {integrity: sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.50.0': - resolution: {integrity: sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA==} + '@oxlint/binding-linux-arm64-gnu@1.60.0': + resolution: {integrity: sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-arm64-musl@1.50.0': - resolution: {integrity: sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ==} + '@oxlint/binding-linux-arm64-musl@1.60.0': + resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-ppc64-gnu@1.50.0': - resolution: {integrity: sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw==} + '@oxlint/binding-linux-ppc64-gnu@1.60.0': + resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxlint/binding-linux-riscv64-gnu@1.50.0': - resolution: {integrity: sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg==} + '@oxlint/binding-linux-riscv64-gnu@1.60.0': + resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-riscv64-musl@1.50.0': - resolution: {integrity: sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA==} + '@oxlint/binding-linux-riscv64-musl@1.60.0': + resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-s390x-gnu@1.50.0': - resolution: {integrity: sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ==} + '@oxlint/binding-linux-s390x-gnu@1.60.0': + resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxlint/binding-linux-x64-gnu@1.50.0': - resolution: {integrity: sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA==} + '@oxlint/binding-linux-x64-gnu@1.60.0': + resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-linux-x64-musl@1.50.0': - resolution: {integrity: sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ==} + '@oxlint/binding-linux-x64-musl@1.60.0': + resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-openharmony-arm64@1.50.0': - resolution: {integrity: sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw==} + '@oxlint/binding-openharmony-arm64@1.60.0': + resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.50.0': - resolution: {integrity: sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ==} + '@oxlint/binding-win32-arm64-msvc@1.60.0': + resolution: {integrity: sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.50.0': - resolution: {integrity: sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w==} + '@oxlint/binding-win32-ia32-msvc@1.60.0': + resolution: {integrity: sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.50.0': - resolution: {integrity: sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ==} + '@oxlint/binding-win32-x64-msvc@1.60.0': + resolution: {integrity: sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2121,72 +1947,92 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@react-native/assets-registry@0.82.1': - resolution: {integrity: sha512-B1SRwpntaAcckiatxbjzylvNK562Ayza05gdJCjDQHTiDafa1OABmyB5LHt7qWDOpNkaluD+w11vHF7pBmTpzQ==} - engines: {node: '>= 20.19.4'} + '@react-native/assets-registry@0.85.1': + resolution: {integrity: sha512-QODQ15teXThKaKdb7lnx4RifNUGnsGZ/NMKtkNBE89nJuK93+mPsb1ozp5xkGyLw7ZNVYO4Nkqsp4MsBOuAX8g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/babel-plugin-codegen@0.85.1': + resolution: {integrity: sha512-Klex4kTsRxoswZmo7EBXobvpg+HO6h7xeGo87CLXSKPq3qHlJ8ilpgtmzYCTK+Qr/0Mk3cz2zv3bA9VTXR+NDA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/codegen@0.82.1': - resolution: {integrity: sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg==} - engines: {node: '>= 20.19.4'} + '@react-native/babel-preset@0.85.1': + resolution: {integrity: sha512-Mplsn13fCxQElOfWg6wIuXJP+tyO980etTQ1gQFTt5Zstj3rs33GzTLMNlo6EnT8PQghO3GxIrg/2im5GwodnA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: '@babel/core': '*' - '@react-native/community-cli-plugin@0.82.1': - resolution: {integrity: sha512-H/eMdtOy9nEeX7YVeEG1N2vyCoifw3dr9OV8++xfUElNYV7LtSmJ6AqxZUUfxGJRDFPQvaU/8enmJlM/l11VxQ==} - engines: {node: '>= 20.19.4'} + '@react-native/codegen@0.85.1': + resolution: {integrity: sha512-Ge8F5VejnI7ng/NGObqBBovuLbItvmmZDFQ1Qwt/nBhHtk7l2tOffNMVNTta9Jt8TW0oXxVj6FG3hr6nx03JrQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.85.1': + resolution: {integrity: sha512-vZtNEYv5qMYvbA9cTBMuZ3QkCqyJ7lDQgbxh4MpoZHZ0+62qjJpCXn9xzFM0Rm5ZG2hO8WDDxcFdI581BdASdg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: '@react-native-community/cli': '*' - '@react-native/metro-config': '*' + '@react-native/metro-config': 0.85.1 peerDependenciesMeta: '@react-native-community/cli': optional: true '@react-native/metro-config': optional: true - '@react-native/debugger-frontend@0.82.1': - resolution: {integrity: sha512-a2O6M7/OZ2V9rdavOHyCQ+10z54JX8+B+apYKCQ6a9zoEChGTxUMG2YzzJ8zZJVvYf1ByWSNxv9Se0dca1hO9A==} - engines: {node: '>= 20.19.4'} + '@react-native/debugger-frontend@0.85.1': + resolution: {integrity: sha512-GUC2ZEy+J/Goc4l243XeeY/8NdNXVXPXoRTc6Yy14OiDcy7Yk87VyrMARbp23wCbzhnrz0dnYB8rxJ+AJvMzCg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/debugger-shell@0.82.1': - resolution: {integrity: sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw==} - engines: {node: '>= 20.19.4'} + '@react-native/debugger-shell@0.85.1': + resolution: {integrity: sha512-M/ogODh0uDFJ7xOlCc+v9nKUucUXGJwVOupl+zb3VT8tJnI2Cie/Fiv9NszAD/bzRQhJSrPZkJSAO6VW0XbWyA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/dev-middleware@0.82.1': - resolution: {integrity: sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w==} - engines: {node: '>= 20.19.4'} + '@react-native/dev-middleware@0.85.1': + resolution: {integrity: sha512-vJSIZP7yymZMnwOrdNjalVf8jqcAFtmi6zT3sC9MRMgJPGkDy05g8y5zgAkgTxpNtVsv+/q5pst8woYp7pgRkA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/gradle-plugin@0.82.1': - resolution: {integrity: sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q==} - engines: {node: '>= 20.19.4'} + '@react-native/gradle-plugin@0.85.1': + resolution: {integrity: sha512-KeTntbnsH/NOdzZrSE8tgep+9jEMlEfklVDtgxnjjb5nDhhBD016judwyo9bsinZnuwXxmemXnOOqOfcEawxbg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/js-polyfills@0.82.1': - resolution: {integrity: sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA==} - engines: {node: '>= 20.19.4'} + '@react-native/js-polyfills@0.85.1': + resolution: {integrity: sha512-VseQZAKnDbmpZThLWviDIJ0NmuSiwiHA6vc2HNJTTVqTy2mQR0+858y9kDdDBQPYe0HH8+W1mYui2i4eUWGh4g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/metro-babel-transformer@0.85.1': + resolution: {integrity: sha512-oXAVv9GfGYxkqdf20o+gbJSw4yqaUZr7AZMZ4bJG8Nom/T9GmLu/Pd2kJo5U6NQYIndgfgU73pzRgL8H7YCIWw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/metro-config@0.85.1': + resolution: {integrity: sha512-Na0OD2YFM7rESHJ3ETuYHnXNc5TJU/fpwlLmN2/uDTM9ZDb6EaEfFKZaXGbUm2lBYyeo/FG3Ur4glu8jLWMNgQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} '@react-native/normalize-colors@0.74.89': resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==} - '@react-native/normalize-colors@0.82.1': - resolution: {integrity: sha512-CCfTR1uX+Z7zJTdt3DNX9LUXr2zWXsNOyLbwupW2wmRzrxlHRYfmLgTABzRL/cKhh0Ubuwn15o72MQChvCRaHw==} + '@react-native/normalize-colors@0.85.1': + resolution: {integrity: sha512-w+4ZZ2PvvtC0IODEmxizYOrHmeDgdzpM7CKhtTNWoNtDWZoi7/ZY3UmNntn9poPorUy5cwFbfYiP/8rJFEsFvQ==} - '@react-native/virtualized-lists@0.82.1': - resolution: {integrity: sha512-f5zpJg9gzh7JtCbsIwV+4kP3eI0QBuA93JGmwFRd4onQ3DnCjV2J5pYqdWtM95sjSKK1dyik59Gj01lLeKqs1Q==} - engines: {node: '>= 20.19.4'} + '@react-native/virtualized-lists@0.85.1': + resolution: {integrity: sha512-RLpoATkxeTaYxna5dDLIxEtoStp9UL7ryHIIOmKnE9NQW3ggR+U9DWQPXQkOfRc7/kPYba4ynKA2fIISGysVTg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: - '@types/react': ^19.1.1 + '@types/react': ^19.2.0 react: '*' - react-native: '*' + react-native: 0.85.1 peerDependenciesMeta: '@types/react': optional: true - '@react-navigation/core@7.14.0': - resolution: {integrity: sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g==} + '@react-navigation/core@7.17.2': + resolution: {integrity: sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==} peerDependencies: react: '>= 18.2.0' - '@react-navigation/native@7.1.28': - resolution: {integrity: sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==} + '@react-navigation/native@7.2.2': + resolution: {integrity: sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==} peerDependencies: react: '>= 18.2.0' react-native: '*' @@ -2194,168 +2040,100 @@ packages: '@react-navigation/routers@7.5.3': resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} - '@rolldown/binding-android-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-android-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ==} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.59': - resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-beta.60': - resolution: {integrity: sha512-WnxyqxAKP2BsxouwGY/RCF5UFw/LA4QOHhJ7VEl+UCelHokiwqNHRbryLAyRy3TE1FZ5eae+vAFcaetAu/kWLw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.59': - resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-freebsd-x64@1.0.0-beta.60': - resolution: {integrity: sha512-JtyWJ+zXOHof5gOUYwdTWI2kL6b8q9eNwqB/oD4mfUFaC/COEB2+47JMhcq78dey9Ahmec3DZKRDZPRh9hNAMQ==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': - resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': - resolution: {integrity: sha512-LrMoKqpHx+kCaNSk84iSBd4yVOymLIbxJQtvFjDN2CjQraownR+IXcwYDblFcj9ivmS54T3vCboXBbm3s1zbPQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': - resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': - resolution: {integrity: sha512-sqI+Vdx1gmXJMsXN3Fsewm3wlt7RHvRs1uysSp//NLsCoh9ZFEUr4ZzGhWKOg6Rvf+njNu/vCsz96x7wssLejQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': - resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': - resolution: {integrity: sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': - resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': - resolution: {integrity: sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': - resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': - resolution: {integrity: sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': - resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': - resolution: {integrity: sha512-6roXGbHMdR2ucnxXuwbmQvk8tuYl3VGu0yv13KxspyKBxxBd4RS6iykzLD6mX2gMUHhfX8SVWz7n/62gfyKHow==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': - resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-JBOm8/DC/CKnHyMHoJFdvzVHxUixid4dGkiTqGflxOxO43uSJMpl77pSPXvzwZ/VXwqblU2V0/PanyCBcRLowQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': - resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-MKF0B823Efp+Ot8KsbwIuGhKH58pf+2rSM6VcqyNMlNBHheOM0Gf7JmEu+toc1jgN6fqjH7Et+8hAzsLVkIGfA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.53': - resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - '@rolldown/pluginutils@1.0.0-beta.59': - resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==} - - '@rolldown/pluginutils@1.0.0-beta.60': - resolution: {integrity: sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==} + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} '@rollup/plugin-babel@7.0.0': resolution: {integrity: sha512-NS2+P7v80N3MQqehZEjgpaFb9UyX3URNMW/zvoECKGo4PY4DvJfQusTI7BX/Ks+CPvtTfk3TqcR6S9VYBi/C+A==} @@ -2511,73 +2289,77 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@9.1.17': - resolution: {integrity: sha512-yc4hlgkrwNi045qk210dRuIMijkgbLmo3ft6F4lOdpPRn4IUnPDj7FfZR8syGzUzKidxRfNtLx5m0yHIz83xtA==} + '@storybook/addon-docs@10.3.5': + resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==} peerDependencies: - storybook: ^9.1.17 + storybook: ^10.3.5 - '@storybook/addon-links@9.1.17': - resolution: {integrity: sha512-LqtrDXRJrdMfZJ0om38SjmY2lQUGmpL8Zt2xTZtQjUy1V+ZiQpuUx7+TJZIOWujzRp75fxhM4AB0HuLDsemlcA==} + '@storybook/addon-links@10.3.5': + resolution: {integrity: sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.5 peerDependenciesMeta: react: optional: true - '@storybook/builder-vite@9.1.17': - resolution: {integrity: sha512-OQCYaFWoTBvovN2IJmkAW+7FgHMJiih1WA/xqgpKIx0ImZjB4z5FrKgzQeXsrYcLEsynyaj+xN3JFUKsz5bzGQ==} + '@storybook/builder-vite@10.3.5': + resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==} peerDependencies: - storybook: ^9.1.17 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + storybook: ^10.3.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@9.1.17': - resolution: {integrity: sha512-o+ebQDdSfZHDRDhu2hNDGhCLIazEB4vEAqJcHgz1VsURq+l++bgZUcKojPMCAbeblptSEz2bwS0eYAOvG7aSXg==} + '@storybook/csf-plugin@10.3.5': + resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==} peerDependencies: - storybook: ^9.1.17 + esbuild: '*' + rollup: '*' + storybook: ^10.3.5 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - '@storybook/icons@1.6.0': - resolution: {integrity: sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==} - engines: {node: '>=14.0.0'} + '@storybook/icons@2.0.1': + resolution: {integrity: sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@9.1.17': - resolution: {integrity: sha512-Ss/lNvAy0Ziynu+KniQIByiNuyPz3dq7tD62hqSC/pHw190X+M7TKU3zcZvXhx2AQx1BYyxtdSHIZapb+P5mxQ==} + '@storybook/react-dom-shim@10.3.5': + resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.5 - '@storybook/react-vite@9.1.17': - resolution: {integrity: sha512-RZHsqD1mnTMo4MCJw68t3swS5BTMSTpeRhlelMwjoTEe7jJCPa+qx00uMlWliR1QBN1hMO8Y1dkchxSiUS9otA==} - engines: {node: '>=20.0.0'} + '@storybook/react-vite@10.3.5': + resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@9.1.17': - resolution: {integrity: sha512-TZCplpep5BwjHPIIcUOMHebc/2qKadJHYPisRn5Wppl014qgT3XkFLpYkFgY1BaRXtqw8Mn3gqq4M/49rQ7Iww==} - engines: {node: '>=20.0.0'} + '@storybook/react@10.3.5': + resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.5 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -2600,6 +2382,36 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@turbo/darwin-64@2.9.6': + resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.6': + resolution: {integrity: sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.6': + resolution: {integrity: sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.6': + resolution: {integrity: sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.6': + resolution: {integrity: sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.6': + resolution: {integrity: sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A==} + cpu: [arm64] + os: [win32] + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2630,9 +2442,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -2642,6 +2451,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -2654,8 +2466,8 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@25.0.9': - resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2670,61 +2482,54 @@ packages: peerDependencies: '@types/react': '*' - '@types/react-reconciler@0.32.3': - resolution: {integrity: sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA==} + '@types/react-reconciler@0.33.0': + resolution: {integrity: sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==} peerDependencies: '@types/react': '*' - '@types/react@19.2.8': - resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@vitejs/plugin-legacy@7.2.1': - resolution: {integrity: sha512-CaXb/y0mlfu7jQRELEJJc2/5w2bX2m1JraARgFnvSB2yfvnCNJVWWlqAo6WjnKoepOwKx8gs0ugJThPLKCOXIg==} + '@vitejs/plugin-legacy@8.0.1': + resolution: {integrity: sha512-8zeDeuNPqXd49rIVgFgluQYB8vQICHR7l+W2I3CxYK4gTjTorajVr0wLvSjALIwEwLRxBn68EgNVyGP4j6hP7w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: terser: ^5.16.0 - vite: ^7.0.0 + vite: ^8.0.0 - '@vitejs/plugin-react@5.1.2': - resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.0.17': - resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} - '@vitest/mocker@4.0.17': - resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true @@ -2734,26 +2539,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.0.17': - resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} - '@vitest/runner@4.0.17': - resolution: {integrity: sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==} + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} - '@vitest/snapshot@4.0.17': - resolution: {integrity: sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==} + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.0.17': - resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==} + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.0.17': - resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} '@vue/compiler-core@3.5.27': resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} @@ -2770,12 +2575,15 @@ packages: '@vue/shared@3.5.27': resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} acorn@8.15.0: @@ -2828,10 +2636,6 @@ packages: any-base@1.1.0: resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - arabic-persian-reshaper@1.0.1: resolution: {integrity: sha512-VYBjkhz6o4W1Xt4mD2LAReljJpLSw5CUZMqSBDIQRvFgUSlTKEYghapgBWvkeMWF4W+KF3Fm+/z8EywJU4PBeg==} @@ -2867,17 +2671,14 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-kit@2.2.0: - resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} engines: {node: '>=20.19.0'} ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - async-limiter@1.0.1: - resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} - atomically@2.1.0: resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} @@ -2885,22 +2686,8 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-plugin-polyfill-corejs2@0.4.14: - resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -2909,31 +2696,32 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.5: - resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} - babel-plugin-syntax-hermes-parser@0.32.0: - resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} + babel-plugin-syntax-hermes-parser@0.33.3: + resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==} - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 + babel-plugin-transform-flow-enums@0.0.2: + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2941,10 +2729,6 @@ packages: resolution: {integrity: sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==} hasBin: true - better-opn@3.0.2: - resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} - engines: {node: '>=12.0.0'} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -2965,6 +2749,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2990,9 +2778,13 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} @@ -3001,10 +2793,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -3044,8 +2832,8 @@ packages: engines: {node: '>=12.13.0'} hasBin: true - chromium-edge-launcher@0.2.0: - resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} + chromium-edge-launcher@0.3.0: + resolution: {integrity: sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==} ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -3066,8 +2854,8 @@ packages: resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} engines: {node: '>=4'} - cli-truncate@5.1.1: - resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} cliui@7.0.4: @@ -3084,9 +2872,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3123,11 +2908,11 @@ packages: resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} hasBin: true - core-js-compat@3.47.0: - resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} - core-js@3.47.0: - resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3186,12 +2971,20 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} del-cli@7.0.0: resolution: {integrity: sha512-fRl4pWJYu9WFQH8jXdQUYvcD0IMtij9WEc7qmB7xOyJEweNJNuE7iKmqNeoOT1DbBUjtRjxlw8Y63qKBI/NQ1g==} @@ -3230,6 +3023,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3257,9 +3054,6 @@ packages: oxc-resolver: optional: true - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3272,9 +3066,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -3305,18 +3096,8 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} @@ -3334,10 +3115,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3396,9 +3173,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3445,10 +3219,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} - findup-sync@5.0.0: resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==} engines: {node: '>= 10.13.0'} @@ -3456,10 +3226,6 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -3499,13 +3265,16 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + gifwrap@0.10.1: resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} @@ -3513,14 +3282,9 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -3568,21 +3332,27 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hermes-compiler@0.0.0: - resolution: {integrity: sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA==} + hermes-compiler@250829098.0.10: + resolution: {integrity: sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==} - hermes-estree@0.32.0: - resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} + hermes-estree@0.33.3: + resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} - hermes-parser@0.32.0: - resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} + hermes-estree@0.35.0: + resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} + + hermes-parser@0.33.3: + resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} + + hermes-parser@0.35.0: + resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} - hookable@6.0.1: - resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + hookable@6.1.0: + resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} @@ -3635,10 +3405,6 @@ packages: resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} engines: {node: '>=20.19.0'} - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3675,6 +3441,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3696,6 +3467,11 @@ packages: engines: {node: '>=18'} hasBin: true + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-installed-globally@1.0.0: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} engines: {node: '>=18'} @@ -3732,6 +3508,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -3741,46 +3521,15 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - its-fine@2.0.0: resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} peerDependencies: react: ^19.0.0 - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-util@29.7.0: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3851,21 +3600,87 @@ packages: lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - listr2@10.0.0: - resolution: {integrity: sha512-/hexbwaVUnXaKtJQWjcoMnLQhseLD9uv+5g0Lprtj677jWOX9rJChBVqoN51bwu94QLz6C2q7almMYJ0f97bLQ==} - engines: {node: '>=22.0.0'} + listr2@10.2.1: + resolution: {integrity: sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==} + engines: {node: '>=22.13.0'} locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -3878,9 +3693,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-update@7.0.2: - resolution: {integrity: sha512-cSSF1K5w9juI2+JeSRAdaTUZJf6cJB0aWwWO1nQQkcWw44+bIfXmhZMwK2eEsv6tXvU3UfKX/kzcX6SP+1tLAw==} - engines: {node: '>=20'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -3889,9 +3704,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -3939,75 +3751,75 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - metro-babel-transformer@0.83.3: - resolution: {integrity: sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==} - engines: {node: '>=20.19.4'} + metro-babel-transformer@0.84.3: + resolution: {integrity: sha512-svAA+yMLpeMiGcz/jKJs4oHpIGEx4nBqNEJ5AGj4CYIg1efvK+A0TjR6tgIuc6tKO5e8JmN/1lglpN2+f3/z/w==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-cache-key@0.83.3: - resolution: {integrity: sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==} - engines: {node: '>=20.19.4'} + metro-cache-key@0.84.3: + resolution: {integrity: sha512-TnSL1Fdvrw+2glTdBSRmA5TL8l/i16ECjsrUdf3E5HncA+sNx8KcwDG8r+3ct1UhfYcusJypzZqTN55FZZcwGg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-cache@0.83.3: - resolution: {integrity: sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==} - engines: {node: '>=20.19.4'} + metro-cache@0.84.3: + resolution: {integrity: sha512-0QElxwLaHqLZf+Xqio8QrjVbuXP/8sJfQBGSPiITlKDVXrVLefuzYVSH9Sj+QL6lrPj2gYZd/iwQh1yZuVKnLA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-config@0.83.3: - resolution: {integrity: sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==} - engines: {node: '>=20.19.4'} + metro-config@0.84.3: + resolution: {integrity: sha512-JmCzZWOETR+O22q8oPBWyQppx3roU9EbkbGzD8Gf1jukQ4b5T1fTzqqHruu6K4sTiNq5zVQySmKF6bp4kVARew==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-core@0.83.3: - resolution: {integrity: sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==} - engines: {node: '>=20.19.4'} + metro-core@0.84.3: + resolution: {integrity: sha512-cc0pvAa80ai1nDmqqz0P59a+0ZqCZ/YHU/3jEekZL6spFnYDfX8iDLdn9FR6kX+67rmzKxHNrbrSRFLX2AYocw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-file-map@0.83.3: - resolution: {integrity: sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==} - engines: {node: '>=20.19.4'} + metro-file-map@0.84.3: + resolution: {integrity: sha512-1cL4m4Jv1yRUt9RJExZQLfccscdlMNOcRG6LHLtmJhf3BG9j3MujPVc7CIpKYdFl+KUl+sdjge6oO3+meKCHQA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-minify-terser@0.83.3: - resolution: {integrity: sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==} - engines: {node: '>=20.19.4'} + metro-minify-terser@0.84.3: + resolution: {integrity: sha512-3ofrG2OQyJbO9RNhCfOcl8QU7EE2WrSsnN5dFkuZaJO5+4Imujr9bUXmspeNlXRsOVk0F/rVRbEFH98lFSCkBQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-resolver@0.83.3: - resolution: {integrity: sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==} - engines: {node: '>=20.19.4'} + metro-resolver@0.84.3: + resolution: {integrity: sha512-pjEzGDtoM8DTHAIPK/9u9ZxszEiuRohYUVImWvgbnB91V4gqYJpQcoEYUugf2NIm1lrX5HNu0OvNqWmPBnGYjA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-runtime@0.83.3: - resolution: {integrity: sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==} - engines: {node: '>=20.19.4'} + metro-runtime@0.84.3: + resolution: {integrity: sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-source-map@0.83.3: - resolution: {integrity: sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==} - engines: {node: '>=20.19.4'} + metro-source-map@0.84.3: + resolution: {integrity: sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-symbolicate@0.83.3: - resolution: {integrity: sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==} - engines: {node: '>=20.19.4'} + metro-symbolicate@0.84.3: + resolution: {integrity: sha512-J9Tpo8NCycYrozRvBIUyOwGAu4xkawOsAppmTscFiaegK0WvuDGwIM53GbzVSnytCHjVAF0io5GQxpkrKTuc7g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true - metro-transform-plugins@0.83.3: - resolution: {integrity: sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==} - engines: {node: '>=20.19.4'} + metro-transform-plugins@0.84.3: + resolution: {integrity: sha512-8S3baq2XhBaafHEH5Q8sJW6tmzsEJk80qKc3RU/nZV1MsnYq94RdjTUR6AyKjQd6Rfsk1BtBxhtiNnk7mgslCg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-transform-worker@0.83.3: - resolution: {integrity: sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==} - engines: {node: '>=20.19.4'} + metro-transform-worker@0.84.3: + resolution: {integrity: sha512-Wjba7PyYktNRsHbPmkx2J2UX32rAzcDXjCu49zPHeF/viJlYJhwRaNePQcHaCRqQ+kmgQT4ThprsnJfDj71ZMA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro@0.83.3: - resolution: {integrity: sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==} - engines: {node: '>=20.19.4'} + metro@0.84.3: + resolution: {integrity: sha512-1h3lbVrE6hGf1e/764HfhPGg/bGrWMJDDh7G2rc4gFYZboVuI40BlG/y+UhtbhQDNlO/csMvrcnK0YrTlHUVew==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} @@ -4027,9 +3839,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4038,15 +3850,11 @@ packages: resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} mkdirp@1.0.4: @@ -4077,8 +3885,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} neo-async@2.6.2: @@ -4102,16 +3910,12 @@ packages: noms@0.0.0: resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - ob1@0.83.3: - resolution: {integrity: sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==} - engines: {node: '>=20.19.4'} + ob1@0.84.3: + resolution: {integrity: sha512-J7554Ef8bzmKaDY365Afq6PF+qtdnY/d5PKUQFrsKlZHV/N3OGZewVrvDrQDyX5V5NJjTpcAKtlrFZcDr+HvpQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -4141,14 +3945,14 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - opentype.js@1.3.4: resolution: {integrity: sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==} engines: {node: '>= 8.0.0'} @@ -4157,21 +3961,21 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - oxfmt@0.35.0: - resolution: {integrity: sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q==} + oxfmt@0.45.0: + resolution: {integrity: sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.15.0: - resolution: {integrity: sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw==} + oxlint-tsgolint@0.20.0: + resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} hasBin: true - oxlint@1.50.0: - resolution: {integrity: sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ==} + oxlint@1.60.0: + resolution: {integrity: sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.14.1' + oxlint-tsgolint: '>=0.18.0' peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -4184,18 +3988,10 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -4208,9 +4004,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-json@10.0.1: resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} engines: {node: '>=18'} @@ -4250,10 +4043,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -4265,13 +4054,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -4303,14 +4088,14 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - pixelmatch@5.3.0: resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} hasBin: true @@ -4333,6 +4118,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + presentable-error@0.0.1: resolution: {integrity: sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==} engines: {node: '>=16'} @@ -4406,10 +4195,10 @@ packages: resolution: {integrity: sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==} engines: {node: ^20.9.0 || >=22} - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: - react: ^19.2.3 + react: ^19.2.5 react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -4420,18 +4209,18 @@ packages: react-is@19.2.3: resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==} - react-native-is-edge-to-edge@1.2.1: - resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} + react-native-is-edge-to-edge@1.3.1: + resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==} peerDependencies: react: '*' react-native: '*' - react-native-reanimated@4.2.1: - resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==} + react-native-reanimated@4.3.0: + resolution: {integrity: sha512-HOTTPdKtddXTOsmQxDASXEwLS3lqEHrKERD3XOgzSqWJ7L3x81Pnx7mTcKx1FKdkgomMug/XSmm1C6Z7GIowxA==} peerDependencies: react: '*' - react-native: '*' - react-native-worklets: '>=0.7.0' + react-native: 0.81 - 0.85 + react-native-worklets: 0.8.x react-native-web@0.21.2: resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} @@ -4439,21 +4228,25 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - react-native-worklets@0.7.2: - resolution: {integrity: sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog==} + react-native-worklets@0.8.1: + resolution: {integrity: sha512-oWP/lStsAHU6oYCaWDXrda/wOHVdhusQJz1e6x9gPnXdFf4ndNDAOtWCmk2zGrAnlapfyA3rM6PCQq94mPg9cw==} peerDependencies: '@babel/core': '*' + '@react-native/metro-config': '*' react: '*' - react-native: '*' + react-native: 0.81 - 0.85 - react-native@0.82.1: - resolution: {integrity: sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA==} - engines: {node: '>= 20.19.4'} + react-native@0.85.1: + resolution: {integrity: sha512-1K2TIvu2M1C8gqkPevi/MuLan16mQvEdURiTlwHgrb6S2vvkDyik6TrkkXMlMMhz9hF5RT8wFyDUdlpGFlkpXg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true peerDependencies: + '@react-native/jest-preset': 0.85.1 '@types/react': ^19.1.1 - react: ^19.1.1 + react: ^19.2.3 peerDependenciesMeta: + '@react-native/jest-preset': + optional: true '@types/react': optional: true @@ -4467,19 +4260,15 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react-router-dom@7.12.0: - resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + react-router-dom@7.14.1: + resolution: {integrity: sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.12.0: - resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + react-router@7.14.1: + resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -4488,8 +4277,8 @@ packages: react-dom: optional: true - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} read-yaml-file@1.1.0: @@ -4592,19 +4381,14 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rolldown-plugin-dts@0.20.0: - resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==} engines: {node: '>=20.19.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 - '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-beta.57 - typescript: ^5.0.0 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 vue-tsc: ~3.2.0 peerDependenciesMeta: '@ts-macro/tsc': @@ -4616,13 +4400,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.59: - resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rolldown@1.0.0-beta.60: - resolution: {integrity: sha512-YYgpv7MiTp9LdLj1fzGzCtij8Yi2OKEc3HQtfbIxW4yuSgpQz9518I69U72T5ErPA/ATOXqlcisiLrWy+5V9YA==} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4631,6 +4410,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4647,9 +4430,6 @@ packages: resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} engines: {node: '>=11.0.0'} - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4665,6 +4445,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -4701,9 +4486,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -4724,6 +4506,10 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4749,10 +4535,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4771,11 +4553,11 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - storybook@9.1.17: - resolution: {integrity: sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==} + storybook@10.3.5: + resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -4791,16 +4573,12 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.1.0: - resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} string.prototype.codepointat@0.2.1: @@ -4864,8 +4642,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.3.8: - resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4885,10 +4663,6 @@ packages: engines: {node: '>=10'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -4911,10 +4685,18 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -4923,8 +4705,8 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tinyspy@4.0.4: @@ -4971,28 +4753,31 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tsdown@0.19.0: - resolution: {integrity: sha512-uqg8yzlS7GemFWcM6aCp/sptF4bJiJbWUibuHTRLLCBEsGCgJxuqxPhuVTqyHXqoEkh9ohwAdlyDKli5MEWCyQ==} + tsdown@0.21.8: + resolution: {integrity: sha512-rHDIER4JU5owYTWptvyDk6pwfA5lCft1P+11HLGeF0uj0CB7vopFvr/E8QOaRmegeyHIEsu4+03j7ysvdgBAVA==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.8 + '@tsdown/exe': 0.21.8 '@vitejs/devtools': '*' publint: ^0.3.0 - typescript: ^5.0.0 - unplugin-lightningcss: ^0.4.0 + typescript: ^5.0.0 || ^6.0.0 unplugin-unused: ^0.5.0 peerDependenciesMeta: '@arethetypeswrong/core': optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true '@vitejs/devtools': optional: true publint: optional: true typescript: optional: true - unplugin-lightningcss: - optional: true unplugin-unused: optional: true @@ -5007,44 +4792,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.7.5: - resolution: {integrity: sha512-nN3wfLLj4OES/7awYyyM7fkU8U8sAFxsXau2bYJwAWi6T09jd87DgHD8N31zXaJ7LcpyppHWPRI2Ov9MuZEwnQ==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.7.5: - resolution: {integrity: sha512-wCoDHMiTf3FgLAbZHDDx/unNNonSGhsF5AbbYODbxnpYyoKDpEYacUEPjZD895vDhNvYCH0Nnk24YsP4n/cD6g==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.7.5: - resolution: {integrity: sha512-KKPvhOmJMmzWj/yjeO4LywkQ85vOJyhru7AZk/+c4B6OUh/odQ++SiIJBSbTG2lm1CuV5gV5vXZnf/2AMlu3Zg==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.7.5: - resolution: {integrity: sha512-8PIva4L6BQhiPikUTds9lSFSHXVDAsEvV6QUlgwPsXrtXVQMVi6Sv9p+IxtlWQFvGkdYJUgX9GnK2rC030Xcmw==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.7.5: - resolution: {integrity: sha512-rupskv/mkIUgQXzX/wUiK00mKMorQcK8yzhGFha/D5lm05FEnLx8dsip6rWzMcVpvh+4GUMA56PgtnOgpel2AA==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.7.5: - resolution: {integrity: sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw==} - cpu: [arm64] - os: [win32] - - turbo@2.7.5: - resolution: {integrity: sha512-7Imdmg37joOloTnj+DPrab9hIaQcDdJ5RwSzcauo/wMOSAgO+A/I/8b3hsGGs6PWQz70m/jkPgdqWsfNKtwwDQ==} + turbo@2.9.6: + resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} hasBin: true - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - type-fest@0.7.1: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} @@ -5053,8 +4804,8 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type-fest@5.4.1: - resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==} + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} typescript@5.9.3: @@ -5062,6 +4813,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + ua-parser-js@1.0.41: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true @@ -5071,11 +4827,11 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - unconfig-core@7.4.2: - resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -5093,10 +4849,6 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5113,12 +4865,12 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin@1.16.1: - resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} - engines: {node: '>=14.0.0'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} - unrun@0.2.25: - resolution: {integrity: sha512-ZOr5uQL+JlcUT8hZsQbtuUgb1zzcFx3juhXyLSsciaWa3DW1ldMY9r4KSF3+k/LR1Evj2ggAZo1usK4/knBjMQ==} + unrun@0.2.35: + resolution: {integrity: sha512-nDP7mA4Fu5owDarQtLiiN3lq7tJZHFEAVIchnwP8U3wMeEkLoUNT37Hva85H05Rdw8CArpGmtY+lBjpk9fruVQ==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -5166,23 +4918,21 @@ packages: peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - vite-tsconfig-paths@6.0.4: - resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} + vite-tsconfig-paths@6.1.1: + resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: vite: '*' - peerDependenciesMeta: - vite: - optional: true - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -5193,12 +4943,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -5214,20 +4966,23 @@ packages: yaml: optional: true - vitest@4.0.17: - resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.17 - '@vitest/browser-preview': 4.0.17 - '@vitest/browser-webdriverio': 4.0.17 - '@vitest/ui': 4.0.17 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -5241,6 +4996,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -5290,14 +5049,14 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -5305,21 +5064,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - ws@6.2.3: - resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -5344,6 +5088,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -5377,8 +5125,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -5398,10 +5146,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -5418,8 +5162,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.6': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.6': dependencies: '@babel/code-frame': 7.28.6 @@ -5440,6 +5192,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.28.6': dependencies: '@babel/parser': 7.28.6 @@ -5448,6 +5220,23 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/generator@8.0.0-rc.3': + dependencies: + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.6 @@ -5473,6 +5262,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.6 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 @@ -5480,7 +5282,14 @@ snapshots: regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.6)': + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-compilation-targets': 7.28.6 @@ -5491,6 +5300,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + '@babel/helper-globals@7.28.0': {} '@babel/helper-member-expression-to-functions@7.28.5': @@ -5512,7 +5332,16 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5527,7 +5356,16 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5540,6 +5378,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.6 @@ -5549,14 +5396,18 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@8.0.0-rc.3': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.6': dependencies: '@babel/template': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 '@babel/types': 7.28.6 transitivePeerDependencies: - supports-color @@ -5564,119 +5415,127 @@ snapshots: '@babel/helpers@7.28.6': dependencies: '@babel/template': 7.28.6 - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@babel/parser@7.28.6': dependencies: '@babel/types': 7.28.6 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.6)': + '@babel/parser@7.29.2': dependencies: - '@babel/core': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@8.0.0-rc.3': + dependencies: + '@babel/types': 8.0.0-rc.3 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/traverse': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/traverse': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.6)': + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.6)': + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.6)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.0 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.6)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.6)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.6)': + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.6)': + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.6)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.6)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.6)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.6)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.6)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.6)': @@ -5684,25 +5543,25 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.6)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.6)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.6)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.6)': @@ -5710,12 +5569,26 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-async-generator-functions@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.6) - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5728,9 +5601,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.28.6)': @@ -5738,7 +5620,12 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.6) @@ -5746,23 +5633,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.6)': + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 @@ -5774,21 +5661,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.6) + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) '@babel/traverse': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/template': 7.28.6 @@ -5800,45 +5687,65 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.28.6) + + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.6)': dependencies: @@ -5848,39 +5755,47 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/traverse': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -5893,38 +5808,47 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.6)': + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.28.6)': @@ -5932,27 +5856,32 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.6) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) '@babel/traverse': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.6) + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -5961,7 +5890,12 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 @@ -5969,17 +5903,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.6)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.28.6)': @@ -5990,6 +5924,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 @@ -5999,53 +5941,138 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regenerator@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.28.6) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.6) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.28.6) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.6)': @@ -6053,9 +6080,14 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.28.6)': @@ -6069,15 +6101,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.6)': + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.6)': @@ -6086,91 +6129,97 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/preset-env@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/compat-data': 7.28.6 - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-env@7.29.2(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.28.6) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.6) - '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.6) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-async-generator-functions': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) - '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.6) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.6) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-regenerator': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.28.6) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.6) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.6) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.6) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.6) - core-js-compat: 3.47.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.6)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/types': 7.28.6 esutils: 2.0.3 @@ -6186,6 +6235,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-typescript@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.28.6': {} '@babel/template@7.28.6': @@ -6206,14 +6266,36 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.6': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@changesets/apply-release-plan@7.0.14': + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@8.0.0-rc.3': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + + '@changesets/apply-release-plan@7.1.0': dependencies: - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 @@ -6240,30 +6322,28 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.8(@types/node@25.0.9)': + '@changesets/cli@2.30.0(@types/node@25.6.0)': dependencies: - '@changesets/apply-release-plan': 7.0.14 + '@changesets/apply-release-plan': 7.1.0 '@changesets/assemble-release-plan': 6.0.9 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.14 + '@changesets/get-release-plan': 4.0.15 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.6 + '@changesets/read': 0.6.7 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.0.9) + '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 - ci-info: 3.9.0 enquirer: 2.4.1 fs-extra: 7.0.1 mri: 1.2.0 - p-limit: 2.3.0 package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 @@ -6273,11 +6353,12 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@changesets/config@3.1.2': + '@changesets/config@3.1.3': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 @@ -6294,12 +6375,12 @@ snapshots: picocolors: 1.1.1 semver: 7.7.3 - '@changesets/get-release-plan@4.0.14': + '@changesets/get-release-plan@4.0.15': dependencies: '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.6 + '@changesets/read': 0.6.7 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -6317,7 +6398,7 @@ snapshots: dependencies: picocolors: 1.1.1 - '@changesets/parse@0.4.2': + '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 js-yaml: 4.1.1 @@ -6329,11 +6410,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.6': + '@changesets/read@0.6.7': dependencies: '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.2 + '@changesets/parse': 0.4.3 '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 @@ -6355,262 +6436,119 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@emnapi/core@1.8.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': - optional: true - '@esbuild/aix-ppc64@0.27.2': optional: true - '@esbuild/android-arm64@0.25.12': - optional: true - '@esbuild/android-arm64@0.27.2': optional: true - '@esbuild/android-arm@0.25.12': - optional: true - '@esbuild/android-arm@0.27.2': optional: true - '@esbuild/android-x64@0.25.12': - optional: true - '@esbuild/android-x64@0.27.2': optional: true - '@esbuild/darwin-arm64@0.25.12': - optional: true - '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.12': - optional: true - '@esbuild/darwin-x64@0.27.2': optional: true - '@esbuild/freebsd-arm64@0.25.12': - optional: true - '@esbuild/freebsd-arm64@0.27.2': optional: true - '@esbuild/freebsd-x64@0.25.12': - optional: true - '@esbuild/freebsd-x64@0.27.2': optional: true - '@esbuild/linux-arm64@0.25.12': - optional: true - '@esbuild/linux-arm64@0.27.2': optional: true - '@esbuild/linux-arm@0.25.12': - optional: true - '@esbuild/linux-arm@0.27.2': optional: true - '@esbuild/linux-ia32@0.25.12': - optional: true - '@esbuild/linux-ia32@0.27.2': optional: true - '@esbuild/linux-loong64@0.25.12': - optional: true - '@esbuild/linux-loong64@0.27.2': optional: true - '@esbuild/linux-mips64el@0.25.12': - optional: true - '@esbuild/linux-mips64el@0.27.2': optional: true - '@esbuild/linux-ppc64@0.25.12': - optional: true - '@esbuild/linux-ppc64@0.27.2': optional: true - '@esbuild/linux-riscv64@0.25.12': - optional: true - '@esbuild/linux-riscv64@0.27.2': optional: true - '@esbuild/linux-s390x@0.25.12': - optional: true - '@esbuild/linux-s390x@0.27.2': optional: true - '@esbuild/linux-x64@0.25.12': - optional: true - '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.12': - optional: true - '@esbuild/netbsd-arm64@0.27.2': optional: true - '@esbuild/netbsd-x64@0.25.12': - optional: true - '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.12': - optional: true - '@esbuild/openbsd-arm64@0.27.2': optional: true - '@esbuild/openbsd-x64@0.25.12': - optional: true - '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/openharmony-arm64@0.25.12': - optional: true - '@esbuild/openharmony-arm64@0.27.2': optional: true - '@esbuild/sunos-x64@0.25.12': - optional: true - '@esbuild/sunos-x64@0.27.2': optional: true - '@esbuild/win32-arm64@0.25.12': - optional: true - '@esbuild/win32-arm64@0.27.2': optional: true - '@esbuild/win32-ia32@0.25.12': - optional: true - '@esbuild/win32-ia32@0.27.2': optional: true - '@esbuild/win32-x64@0.25.12': - optional: true - '@esbuild/win32-x64@0.27.2': - optional: true - - '@inquirer/external-editor@1.0.3(@types/node@25.0.9)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 25.0.9 - - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@isaacs/ttlcache@1.4.1': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/create-cache-key-function@29.7.0': - dependencies: - '@jest/types': 29.6.3 + optional: true - '@jest/environment@29.7.0': + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.0.9 - jest-mock: 29.7.0 + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.6.0 - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 25.0.9 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 + '@isaacs/ttlcache@1.4.1': {} '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.28.6 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - '@jest/types@29.6.3': dependencies: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.0.9 + '@types/node': 25.6.0 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -6803,14 +6741,13 @@ snapshots: '@jimp/types': 1.6.0 tinycolor2: 1.6.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - glob: 10.5.0 - magic-string: 0.30.21 - react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@6.0.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -6843,7 +6780,7 @@ snapshots: msdf-bmfont-xml: 2.8.0 opentype.js: 1.3.4 - '@lightningjs/renderer@3.0.0-beta20': {} + '@lightningjs/renderer@3.0.1': {} '@manypkg/find-root@1.1.0': dependencies: @@ -6861,16 +6798,16 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3)': + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.8 - react: 19.2.3 + '@types/react': 19.2.14 + react: 19.2.5 - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -6886,143 +6823,138 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.107.0': {} + '@oxc-project/types@0.124.0': {} - '@oxc-project/types@0.108.0': {} - - '@oxfmt/binding-android-arm-eabi@0.35.0': - optional: true - - '@oxfmt/binding-android-arm64@0.35.0': + '@oxfmt/binding-android-arm-eabi@0.45.0': optional: true - '@oxfmt/binding-darwin-arm64@0.35.0': + '@oxfmt/binding-android-arm64@0.45.0': optional: true - '@oxfmt/binding-darwin-x64@0.35.0': + '@oxfmt/binding-darwin-arm64@0.45.0': optional: true - '@oxfmt/binding-freebsd-x64@0.35.0': + '@oxfmt/binding-darwin-x64@0.45.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': + '@oxfmt/binding-freebsd-x64@0.45.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.35.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.45.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.35.0': + '@oxfmt/binding-linux-arm-musleabihf@0.45.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.35.0': + '@oxfmt/binding-linux-arm64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.35.0': + '@oxfmt/binding-linux-arm64-musl@0.45.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.35.0': + '@oxfmt/binding-linux-ppc64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.35.0': + '@oxfmt/binding-linux-riscv64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.35.0': + '@oxfmt/binding-linux-riscv64-musl@0.45.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.35.0': + '@oxfmt/binding-linux-s390x-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.35.0': + '@oxfmt/binding-linux-x64-gnu@0.45.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.35.0': + '@oxfmt/binding-linux-x64-musl@0.45.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.35.0': + '@oxfmt/binding-openharmony-arm64@0.45.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.35.0': + '@oxfmt/binding-win32-arm64-msvc@0.45.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.35.0': + '@oxfmt/binding-win32-ia32-msvc@0.45.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.15.0': + '@oxfmt/binding-win32-x64-msvc@0.45.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.15.0': + '@oxlint-tsgolint/darwin-arm64@0.20.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.15.0': + '@oxlint-tsgolint/darwin-x64@0.20.0': optional: true - '@oxlint-tsgolint/linux-x64@0.15.0': + '@oxlint-tsgolint/linux-arm64@0.20.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.15.0': + '@oxlint-tsgolint/linux-x64@0.20.0': optional: true - '@oxlint-tsgolint/win32-x64@0.15.0': + '@oxlint-tsgolint/win32-arm64@0.20.0': optional: true - '@oxlint/binding-android-arm-eabi@1.50.0': + '@oxlint-tsgolint/win32-x64@0.20.0': optional: true - '@oxlint/binding-android-arm64@1.50.0': + '@oxlint/binding-android-arm-eabi@1.60.0': optional: true - '@oxlint/binding-darwin-arm64@1.50.0': + '@oxlint/binding-android-arm64@1.60.0': optional: true - '@oxlint/binding-darwin-x64@1.50.0': + '@oxlint/binding-darwin-arm64@1.60.0': optional: true - '@oxlint/binding-freebsd-x64@1.50.0': + '@oxlint/binding-darwin-x64@1.60.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + '@oxlint/binding-freebsd-x64@1.60.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.50.0': + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.50.0': + '@oxlint/binding-linux-arm-musleabihf@1.60.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.50.0': + '@oxlint/binding-linux-arm64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.50.0': + '@oxlint/binding-linux-arm64-musl@1.60.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.50.0': + '@oxlint/binding-linux-ppc64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.50.0': + '@oxlint/binding-linux-riscv64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.50.0': + '@oxlint/binding-linux-riscv64-musl@1.60.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.50.0': + '@oxlint/binding-linux-s390x-gnu@1.60.0': optional: true - '@oxlint/binding-linux-x64-musl@1.50.0': + '@oxlint/binding-linux-x64-gnu@1.60.0': optional: true - '@oxlint/binding-openharmony-arm64@1.50.0': + '@oxlint/binding-linux-x64-musl@1.60.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.50.0': + '@oxlint/binding-openharmony-arm64@1.60.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.50.0': + '@oxlint/binding-win32-arm64-msvc@1.60.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.50.0': + '@oxlint/binding-win32-ia32-msvc@1.60.0': optional: true - '@pkgjs/parseargs@0.11.0': + '@oxlint/binding-win32-x64-msvc@1.60.0': optional: true '@pnpm/config.env-replace@1.1.0': {} @@ -7041,192 +6973,327 @@ snapshots: dependencies: quansync: 1.0.0 - '@react-native/assets-registry@0.82.1': {} + '@react-native/assets-registry@0.85.1': {} + + '@react-native/babel-plugin-codegen@0.85.1(@babel/core@7.28.6)': + dependencies: + '@babel/traverse': 7.29.0 + '@react-native/codegen': 0.85.1(@babel/core@7.28.6) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@react-native/babel-plugin-codegen@0.85.1(@babel/core@7.29.0)': + dependencies: + '@babel/traverse': 7.29.0 + '@react-native/codegen': 0.85.1(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color - '@react-native/codegen@0.82.1(@babel/core@7.28.6)': + '@react-native/babel-preset@0.85.1(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 - '@babel/parser': 7.28.6 - glob: 7.2.3 - hermes-parser: 0.32.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.28.6) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.28.6) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.28.6) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.28.6) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.6) + '@react-native/babel-plugin-codegen': 0.85.1(@babel/core@7.28.6) + babel-plugin-syntax-hermes-parser: 0.33.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.6) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + + '@react-native/babel-preset@0.85.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@react-native/babel-plugin-codegen': 0.85.1(@babel/core@7.29.0) + babel-plugin-syntax-hermes-parser: 0.33.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + + '@react-native/codegen@0.85.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.29.2 + hermes-parser: 0.33.3 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.15 + yargs: 17.7.2 + + '@react-native/codegen@0.85.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + hermes-parser: 0.33.3 invariant: 2.2.4 nullthrows: 1.1.1 + tinyglobby: 0.2.15 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.82.1': + '@react-native/community-cli-plugin@0.85.1(@react-native/metro-config@0.85.1(@babel/core@7.28.6))': + dependencies: + '@react-native/dev-middleware': 0.85.1 + debug: 4.4.3 + invariant: 2.2.4 + metro: 0.84.3 + metro-config: 0.84.3 + metro-core: 0.84.3 + semver: 7.7.3 + optionalDependencies: + '@react-native/metro-config': 0.85.1(@babel/core@7.28.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/community-cli-plugin@0.85.1(@react-native/metro-config@0.85.1(@babel/core@7.29.0))': dependencies: - '@react-native/dev-middleware': 0.82.1 + '@react-native/dev-middleware': 0.85.1 debug: 4.4.3 invariant: 2.2.4 - metro: 0.83.3 - metro-config: 0.83.3 - metro-core: 0.83.3 + metro: 0.84.3 + metro-config: 0.84.3 + metro-core: 0.84.3 semver: 7.7.3 + optionalDependencies: + '@react-native/metro-config': 0.85.1(@babel/core@7.29.0) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/debugger-frontend@0.82.1': {} + '@react-native/debugger-frontend@0.85.1': {} - '@react-native/debugger-shell@0.82.1': + '@react-native/debugger-shell@0.85.1': dependencies: cross-spawn: 7.0.6 + debug: 4.4.3 fb-dotslash: 0.5.8 + transitivePeerDependencies: + - supports-color - '@react-native/dev-middleware@0.82.1': + '@react-native/dev-middleware@0.85.1': dependencies: '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.82.1 - '@react-native/debugger-shell': 0.82.1 + '@react-native/debugger-frontend': 0.85.1 + '@react-native/debugger-shell': 0.85.1 chrome-launcher: 0.15.2 - chromium-edge-launcher: 0.2.0 + chromium-edge-launcher: 0.3.0 connect: 3.7.0 debug: 4.4.3 invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 serve-static: 1.16.3 - ws: 6.2.3 + ws: 7.5.10 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/gradle-plugin@0.82.1': {} + '@react-native/gradle-plugin@0.85.1': {} + + '@react-native/js-polyfills@0.85.1': {} + + '@react-native/metro-babel-transformer@0.85.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@react-native/babel-preset': 0.85.1(@babel/core@7.28.6) + hermes-parser: 0.33.3 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@react-native/metro-babel-transformer@0.85.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@react-native/babel-preset': 0.85.1(@babel/core@7.29.0) + hermes-parser: 0.33.3 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@react-native/metro-config@0.85.1(@babel/core@7.28.6)': + dependencies: + '@react-native/js-polyfills': 0.85.1 + '@react-native/metro-babel-transformer': 0.85.1(@babel/core@7.28.6) + metro-config: 0.84.3 + metro-runtime: 0.84.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color - '@react-native/js-polyfills@0.82.1': {} + '@react-native/metro-config@0.85.1(@babel/core@7.29.0)': + dependencies: + '@react-native/js-polyfills': 0.85.1 + '@react-native/metro-babel-transformer': 0.85.1(@babel/core@7.29.0) + metro-config: 0.84.3 + metro-runtime: 0.84.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color '@react-native/normalize-colors@0.74.89': {} - '@react-native/normalize-colors@0.82.1': {} + '@react-native/normalize-colors@0.85.1': {} + + '@react-native/virtualized-lists@0.85.1(@types/react@19.2.14)(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 - '@react-native/virtualized-lists@0.82.1(@types/react@19.2.8)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3)': + '@react-native/virtualized-lists@0.85.1(@types/react@19.2.14)(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) optionalDependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.14 - '@react-navigation/core@7.14.0(react@19.2.3)': + '@react-navigation/core@7.17.2(react@19.2.5)': dependencies: '@react-navigation/routers': 7.5.3 escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.11 query-string: 7.1.3 - react: 19.2.3 + react: 19.2.5 react-is: 19.2.3 - use-latest-callback: 0.2.6(react@19.2.3) - use-sync-external-store: 1.6.0(react@19.2.3) + use-latest-callback: 0.2.6(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) - '@react-navigation/native@7.1.28(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3)': + '@react-navigation/native@7.2.2(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': dependencies: - '@react-navigation/core': 7.14.0(react@19.2.3) + '@react-navigation/core': 7.17.2(react@19.2.5) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.11 - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) - use-latest-callback: 0.2.6(react@19.2.3) + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + use-latest-callback: 0.2.6(react@19.2.5) '@react-navigation/routers@7.5.3': dependencies: nanoid: 3.3.11 - '@rolldown/binding-android-arm64@1.0.0-beta.59': - optional: true - - '@rolldown/binding-android-arm64@1.0.0-beta.60': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-beta.59': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-beta.60': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-beta.59': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-beta.60': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-beta.59': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-beta.60': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': + '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': + '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': - optional: true - - '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rolldown/pluginutils@1.0.0-rc.15': {} - '@rolldown/pluginutils@1.0.0-beta.59': {} + '@rolldown/pluginutils@1.0.0-rc.7': {} - '@rolldown/pluginutils@1.0.0-beta.60': {} - - '@rollup/plugin-babel@7.0.0(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.55.2)': + '@rollup/plugin-babel@7.0.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.55.2)': dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@rollup/pluginutils': 5.3.0(rollup@4.55.2) optionalDependencies: @@ -7322,94 +7389,104 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 - '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@9.1.17(@types/react@19.2.8)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@19.2.3) - '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) - '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) + '@storybook/csf-plugin': 10.3.5(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' + - esbuild + - rollup + - vite + - webpack - '@storybook/addon-links@9.1.17(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + '@storybook/addon-links@10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: - react: 19.2.3 + react: 19.2.5 - '@storybook/builder-vite@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.3.5(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.3.5(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - esbuild + - rollup + - webpack - '@storybook/csf-plugin@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + '@storybook/csf-plugin@10.3.5(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - unplugin: 1.16.1 + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.27.2 + rollup: 4.55.2 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@storybook/global@5.0.0': {} - '@storybook/icons@1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@storybook/icons@2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + '@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-vite@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.55.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/react-vite@10.3.5(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.55.2) - '@storybook/builder-vite': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@storybook/react': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3) - find-up: 7.0.0 + '@storybook/builder-vite': 10.3.5(esbuild@0.27.2)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + empathic: 2.0.0 magic-string: 0.30.21 - react: 19.2.3 + react: 19.2.5 react-docgen: 8.0.2 - react-dom: 19.2.3(react@19.2.3) + react-dom: 19.2.5(react@19.2.5) resolve: 1.22.11 - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tsconfig-paths: 4.2.0 - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: + - esbuild - rollup - supports-color - typescript + - webpack - '@storybook/react@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)': + '@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + react: 19.2.5 + react-docgen: 8.0.2 + react-docgen-typescript: 2.4.0(typescript@6.0.2) + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -7433,6 +7510,24 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@turbo/darwin-64@2.9.6': + optional: true + + '@turbo/darwin-arm64@2.9.6': + optional: true + + '@turbo/linux-64@2.9.6': + optional: true + + '@turbo/linux-arm64@2.9.6': + optional: true + + '@turbo/windows-64@2.9.6': + optional: true + + '@turbo/windows-arm64@2.9.6': + optional: true + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -7472,10 +7567,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 25.0.9 - '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -7486,6 +7577,8 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jsesc@2.5.1': {} + '@types/mdx@2.0.13': {} '@types/minimatch@3.0.5': {} @@ -7494,68 +7587,61 @@ snapshots: '@types/node@16.9.1': {} - '@types/node@25.0.9': + '@types/node@25.6.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.19.2 '@types/parse-json@4.0.2': {} - '@types/react-dom@19.2.3(@types/react@19.2.8)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.14 - '@types/react-reconciler@0.28.9(@types/react@19.2.8)': + '@types/react-reconciler@0.28.9(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.14 - '@types/react-reconciler@0.32.3(@types/react@19.2.8)': + '@types/react-reconciler@0.33.0(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.14 - '@types/react@19.2.8': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 '@types/resolve@1.20.6': {} - '@types/stack-utils@2.0.3': {} - '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': dependencies: '@types/yargs-parser': 21.0.3 - '@vitejs/plugin-legacy@7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-legacy@8.0.1(terser@5.46.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@babel/core': 7.28.6 - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.6) - '@babel/preset-env': 7.28.6(@babel/core@7.28.6) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.6) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.6) + '@babel/core': 7.29.0 + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) browserslist: 4.28.1 browserslist-to-esbuild: 2.1.1(browserslist@4.28.1) - core-js: 3.47.0 + core-js: 3.49.0 magic-string: 0.30.21 regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.46.0 - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@babel/core': 7.28.6 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) - '@rolldown/pluginutils': 1.0.0-beta.53 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + optionalDependencies: + babel-plugin-react-compiler: 1.0.0 '@vitest/expect@3.2.4': dependencies: @@ -7565,47 +7651,40 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.0.17': + '@vitest/expect@4.1.4': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.17 - '@vitest/utils': 4.0.17 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 chai: 6.2.2 - tinyrainbow: 3.0.3 - - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.0.17 + '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.0.17': + '@vitest/pretty-format@4.1.4': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.17': + '@vitest/runner@4.1.4': dependencies: - '@vitest/utils': 4.0.17 + '@vitest/utils': 4.1.4 pathe: 2.0.3 - '@vitest/snapshot@4.0.17': + '@vitest/snapshot@4.1.4': dependencies: - '@vitest/pretty-format': 4.0.17 + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 magic-string: 0.30.21 pathe: 2.0.3 @@ -7613,7 +7692,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.0.17': {} + '@vitest/spy@4.1.4': {} '@vitest/utils@3.2.4': dependencies: @@ -7621,10 +7700,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.0.17': + '@vitest/utils@4.1.4': dependencies: - '@vitest/pretty-format': 4.0.17 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@vue/compiler-core@3.5.27': dependencies: @@ -7658,14 +7738,16 @@ snapshots: '@vue/shared@3.5.27': {} + '@webcontainer/env@1.1.1': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - accepts@1.3.8: + accepts@2.0.0: dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + mime-types: 3.0.2 + negotiator: 1.0.0 acorn@8.15.0: {} @@ -7699,11 +7781,6 @@ snapshots: any-base@1.1.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - arabic-persian-reshaper@1.0.1: {} argparse@1.0.10: @@ -7728,17 +7805,16 @@ snapshots: assertion-error@2.0.1: {} - ast-kit@2.2.0: + ast-kit@3.0.0-beta.1: dependencies: - '@babel/parser': 7.28.6 + '@babel/parser': 8.0.0-rc.3 + estree-walker: 3.0.3 pathe: 2.0.3 ast-types@0.16.1: dependencies: tslib: 2.8.1 - async-limiter@1.0.1: {} - atomically@2.1.0: dependencies: stubborn-fs: 2.0.0 @@ -7746,57 +7822,59 @@ snapshots: await-to-js@3.0.0: {} - babel-jest@29.7.0(@babel/core@7.28.6): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.28.6): dependencies: + '@babel/compat-data': 7.29.0 '@babel/core': 7.28.6 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.6) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.28.6) + semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@6.1.1: + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): dependencies: - '@babel/helper-plugin-utils': 7.28.6 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@29.6.3: + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.6): dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.28.6 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 + '@babel/core': 7.28.6 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.28.6) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.6): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): dependencies: - '@babel/compat-data': 7.28.6 - '@babel/core': 7.28.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) - semver: 6.3.1 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.6): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) - core-js-compat: 3.47.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.6): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.28.6): dependencies: '@babel/core': 7.28.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -7804,45 +7882,30 @@ snapshots: dependencies: '@babel/types': 7.28.6 - babel-plugin-syntax-hermes-parser@0.32.0: + babel-plugin-syntax-hermes-parser@0.33.3: dependencies: - hermes-parser: 0.32.0 + hermes-parser: 0.33.3 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.6): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.28.6): dependencies: - '@babel/core': 7.28.6 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.6) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.6) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.6) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.28.6) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.6) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.6) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.6) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.6) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.6) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.6) + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - '@babel/core' - babel-preset-jest@29.6.3(@babel/core@7.28.6): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.6 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.6) + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.16: {} - better-opn@3.0.2: - dependencies: - open: 8.4.2 - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -7871,6 +7934,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -7899,14 +7966,16 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - cac@6.7.14: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@7.0.0: {} callsite@1.0.0: {} callsites@3.1.0: {} - camelcase@5.3.1: {} - camelcase@6.3.0: {} camelcase@8.0.0: {} @@ -7936,21 +8005,20 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 25.0.9 + '@types/node': 25.6.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 transitivePeerDependencies: - supports-color - chromium-edge-launcher@0.2.0: + chromium-edge-launcher@0.3.0: dependencies: - '@types/node': 25.0.9 + '@types/node': 25.6.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 mkdirp: 1.0.4 - rimraf: 3.0.2 transitivePeerDependencies: - supports-color @@ -7968,10 +8036,10 @@ snapshots: dependencies: string-width: 4.2.3 - cli-truncate@5.1.1: + cli-truncate@5.2.0: dependencies: - slice-ansi: 7.1.2 - string-width: 8.1.0 + slice-ansi: 8.0.0 + string-width: 8.2.0 cliui@7.0.4: dependencies: @@ -7991,8 +8059,6 @@ snapshots: color-name@1.1.4: {} - colorette@2.0.20: {} - commander@12.1.0: {} commander@14.0.2: {} @@ -8036,11 +8102,11 @@ snapshots: untildify: 4.0.0 yargs: 16.2.0 - core-js-compat@3.47.0: + core-js-compat@3.49.0: dependencies: browserslist: 4.28.1 - core-js@3.47.0: {} + core-js@3.49.0: {} core-util-is@1.0.3: {} @@ -8088,9 +8154,16 @@ snapshots: deep-extend@0.6.0: {} - define-lazy-prop@2.0.0: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} - defu@6.1.4: {} + defu@6.1.7: {} del-cli@7.0.0: dependencies: @@ -8148,6 +8221,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -8166,8 +8241,6 @@ snapshots: dts-resolver@2.1.3: {} - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -8176,8 +8249,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - empathic@2.0.0: {} encodeurl@1.0.2: {} @@ -8201,43 +8272,7 @@ snapshots: dependencies: stackframe: 1.3.4 - es-module-lexer@1.7.0: {} - - esbuild-register@3.6.0(esbuild@0.25.12): - dependencies: - debug: 4.4.3 - esbuild: 0.25.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + es-module-lexer@2.0.0: {} esbuild@0.27.2: optionalDependencies: @@ -8274,8 +8309,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} esprima@4.0.1: {} @@ -8318,8 +8351,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8346,7 +8377,11 @@ snapshots: fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 file-type@16.5.4: dependencies: @@ -8377,12 +8412,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@7.0.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - unicorn-magic: 0.1.0 - findup-sync@5.0.0: dependencies: detect-file: 1.0.0 @@ -8392,11 +8421,6 @@ snapshots: flow-enums-runtime@0.0.6: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - fresh@0.5.2: {} fs-extra@11.3.3: @@ -8430,12 +8454,16 @@ snapshots: get-east-asian-width@1.4.0: {} - get-package-type@0.1.0: {} + get-east-asian-width@1.5.0: {} get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + gifwrap@0.10.1: dependencies: image-q: 4.0.0 @@ -8445,20 +8473,11 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - glob@13.0.0: + glob@13.0.6: dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 glob@7.2.3: dependencies: @@ -8526,19 +8545,25 @@ snapshots: dependencies: function-bind: 1.1.2 - hermes-compiler@0.0.0: {} + hermes-compiler@250829098.0.10: {} - hermes-estree@0.32.0: {} + hermes-estree@0.33.3: {} - hermes-parser@0.32.0: + hermes-estree@0.35.0: {} + + hermes-parser@0.33.3: + dependencies: + hermes-estree: 0.33.3 + + hermes-parser@0.35.0: dependencies: - hermes-estree: 0.32.0 + hermes-estree: 0.35.0 homedir-polyfill@1.0.3: dependencies: parse-passwd: 1.0.0 - hookable@6.0.1: {} + hookable@6.1.0: {} http-errors@2.0.1: dependencies: @@ -8586,8 +8611,6 @@ snapshots: import-without-cache@0.2.5: {} - imurmurhash@0.1.4: {} - indent-string@4.0.0: {} inflight@1.0.6: @@ -8617,6 +8640,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8631,6 +8656,10 @@ snapshots: is-in-ci@1.0.0: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-installed-globally@1.0.0: dependencies: global-directory: 4.0.1 @@ -8656,88 +8685,29 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@0.0.1: {} isarray@1.0.0: {} isexe@2.0.0: {} - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.6 - '@babel/parser': 7.28.6 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - its-fine@2.0.0(@types/react@19.2.8)(react@19.2.3): + its-fine@2.0.0(@types/react@19.2.14)(react@19.2.5): dependencies: - '@types/react-reconciler': 0.28.9(@types/react@19.2.8) - react: 19.2.3 + '@types/react-reconciler': 0.28.9(@types/react@19.2.14) + react: 19.2.5 transitivePeerDependencies: - '@types/react' - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.0.9 - jest-mock: 29.7.0 - jest-util: 29.7.0 - jest-get-type@29.6.3: {} - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 25.0.9 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.28.6 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 25.0.9 - jest-util: 29.7.0 - - jest-regex-util@29.6.3: {} - jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.0.9 + '@types/node': 25.6.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -8754,7 +8724,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 25.0.9 + '@types/node': 25.6.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -8839,25 +8809,69 @@ snapshots: transitivePeerDependencies: - supports-color + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + lines-and-columns@1.2.4: {} - listr2@10.0.0: + listr2@10.2.1: dependencies: - cli-truncate: 5.1.1 - colorette: 2.0.20 + cli-truncate: 5.2.0 eventemitter3: 5.0.4 - log-update: 7.0.2 + log-update: 6.1.0 rfdc: 1.4.1 - wrap-ansi: 9.0.2 + wrap-ansi: 10.0.0 locate-path@5.0.0: dependencies: p-locate: 4.1.0 - locate-path@7.2.0: - dependencies: - p-locate: 6.0.0 - lodash.debounce@4.0.8: {} lodash.startcase@4.4.0: {} @@ -8866,7 +8880,7 @@ snapshots: lodash@4.17.21: {} - log-update@7.0.2: + log-update@6.1.0: dependencies: ansi-escapes: 7.2.0 cli-cursor: 5.0.0 @@ -8880,8 +8894,6 @@ snapshots: loupe@3.2.1: {} - lru-cache@10.4.3: {} - lru-cache@11.2.4: {} lru-cache@5.1.1: @@ -8918,50 +8930,51 @@ snapshots: merge2@1.4.1: {} - metro-babel-transformer@0.83.3: + metro-babel-transformer@0.84.3: dependencies: '@babel/core': 7.28.6 flow-enums-runtime: 0.0.6 - hermes-parser: 0.32.0 + hermes-parser: 0.35.0 + metro-cache-key: 0.84.3 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-cache-key@0.83.3: + metro-cache-key@0.84.3: dependencies: flow-enums-runtime: 0.0.6 - metro-cache@0.83.3: + metro-cache@0.84.3: dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 https-proxy-agent: 7.0.6 - metro-core: 0.83.3 + metro-core: 0.84.3 transitivePeerDependencies: - supports-color - metro-config@0.83.3: + metro-config@0.84.3: dependencies: connect: 3.7.0 flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.83.3 - metro-cache: 0.83.3 - metro-core: 0.83.3 - metro-runtime: 0.83.3 - yaml: 2.8.2 + metro: 0.84.3 + metro-cache: 0.84.3 + metro-core: 0.84.3 + metro-runtime: 0.84.3 + yaml: 2.8.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro-core@0.83.3: + metro-core@0.84.3: dependencies: flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 - metro-resolver: 0.83.3 + metro-resolver: 0.84.3 - metro-file-map@0.83.3: + metro-file-map@0.84.3: dependencies: debug: 4.4.3 fb-watchman: 2.0.2 @@ -8975,87 +8988,86 @@ snapshots: transitivePeerDependencies: - supports-color - metro-minify-terser@0.83.3: + metro-minify-terser@0.84.3: dependencies: flow-enums-runtime: 0.0.6 terser: 5.46.0 - metro-resolver@0.83.3: + metro-resolver@0.84.3: dependencies: flow-enums-runtime: 0.0.6 - metro-runtime@0.83.3: + metro-runtime@0.84.3: dependencies: '@babel/runtime': 7.28.6 flow-enums-runtime: 0.0.6 - metro-source-map@0.83.3: + metro-source-map@0.84.3: dependencies: - '@babel/traverse': 7.28.6 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.6' - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.83.3 + metro-symbolicate: 0.84.3 nullthrows: 1.1.1 - ob1: 0.83.3 + ob1: 0.84.3 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-symbolicate@0.83.3: + metro-symbolicate@0.84.3: dependencies: flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.83.3 + metro-source-map: 0.84.3 nullthrows: 1.1.1 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.83.3: + metro-transform-plugins@0.84.3: dependencies: '@babel/core': 7.28.6 - '@babel/generator': 7.28.6 + '@babel/generator': 7.29.1 '@babel/template': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-transform-worker@0.83.3: + metro-transform-worker@0.84.3: dependencies: '@babel/core': 7.28.6 - '@babel/generator': 7.28.6 - '@babel/parser': 7.28.6 - '@babel/types': 7.28.6 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 - metro: 0.83.3 - metro-babel-transformer: 0.83.3 - metro-cache: 0.83.3 - metro-cache-key: 0.83.3 - metro-minify-terser: 0.83.3 - metro-source-map: 0.83.3 - metro-transform-plugins: 0.83.3 + metro: 0.84.3 + metro-babel-transformer: 0.84.3 + metro-cache: 0.84.3 + metro-cache-key: 0.84.3 + metro-minify-terser: 0.84.3 + metro-source-map: 0.84.3 + metro-transform-plugins: 0.84.3 nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro@0.83.3: + metro@0.84.3: dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/core': 7.28.6 - '@babel/generator': 7.28.6 - '@babel/parser': 7.28.6 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 - accepts: 1.3.8 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + accepts: 2.0.0 chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 @@ -9063,25 +9075,25 @@ snapshots: error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 - hermes-parser: 0.32.0 + hermes-parser: 0.35.0 image-size: 1.2.1 invariant: 2.2.4 jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.83.3 - metro-cache: 0.83.3 - metro-cache-key: 0.83.3 - metro-config: 0.83.3 - metro-core: 0.83.3 - metro-file-map: 0.83.3 - metro-resolver: 0.83.3 - metro-runtime: 0.83.3 - metro-source-map: 0.83.3 - metro-symbolicate: 0.83.3 - metro-transform-plugins: 0.83.3 - metro-transform-worker: 0.83.3 - mime-types: 2.1.35 + metro-babel-transformer: 0.84.3 + metro-cache: 0.84.3 + metro-cache-key: 0.84.3 + metro-config: 0.84.3 + metro-core: 0.84.3 + metro-file-map: 0.84.3 + metro-resolver: 0.84.3 + metro-runtime: 0.84.3 + metro-source-map: 0.84.3 + metro-symbolicate: 0.84.3 + metro-transform-plugins: 0.84.3 + metro-transform-worker: 0.84.3 + mime-types: 3.0.2 nullthrows: 1.1.1 serialize-error: 2.1.0 source-map: 0.5.7 @@ -9098,11 +9110,11 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} + mime-db@1.54.0: {} - mime-types@2.1.35: + mime-types@3.0.2: dependencies: - mime-db: 1.52.0 + mime-db: 1.54.0 mime@1.6.0: {} @@ -9112,9 +9124,9 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.1.1: + minimatch@10.2.5: dependencies: - '@isaacs/brace-expansion': 5.0.0 + brace-expansion: 5.0.5 minimatch@3.1.2: dependencies: @@ -9124,13 +9136,9 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - minimist@1.2.8: {} - minipass@7.1.2: {} + minipass@7.1.3: {} mkdirp@1.0.4: {} @@ -9164,7 +9172,7 @@ snapshots: nanoid@3.3.11: {} - negotiator@0.6.3: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -9181,11 +9189,9 @@ snapshots: inherits: 2.0.4 readable-stream: 1.0.34 - normalize-path@3.0.0: {} - nullthrows@1.1.1: {} - ob1@0.83.3: + ob1@0.84.3: dependencies: flow-enums-runtime: 0.0.6 @@ -9215,14 +9221,15 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@7.4.2: + open@10.2.0: dependencies: - is-docker: 2.2.1 - is-wsl: 2.2.0 + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 - open@8.4.2: + open@7.4.2: dependencies: - define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 @@ -9233,61 +9240,61 @@ snapshots: outdent@0.5.0: {} - oxfmt@0.35.0: + oxfmt@0.45.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.35.0 - '@oxfmt/binding-android-arm64': 0.35.0 - '@oxfmt/binding-darwin-arm64': 0.35.0 - '@oxfmt/binding-darwin-x64': 0.35.0 - '@oxfmt/binding-freebsd-x64': 0.35.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.35.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.35.0 - '@oxfmt/binding-linux-arm64-gnu': 0.35.0 - '@oxfmt/binding-linux-arm64-musl': 0.35.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.35.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.35.0 - '@oxfmt/binding-linux-riscv64-musl': 0.35.0 - '@oxfmt/binding-linux-s390x-gnu': 0.35.0 - '@oxfmt/binding-linux-x64-gnu': 0.35.0 - '@oxfmt/binding-linux-x64-musl': 0.35.0 - '@oxfmt/binding-openharmony-arm64': 0.35.0 - '@oxfmt/binding-win32-arm64-msvc': 0.35.0 - '@oxfmt/binding-win32-ia32-msvc': 0.35.0 - '@oxfmt/binding-win32-x64-msvc': 0.35.0 - - oxlint-tsgolint@0.15.0: + '@oxfmt/binding-android-arm-eabi': 0.45.0 + '@oxfmt/binding-android-arm64': 0.45.0 + '@oxfmt/binding-darwin-arm64': 0.45.0 + '@oxfmt/binding-darwin-x64': 0.45.0 + '@oxfmt/binding-freebsd-x64': 0.45.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.45.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.45.0 + '@oxfmt/binding-linux-arm64-gnu': 0.45.0 + '@oxfmt/binding-linux-arm64-musl': 0.45.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.45.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.45.0 + '@oxfmt/binding-linux-riscv64-musl': 0.45.0 + '@oxfmt/binding-linux-s390x-gnu': 0.45.0 + '@oxfmt/binding-linux-x64-gnu': 0.45.0 + '@oxfmt/binding-linux-x64-musl': 0.45.0 + '@oxfmt/binding-openharmony-arm64': 0.45.0 + '@oxfmt/binding-win32-arm64-msvc': 0.45.0 + '@oxfmt/binding-win32-ia32-msvc': 0.45.0 + '@oxfmt/binding-win32-x64-msvc': 0.45.0 + + oxlint-tsgolint@0.20.0: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.15.0 - '@oxlint-tsgolint/darwin-x64': 0.15.0 - '@oxlint-tsgolint/linux-arm64': 0.15.0 - '@oxlint-tsgolint/linux-x64': 0.15.0 - '@oxlint-tsgolint/win32-arm64': 0.15.0 - '@oxlint-tsgolint/win32-x64': 0.15.0 - - oxlint@1.50.0(oxlint-tsgolint@0.15.0): + '@oxlint-tsgolint/darwin-arm64': 0.20.0 + '@oxlint-tsgolint/darwin-x64': 0.20.0 + '@oxlint-tsgolint/linux-arm64': 0.20.0 + '@oxlint-tsgolint/linux-x64': 0.20.0 + '@oxlint-tsgolint/win32-arm64': 0.20.0 + '@oxlint-tsgolint/win32-x64': 0.20.0 + + oxlint@1.60.0(oxlint-tsgolint@0.20.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.50.0 - '@oxlint/binding-android-arm64': 1.50.0 - '@oxlint/binding-darwin-arm64': 1.50.0 - '@oxlint/binding-darwin-x64': 1.50.0 - '@oxlint/binding-freebsd-x64': 1.50.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.50.0 - '@oxlint/binding-linux-arm-musleabihf': 1.50.0 - '@oxlint/binding-linux-arm64-gnu': 1.50.0 - '@oxlint/binding-linux-arm64-musl': 1.50.0 - '@oxlint/binding-linux-ppc64-gnu': 1.50.0 - '@oxlint/binding-linux-riscv64-gnu': 1.50.0 - '@oxlint/binding-linux-riscv64-musl': 1.50.0 - '@oxlint/binding-linux-s390x-gnu': 1.50.0 - '@oxlint/binding-linux-x64-gnu': 1.50.0 - '@oxlint/binding-linux-x64-musl': 1.50.0 - '@oxlint/binding-openharmony-arm64': 1.50.0 - '@oxlint/binding-win32-arm64-msvc': 1.50.0 - '@oxlint/binding-win32-ia32-msvc': 1.50.0 - '@oxlint/binding-win32-x64-msvc': 1.50.0 - oxlint-tsgolint: 0.15.0 + '@oxlint/binding-android-arm-eabi': 1.60.0 + '@oxlint/binding-android-arm64': 1.60.0 + '@oxlint/binding-darwin-arm64': 1.60.0 + '@oxlint/binding-darwin-x64': 1.60.0 + '@oxlint/binding-freebsd-x64': 1.60.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.60.0 + '@oxlint/binding-linux-arm-musleabihf': 1.60.0 + '@oxlint/binding-linux-arm64-gnu': 1.60.0 + '@oxlint/binding-linux-arm64-musl': 1.60.0 + '@oxlint/binding-linux-ppc64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-musl': 1.60.0 + '@oxlint/binding-linux-s390x-gnu': 1.60.0 + '@oxlint/binding-linux-x64-gnu': 1.60.0 + '@oxlint/binding-linux-x64-musl': 1.60.0 + '@oxlint/binding-openharmony-arm64': 1.60.0 + '@oxlint/binding-win32-arm64-msvc': 1.60.0 + '@oxlint/binding-win32-ia32-msvc': 1.60.0 + '@oxlint/binding-win32-x64-msvc': 1.60.0 + oxlint-tsgolint: 0.20.0 p-filter@2.1.0: dependencies: @@ -9297,32 +9304,22 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@4.0.0: - dependencies: - yocto-queue: 1.2.2 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 - p-locate@6.0.0: - dependencies: - p-limit: 4.0.0 - p-map@2.1.0: {} p-map@7.0.4: {} p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - package-json@10.0.1: dependencies: ky: 1.14.2 registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.7.4 package-manager-detector@0.2.11: dependencies: @@ -9356,23 +9353,16 @@ snapshots: path-exists@4.0.0: {} - path-exists@5.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} path-parse@1.0.7: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: lru-cache: 11.2.4 - minipass: 7.1.2 + minipass: 7.1.3 path-type@4.0.0: {} @@ -9390,9 +9380,9 @@ snapshots: picomatch@4.0.3: {} - pify@4.0.1: {} + picomatch@4.0.4: {} - pirates@4.0.7: {} + pify@4.0.1: {} pixelmatch@5.3.0: dependencies: @@ -9414,6 +9404,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + presentable-error@0.0.1: {} prettier@2.8.8: {} @@ -9482,9 +9478,9 @@ snapshots: - bufferutil - utf-8-validate - react-docgen-typescript@2.4.0(typescript@5.9.3): + react-docgen-typescript@2.4.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 react-docgen@8.0.2: dependencies: @@ -9501,9 +9497,9 @@ snapshots: transitivePeerDependencies: - supports-color - react-dom@19.2.3(react@19.2.3): + react-dom@19.2.5(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 scheduler: 0.27.0 react-is@17.0.2: {} @@ -9512,20 +9508,33 @@ snapshots: react-is@19.2.3: {} - react-native-is-edge-to-edge@1.2.1(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3): + react-native-is-edge-to-edge@1.3.1(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + + react-native-is-edge-to-edge@1.3.1(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) + + react-native-reanimated@4.3.0(react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + react-native-worklets: 0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + semver: 7.7.3 - react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3): + react-native-reanimated@4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) - react-native-is-edge-to-edge: 1.2.1(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) - react-native-worklets: 0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + react-native-worklets: 0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) semver: 7.7.3 - react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-native-web@0.21.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@babel/runtime': 7.28.6 '@react-native/normalize-colors': 0.74.89 @@ -9534,71 +9543,134 @@ snapshots: memoize-one: 6.0.0 nullthrows: 1.1.1 postcss-value-parser: 4.2.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) styleq: 0.1.3 transitivePeerDependencies: - encoding - react-native-worklets@0.7.2(@babel/core@7.28.6)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3): + react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.6) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.6) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.28.6) '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.6) '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.6) '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.6) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.6) + '@react-native/metro-config': 0.85.1(@babel/core@7.28.6) convert-source-map: 2.0.0 - react: 19.2.3 - react-native: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.29.0) + '@react-native/metro-config': 0.85.1(@babel/core@7.29.0) + convert-source-map: 2.0.0 + react: 19.2.5 + react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5): + dependencies: + '@react-native/assets-registry': 0.85.1 + '@react-native/codegen': 0.85.1(@babel/core@7.28.6) + '@react-native/community-cli-plugin': 0.85.1(@react-native/metro-config@0.85.1(@babel/core@7.28.6)) + '@react-native/gradle-plugin': 0.85.1 + '@react-native/js-polyfills': 0.85.1 + '@react-native/normalize-colors': 0.85.1 + '@react-native/virtualized-lists': 0.85.1(@types/react@19.2.14)(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.33.3 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.10 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.84.3 + metro-source-map: 0.84.3 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.5 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 semver: 7.7.3 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.15 + whatwg-fetch: 3.6.20 + ws: 7.5.10 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.2.14 transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil - supports-color + - utf-8-validate - react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3): + react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5): dependencies: - '@jest/create-cache-key-function': 29.7.0 - '@react-native/assets-registry': 0.82.1 - '@react-native/codegen': 0.82.1(@babel/core@7.28.6) - '@react-native/community-cli-plugin': 0.82.1 - '@react-native/gradle-plugin': 0.82.1 - '@react-native/js-polyfills': 0.82.1 - '@react-native/normalize-colors': 0.82.1 - '@react-native/virtualized-lists': 0.82.1(@types/react@19.2.8)(react-native@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3))(react@19.2.3) + '@react-native/assets-registry': 0.85.1 + '@react-native/codegen': 0.85.1(@babel/core@7.29.0) + '@react-native/community-cli-plugin': 0.85.1(@react-native/metro-config@0.85.1(@babel/core@7.29.0)) + '@react-native/gradle-plugin': 0.85.1 + '@react-native/js-polyfills': 0.85.1 + '@react-native/normalize-colors': 0.85.1 + '@react-native/virtualized-lists': 0.85.1(@types/react@19.2.14)(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.28.6) - babel-plugin-syntax-hermes-parser: 0.32.0 + babel-plugin-syntax-hermes-parser: 0.33.3 base64-js: 1.5.1 commander: 12.1.0 flow-enums-runtime: 0.0.6 - glob: 7.2.3 - hermes-compiler: 0.0.0 + hermes-compiler: 250829098.0.10 invariant: 2.2.4 - jest-environment-node: 29.7.0 memoize-one: 5.2.1 - metro-runtime: 0.83.3 - metro-source-map: 0.83.3 + metro-runtime: 0.84.3 + metro-source-map: 0.84.3 nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 - react: 19.2.3 + react: 19.2.5 react-devtools-core: 6.1.5 react-refresh: 0.14.2 regenerator-runtime: 0.13.11 - scheduler: 0.26.0 + scheduler: 0.27.0 semver: 7.7.3 stacktrace-parser: 0.1.11 + tinyglobby: 0.2.15 whatwg-fetch: 3.6.20 - ws: 6.2.3 + ws: 7.5.10 yargs: 17.7.2 optionalDependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.14 transitivePeerDependencies: - '@babel/core' - '@react-native-community/cli' @@ -9607,30 +9679,28 @@ snapshots: - supports-color - utf-8-validate - react-reconciler@0.33.0(react@19.2.3): + react-reconciler@0.33.0(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 scheduler: 0.27.0 react-refresh@0.14.2: {} - react-refresh@0.18.0: {} - - react-router-dom@7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-router-dom@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-router: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react-router@7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: cookie: 1.1.1 - react: 19.2.3 + react: 19.2.5 set-cookie-parser: 2.7.2 optionalDependencies: - react-dom: 19.2.3(react@19.2.3) + react-dom: 19.2.5(react@19.2.5) - react@19.2.3: {} + react@19.2.5: {} read-yaml-file@1.1.0: dependencies: @@ -9748,63 +9818,44 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.59)(typescript@5.9.3): + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.15)(typescript@6.0.2): dependencies: - '@babel/generator': 7.28.6 - '@babel/parser': 7.28.6 - '@babel/types': 7.28.6 - ast-kit: 2.2.0 + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0-beta.1 birpc: 4.0.0 dts-resolver: 2.1.3 - get-tsconfig: 4.13.0 + get-tsconfig: 4.13.7 obug: 2.1.1 - rolldown: 1.0.0-beta.59 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.15 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0-beta.59: + rolldown@1.0.0-rc.15: dependencies: - '@oxc-project/types': 0.107.0 - '@rolldown/pluginutils': 1.0.0-beta.59 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.59 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.59 - '@rolldown/binding-darwin-x64': 1.0.0-beta.59 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.59 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.59 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.59 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.59 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59 - - rolldown@1.0.0-beta.60: - dependencies: - '@oxc-project/types': 0.108.0 - '@rolldown/pluginutils': 1.0.0-beta.60 + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.60 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.60 - '@rolldown/binding-darwin-x64': 1.0.0-beta.60 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.60 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.60 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.60 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.60 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.60 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.60 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.60 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.60 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.60 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.60 + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 rollup@4.55.2: dependencies: @@ -9836,6 +9887,9 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.55.2 '@rollup/rollup-win32-x64-msvc': 4.55.2 fsevents: 2.3.3 + optional: true + + run-applescript@7.1.0: {} run-parallel@1.2.0: dependencies: @@ -9849,8 +9903,6 @@ snapshots: sax@1.4.4: {} - scheduler@0.26.0: {} - scheduler@0.27.0: {} semver-compare@1.0.0: {} @@ -9859,6 +9911,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -9904,8 +9958,6 @@ snapshots: siginfo@2.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} simple-xml-to-json@1.2.3: {} @@ -9919,6 +9971,11 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -9939,10 +9996,6 @@ snapshots: sprintf-js@1.0.3: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - stackback@0.0.2: {} stackframe@1.3.4: {} @@ -9955,31 +10008,31 @@ snapshots: statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} - storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@2.8.8)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/spy': 3.2.4 - better-opn: 3.0.2 - esbuild: 0.25.12 - esbuild-register: 3.6.0(esbuild@0.25.12) + '@webcontainer/env': 1.1.1 + esbuild: 0.27.2 + open: 10.2.0 recast: 0.23.11 semver: 7.7.3 + use-sync-external-store: 1.6.0(react@19.2.5) ws: 8.19.0 optionalDependencies: prettier: 2.8.8 transitivePeerDependencies: - '@testing-library/dom' - bufferutil - - msw - - supports-color + - react + - react-dom - utf-8-validate - - vite strict-uri-encode@2.0.0: {} @@ -9989,21 +10042,15 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 - string-width@8.1.0: + string-width@8.2.0: dependencies: - get-east-asian-width: 1.4.0 + get-east-asian-width: 1.5.0 strip-ansi: 7.1.2 string.prototype.codepointat@0.2.1: {} @@ -10059,11 +10106,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.8(react@19.2.3): + swr@2.4.1(react@19.2.5): dependencies: dequal: 2.0.3 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) systemjs@6.15.1: {} @@ -10078,12 +10125,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - throat@5.0.0: {} through2@2.0.5: @@ -10101,16 +10142,23 @@ snapshots: tinyexec@1.0.2: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@2.1.0: {} tinyrainbow@2.0.0: {} - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tinyspy@4.0.4: {} @@ -10143,26 +10191,26 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.19.0(typescript@5.9.3): + tsdown@0.21.8(typescript@6.0.2): dependencies: ansis: 4.2.0 - cac: 6.7.14 - defu: 6.1.4 + cac: 7.0.0 + defu: 6.1.7 empathic: 2.0.0 - hookable: 6.0.1 + hookable: 6.1.0 import-without-cache: 0.2.5 obug: 2.1.1 - picomatch: 4.0.3 - rolldown: 1.0.0-beta.59 - rolldown-plugin-dts: 0.20.0(rolldown@1.0.0-beta.59)(typescript@5.9.3) - semver: 7.7.3 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.15 + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.15)(typescript@6.0.2) + semver: 7.7.4 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 tree-kill: 1.2.2 - unconfig-core: 7.4.2 - unrun: 0.2.25 + unconfig-core: 7.5.0 + unrun: 0.2.35 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' @@ -10181,56 +10229,39 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.7.5: - optional: true - - turbo-darwin-arm64@2.7.5: - optional: true - - turbo-linux-64@2.7.5: - optional: true - - turbo-linux-arm64@2.7.5: - optional: true - - turbo-windows-64@2.7.5: - optional: true - - turbo-windows-arm64@2.7.5: - optional: true - - turbo@2.7.5: + turbo@2.9.6: optionalDependencies: - turbo-darwin-64: 2.7.5 - turbo-darwin-arm64: 2.7.5 - turbo-linux-64: 2.7.5 - turbo-linux-arm64: 2.7.5 - turbo-windows-64: 2.7.5 - turbo-windows-arm64: 2.7.5 - - type-detect@4.0.8: {} + '@turbo/darwin-64': 2.9.6 + '@turbo/darwin-arm64': 2.9.6 + '@turbo/linux-64': 2.9.6 + '@turbo/linux-arm64': 2.9.6 + '@turbo/windows-64': 2.9.6 + '@turbo/windows-arm64': 2.9.6 type-fest@0.7.1: {} type-fest@4.41.0: {} - type-fest@5.4.1: + type-fest@5.5.0: dependencies: tagged-tag: 1.0.0 - typescript@5.9.3: {} + typescript@5.9.3: + optional: true + + typescript@6.0.2: {} ua-parser-js@1.0.41: {} uglify-js@3.19.3: optional: true - unconfig-core@7.4.2: + unconfig-core@7.5.0: dependencies: '@quansync/fs': 1.0.0 quansync: 1.0.0 - undici-types@7.16.0: {} + undici-types@7.19.2: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -10243,8 +10274,6 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} - unicorn-magic@0.1.0: {} - unicorn-magic@0.3.0: {} universalify@0.1.2: {} @@ -10253,14 +10282,16 @@ snapshots: unpipe@1.0.0: {} - unplugin@1.16.1: + unplugin@2.3.11: dependencies: + '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 + picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - unrun@0.2.25: + unrun@0.2.35: dependencies: - rolldown: 1.0.0-beta.60 + rolldown: 1.0.0-rc.15 untildify@4.0.0: {} @@ -10283,13 +10314,13 @@ snapshots: semver: 7.7.3 xdg-basedir: 5.1.0 - use-latest-callback@0.2.6(react@19.2.3): + use-latest-callback@0.2.6(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 - use-sync-external-store@1.6.0(react@19.2.3): + use-sync-external-store@1.6.0(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 utif2@4.1.0: dependencies: @@ -10299,72 +10330,61 @@ snapshots: utils-merge@1.0.1: {} - vite-plugin-externalize-deps@0.10.0(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-plugin-externalize-deps@0.10.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.55.2 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.9 + '@types/node': 25.6.0 + esbuild: 0.27.2 fsevents: 2.3.3 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 - - vitest@4.0.17(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): - dependencies: - '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.17 - '@vitest/runner': 4.0.17 - '@vitest/snapshot': 4.0.17 - '@vitest/spy': 4.0.17 - '@vitest/utils': 4.0.17 - es-module-lexer: 1.7.0 + yaml: 2.8.3 + + vitest@4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.10.0 + std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.0.9 + '@types/node': 25.6.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml vlq@1.0.1: {} @@ -10404,18 +10424,18 @@ snapshots: wordwrap@1.0.0: {} + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.0 + strip-ansi: 7.1.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -10424,19 +10444,14 @@ snapshots: wrappy@1.0.2: {} - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - ws@6.2.3: - dependencies: - async-limiter: 1.0.1 - ws@7.5.10: {} ws@8.19.0: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xdg-basedir@5.1.0: {} xml-parse-from-string@1.0.1: {} @@ -10458,7 +10473,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yargs-parser@20.2.9: {} @@ -10484,8 +10499,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yocto-queue@1.2.2: {} - yoga-layout@3.2.1: {} zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6c6fb7..548d8e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,27 +3,27 @@ packages: - packages/* catalog: - '@lightningjs/renderer': 3.0.0-beta20 - '@types/react': 19.2.8 + '@lightningjs/renderer': 3.0.1 + '@types/react': 19.2.14 '@types/react-dom': 19.2.3 - '@types/react-reconciler': 0.32.3 - '@vitejs/plugin-legacy': 7.2.1 - '@vitejs/plugin-react': 5.1.2 + '@types/react-reconciler': 0.33.0 + '@vitejs/plugin-legacy': 8.0.1 + '@vitejs/plugin-react': 6.0.1 babel-plugin-react-compiler: 1.0.0 - react: 19.2.3 - react-dom: 19.2.3 - react-native: 0.82.1 - react-native-reanimated: 4.2.1 + react: 19.2.5 + react-dom: 19.2.5 + react-native: 0.85.1 + react-native-reanimated: 4.3.0 tseep: 1.3.1 - type-fest: 5.4.1 + type-fest: 5.5.0 catalogs: peers: - '@vitejs/plugin-react': ^5.0.0 - react: ^19.2.3 - react-native: ^0.82.1 - react-native-reanimated: ^4.2.1 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitejs/plugin-react': ^6.0.0 + react: ^19.2.5 + react-native: ^0.85.1 + react-native-reanimated: ^4.3.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 onlyBuiltDependencies: - core-js From 52e08ef7aaa7010a9bb90a9e8061630ec4179827 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Mon, 13 Apr 2026 23:06:45 -0700 Subject: [PATCH 05/10] Add changesets --- .changeset/bright-lions-march.md | 6 ++++++ .changeset/cold-pens-glow.md | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .changeset/bright-lions-march.md create mode 100644 .changeset/cold-pens-glow.md diff --git a/.changeset/bright-lions-march.md b/.changeset/bright-lions-march.md new file mode 100644 index 0000000..925ed75 --- /dev/null +++ b/.changeset/bright-lions-march.md @@ -0,0 +1,6 @@ +--- +"@plextv/react-lightning-components": patch +"@plextv/react-native-lightning-components": patch +--- + +feat: Add VirtualList component for fast virtualized lists with view recycling diff --git a/.changeset/cold-pens-glow.md b/.changeset/cold-pens-glow.md new file mode 100644 index 0000000..157c372 --- /dev/null +++ b/.changeset/cold-pens-glow.md @@ -0,0 +1,13 @@ +--- +"@plextv/react-lightning": patch +"@plextv/react-lightning-plugin-css-transform": patch +"@plextv/react-lightning-plugin-flexbox": patch +"@plextv/react-lightning-plugin-flexbox-lite": patch +"@plextv/react-lightning-plugin-reanimated": patch +"@plextv/react-native-lightning": patch +"@plextv/vite-plugin-msdf-fontgen": patch +"@plextv/vite-plugin-react-native-lightning": patch +"@plextv/vite-plugin-react-reanimated-lightning": patch +--- + +chore: Update dependencies and migrate from Biome to oxc From 2f383b42c223f16151e4b810d5a47035606f743c Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Wed, 15 Apr 2026 02:28:57 -0700 Subject: [PATCH 06/10] Multiple fixes for VirtualList and Flexbox --- apps/react-lightning-example/package.json | 10 +- .../src/components/ScrollItem.tsx | 9 +- apps/react-lightning-example/src/index.tsx | 11 +- .../src/pages/NestedListPage.tsx | 115 +++ .../src/pages/VirtualListPage.tsx | 9 + apps/react-lightning-example/vite.config.mjs | 18 +- .../package.json | 11 +- .../src/index.tsx | 7 +- .../src/pages/LibraryTest.tsx | 1 + .../vite.config.mjs | 14 +- apps/storybook/package.json | 11 +- .../src/components/StorybookDecorator.tsx | 6 +- .../lists/VirtualList.stories.tsx | 1 - apps/storybook/vite.config.mjs | 21 +- packages/plugin-css-transform/package.json | 2 +- packages/plugin-flexbox/package.json | 4 +- .../plugin-flexbox/src/LightningManager.ts | 305 ++++++- .../plugin-flexbox/src/YogaManager.spec.ts | 169 ++-- packages/plugin-flexbox/src/YogaManager.ts | 197 +++-- .../plugin-flexbox/src/YogaManagerWorker.ts | 371 +++++--- packages/plugin-flexbox/src/index.ts | 42 +- packages/plugin-flexbox/src/manager.ts | 11 + .../src/types/NodeOperations.ts | 3 + .../plugin-flexbox/src/util/SimpleDataView.ts | 25 +- .../src/util/applyReactPropsToYoga.ts | 27 +- .../src/util/isFlexStyleProp.ts | 7 +- packages/plugin-flexbox/src/worker.ts | 94 +- packages/plugin-flexbox/src/wrappers.tsx | 100 +++ packages/plugin-reanimated/package.json | 6 +- .../react-lightning-components/package.json | 2 +- .../VirtualList/LayoutManager.spec.ts | 352 ++++++-- .../components/VirtualList/LayoutManager.ts | 516 ++++++++--- .../VirtualList/RecyclerPool.spec.ts | 32 + .../components/VirtualList/RecyclerPool.ts | 123 ++- .../VirtualList/ViewabilityTracker.spec.ts | 1 + .../src/components/VirtualList/VirtualList.md | 405 +++++++++ .../components/VirtualList/VirtualList.tsx | 818 ++++++++++++------ .../VirtualList/VirtualListCell.tsx | 405 +++++++-- .../VirtualList/VirtualListContent.tsx | 26 - .../VirtualList/VirtualListContext.ts | 51 ++ .../VirtualList/VirtualListTypes.ts | 6 + .../VirtualList/useScrollHandler.ts | 153 ++-- packages/react-lightning/package.json | 2 +- .../src/element/LightningViewElement.ts | 86 +- .../react-lightning/src/focus/FocusManager.ts | 35 + .../react-lightning/src/focus/useFocus.tsx | 6 - packages/react-lightning/src/index.ts | 1 + .../src/observer/NodeResizeObserver.spec.ts | 302 +++++++ .../src/observer/NodeResizeObserver.ts | 89 ++ .../src/types/LightningElementEvents.ts | 1 + packages/react-lightning/src/types/Props.ts | 1 + .../package.json | 4 +- packages/react-native-lightning/package.json | 9 +- .../src/exports/NativeCanvas.tsx | 20 + packages/react-native-lightning/src/index.ts | 1 + .../vite-plugin-msdf-fontgen/package.json | 2 +- .../package.json | 5 +- .../src/index.ts | 5 +- .../package.json | 4 +- pnpm-lock.yaml | 304 +++---- pnpm-workspace.yaml | 25 +- 61 files changed, 4159 insertions(+), 1240 deletions(-) create mode 100644 apps/react-lightning-example/src/pages/NestedListPage.tsx create mode 100644 packages/plugin-flexbox/src/manager.ts create mode 100644 packages/plugin-flexbox/src/wrappers.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualList.md delete mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualListContent.tsx create mode 100644 packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts create mode 100644 packages/react-lightning/src/observer/NodeResizeObserver.spec.ts create mode 100644 packages/react-lightning/src/observer/NodeResizeObserver.ts create mode 100644 packages/react-native-lightning/src/exports/NativeCanvas.tsx diff --git a/apps/react-lightning-example/package.json b/apps/react-lightning-example/package.json index 072e468..249f8db 100644 --- a/apps/react-lightning-example/package.json +++ b/apps/react-lightning-example/package.json @@ -22,25 +22,25 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "catalog:", + "@lightningjs/renderer": "catalog:apps", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", "@plextv/react-lightning-plugin-css-transform": "workspace:*", "@plextv/react-lightning-plugin-flexbox": "workspace:*", - "react": "catalog:", - "react-dom": "catalog:", + "react": "catalog:apps", + "react-dom": "catalog:apps", "react-router-dom": "7.14.1", "swr": "2.4.1" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", "@repo/configs": "workspace:*", + "@rolldown/plugin-babel": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@vitejs/plugin-legacy": "catalog:", "@vitejs/plugin-react": "catalog:", - "babel-plugin-react-compiler": "catalog:", - "vite-tsconfig-paths": "6.1.1" + "babel-plugin-react-compiler": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/apps/react-lightning-example/src/components/ScrollItem.tsx b/apps/react-lightning-example/src/components/ScrollItem.tsx index d866a1d..36d1068 100644 --- a/apps/react-lightning-example/src/components/ScrollItem.tsx +++ b/apps/react-lightning-example/src/components/ScrollItem.tsx @@ -28,22 +28,21 @@ export const ScrollItem = focusable( {imageUrl ? ( , }, + { + path: '/nested-list', + element: , + }, { path: '/page60', element: , @@ -90,7 +95,9 @@ const options: RenderOptions = { const App = () => ( - + + + ); diff --git a/apps/react-lightning-example/src/pages/NestedListPage.tsx b/apps/react-lightning-example/src/pages/NestedListPage.tsx new file mode 100644 index 0000000..e6d3391 --- /dev/null +++ b/apps/react-lightning-example/src/pages/NestedListPage.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react'; + +import { focusable } from '@plextv/react-lightning'; +import VirtualList from '@plextv/react-lightning-components/lists/VirtualList'; + +type RowData = { + id: number; + label: string; + items: string[]; +}; + +const COLORS = [0x4fafafff, 0xafaf4fff, 0x9f5fdfff, 0xdf5f9fff, 0x5fdf5fff]; +const ROWS: RowData[] = Array.from({ length: 30 }, (_, i) => ({ + id: i, + label: `Row ${i}`, + items: i === 2 ? [] : Array.from({ length: 40 }, (_, j) => `R${i} Item ${j}`), +})); + +const SMALL_ROW_IDS = new Set([3, 23]); +const isSmallRow = (id: number) => SMALL_ROW_IDS.has(id); + +const InnerItem = focusable<{ + focused?: boolean; + label: string; + color: number; + small?: boolean; + autoFocus?: boolean; +}>( + ({ focused, label, color, small }, ref) => { + const timeoutDuration = Math.random() * 1000 + 6; + + const [w, setW] = useState(0); + const [h, setH] = useState(0); + + useEffect(() => { + setTimeout(() => { + setW(small ? 130 : 160); + setH(small ? 50 : 90); + }, timeoutDuration); + }, [small]); + + return ( + + + {label} + + + ); + }, + 'InnerItem', + (props) => ({ autoFocus: props.autoFocus ?? false }), +); + +const RowRenderer = ({ item }: { item: RowData }) => { + const color = COLORS?.[item.id % COLORS.length] ?? 0xff4f4fff; + const small = isSmallRow(item.id); + const itemHeight = small ? 50 : 90; + + return ( + + {item.label} + + `${item.id}-${index}`} + renderItem={({ item: label, shouldFocus }) => ( + + )} + /> + + ); +}; + +export const NestedListPage = () => ( + + + Nested VirtualList — Scroll Persistence + + + Scroll inner rows, then scroll outer list away and back. Position should be preserved. + + { + if (row.items.length === 0) { + layout.size = 0; + } + }} + style={{ w: 960, h: 480, y: 55 }} + keyExtractor={(item) => String(item.id)} + renderItem={({ item }) => } + /> + +); diff --git a/apps/react-lightning-example/src/pages/VirtualListPage.tsx b/apps/react-lightning-example/src/pages/VirtualListPage.tsx index c848552..b53e7e2 100644 --- a/apps/react-lightning-example/src/pages/VirtualListPage.tsx +++ b/apps/react-lightning-example/src/pages/VirtualListPage.tsx @@ -80,6 +80,11 @@ export const VirtualListPage = () => { listFooterSize={46} ItemSeparatorComponent={Separator2} contentContainerStyle={{ paddingVertical: 25 }} + // ScrollItem multiplies width by 1.25 on every 3rd item; mirror + // that here so the cell wrapper allocates the right slot. + overrideItemLayout={(layout, _item, index) => { + layout.size = Math.round(index % 3 === 0 ? 75 * 1.25 : 75); + }} renderItem={({ index, item }) => ( { ListFooterComponent={Footer} listFooterSize={100} ItemSeparatorComponent={Separator} + // ScrollItem multiplies height by 1.5 on every 3rd item. + overrideItemLayout={(layout, _item, index) => { + layout.size = Math.round(index % 3 === 0 ? 50 * 1.5 : 50); + }} renderItem={({ index, item }) => ( dir.includes('app-template'), - }), - react({ - babel: { - plugins: ['babel-plugin-react-compiler'], - }, - }), + react(), + // React Compiler. @vitejs/plugin-react v6 dropped its built-in Babel + // pipeline in favour of oxc, so the legacy `react({ babel: { plugins: [...] } })` + // config is silently ignored. The compiler runs through @rolldown/plugin-babel + // with the preset exposed by @vitejs/plugin-react. + babel({ presets: [reactCompilerPreset()] }), fontGen({ inputs: [ { diff --git a/apps/react-native-lightning-example/package.json b/apps/react-native-lightning-example/package.json index bc13397..bc07e2f 100644 --- a/apps/react-native-lightning-example/package.json +++ b/apps/react-native-lightning-example/package.json @@ -22,7 +22,7 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "catalog:", + "@lightningjs/renderer": "catalog:apps", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", "@plextv/react-lightning-plugin-css-transform": "workspace:*", @@ -31,10 +31,10 @@ "@plextv/react-native-lightning": "workspace:*", "@plextv/react-native-lightning-components": "workspace:*", "@react-navigation/native": "7.2.2", - "react": "catalog:", - "react-dom": "catalog:", - "react-native": "catalog:", - "react-native-reanimated": "catalog:", + "react": "catalog:apps", + "react-dom": "catalog:apps", + "react-native": "catalog:apps", + "react-native-reanimated": "catalog:apps", "react-native-worklets": "0.8.1" }, "devDependencies": { @@ -42,6 +42,7 @@ "@plextv/vite-plugin-react-native-lightning": "workspace:*", "@plextv/vite-plugin-react-reanimated-lightning": "workspace:*", "@repo/configs": "workspace:*", + "@rolldown/plugin-babel": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@vitejs/plugin-legacy": "catalog:", diff --git a/apps/react-native-lightning-example/src/index.tsx b/apps/react-native-lightning-example/src/index.tsx index da1d2b9..32a4a68 100644 --- a/apps/react-native-lightning-example/src/index.tsx +++ b/apps/react-native-lightning-example/src/index.tsx @@ -12,9 +12,8 @@ import '@plextv/react-lightning-plugin-flexbox/jsx'; import { createRoot } from 'react-dom/client'; import { Button } from 'react-native'; -import { Canvas } from '@plextv/react-lightning'; import { Column, Row } from '@plextv/react-lightning-components'; -import { getReactNativePlugins } from '@plextv/react-native-lightning'; +import { getReactNativePlugins, NativeCanvas } from '@plextv/react-native-lightning'; import { ErrorBoundary } from './ErrorBoundary'; import { keyMap } from './keyMap'; @@ -145,7 +144,7 @@ const MainApp = () => { const App = () => { return ( - { - + ); }; diff --git a/apps/react-native-lightning-example/src/pages/LibraryTest.tsx b/apps/react-native-lightning-example/src/pages/LibraryTest.tsx index 78dfa0a..a32d687 100644 --- a/apps/react-native-lightning-example/src/pages/LibraryTest.tsx +++ b/apps/react-native-lightning-example/src/pages/LibraryTest.tsx @@ -94,6 +94,7 @@ const LibraryView = ({ items }: { items: PosterItem[] }) => { snapToAlignment="center" drawDistance={100} numColumns={6} + estimatedItemSize={400} ItemSeparatorComponent={() => } contentContainerStyle={{ paddingHorizontal: 25 }} style={{ w: 1670, h: 1080 }} diff --git a/apps/react-native-lightning-example/vite.config.mjs b/apps/react-native-lightning-example/vite.config.mjs index 4a764cd..287b525 100644 --- a/apps/react-native-lightning-example/vite.config.mjs +++ b/apps/react-native-lightning-example/vite.config.mjs @@ -1,4 +1,6 @@ +import babel from '@rolldown/plugin-babel'; import legacy from '@vitejs/plugin-legacy'; +import { reactCompilerPreset } from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import fontGen from '@plextv/vite-plugin-msdf-fontgen'; @@ -8,13 +10,11 @@ import reactReanimatedLightningPlugin from '@plextv/vite-plugin-react-reanimated const config = defineConfig((env) => ({ base: './', plugins: [ - reactNativeLightningPlugin({ - reactOptions: { - babel: { - plugins: ['babel-plugin-react-compiler'], - }, - }, - }), + reactNativeLightningPlugin(), + // React Compiler. @vitejs/plugin-react v6 uses oxc and ignores any + // `babel` option, so the compiler runs through @rolldown/plugin-babel + // with the preset exported by @vitejs/plugin-react. + babel({ presets: [reactCompilerPreset()] }), reactReanimatedLightningPlugin(), fontGen({ inputs: [ diff --git a/apps/storybook/package.json b/apps/storybook/package.json index cf9b622..f351ace 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -19,7 +19,7 @@ "dev": "storybook dev -p 6006 --no-open" }, "dependencies": { - "@lightningjs/renderer": "catalog:", + "@lightningjs/renderer": "catalog:apps", "@plextv/react-lightning": "workspace:*", "@plextv/react-lightning-components": "workspace:*", "@plextv/react-lightning-plugin-css-transform": "workspace:*", @@ -31,10 +31,10 @@ "@storybook/addon-docs": "10.3.5", "@storybook/addon-links": "10.3.5", "@storybook/react-vite": "10.3.5", - "react": "catalog:", - "react-dom": "catalog:", - "react-native": "catalog:", - "react-native-reanimated": "catalog:", + "react": "catalog:apps", + "react-dom": "catalog:apps", + "react-native": "catalog:apps", + "react-native-reanimated": "catalog:apps", "react-native-worklets": "0.8.1", "storybook": "10.3.5" }, @@ -43,6 +43,7 @@ "@plextv/vite-plugin-react-native-lightning": "workspace:*", "@plextv/vite-plugin-react-reanimated-lightning": "workspace:*", "@repo/configs": "workspace:*", + "@rolldown/plugin-babel": "catalog:", "@types/react": "catalog:", "@vitejs/plugin-react": "catalog:", "babel-plugin-react-compiler": "catalog:" diff --git a/apps/storybook/src/components/StorybookDecorator.tsx b/apps/storybook/src/components/StorybookDecorator.tsx index 57a7ce3..af5aeec 100644 --- a/apps/storybook/src/components/StorybookDecorator.tsx +++ b/apps/storybook/src/components/StorybookDecorator.tsx @@ -1,7 +1,7 @@ import { type JSX, useMemo } from 'react'; import { Canvas, type RenderOptions } from '@plextv/react-lightning'; -import { plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox'; +import { FlexRoot, plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox'; import { getReactNativePlugins } from '@plextv/react-native-lightning'; import { keyMap } from '../../keyMap'; @@ -46,7 +46,9 @@ export function StorybookDecorator({ story: Story, tags, canvasOptions }: Props) return ( - + + + ); } diff --git a/apps/storybook/src/react-lightning-components/lists/VirtualList.stories.tsx b/apps/storybook/src/react-lightning-components/lists/VirtualList.stories.tsx index f9d1bb8..74146f3 100644 --- a/apps/storybook/src/react-lightning-components/lists/VirtualList.stories.tsx +++ b/apps/storybook/src/react-lightning-components/lists/VirtualList.stories.tsx @@ -5,7 +5,6 @@ import { focusable } from '@plextv/react-lightning'; import VirtualList from '@plextv/react-lightning-components/lists/VirtualList'; import type { VirtualListRef } from '@plextv/react-lightning-components/lists/VirtualList'; -import type { ScrollItemProps } from '../../components/ScrollItem'; import ScrollItem from '../../components/ScrollItem'; export default { diff --git a/apps/storybook/vite.config.mjs b/apps/storybook/vite.config.mjs index 3a44951..c32f0f4 100644 --- a/apps/storybook/vite.config.mjs +++ b/apps/storybook/vite.config.mjs @@ -1,3 +1,5 @@ +import babel from '@rolldown/plugin-babel'; +import { reactCompilerPreset } from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import fontGen from '@plextv/vite-plugin-msdf-fontgen'; @@ -16,13 +18,11 @@ const config = defineConfig((env) => ({ }, plugins: [ - reactNativeLightningPlugin({ - reactOptions: { - babel: { - plugins: ['babel-plugin-react-compiler'], - }, - }, - }), + reactNativeLightningPlugin(), + // React Compiler. @vitejs/plugin-react v6 uses oxc and ignores any + // `babel` option, so the compiler runs through @rolldown/plugin-babel + // with the preset exported by @vitejs/plugin-react. + babel({ presets: [reactCompilerPreset()] }), reactReanimatedLightningPlugin(), fontGen({ inputs: [ @@ -36,6 +36,13 @@ const config = defineConfig((env) => ({ }), ], + optimizeDeps: { + // plugin-flexbox uses a ?worker&inline Vite import that rolldown's + // dep optimizer can't resolve. Exclude it so Vite handles it via its + // normal transform pipeline instead. + exclude: ['@plextv/react-lightning-plugin-flexbox'], + }, + server: { port: 3333, host: true, diff --git a/packages/plugin-css-transform/package.json b/packages/plugin-css-transform/package.json index b313ef2..7636101 100644 --- a/packages/plugin-css-transform/package.json +++ b/packages/plugin-css-transform/package.json @@ -51,7 +51,7 @@ "@lightningjs/renderer": "catalog:", "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", - "react-native": "catalog:peers" + "react-native": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-flexbox/package.json b/packages/plugin-flexbox/package.json index 915e404..1d86098 100644 --- a/packages/plugin-flexbox/package.json +++ b/packages/plugin-flexbox/package.json @@ -54,10 +54,12 @@ }, "devDependencies": { "@repo/configs": "workspace:*", + "@types/react": "catalog:", "copyfiles": "2.4.1" }, "peerDependencies": { - "@plextv/react-lightning": "workspace:^" + "@plextv/react-lightning": "workspace:^", + "react": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-flexbox/src/LightningManager.ts b/packages/plugin-flexbox/src/LightningManager.ts index 57d50c8..aaa0d74 100644 --- a/packages/plugin-flexbox/src/LightningManager.ts +++ b/packages/plugin-flexbox/src/LightningManager.ts @@ -7,7 +7,6 @@ import type { } from '@plextv/react-lightning'; import type { YogaOptions } from './types/YogaOptions'; -import { SimpleDataView } from './util/SimpleDataView'; import loadYoga from './yoga'; import type { YogaManager } from './YogaManager'; import type { Workerized } from './YogaManagerWorker'; @@ -18,6 +17,23 @@ import type { Workerized } from './YogaManagerWorker'; */ export class LightningManager { private _elements = new Map(); + private _boundaries = new Set(); + private _flexRoots = new Set(); + /** + * Tracks the yoga-side parent for every attached node (childId -> parentId). + * Used to make boundary/flex-root marking idempotent without a sync round-trip + * to a worker. + */ + private _yogaParents = new Map(); + /** + * Per-parent count of children currently attached in yoga. Lets + * `_yogaIndexFor` short-circuit the O(n) sibling walk for the common + * append-at-end case (the new child's yoga index equals the parent's + * current attached count). Maintained alongside `_yogaParents`: any + * change to a child's yoga parent updates the old parent's count down + * and the new parent's count up. + */ + private _yogaChildCounts = new Map(); private _yogaManager: YogaManager | Workerized | undefined; public async init(yogaOptions?: YogaOptions): Promise { @@ -25,6 +41,207 @@ export class LightningManager { this._yogaManager.on('render', this._applyUpdates); } + /** + * Marks an element as a flex boundary inside a flex tree. Its existing + * children are detached from yoga and any future children added to its + * subtree are not added to yoga either. A nested {@link markFlexRoot} + * restores yoga participation for everything below it. + * + * Note: flex is opt-in, so calling this outside a {@link markFlexRoot} + * subtree is a no-op — those elements are already excluded from yoga. + */ + public markBoundary(element: LightningElement): () => void { + if (this._boundaries.has(element.id)) { + return () => this.unmarkBoundary(element.id); + } + + this._boundaries.add(element.id); + + if (this._yogaManager) { + for (const child of element.children) { + if (this._yogaParents.get(child.id) === element.id) { + this._yogaManager.detachChildNode(element.id, child.id); + this._clearYogaParent(child.id, element.id); + } + } + + // Tree shape changed — re-layout any flex roots that contain it. + this._yogaManager.queueRender(element.id); + } + + return () => this.unmarkBoundary(element.id); + } + + public unmarkBoundary(elementId: number): void { + this._boundaries.delete(elementId); + } + + /** + * Opts an element and its subtree into flex layout. The element becomes an + * independent yoga root — its subtree is laid out on its own each render, + * separately from any other flex tree. + * + * Flex is opt-in for this plugin. Without a flex root somewhere above an + * element, that element is invisible to yoga and gets no flex behavior. + */ + public markFlexRoot(element: LightningElement): () => void { + if (this._flexRoots.has(element.id)) { + return () => this.unmarkFlexRoot(element.id); + } + + this._flexRoots.add(element.id); + + if (this._yogaManager) { + const yogaParent = this._yogaParents.get(element.id); + + if (yogaParent !== undefined) { + this._yogaManager.detachChildNode(yogaParent, element.id); + this._clearYogaParent(element.id, yogaParent); + } + + this._yogaManager.addIndependentRoot(element.id); + + this._reattachChildren(element); + + // Trigger the first layout pass for the freshly-attached subtree. + // Without this, the new independent root sits with default 0,0 sizes + // until something else happens to call applyStyle. + this._yogaManager.queueRender(element.id); + } + + return () => this.unmarkFlexRoot(element.id); + } + + public unmarkFlexRoot(elementId: number): void { + this._flexRoots.delete(elementId); + this._yogaManager?.removeIndependentRoot(elementId); + } + + /** + * Returns true when an element should NOT participate in yoga layout. Flex + * is opt-in: an element is in flex only when it has an ancestor (or is one) + * marked as a {@link markFlexRoot}. A nested {@link markBoundary} between + * the element and that flex root re-disables flex for the subtree. + */ + private _isInBoundary(parent: LightningElement): boolean { + let curr: LightningElement | null = parent; + + while (curr) { + if (this._flexRoots.has(curr.id)) { + return false; + } + + if (this._boundaries.has(curr.id)) { + return true; + } + + curr = curr.parent; + } + + return true; + } + + /** + * Counts how many preceding React siblings of `parent` are currently + * attached to `parent` in yoga. The result is the yoga-side index at which + * a child should be inserted to preserve relative order with React. + * + * Fast path: when the new child is being appended at the end of + * `parent.children` (the common case for React mounts and most list + * additions), the yoga-side index is exactly the parent's current + * attached-children count — no sibling walk needed. This turns the + * O(N²) cost of mass-mounting a list into O(N). + */ + private _yogaIndexFor(parent: LightningElement, reactIndex: number): number { + if (reactIndex === parent.children.length - 1) { + return this._yogaChildCounts.get(parent.id) ?? 0; + } + + let yogaIndex = 0; + + for (let i = 0; i < reactIndex; i++) { + const sibling = parent.children[i]; + + if (sibling && this._yogaParents.get(sibling.id) === parent.id) { + yogaIndex++; + } + } + + return yogaIndex; + } + + /** + * Marks `childId` as yoga-attached to `parentId`. Updates `_yogaParents` + * AND `_yogaChildCounts` together so `_yogaIndexFor`'s fast path stays + * accurate. + */ + private _setYogaParent(childId: number, parentId: number): void { + this._yogaParents.set(childId, parentId); + this._yogaChildCounts.set(parentId, (this._yogaChildCounts.get(parentId) ?? 0) + 1); + } + + /** + * Records that `childId` is no longer yoga-attached to `parentId`. + * Symmetric counterpart of `_setYogaParent`. The `parentId` argument is + * required because at call time `_yogaParents.get(childId)` may have + * already been deleted. + */ + private _clearYogaParent(childId: number, parentId: number): void { + this._yogaParents.delete(childId); + + const count = this._yogaChildCounts.get(parentId); + + if (count !== undefined && count > 0) { + this._yogaChildCounts.set(parentId, count - 1); + } + } + + private _reattachChildren(parent: LightningElement): void { + if (!this._yogaManager) { + return; + } + + let yogaIndex = 0; + + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + + if (!child) { + continue; + } + + if (this._flexRoots.has(child.id)) { + // Independent yoga root — does not count toward parent's yoga children + continue; + } + + const currentParent = this._yogaParents.get(child.id); + + if (currentParent === parent.id) { + yogaIndex++; + + if (!this._boundaries.has(child.id)) { + this._reattachChildren(child); + } + + continue; + } + + if (currentParent !== undefined) { + this._yogaManager.detachChildNode(currentParent, child.id); + this._clearYogaParent(child.id, currentParent); + } + + this._yogaManager.addChildNode(parent.id, child.id, yogaIndex); + this._setYogaParent(child.id, parent.id); + yogaIndex++; + + if (!this._boundaries.has(child.id)) { + this._reattachChildren(child); + } + } + } + public trackElement(element: LightningElement): void { if (this._elements.has(element.id)) { console.warn(`Yoga node is already attached to element #${element.id}.`); @@ -44,26 +261,71 @@ export class LightningManager { dispose(); } + const yogaParent = this._yogaParents.get(element.id); + + if (yogaParent !== undefined) { + this._clearYogaParent(element.id, yogaParent); + } + this._elements.delete(element.id); + this._boundaries.delete(element.id); + this._flexRoots.delete(element.id); + this._yogaChildCounts.delete(element.id); // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. But avoiding the nullish operator for perf reasons this._yogaManager!.applyStyle(element.id, null, true); // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above + this._yogaManager!.removeIndependentRoot(element.id); + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.removeNode(element.id); }), element.on('childAdded', (child, index) => { + if (this._isInBoundary(element)) { + return; + } + + // Translate the React-side index to a yoga-side index by counting + // preceding siblings that are actually attached to this parent in + // yoga. Without this, skipped siblings (nested boundaries, flex + // roots) cause yoga's insertChild to walk off the end of its + // children array — which surfaces as "memory access out of bounds". + const yogaIndex = this._yogaIndexFor(element, index); + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above - this._yogaManager!.addChildNode(element.id, child.id, index); + this._yogaManager!.addChildNode(element.id, child.id, yogaIndex); + this._setYogaParent(child.id, element.id); this.applyStyle(element.id, element.style); + + // React mounts bottom-up: `child` may already have its own subtree + // that was inserted before `child` itself joined the flex tree. Those + // descendants were skipped at their own childAdded time (no flex + // ancestor existed then). Promote them now. + if (!this._boundaries.has(child.id)) { + this._reattachChildren(child); + } }), element.on('childRemoved', (child) => { // This will remove any pending worker style updates that haven't been sent + const childYogaParent = this._yogaParents.get(child.id); + + if (childYogaParent !== undefined) { + this._clearYogaParent(child.id, childYogaParent); + } + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.applyStyle(child.id, null, true); // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.removeNode(child.id); + + // Schedule a yoga re-layout. Without this, a parent that shrink-fits + // its children (no explicit w/h) keeps the old size when a child is + // removed — its node.w/h stay at the last computed values, and + // NodeResizeObserver never fires the shrink event up to consumers + // (e.g., VirtualList's reportItemSize). + // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above + this._yogaManager!.queueRender(element.id); }), element.on('inViewport', () => { @@ -114,15 +376,24 @@ export class LightningManager { } private _applyUpdates = (buffer: ArrayBuffer) => { - const dataView = new SimpleDataView(buffer); + // Raw `DataView` instead of `SimpleDataView` here — this is a pure + // read loop fired per render frame, and `SimpleDataView` adds an + // outer object plus a layer of `_readInt` indirection that's pure + // overhead when we don't need overflow handling, write tracking, or + // auto-incrementing offsets across method calls. Manual offset + // arithmetic is the cheapest option for a hot per-frame path. + const view = new DataView(buffer); + const length = buffer.byteLength; + let offset = 0; - // See YogaManager.ts for the structure of the updates - while (dataView.hasSpace(12)) { - const elementId = dataView.readUint32(); - const x = dataView.readInt16(); - const y = dataView.readInt16(); - const width = dataView.readUint16(); - const height = dataView.readUint16(); + // See YogaManager.ts for the structure of the updates (12 bytes/entry) + while (offset + 12 <= length) { + const elementId = view.getUint32(offset, true); + const x = view.getInt16(offset + 4, true); + const y = view.getInt16(offset + 6, true); + const width = view.getUint16(offset + 8, true); + const height = view.getUint16(offset + 10, true); + offset += 12; const el = this._elements.get(elementId); @@ -135,6 +406,10 @@ export class LightningManager { let skipX = false; let skipY = false; let dirty = false; + let resize = false; + + // `isTextElement` is a getter — cache once, read twice below. + const isText = el.isTextElement; if (el.parent?.style.display !== 'flex') { skipX = @@ -158,12 +433,18 @@ export class LightningManager { // If width is 0, we should not set it on the node, as it will cause // layout issues. We also ignore setting width/height for text elements, // as their size is handled by lightning. - if (width !== 0 && !el.isTextElement) { + if (width !== 0 && !isText) { dirty = el.setNodeProp('w', width) || dirty; + resize = true; } - if (height !== 0 && !el.isTextElement) { + if (height !== 0 && !isText) { dirty = el.setNodeProp('h', height) || dirty; + resize = true; + } + + if (resize) { + el.emit('resized', el, { w: width, h: height }); } if (dirty || !el.hasLayout) { diff --git a/packages/plugin-flexbox/src/YogaManager.spec.ts b/packages/plugin-flexbox/src/YogaManager.spec.ts index d66f9e1..c4e75ce 100644 --- a/packages/plugin-flexbox/src/YogaManager.spec.ts +++ b/packages/plugin-flexbox/src/YogaManager.spec.ts @@ -251,11 +251,12 @@ describe('YogaManager', () => { }).toThrow('Yoga is not initialized! Did you call `init()`?'); }); - it('should queue render for existing node', () => { + it('should queue render for an independent root', () => { return new Promise((resolve) => { const elementId = 123; yogaManager.addNode(elementId); + yogaManager.addIndependentRoot(elementId); yogaManager.on('render', (buffer) => { expect(buffer).toBeInstanceOf(ArrayBuffer); @@ -267,18 +268,18 @@ describe('YogaManager', () => { }); }); - it('should handle queueing render for non-existent node', () => { + it('should not emit render when there are no independent roots', () => { return new Promise((resolve, reject) => { - const elementId = 999; + const elementId = 123; + + yogaManager.addNode(elementId); - // Should not emit render event yogaManager.on('render', () => { - reject('Should not emit render event for non-existent node'); + reject(new Error('Should not emit render when no independent roots are registered')); }); yogaManager.queueRender(elementId); - // Wait a bit to ensure no event is emitted setTimeout(() => { resolve(); }, 10); @@ -290,6 +291,8 @@ describe('YogaManager', () => { const elementId = 123; yogaManager.addNode(elementId); + yogaManager.addIndependentRoot(elementId); + yogaManager.queueRender(elementId); yogaManager.queueRender(elementId); // Should only calculate layout once after both calls @@ -306,6 +309,7 @@ describe('YogaManager', () => { const numNodes = 100; yogaManager.addNode(rootId); + yogaManager.addIndependentRoot(rootId); for (let i = 1; i < numNodes; i++) { yogaManager.addNode(i); @@ -332,6 +336,83 @@ describe('YogaManager', () => { yogaManager.queueRender(rootId); }); }); + + it('should iterate every independent root on render', () => { + return new Promise((resolve) => { + const rootA = 1; + const rootB = 2; + + yogaManager.addNode(rootA); + yogaManager.addNode(rootB); + yogaManager.addIndependentRoot(rootA); + yogaManager.addIndependentRoot(rootB); + + yogaManager.on('render', () => { + expect(mockNode.calculateLayout).toHaveBeenCalledTimes(2); + resolve(); + }); + + yogaManager.queueRender(rootA); + }); + }); + }); + + describe('detach + independent root management', () => { + beforeEach(async () => { + await yogaManager.init(); + }); + + it('should detach a child without freeing it', () => { + const parentId = 1; + const childId = 2; + + yogaManager.addNode(parentId); + yogaManager.addNode(childId); + yogaManager.addChildNode(parentId, childId, 0); + + const parentNode = yogaManager.addNode(parentId); + + // sanity — child is in parent's children + expect(parentNode.children).toHaveLength(1); + + yogaManager.detachChildNode(parentId, childId); + + expect(parentNode.children).toHaveLength(0); + expect(mockNode.free).not.toHaveBeenCalled(); + }); + + it('should be a no-op to detach an unattached child', () => { + const parentId = 1; + const childId = 2; + + yogaManager.addNode(parentId); + yogaManager.addNode(childId); + + // Not attached — detach should silently do nothing + yogaManager.detachChildNode(parentId, childId); + + expect(mockNode.free).not.toHaveBeenCalled(); + }); + + it('should remove an independent root', () => { + return new Promise((resolve, reject) => { + const elementId = 123; + + yogaManager.addNode(elementId); + yogaManager.addIndependentRoot(elementId); + yogaManager.removeIndependentRoot(elementId); + + yogaManager.on('render', () => { + reject(new Error('Removed independent root should not render')); + }); + + yogaManager.queueRender(elementId); + + setTimeout(() => { + resolve(); + }, 10); + }); + }); }); describe('style application', () => { @@ -426,82 +507,6 @@ describe('YogaManager', () => { }); }); - describe('clamped size calculation', () => { - beforeEach(async () => { - await yogaManager.init(); - }); - - it('should throw error when not initialized', () => { - const uninitializedManager = new YogaManager(); - expect(() => { - uninitializedManager.getClampedSize(123); - }).toThrow('Yoga was not initialized! Did you call `init()`?'); - }); - - it('should return null for non-existent node', () => { - const result = yogaManager.getClampedSize(999); - expect(result).toBeNull(); - }); - - it('should return null when no max width is set', () => { - const elementId = 123; - yogaManager.addNode(elementId); - - mockNode.getMaxWidth.mockReturnValue({ - value: NaN, - unit: mockYoga.UNIT_UNDEFINED, - }); - - const result = yogaManager.getClampedSize(elementId); - expect(result).toBeNull(); - }); - - it('should return computed width when max width is set', () => { - const elementId = 123; - yogaManager.addNode(elementId); - - mockNode.getMaxWidth.mockReturnValue({ - value: 150, - unit: mockYoga.UNIT_POINT, - }); - mockNode.getComputedWidth.mockReturnValue(100); - - const result = yogaManager.getClampedSize(elementId); - expect(result).toBe(100); - }); - - it('should calculate percentage-based max width', () => { - const elementId = 123; - yogaManager.addNode(elementId); - - const parentNode = { getComputedWidth: vi.fn(() => 200) }; - mockNode.getMaxWidth.mockReturnValue({ - value: 50, - unit: mockYoga.UNIT_PERCENT, - }); - mockNode.getComputedWidth.mockReturnValue(NaN); - mockNode.getParent.mockReturnValue(parentNode); - - const result = yogaManager.getClampedSize(elementId); - expect(result).toBe(100); // parent width when percentage - }); - - it('should use max width value when no parent and unit is point', () => { - const elementId = 123; - yogaManager.addNode(elementId); - - mockNode.getMaxWidth.mockReturnValue({ - value: 150, - unit: mockYoga.UNIT_POINT, - }); - mockNode.getComputedWidth.mockReturnValue(NaN); - mockNode.getParent.mockReturnValue(null); - - const result = yogaManager.getClampedSize(elementId); - expect(result).toBe(150); - }); - }); - describe('event emitter', () => { it('should support on/off event listeners', () => { const listener = vi.fn(); diff --git a/packages/plugin-flexbox/src/YogaManager.ts b/packages/plugin-flexbox/src/YogaManager.ts index 06d6fa8..4131d8e 100644 --- a/packages/plugin-flexbox/src/YogaManager.ts +++ b/packages/plugin-flexbox/src/YogaManager.ts @@ -32,12 +32,11 @@ const MAX_SIZEOF_UPDATE = 1024 * 10; export class YogaManager { private _elementMap: Map = new Map(); private _hiddenElements: Set = new Set(); + private _independentRoots: Set = new Set(); private _yoga?: Yoga; private _config?: Config; - private _rootNode?: ManagerNode; private _initialized = false; private _isRenderQueued = false; - private _queueTimeout: NodeJS.Timeout | undefined; private _yogaOptions: Required = { useWebDefaults: false, errata: 'none', @@ -139,47 +138,84 @@ export class YogaManager { childYogaNode.parent = parentYogaNode; } - public queueRender(elementId: number, force = false): void { - if (!this._initialized || !this._yoga) { - throw new Error('Yoga is not initialized! Did you call `init()`?'); + public detachChildNode(parentId: number, childId: number): void { + const parentYogaNode = this._elementMap.get(parentId); + const childYogaNode = this._elementMap.get(childId); + + if (!parentYogaNode || !childYogaNode) { + return; } - if (this._isRenderQueued && this._queueTimeout) { + const idx = parentYogaNode.children.indexOf(childYogaNode); + + if (idx === -1) { return; } - this._isRenderQueued = true; + parentYogaNode.node.removeChild(childYogaNode.node); + parentYogaNode.children.splice(idx, 1); + childYogaNode.parent = undefined; + } - this._queueTimeout = setTimeout(() => { - let root = this._rootNode; + public addIndependentRoot(elementId: number): void { + const node = this._elementMap.get(elementId); - if (!root) { - const node = this._elementMap.get(elementId); + if (node) { + this._independentRoots.add(node); + } + } - if (!node) { - return; - } + public removeIndependentRoot(elementId: number): void { + const node = this._elementMap.get(elementId); - root = node; - let curr: ManagerNode | undefined = node; + if (node) { + this._independentRoots.delete(node); + } + } - while (curr) { - root = curr; - curr = curr.parent; - } + public queueRender(_elementId: number, force = false): void { + if (!this._initialized || !this._yoga) { + throw new Error('Yoga is not initialized! Did you call `init()`?'); + } - this._rootNode = root; - } + if (this._isRenderQueued) { + return; + } - // oxlint-disable-next-line typescript/no-non-null-assertion -- Already checked this._yoga above - root.node.calculateLayout(1920, 1080, this._yoga!.DIRECTION_LTR); + this._isRenderQueued = true; + + // Microtask instead of setTimeout(_, 1): we want the layout pass to + // run AFTER the current synchronous batch of style/node operations + // (which arrive in succession from postMessage handlers and + // worker-side updates), and a microtask achieves that with no timer + // overhead. setTimeout enforces a 1ms minimum (4ms after nesting) and + // fragments large batches into many separate render passes. + queueMicrotask(() => { + this._isRenderQueued = false; + + if (this._independentRoots.size === 0) { + return; + } this._initializeArrayBuffer(); - this._getUpdatedStyles(root, force); - this._flushArrayBuffer(this._dataView.buffer); - this._isRenderQueued = false; - }, 1); + for (const independentRoot of this._independentRoots) { + // Pass undefined for available size so yoga uses the root's own + // explicit w/h if set, and shrinks-to-fit otherwise. Hardcoding + // 1920×1080 here makes every root with an unset axis stretch to the + // canvas dimensions — which breaks measurement-driven roots like + // VirtualList cells. + independentRoot.node.calculateLayout( + undefined, + undefined, + // oxlint-disable-next-line typescript/no-non-null-assertion -- Already checked this._yoga above + this._yoga!.DIRECTION_LTR, + ); + this._getUpdatedStyles(independentRoot, force); + } + + this._flushArrayBuffer(this._dataView.buffer); + }); } public applyStyles( @@ -190,10 +226,13 @@ export class YogaManager { throw new Error('Yoga was not initialized! Did you call `init()`?'); } - const styleEntries = Object.entries(styles); - - for (const [elementId, style] of styleEntries) { - this.applyStyle(Number(elementId), style, skipRender); + // `for...in` instead of `Object.entries(styles)` to skip the per-call + // [key, value] tuple allocation. This iterates every batched style + // update on the worker side per `'flushBoth'` / `'applyStyles'` + // message. + for (const elementId in styles) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- key from for..in iteration of own props + this.applyStyle(+elementId, styles[elementId as unknown as number]!, skipRender); } } @@ -257,44 +296,6 @@ export class YogaManager { } } - public getClampedSize(elementId: number): number | null { - // Text elements will already have its height and width set on the - // node before loaded event is fired, so we need to set it on the yoga - // node. If there's a maxWidth set, we should clamp the text to that size. - const yogaNode = this._elementMap.get(elementId); - - if (!this._initialized || !this._yoga) { - throw new Error('Yoga was not initialized! Did you call `init()`?'); - } - - if (!yogaNode) { - return null; - } - - const maxWidth = yogaNode.node.getMaxWidth(); - - if (!Number.isNaN(maxWidth.value) && maxWidth.unit !== this._yoga.UNIT_UNDEFINED) { - // If there is a max width specified, the width on the yogaNode will - // be the computed width - let computedWidth = yogaNode.node.getComputedWidth(); - const isPercentage = maxWidth.unit === this._yoga.UNIT_PERCENT; - - if (Number.isNaN(computedWidth) || isPercentage) { - const parentWidth = yogaNode.node.getParent()?.getComputedWidth(); - - if (parentWidth) { - computedWidth = isPercentage ? parentWidth * (maxWidth.value / 100) : parentWidth; - } else if (maxWidth.unit === this._yoga.UNIT_POINT) { - computedWidth = maxWidth.value; - } - } - - return !Number.isNaN(computedWidth) ? computedWidth : null; - } - - return null; - } - private _flushArrayBuffer(buffer: ArrayBuffer) { // Emit the current buffer this._eventEmitter.emit('render', buffer); @@ -323,33 +324,51 @@ export class YogaManager { return yogaNode; } - // returns the new offset in the dataView + // Walks the yoga subtree and emits an update for every node with a fresh + // layout. Critically, recursion is unconditional — yoga's hasNewLayout is + // per-node, so a child's layout may have changed even when its parent's + // didn't (e.g. absolute children laid out independently of flow siblings, + // or a subtree just attached via _reattachChildren). private _getUpdatedStyles(yogaNode: ManagerNode, force = false) { const skipHiddenNode = !this._yogaOptions.processHiddenNodes && this._hiddenElements.has(yogaNode.id); - if (!force && (skipHiddenNode || !yogaNode.node.hasNewLayout())) { - return; - } + if (!skipHiddenNode && (force || yogaNode.node.hasNewLayout())) { + // We want to keep chunks together, so check the size to ensure we have enough space + if (!this._dataView.hasSpace(APPROX_SIZEOF_UPDATE)) { + // If we don't have enough space, flush the current buffer + this._flushArrayBuffer(this._dataView.buffer); + } - // We want to keep chunks together, so check the size to ensure we have enough space - if (!this._dataView.hasSpace(APPROX_SIZEOF_UPDATE)) { - // If we don't have enough space, flush the current buffer - this._flushArrayBuffer(this._dataView.buffer); + // Read layout via individual getters instead of `getComputedLayout()`, + // which allocates a fresh `{ left, top, width, height }` object per + // node. This function recurses through every yoga descendant on every + // layout pass, so the saved allocations add up quickly on big trees. + const node = yogaNode.node; + + // Direct `DataView` writes — `hasSpace(APPROX_SIZEOF_UPDATE)` above + // already validated the full 12-byte run, so the per-call overflow + // check and `_writeInt` switch dispatch inside `writeUint32`/ + // `writeInt16` are pure overhead here. This fires for every + // freshly-laid-out yoga descendant on every layout pass. + const view = this._dataView.dataView; + const offset = this._dataView.offset; + + view.setUint32(offset, yogaNode.id, true); + view.setInt16(offset + 4, node.getComputedLeft(), true); + view.setInt16(offset + 6, node.getComputedTop(), true); + view.setInt16(offset + 8, node.getComputedWidth(), true); + view.setInt16(offset + 10, node.getComputedHeight(), true); + this._dataView.advance(APPROX_SIZEOF_UPDATE); + + node.markLayoutSeen(); } - const layout = yogaNode.node.getComputedLayout(); - - this._dataView.writeUint32(yogaNode.id); - this._dataView.writeInt16(layout.left); - this._dataView.writeInt16(layout.top); - this._dataView.writeInt16(layout.width); - this._dataView.writeInt16(layout.height); - - yogaNode.node.markLayoutSeen(); + const children = yogaNode.children; - for (const child of yogaNode.children) { - this._getUpdatedStyles(child, force); + for (let i = 0, len = children.length; i < len; i++) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- length-bounded + this._getUpdatedStyles(children[i]!, force); } } } diff --git a/packages/plugin-flexbox/src/YogaManagerWorker.ts b/packages/plugin-flexbox/src/YogaManagerWorker.ts index 1dd2136..a6a566f 100644 --- a/packages/plugin-flexbox/src/YogaManagerWorker.ts +++ b/packages/plugin-flexbox/src/YogaManagerWorker.ts @@ -3,17 +3,14 @@ import { EventEmitter } from 'tseep'; import type { LightningElementStyle } from '@plextv/react-lightning'; import { NodeOperations } from './types/NodeOperations'; +import { isFlexStyleProp } from './util/isFlexStyleProp'; import { SimpleDataView } from './util/SimpleDataView'; import { toSerializableValue } from './util/toSerializableValue'; import Worker from './worker?worker&inline'; import type { YogaManager, YogaManagerEvents } from './YogaManager'; -const DELAY_DURATION = 1; - // oxlint-disable-next-line typescript/no-explicit-any -- Basic type for function signatures type AnyFunc = (...args: any[]) => any; -// oxlint-disable-next-line typescript/no-explicit-any -- We don't care about the first parameter type here -type ParametersExceptFirst = T extends (first: any, ...args: infer U) => any ? U : never; export type Workerized = { [K in keyof T]: T[K] extends AnyFunc @@ -23,24 +20,36 @@ export type Workerized = { : never; }; -function delay void | Promise>(fn: T, delay: number): T { - let timeout: ReturnType | null = null; +/** + * Coalesces calls within a single synchronous task. The first call schedules + * a microtask; subsequent calls before that microtask fires are no-ops (they + * just keep `latestArgs` updated). At the end of the current synchronous + * code, the microtask runs `fn` with the latest args. + * + * We use a microtask instead of `setTimeout(_, 1)` because the timer's + * minimum 1ms (and 4ms after nesting) breaks coalescing into many small + * postMessage flushes during a React commit pass. A microtask collapses a + * whole commit's worth of writes into one flush. + */ +function debounceMicrotask void | Promise>(fn: T): T { + let scheduled = false; let latestArgs: unknown[]; - const delayedFn = function (this: unknown, ...args: unknown[]) { + const debouncedFn = function (this: unknown, ...args: unknown[]) { latestArgs = args; - if (timeout) { + if (scheduled) { return; } - timeout = setTimeout(() => { - timeout = null; + scheduled = true; + queueMicrotask(() => { + scheduled = false; fn.apply(this, latestArgs); - }, delay); + }); }; - return delayedFn as T; + return debouncedFn as T; } function wrapWorker(worker: Worker): Workerized { @@ -49,18 +58,43 @@ function wrapWorker(worker: Worker): Workerized { let _stylesToSend: Record> = {}; let _numStylesToSend = 0; let _needsRender = false; - const _childOperations = new SimpleDataView(undefined, undefined, flushChildOperations); - const _sizeRequests = new SimpleDataView(undefined, undefined, flushSizeRequests); - let _sizeRequestPromise: Promise | null = null; + const _childOperations = new SimpleDataView(undefined, undefined, _onChildOpsOverflow); + + /** + * Overflow handler for `_childOperations`. Deliberately distinct from + * `flushChildOperations` (which combines with pending styles): an + * overflow happens mid-write, so the buffer being flushed is a *partial* + * batch of node operations. Combining pending styles with this partial + * batch would land them on the worker before the rest of the nodeOps + * arrive — styles would target nodes that don't exist yet, causing + * "node not found" warnings and silently-skipped layout. nodeOps-only + * here is the only safe choice. + */ + function _onChildOpsOverflow(filledBuffer: ArrayBuffer) { + worker.postMessage( + { + method: 'nodeOperations', + args: [filledBuffer], + }, + [filledBuffer], + ); + } function flushSendStyles() { - if (Object.keys(_stylesToSend).length === 0) { + // Cheap counter check instead of `Object.keys(_stylesToSend).length` — + // the latter walks every key in the record on each call. + if (_numStylesToSend === 0) { return; } - // If we need to send styles, make sure we send any pending - // child operations first - flushChildOperations(); + // Combine with pending nodeOps if any. See `_flushBothInternal` for + // the full reasoning — the short version: this collapses two separate + // postMessages into one when both flush types fire in the same + // microtask cycle. + if (_childOperations.offset > 0) { + _flushBothInternal(); + return; + } worker.postMessage({ method: 'applyStyles', @@ -72,7 +106,7 @@ function wrapWorker(worker: Worker): Workerized { _numStylesToSend = 0; } - const queueSendStyles = delay(flushSendStyles, DELAY_DURATION); + const queueSendStyles = debounceMicrotask(flushSendStyles); function applyStyle( elementId: number, @@ -88,9 +122,26 @@ function wrapWorker(worker: Worker): Workerized { _stylesToSend[elementId] = styleToSend; } - // Add style props if they're serializable - for (const [key, value] of Object.entries(style)) { - const serializedValue = toSerializableValue(key, value); + // `for...in` instead of `Object.entries(style)` — avoids the per-call + // [key, value] tuple array allocation. This loop runs on every + // applyStyle, which fires hundreds of times during a busy commit. + // + // We also short-circuit non-flex keys here. LightningManager.applyStyle + // is called with the element's full `props.style` from event handlers + // (`stylesChanged`, `inViewport`, `childAdded`'s parent applyStyle), + // so the input often contains color/alpha/font/etc. — keys yoga + // doesn't care about. Filtering here saves: a `toSerializableValue` + // call per skipped key, the bytes those keys would add to the + // postMessage payload (structured-clone cost), and the + // `isFlexStyleProp` check the worker's `applyReactPropsToYoga` + // would do on the same key anyway. + for (const key in style) { + if (!isFlexStyleProp(key)) { + continue; + } + + // oxlint-disable-next-line typescript/no-explicit-any -- intentional: style values can be many shapes; toSerializableValue guards + const serializedValue = toSerializableValue(key, (style as any)[key]); if (serializedValue != null) { // @ts-expect-error @@ -98,6 +149,17 @@ function wrapWorker(worker: Worker): Workerized { } } } else { + // Drop a pending entry if one exists. Without the existence check the + // counter underflows whenever `applyStyle(id, null)` runs for an id + // with nothing pending — the common case for `childRemoved`, which + // calls `applyStyle(child.id, null, true)` regardless of whether any + // style was buffered for that child. A negative counter then breaks + // the `> 50` early-flush threshold and the `=== 0` short-circuit in + // `flushSendStyles`. + if (!_stylesToSend[elementId]) { + return; + } + delete _stylesToSend[elementId]; _numStylesToSend--; } @@ -117,23 +179,102 @@ function wrapWorker(worker: Worker): Workerized { if (buffer.byteLength === 0) { return; - } else { - worker.postMessage( - { - method: 'nodeOperations', - args: [buffer], - }, - [buffer], - ); } + // Combine with pending styles if any. See `_flushBothInternal`. + if (_numStylesToSend > 0) { + _flushBothInternal(); + return; + } + + worker.postMessage( + { + method: 'nodeOperations', + args: [buffer], + }, + [buffer], + ); + _childOperations.reset(); } - const queueSendNodeOperations = delay(flushChildOperations, DELAY_DURATION); + /** + * Drain both pending node operations and pending styles into a single + * `'flushBoth'` postMessage. The worker handler applies node operations + * first, then styles — preserving the causal ordering the two-message + * setup enforced by hand (`flushChildOperations()` before + * `flushSendStyles()` posted `applyStyles`). + * + * Caller must have verified that BOTH queues have data; this function + * blindly transfers/clears both. The overflow handler + * (`_onChildOpsOverflow`) intentionally bypasses this and posts + * nodeOps-only, since combining at overflow would land pre-overflow + * styles on the worker before the rest of the nodeOps catch up. + */ + function _flushBothInternal() { + const buffer = _childOperations.buffer; + + worker.postMessage( + { + method: 'flushBoth', + args: [buffer, _stylesToSend, !_needsRender], + }, + [buffer], + ); + + _childOperations.reset(); + _stylesToSend = {}; + _numStylesToSend = 0; + _needsRender = false; + } + + const queueSendNodeOperations = debounceMicrotask(flushChildOperations); + + // Coalesce N synchronous `queueRender` calls into a single postMessage. + // During React unmount cascades the prior fire-immediate path produced + // ~2 postMessages per destroyed node (a flush + a queueRender). With + // these state vars + the debounced drain below, all `queueRender`s in a + // sync block collapse into one message at end-of-microtask. + let _wantsRender = false; + let _renderElementId = 0; + let _renderForce = false; + + function flushRender() { + if (!_wantsRender) { + return; + } + + // Capture before flushSendStyles resets `_needsRender`. When pending + // styles are flushed with skipRender=false (i.e. `_needsRender` was + // true), `applyStyles` on the worker auto-triggers `queueRender` + // already — so the explicit message below is redundant and we skip it. + const willAutoRender = _numStylesToSend > 0 && _needsRender; + + flushChildOperations(); + flushSendStyles(); + + if (!willAutoRender) { + worker.postMessage({ + method: 'queueRender', + args: [_renderElementId, _renderForce], + }); + } + + _wantsRender = false; + _renderElementId = 0; + _renderForce = false; + } + + const queueRenderDrain = debounceMicrotask(flushRender); function nodeOperation( - method: 'addNode' | 'removeNode' | 'addChildNode', + method: + | 'addNode' + | 'removeNode' + | 'addChildNode' + | 'detachChildNode' + | 'addIndependentRoot' + | 'removeIndependentRoot', elementOrParentId: number, childId?: number, index?: number, @@ -148,6 +289,14 @@ function wrapWorker(worker: Worker): Workerized { _childOperations.writeUint8(NodeOperations.RemoveNode); _childOperations.writeUint32(elementOrParentId); break; + case 'addIndependentRoot': + _childOperations.writeUint8(NodeOperations.AddIndependentRoot); + _childOperations.writeUint32(elementOrParentId); + break; + case 'removeIndependentRoot': + _childOperations.writeUint8(NodeOperations.RemoveIndependentRoot); + _childOperations.writeUint32(elementOrParentId); + break; case 'addChildNode': if (childId === undefined) { throw new Error('Child ID must be provided for addChildNode operation'); @@ -163,6 +312,15 @@ function wrapWorker(worker: Worker): Workerized { _childOperations.writeUint32(index); } break; + case 'detachChildNode': + if (childId === undefined) { + throw new Error('Child ID must be provided for detachChildNode operation'); + } + + _childOperations.writeUint8(NodeOperations.DetachChildNode); + _childOperations.writeUint32(elementOrParentId); + _childOperations.writeUint32(childId); + break; default: throw new Error(`Unknown node operation: ${method}`); } @@ -170,83 +328,6 @@ function wrapWorker(worker: Worker): Workerized { queueSendNodeOperations(); } - function flushSizeRequests() { - if (_sizeRequestPromise) { - return _sizeRequestPromise; - } - - const buffer = _sizeRequests.buffer; - - if (buffer.byteLength === 0) { - return; - } - - _sizeRequestPromise = new Promise((resolve, reject) => { - const id = getId(); - - _callees[id] = [ - (buffer: ArrayBuffer) => { - const dataView = new SimpleDataView(buffer); - - while (dataView.hasSpace(4)) { - const callbackId = dataView.readUint32(); - const size = dataView.readUint32(); - - const callee = _callees[callbackId]; - - if (!callee) { - console.error(`No handler found for size request id: ${callbackId}`); - continue; - } - - const [resolveCall] = callee; - - delete _callees[callbackId]; - resolveCall(size === 0 ? null : size); - } - - _sizeRequestPromise = null; - resolve(); - }, - () => { - _sizeRequestPromise = null; - reject(); - }, - ]; - - worker.postMessage( - { - id, - method: 'getClampedSize', - args: [buffer], - }, - [buffer], - ); - - _sizeRequests.reset(); - }); - } - - const queueSendSizeRequests = delay(flushSizeRequests, DELAY_DURATION); - - function getClampedSize(elementId: number) { - const callbackId = getId(); - - _sizeRequests.writeUint32(callbackId); - _sizeRequests.writeUint32(elementId); - - queueSendSizeRequests(); - - return new Promise((resolve, reject) => { - _callees[callbackId] = [ - (size: number) => { - resolve(size === -1 ? null : size); - }, - reject, - ]; - }); - } - worker.onmessage = (event: MessageEvent<{ id: string; result?: unknown; error?: string }>) => { const { id, result, error } = event.data; @@ -274,43 +355,51 @@ function wrapWorker(worker: Worker): Workerized { } }; - // @ts-expect-error - const proxy: Workerized = new Proxy(() => {}, { - get(_, prop) { - if (typeof prop !== 'string') { - return undefined; - } + // Awaitable: flush pending ops/styles for causal ordering, then post + // the call and register a callee so the caller can `await` the worker's + // response. Used only by `init` — every other call from + // LightningManager is fire-and-forget and rides the buffered nodeOps + // pipeline (see `nodeOperation`) or the debounced render drain. + function _awaitable(method: string, args: unknown[]): Promise { + return new Promise((resolve, reject) => { + const id = getId(); - // Event handlers like 'on' are not wrapped - if (prop === 'on') { - return _eventEmitter.on.bind(_eventEmitter); - } else if (prop === 'off') { - return _eventEmitter.off.bind(_eventEmitter); - } else if (prop === 'applyStyle') { - // Special case for applyStyle - return applyStyle; - } else if (prop === 'addNode' || prop === 'removeNode' || prop === 'addChildNode') { - return (...args: ParametersExceptFirst) => - nodeOperation(prop, ...args); - } else if (prop === 'getClampedSize') { - return getClampedSize; - } else if (prop === 'then' || prop === 'catch' || prop === 'finally') { - // Ignore Promise methods - return undefined; - } + _callees[id] = [resolve, reject]; - return (...args: unknown[]) => - new Promise((resolve, reject) => { - const id = getId(); + flushChildOperations(); + flushSendStyles(); - _callees[id] = [resolve, reject]; + worker.postMessage({ id, method, args }); + }); + } - worker.postMessage({ id, method: prop, args }); - }); + // Plain object with pre-bound methods instead of a `Proxy`. The Proxy + // version did `Proxy.get` + a string-compare chain + a fresh + // `(...args) => nodeOperation(prop, ...args)` closure allocation per + // node-op call — measurable self-time during VL recycle bursts. + const proxy = { + on: _eventEmitter.on.bind(_eventEmitter), + off: _eventEmitter.off.bind(_eventEmitter), + applyStyle, + addNode: (elementId: number) => nodeOperation('addNode', elementId), + removeNode: (elementId: number) => nodeOperation('removeNode', elementId), + addChildNode: (parentId: number, childId: number, index?: number) => + nodeOperation('addChildNode', parentId, childId, index), + detachChildNode: (parentId: number, childId: number) => + nodeOperation('detachChildNode', parentId, childId), + queueRender: (elementId: number, force?: boolean) => { + _wantsRender = true; + _renderElementId = elementId; + _renderForce = !!force; + queueRenderDrain(); }, - }); + addIndependentRoot: (elementId: number) => nodeOperation('addIndependentRoot', elementId), + removeIndependentRoot: (elementId: number) => + nodeOperation('removeIndependentRoot', elementId), + init: (yogaOptions?: unknown) => _awaitable('init', [yogaOptions]), + }; - return proxy; + return proxy as unknown as Workerized; } let count = 0; diff --git a/packages/plugin-flexbox/src/index.ts b/packages/plugin-flexbox/src/index.ts index 0823682..4bfa000 100644 --- a/packages/plugin-flexbox/src/index.ts +++ b/packages/plugin-flexbox/src/index.ts @@ -1,12 +1,15 @@ import type { LightningElement, LightningElementStyle, Plugin } from '@plextv/react-lightning'; import { LightningManager } from './LightningManager'; +import { setFlexboxManager } from './manager'; import type { YogaOptions } from './types/YogaOptions'; import { flexProps, isFlexStyleProp } from './util/isFlexStyleProp'; export function plugin(yogaOptions?: YogaOptions): Plugin { const lightningManager = new LightningManager(); + setFlexboxManager(lightningManager); + return { handledStyleProps: new Set(Object.keys(flexProps)), @@ -25,11 +28,34 @@ export function plugin(yogaOptions?: YogaOptions): Plugin { return props; } + // Fast scan: detect whether ANY flex prop is in the style object + // before allocating the split objects. The vast majority of elements + // in a typical UI tree (text nodes, image leaves, decorative views) + // carry no flex-relevant props, and this fast path returns the + // original props untouched — saving three allocations per call + // (`flexStyles`, `remainingStyles`, and the `{ ...props }` spread). + let hasFlexStyles = false; + + for (const key in styles) { + if ( + key === 'w' || + key === 'h' || + key === 'maxWidth' || + key === 'maxHeight' || + isFlexStyleProp(key) + ) { + hasFlexStyles = true; + break; + } + } + + if (!hasFlexStyles) { + return props; + } + const flexStyles: Record = {}; const remainingStyles: Record = {}; - let hasFlexStyles = false; - // Direct property iteration is faster than Object.entries + reduce for (const key in styles) { const value = styles[key as keyof LightningElementStyle]; @@ -37,22 +63,14 @@ export function plugin(yogaOptions?: YogaOptions): Plugin { // Width and height go to both flex and remaining styles flexStyles[key] = value; remainingStyles[key] = value; - hasFlexStyles = true; } else if (isFlexStyleProp(key) && value != null) { flexStyles[key] = value; - hasFlexStyles = true; } else { remainingStyles[key] = value; } } - if (hasFlexStyles) { - lightningManager.applyStyle( - instance.id, - flexStyles as Partial, - true, - ); - } + lightningManager.applyStyle(instance.id, flexStyles as Partial, true); return { ...props, @@ -62,4 +80,6 @@ export function plugin(yogaOptions?: YogaOptions): Plugin { }; } +export { FlexBoundary, FlexRoot, useIsInFlex } from './wrappers'; +export type { FlexBoundaryProps, FlexRootProps } from './wrappers'; export * from './types'; diff --git a/packages/plugin-flexbox/src/manager.ts b/packages/plugin-flexbox/src/manager.ts new file mode 100644 index 0000000..3250af1 --- /dev/null +++ b/packages/plugin-flexbox/src/manager.ts @@ -0,0 +1,11 @@ +import type { LightningManager } from './LightningManager'; + +let _instance: LightningManager | undefined; + +export function setFlexboxManager(manager: LightningManager): void { + _instance = manager; +} + +export function getFlexboxManager(): LightningManager | undefined { + return _instance; +} diff --git a/packages/plugin-flexbox/src/types/NodeOperations.ts b/packages/plugin-flexbox/src/types/NodeOperations.ts index 2efe6bb..22f5dca 100644 --- a/packages/plugin-flexbox/src/types/NodeOperations.ts +++ b/packages/plugin-flexbox/src/types/NodeOperations.ts @@ -3,4 +3,7 @@ export enum NodeOperations { RemoveNode = 2, AddChildNode = 3, AddChildNodeAtIndex = 4, + DetachChildNode = 5, + AddIndependentRoot = 6, + RemoveIndependentRoot = 7, } diff --git a/packages/plugin-flexbox/src/util/SimpleDataView.ts b/packages/plugin-flexbox/src/util/SimpleDataView.ts index 4ce1e82..7651418 100644 --- a/packages/plugin-flexbox/src/util/SimpleDataView.ts +++ b/packages/plugin-flexbox/src/util/SimpleDataView.ts @@ -48,9 +48,20 @@ export class SimpleDataView { return this._offset; } + /** + * Zero the write offset so subsequent writes start from the beginning of + * the underlying buffer. The buffer itself is reused — the `buffer` + * getter returns a `slice()` (an independent copy), so a previous flush + * that postMessaged the slice does not affect this buffer's writability. + * + * Pre-fix: this method allocated a fresh `ArrayBuffer` of `_maxSize` + * (10KB on the worker side, 1MB default elsewhere) every time. Combined + * with the per-flush `slice()` allocation in the getter, that produced + * two ArrayBuffers per flush — significant GC pressure during a busy + * UI commit. The underlying buffer never needs reallocation because + * we never transfer it; only the slice is transferred. + */ public reset(): void { - this._buffer = new ArrayBuffer(this._maxSize); - this._view = new DataView(this._buffer); this._offset = 0; } @@ -60,6 +71,16 @@ export class SimpleDataView { return newOffset <= this._maxSize && newOffset >= 0; } + /** + * Advance the write offset by `n` bytes without bounds checks. Pairs with + * direct `dataView` writes after a prior `hasSpace(n)` validated the run. + * Lets a hot writer skip the per-call `_checkOverflow` + switch dispatch + * inside `writeXxx` when batching a known, fixed-size record. + */ + public advance(n: number): void { + this._offset += n; + } + // Shifts the offset back by the specified size. public shift(size: number): void { if (this._offset < size) { diff --git a/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts b/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts index 62e791e..c0f4b4f 100644 --- a/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts +++ b/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts @@ -161,17 +161,24 @@ export default function applyReactPropsToYoga( managerNode: ManagerNode, style: Partial, ): void { - for (const [prop, value] of Object.entries(style)) { - if (isFlexStyleProp(prop) && managerNode.props[prop] !== value) { - managerNode.props[prop] = value; + // `for...in` instead of `Object.entries(style)` to avoid the per-call + // array allocation. This function runs on every applyStyle dispatch, + // which during a UI update can be hundreds of times per frame. + for (const prop in style) { + if (isFlexStyleProp(prop)) { + const value = style[prop]; - applyFlexPropToYoga( - yoga, - config, - managerNode.node, - prop, - value as LightningViewElementStyle[typeof prop], - ); + if (managerNode.props[prop] !== value) { + managerNode.props[prop] = value; + + applyFlexPropToYoga( + yoga, + config, + managerNode.node, + prop, + value as LightningViewElementStyle[typeof prop], + ); + } } } } diff --git a/packages/plugin-flexbox/src/util/isFlexStyleProp.ts b/packages/plugin-flexbox/src/util/isFlexStyleProp.ts index 8d11d61..7bb83df 100644 --- a/packages/plugin-flexbox/src/util/isFlexStyleProp.ts +++ b/packages/plugin-flexbox/src/util/isFlexStyleProp.ts @@ -59,6 +59,11 @@ flexProps satisfies Partial>; export type FlexProps = keyof typeof flexProps; +// `Set.has` is faster than the `in` operator (which walks the prototype +// chain — `'toString' in flexProps` returns true, etc.) and runs on every +// style key during transformProps and the worker proxy's applyStyle. +const _flexPropsSet: Set = new Set(Object.keys(flexProps)); + export function isFlexStyleProp(prop: number | string | symbol): prop is FlexProps { - return prop in flexProps; + return typeof prop === 'string' && _flexPropsSet.has(prop); } diff --git a/packages/plugin-flexbox/src/worker.ts b/packages/plugin-flexbox/src/worker.ts index f4bd278..078d15c 100644 --- a/packages/plugin-flexbox/src/worker.ts +++ b/packages/plugin-flexbox/src/worker.ts @@ -1,5 +1,6 @@ +import type { LightningElementStyle } from '@plextv/react-lightning'; + import { NodeOperations } from './types/NodeOperations'; -import { SimpleDataView } from './util/SimpleDataView'; import { YogaManager } from './YogaManager'; const manager = new YogaManager(); @@ -9,59 +10,75 @@ manager.on('render', (buffer) => { }); function applyNodeOperations(buffer: ArrayBuffer) { - const dataView = new SimpleDataView(buffer); - - while (dataView.hasSpace(1)) { - const method = dataView.readUint8(); + // Raw `DataView` instead of `SimpleDataView` — pure read loop, called + // per `'flushBoth'`/`'nodeOperations'` message. Manual offset + // arithmetic skips `SimpleDataView`'s wrapper object and the + // `_readInt` switch-statement indirection. + const view = new DataView(buffer); + const length = buffer.byteLength; + let offset = 0; + + while (offset < length) { + const method = view.getUint8(offset); + offset += 1; switch (method) { case NodeOperations.AddNode: { - const elementId = dataView.readUint32(); + const elementId = view.getUint32(offset, true); + offset += 4; manager.addNode(elementId); break; } case NodeOperations.RemoveNode: { - const elementId = dataView.readUint32(); + const elementId = view.getUint32(offset, true); + offset += 4; manager.removeNode(elementId); break; } case NodeOperations.AddChildNode: case NodeOperations.AddChildNodeAtIndex: { - const parentId = dataView.readUint32(); - const childId = dataView.readUint32(); - const index = - method === NodeOperations.AddChildNodeAtIndex ? dataView.readUint32() : undefined; + const parentId = view.getUint32(offset, true); + const childId = view.getUint32(offset + 4, true); + offset += 8; + + let index: number | undefined; + + if (method === NodeOperations.AddChildNodeAtIndex) { + index = view.getUint32(offset, true); + offset += 4; + } manager.addChildNode(parentId, childId, index); break; } - } - } -} - -function getClampedSize(id: number, buffer: ArrayBuffer) { - // Buffer contains callback Id and element Id - const dataView = new SimpleDataView(buffer); - - while (dataView.hasSpace(4)) { - // Skip the callback id, we'll just overwrite the element id with the result - dataView.moveBy(4); - - const elementId = dataView.readUint32(); - const result = manager.getClampedSize(elementId); + case NodeOperations.DetachChildNode: { + const parentId = view.getUint32(offset, true); + const childId = view.getUint32(offset + 4, true); + offset += 8; - dataView.moveBy(-4); - dataView.writeUint32(result ?? 0); + manager.detachChildNode(parentId, childId); + break; + } + case NodeOperations.AddIndependentRoot: { + const elementId = view.getUint32(offset, true); + offset += 4; + manager.addIndependentRoot(elementId); + break; + } + case NodeOperations.RemoveIndependentRoot: { + const elementId = view.getUint32(offset, true); + offset += 4; + manager.removeIndependentRoot(elementId); + break; + } + } } - - // Send the buffer back - self.postMessage({ id, result: buffer }, [buffer]); } self.onmessage = async ( event: MessageEvent<{ id: number; - method: keyof typeof manager | 'nodeOperations'; + method: keyof typeof manager | 'nodeOperations' | 'flushBoth'; args?: unknown[]; }>, ) => { @@ -74,9 +91,20 @@ self.onmessage = async ( case 'nodeOperations': applyNodeOperations(args?.[0] as ArrayBuffer); break; - case 'getClampedSize': - getClampedSize(id, args?.[0] as ArrayBuffer); + case 'flushBoth': { + // Combined message: apply node-tree mutations first, then styles. + // Mirrors the causal ordering the previous two-message setup + // enforced by having `flushSendStyles` call `flushChildOperations` + // before posting `applyStyles`. Sent only when BOTH sets are + // pending at flush time — single-purpose paths still use the + // `'nodeOperations'` and `'applyStyles'` (default) messages. + applyNodeOperations(args?.[0] as ArrayBuffer); + manager.applyStyles( + args?.[1] as Record>, + args?.[2] as boolean, + ); break; + } default: { try { if (typeof manager[method] === 'function') { diff --git a/packages/plugin-flexbox/src/wrappers.tsx b/packages/plugin-flexbox/src/wrappers.tsx new file mode 100644 index 0000000..1f6dcee --- /dev/null +++ b/packages/plugin-flexbox/src/wrappers.tsx @@ -0,0 +1,100 @@ +import { + createContext, + forwardRef, + type ForwardRefExoticComponent, + type ReactElement, + type ReactNode, + type RefAttributes, + useContext, + useImperativeHandle, + useLayoutEffect, + useRef, +} from 'react'; + +import type { LightningElement, LightningViewElementStyle } from '@plextv/react-lightning'; + +import { getFlexboxManager } from './manager'; + +const FlexContext = createContext(false); + +/** + * Returns true when the calling component is inside a {@link FlexRoot} + * subtree (and not below a nested {@link FlexBoundary}). Components like + * `VirtualList` use this to decide whether to expose flex layout to the + * content they render. + */ +export function useIsInFlex(): boolean { + return useContext(FlexContext); +} + +export interface FlexBoundaryProps { + children: ReactNode; + style?: LightningViewElementStyle | null; + onResize?: (event: { w: number; h: number }) => void; +} + +/** + * Disables flex layout for everything rendered below it. The wrapper itself + * still participates in any outer flex tree (so it can be sized), but its + * descendants are detached from yoga until a nested {@link FlexRoot} re-opts + * them back in. + */ +export function FlexBoundary({ children, style, onResize }: FlexBoundaryProps): ReactElement { + const ref = useRef(null); + + useLayoutEffect(() => { + const manager = getFlexboxManager(); + const element = ref.current; + + if (!manager || !element) { + return; + } + + return manager.markBoundary(element); + }, []); + + return ( + + {children} + + ); +} + +export interface FlexRootProps { + children: ReactNode; + style?: LightningViewElementStyle | null; + onResize?: (event: { w: number; h: number }) => void; +} + +/** + * Opts a subtree into flex layout by becoming an independent yoga root. Flex + * is opt-in for this plugin — without an ancestor `FlexRoot` somewhere above, + * elements get no flex behavior. Wrap your app's root (or any subtree that + * should use flexbox) with this component. + */ +export const FlexRoot: ForwardRefExoticComponent< + FlexRootProps & RefAttributes +> = forwardRef(({ children, style, onResize }, forwardedRef) => { + const ref = useRef(null); + + useImperativeHandle(forwardedRef, () => ref.current as LightningElement, []); + + useLayoutEffect(() => { + const manager = getFlexboxManager(); + const element = ref.current; + + if (!manager || !element) { + return; + } + + return manager.markFlexRoot(element); + }, []); + + return ( + + {children} + + ); +}); + +FlexRoot.displayName = 'FlexRoot'; diff --git a/packages/plugin-reanimated/package.json b/packages/plugin-reanimated/package.json index 5ad6e22..ff186d6 100644 --- a/packages/plugin-reanimated/package.json +++ b/packages/plugin-reanimated/package.json @@ -51,9 +51,9 @@ "@plextv/react-lightning-plugin-css-transform": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", "@plextv/react-native-lightning": "workspace:^", - "react": "catalog:peers", - "react-native": "catalog:peers", - "react-native-reanimated": "catalog:peers" + "react": "catalog:", + "react-native": "catalog:", + "react-native-reanimated": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning-components/package.json b/packages/react-lightning-components/package.json index 4699915..b615900 100644 --- a/packages/react-lightning-components/package.json +++ b/packages/react-lightning-components/package.json @@ -72,7 +72,7 @@ "peerDependencies": { "@plextv/react-lightning": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", - "react": "catalog:peers" + "react": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts index 05910a3..b1a565f 100644 --- a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts @@ -11,9 +11,12 @@ describe('LayoutManager', () => { data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); - expect(lm.getLayout(0)).toEqual(expect.objectContaining({ offset: 0, size: 100 })); + expect(lm.getLayout(0)).toEqual( + expect.objectContaining({ offset: 0, size: 100, crossSize: 200 }), + ); expect(lm.getLayout(1)).toEqual(expect.objectContaining({ offset: 100, size: 100 })); expect(lm.getLayout(2)).toEqual(expect.objectContaining({ offset: 200, size: 100 })); expect(lm.totalSize).toBe(300); @@ -25,7 +28,7 @@ describe('LayoutManager', () => { data: makeData(3), estimatedItemSize: 100, numColumns: 1, - + cellCrossSize: 200, overrideItemLayout: (layout, _item, index) => { layout.size = sizes[index]; }, @@ -43,22 +46,69 @@ describe('LayoutManager', () => { data: makeData(2), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); expect(lm.getLayout(5)).toBeUndefined(); }); + + it('collapses null data entries to size 0', () => { + const data: Array<{ id: number } | null> = [{ id: 0 }, null, { id: 2 }]; + const lm = new LayoutManager({ + data, + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + expect(lm.getLayout(0)?.size).toBe(100); + expect(lm.getLayout(1)?.size).toBe(0); + expect(lm.getLayout(1)?.offset).toBe(100); + expect(lm.getLayout(2)?.offset).toBe(100); + expect(lm.totalSize).toBe(200); + }); + + it('collapses undefined data entries to size 0', () => { + const data: Array<{ id: number } | undefined> = [{ id: 0 }, undefined, { id: 2 }]; + const lm = new LayoutManager({ + data, + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + expect(lm.getLayout(1)?.size).toBe(0); + expect(lm.getLayout(2)?.offset).toBe(100); + }); + + it('honours override.size = 0 to collapse a row', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + overrideItemLayout: (layout, _item, index) => { + if (index === 1) { + layout.size = 0; + } + }, + }); + + expect(lm.getLayout(1)?.size).toBe(0); + expect(lm.getLayout(2)?.offset).toBe(100); + expect(lm.totalSize).toBe(200); + }); }); describe('multi column', () => { - it('positions items in a grid', () => { + it('positions items in a grid using cellCrossSize', () => { const lm = new LayoutManager({ data: makeData(5), estimatedItemSize: 100, numColumns: 2, + cellCrossSize: 100, }); - // columnWidth = estimatedItemSize = 100 - // Row 0: items 0, 1 expect(lm.getLayout(0)).toEqual( expect.objectContaining({ offset: 0, @@ -75,19 +125,18 @@ describe('LayoutManager', () => { crossSize: 100, }), ); - // Row 1: items 2, 3 expect(lm.getLayout(2)).toEqual(expect.objectContaining({ offset: 100, column: 0 })); expect(lm.getLayout(3)).toEqual(expect.objectContaining({ offset: 100, column: 1 })); - // Row 2: item 4 (partial row) expect(lm.getLayout(4)).toEqual(expect.objectContaining({ offset: 200, column: 0 })); expect(lm.totalSize).toBe(300); }); - it('handles span override', () => { + it('handles span override (crossSize scales with span)', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 3, + cellCrossSize: 100, overrideItemLayout: (layout, _item, index) => { if (index === 0) { layout.span = 2; @@ -95,10 +144,8 @@ describe('LayoutManager', () => { }, }); - // columnWidth = estimatedItemSize = 100, span 2 = 200 expect(lm.getLayout(0)?.crossSize).toBe(200); expect(lm.getLayout(0)?.column).toBe(0); - // Item 1 fills remaining column in the same row expect(lm.getLayout(1)?.column).toBe(2); expect(lm.getLayout(1)?.crossSize).toBe(100); }); @@ -108,12 +155,12 @@ describe('LayoutManager', () => { data: makeData(2), estimatedItemSize: 100, numColumns: 2, + cellCrossSize: 100, overrideItemLayout: (layout) => { - layout.span = 5; // more than numColumns + layout.span = 5; }, }); - // Span clamped to 2, crossSize = 2 * 100 = 200 expect(lm.getLayout(0)?.crossSize).toBe(200); }); }); @@ -124,14 +171,13 @@ describe('LayoutManager', () => { data: makeData(3), estimatedItemSize: 100, numColumns: 1, - + cellCrossSize: 200, separatorSize: 10, }); expect(lm.getLayout(0)).toEqual(expect.objectContaining({ offset: 0, size: 100 })); expect(lm.getLayout(1)).toEqual(expect.objectContaining({ offset: 110, size: 100 })); expect(lm.getLayout(2)).toEqual(expect.objectContaining({ offset: 220, size: 100 })); - // totalSize = 3*100 + 2*10 = 320 expect(lm.totalSize).toBe(320); }); @@ -140,20 +186,33 @@ describe('LayoutManager', () => { data: makeData(5), estimatedItemSize: 100, numColumns: 2, + cellCrossSize: 100, separatorSize: 20, }); - // Row 0: items 0, 1 at offset 0 expect(lm.getLayout(0)?.offset).toBe(0); expect(lm.getLayout(1)?.offset).toBe(0); - // Row 1: items 2, 3 at offset 100 (no separator between rows) expect(lm.getLayout(2)?.offset).toBe(100); expect(lm.getLayout(3)?.offset).toBe(100); - // Row 2: item 4 at offset 200 expect(lm.getLayout(4)?.offset).toBe(200); - // totalSize = 3*100 = 300 (separators handled cross-axis by cell) expect(lm.totalSize).toBe(300); }); + + it('does not add separator gap after a zero-size empty row', () => { + const data: Array<{ id: number } | null> = [{ id: 0 }, null, { id: 2 }]; + const lm = new LayoutManager({ + data, + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + separatorSize: 10, + }); + + expect(lm.getLayout(0)?.offset).toBe(0); + expect(lm.getLayout(1)?.offset).toBe(110); + expect(lm.getLayout(1)?.size).toBe(0); + expect(lm.getLayout(2)?.offset).toBe(110); + }); }); describe('visible range', () => { @@ -162,9 +221,9 @@ describe('LayoutManager', () => { data: makeData(20), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); - // scroll=500, viewport=300, draw=100 → range 400..900 const range = lm.getVisibleRange(500, 300, 100); expect(range.startIndex).toBe(4); expect(range.endIndex).toBe(8); @@ -175,6 +234,7 @@ describe('LayoutManager', () => { data: [], estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); expect(lm.getVisibleRange(0, 300, 100)).toEqual({ startIndex: 0, @@ -187,6 +247,7 @@ describe('LayoutManager', () => { data: makeData(5), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); const range = lm.getVisibleRange(0, 1000, 500); @@ -199,6 +260,7 @@ describe('LayoutManager', () => { data: makeData(10), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); const range = lm.getVisibleRange(800, 200, 0); @@ -207,163 +269,275 @@ describe('LayoutManager', () => { }); }); + describe('findIndexAtOffset', () => { + it('locates the index at a given main-axis offset', () => { + const lm = new LayoutManager({ + data: makeData(5), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + expect(lm.findIndexAtOffset(0)).toBe(0); + expect(lm.findIndexAtOffset(50)).toBe(0); + expect(lm.findIndexAtOffset(100)).toBe(1); + expect(lm.findIndexAtOffset(250)).toBe(2); + }); + + it('returns -1 for empty data', () => { + const lm = new LayoutManager({ + data: [], + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + expect(lm.findIndexAtOffset(0)).toBe(-1); + }); + }); + describe('reportItemSize', () => { - it('returns true and marks dirty when size changes', () => { + it('uses measured size in subsequent layouts', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); // force initial compute - const changed = lm.reportItemSize(0, 150); + expect(lm.getLayout(1)?.offset).toBe(100); + + const changed = lm.reportItemSize('0', 150); expect(changed).toBe(true); expect(lm.getLayout(1)?.offset).toBe(150); + expect(lm.totalSize).toBe(350); }); - it('returns false when size is unchanged', () => { + it('measurement wins over override', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + overrideItemLayout: (layout) => { + layout.size = 80; + }, + }); + + expect(lm.getLayout(0)?.size).toBe(80); + + lm.reportItemSize('0', 120); + expect(lm.getLayout(0)?.size).toBe(120); + expect(lm.getLayout(1)?.offset).toBe(120); + }); + + it('returns false for zero or negative sizes', () => { + const lm = new LayoutManager({ + data: makeData(2), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); - expect(lm.reportItemSize(0, 100)).toBe(false); + expect(lm.reportItemSize('0', 0)).toBe(false); + expect(lm.reportItemSize('0', -5)).toBe(false); + expect(lm.getLayout(0)?.size).toBe(100); }); - it('updates averageItemSize after reports', () => { + it('returns false when reported size matches stored value', () => { const lm = new LayoutManager({ - data: makeData(5), + data: makeData(2), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); - lm.reportItemSize(0, 200); - lm.reportItemSize(1, 200); - expect(lm.averageItemSize).toBe(200); + lm.reportItemSize('0', 150); + expect(lm.reportItemSize('0', 150)).toBe(false); + expect(lm.reportItemSize('0', 150.4)).toBe(false); + expect(lm.reportItemSize('0', 152)).toBe(true); }); - }); - describe('updateConfig', () => { - it('recomputes layouts after data change', () => { + it('measurements survive index shifts (keyed by userKey)', () => { + const data = makeData(3); const lm = new LayoutManager({ - data: makeData(3), + data, estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - expect(lm.totalSize).toBe(300); - lm.updateConfig({ data: makeData(5) }); - expect(lm.totalSize).toBe(500); + lm.reportItemSize('1', 150); + expect(lm.getLayout(1)?.size).toBe(150); + + // Insert a new item at the front — id=1 is now at index 2. + const newData = [{ id: 99 }, ...data]; + lm.updateConfig({ data: newData }); + + expect(lm.getLayout(0)?.size).toBe(100); + expect(lm.getLayout(1)?.size).toBe(100); + expect(lm.getLayout(2)?.size).toBe(150); }); - }); - describe('deferMeasurement / flushDeferred', () => { - it('buffers measurements and applies them on flush', () => { + it('falls back to estimate when no keyExtractor is provided', () => { const lm = new LayoutManager({ - data: makeData(5), + data: makeData(2), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + lm.reportItemSize('0', 150); + expect(lm.getLayout(0)?.size).toBe(100); + }); + + it('null/undefined data overrides measurement (collapses to 0)', () => { + const data: Array<{ id: number } | null> = [{ id: 0 }, null]; + const lm = new LayoutManager({ + data, estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); - lm.deferMeasurement(0, 150, 100); - lm.deferMeasurement(1, 200, 100); - expect(lm.hasPendingMeasurements).toBe(true); + lm.reportItemSize('1', 150); + expect(lm.getLayout(1)?.size).toBe(0); + }); - const result = lm.flushDeferred(); - expect(result.changed).toBe(true); - expect(lm.hasPendingMeasurements).toBe(false); + it('first measurement becomes the implicit estimate for later unmeasured items', () => { + const lm = new LayoutManager({ + data: makeData(4), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + }); + + // Before any measurement: items use the caller's estimatedItemSize. + expect(lm.getLayout(2)?.size).toBe(100); + + // First measurement comes in. Items 1,2,3 are still unmeasured but + // should now use 150 (the first-measured size) as the fallback. + lm.reportItemSize('0', 150); expect(lm.getLayout(0)?.size).toBe(150); - expect(lm.getLayout(1)?.size).toBe(200); + expect(lm.getLayout(1)?.size).toBe(150); + expect(lm.getLayout(2)?.size).toBe(150); }); - it('returns anchorDelta when anchor shifts', () => { + it('later measurements do NOT update the implicit estimate', () => { const lm = new LayoutManager({ data: makeData(5), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); - // Item 0 grows from 100 → 200. The average also shifts to 200, - // so unmeasured item 1 is re-estimated at 200. Total delta for item 2: +200. - lm.deferMeasurement(0, 200, 100); - const result = lm.flushDeferred(2); - expect(result.anchorDelta).toBe(200); + lm.reportItemSize('0', 100); + lm.reportItemSize('1', 200); + + // Item 0 measured at 100, item 1 measured at 200, but the implicit + // estimate stays locked at 100 (the first measurement). Items 2-4 + // use 100 until they're measured individually. + expect(lm.getLayout(0)?.size).toBe(100); + expect(lm.getLayout(1)?.size).toBe(200); + expect(lm.getLayout(2)?.size).toBe(100); + expect(lm.getLayout(3)?.size).toBe(100); }); - it('returns zero delta when no anchor provided', () => { + it('reportItemEmpty collapses the row to size 0', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), }); - lm.getLayout(0); - lm.deferMeasurement(0, 200, 100); - const result = lm.flushDeferred(); - expect(result.anchorDelta).toBe(0); + expect(lm.getLayout(1)?.size).toBe(100); + + expect(lm.reportItemEmpty('1')).toBe(true); + expect(lm.getLayout(1)?.size).toBe(0); + expect(lm.getLayout(2)?.offset).toBe(100); + expect(lm.totalSize).toBe(200); }); - it('returns unchanged when no measurements are pending', () => { + it('reportItemEmpty is idempotent', () => { + const lm = new LayoutManager({ + data: makeData(2), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + }); + + expect(lm.reportItemEmpty('0')).toBe(true); + expect(lm.reportItemEmpty('0')).toBe(false); + }); + + it('per-item override.size wins over the implicit estimate', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + overrideItemLayout: (layout, _item, index) => { + if (index === 2) { + layout.size = 75; + } + }, }); - lm.getLayout(0); - expect(lm.hasPendingMeasurements).toBe(false); - const result = lm.flushDeferred(0); - expect(result.changed).toBe(false); - expect(result.anchorDelta).toBe(0); + lm.reportItemSize('0', 200); + + expect(lm.getLayout(0)?.size).toBe(200); // measured + expect(lm.getLayout(1)?.size).toBe(200); // first-measured fallback + expect(lm.getLayout(2)?.size).toBe(75); // override.size wins }); + }); - it('returns unchanged when deferred sizes match current', () => { + describe('updateConfig', () => { + it('recomputes layouts after data change', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); - lm.getLayout(0); + expect(lm.totalSize).toBe(300); - lm.deferMeasurement(0, 100, 100); - const result = lm.flushDeferred(1); - expect(result.changed).toBe(false); - expect(result.anchorDelta).toBe(0); + expect(lm.updateConfig({ data: makeData(5) })).toBe(true); + expect(lm.totalSize).toBe(500); }); - it('clear also clears pending measurements', () => { + it('recomputes when cellCrossSize changes', () => { const lm = new LayoutManager({ data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); - lm.deferMeasurement(0, 200, 100); - lm.clear(); - expect(lm.hasPendingMeasurements).toBe(false); + expect(lm.updateConfig({ cellCrossSize: 300 })).toBe(true); + expect(lm.getLayout(0)?.crossSize).toBe(300); }); - }); - describe('clear', () => { - it('resets all state', () => { + it('returns false when nothing changed', () => { const lm = new LayoutManager({ - data: makeData(5), + data: makeData(3), estimatedItemSize: 100, numColumns: 1, + cellCrossSize: 200, }); - lm.getLayout(0); - lm.reportItemSize(0, 200); - lm.clear(); - // Data still exists so totalSize recomputes from estimated sizes - expect(lm.totalSize).toBe(500); - // Average was cleared, so falls back to estimatedItemSize - expect(lm.averageItemSize).toBe(100); + expect(lm.updateConfig({ estimatedItemSize: 100, numColumns: 1 })).toBe(false); }); }); }); diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts index 120b7ae..5e6e34d 100644 --- a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts @@ -1,11 +1,15 @@ -import { AverageWindow } from './AverageWindow'; import type { OverrideItemLayoutFn } from './VirtualListTypes'; export interface ComputedLayout { + /** Main-axis offset of this item from the start of the item area. */ offset: number; + /** Main-axis size of this item. */ size: number; + /** Column index in a multi-column grid (0 for single-column). */ column: number; + /** Cross-axis offset within the row (0 for single-column). */ crossOffset: number; + /** Cross-axis size of this item. */ crossSize: number; } @@ -16,26 +20,97 @@ export interface LayoutManagerConfig { overrideItemLayout?: OverrideItemLayoutFn; extraData?: unknown; separatorSize?: number; + /** + * Cross-axis size of a single column, computed by VirtualList from its + * viewport. The LayoutManager treats this as ground truth — no aggregation + * from cell-reported sizes. When 0/unset, layouts still resolve their + * main-axis offsets correctly so visibility math works during the first + * render. + */ + cellCrossSize: number; + /** + * Stable identity function for items, mirroring `VirtualList.keyExtractor`. + * Used to key measurements so they survive recycling and data shifts. + * When not provided, the index is used as the key — measurements still + * work but don't survive inserts/removes that shift indices. + */ + keyExtractor?: (item: T, index: number) => string; } +/** + * Computes per-item offsets in O(n). Sizes come from (in priority order) + * a measurement reported via `reportItemSize`, then `overrideItemLayout`, + * then `estimatedItemSize`. Cross-axis size is unilateral: every item's + * `crossSize` equals the configured `cellCrossSize` (× span for grids). + * Cross-axis is never measured or aggregated — that's the load-bearing + * rule that keeps the layout free of feedback loops. + * + * Measurements are stored by `userKey` (from `keyExtractor`) so they + * survive recycling and data inserts/removes that shift indices. + */ export class LayoutManager { - // Reusable object passed to overrideItemLayout to avoid per-item allocations in hot loops. private static _overrideScratch: { size?: number; span?: number } = {}; private _layouts: ComputedLayout[] = []; private _layoutCount = 0; - private _measuredSizes = new Map(); - private _measuredCrossSizes = new Map(); - private _averageWindow = new AverageWindow(20); private _totalSize = 0; - private _maxCrossSize = 0; private _dirty = true; - private _pending: Array<{ index: number; size: number; crossSize: number }> = []; private _data: ReadonlyArray; private _estimatedItemSize: number; private _numColumns: number; private _overrideItemLayout?: OverrideItemLayoutFn; private _extraData?: unknown; - private _separatorSize = 0; + private _separatorSize: number; + private _cellCrossSize: number; + private _keyExtractor?: (item: T, index: number) => string; + private _measuredSizes: Map = new Map(); + /** + * While `_batching` is on (VL flips it during a scroll/focus-snap + * animation), reports accumulate per-`userKey` here instead of running + * through dampening — the animation is the consumer's clear signal that + * intermediate yoga measurements aren't worth committing. + * `setBatching(false)` drains this directly into `_measuredSizes` at + * animation end. + */ + private _batching = false; + private _batchedSizes: Map = new Map(); + /** + * Per-`userKey` stability window. When a cell's reported size differs + * from the currently-stored one, the new value sits here until either + * (a) the same value is re-reported after `_STABILITY_MS` elapses, or + * (b) the backstop timer fires `_STABILITY_MS` after `firstSeenAt`. A + * different incoming value cancels the timer and replaces the entry. + * + * This dampens the multi-frame cascade where a user's section component + * re-measures during focus/scroll animations or async content settling + * — without dampening, every intermediate measurement reflows the + * layout for every following item. + */ + private _pendingSizes: Map = new Map(); + /** + * Backstop timers per `userKey`. Required because a cell may push + * exactly once for a given size and then go quiet (props stable, no + * further re-renders) — without the timer, the pending value would + * sit forever and the layout would paint at the old size. + */ + private _pendingTimers: Map> = new Map(); + private _onChange?: () => void; + private static readonly _STABILITY_MS = 120; + /** + * The very first non-zero size reported via `reportItemSize` for this + * list. Used as the implicit fallback for unmeasured items in place of + * the caller-provided `estimatedItemSize` once at least one cell has + * been seen — empirically that's a much better predictor than a generic + * estimate, and it cuts the visible reflow when subsequent cells turn + * out to be roughly the same size. + * + * Locked on first measurement; never updates. If we tracked the most + * recent measurement instead, every measured cell would shift the + * implicit estimate and cascade-rerender every later unmeasured item — + * which is the opposite of "less jank". The first measurement is + * usually representative and the remaining error gets corrected only + * when each individual cell measures. + */ + private _firstMeasuredSize = 0; constructor(config: LayoutManagerConfig) { this._data = config.data; @@ -44,6 +119,8 @@ export class LayoutManager { this._overrideItemLayout = config.overrideItemLayout; this._extraData = config.extraData; this._separatorSize = config.separatorSize ?? 0; + this._cellCrossSize = config.cellCrossSize; + this._keyExtractor = config.keyExtractor; } get totalSize(): number { @@ -54,12 +131,102 @@ export class LayoutManager { return this._totalSize; } - get maxCrossSize(): number { - return this._maxCrossSize; + /** + * Register a "layout dirtied" callback. Fires when a pending + * measurement matures via the stability backstop timer — there's no + * incoming report at that moment to bump layoutVersion synchronously, + * so LM has to wake the caller itself. + */ + setOnChange(cb: () => void): void { + this._onChange = cb; + } + + /** + * Returns a copy of the current per-`userKey` measurement map. Used + * by VL to snapshot measurements into the parent state cache so a + * recycled cell's inner VL can restore them on remount instead of + * having to re-measure from estimate. + */ + getMeasurements(): Map { + return new Map(this._measuredSizes); + } + + /** + * Replaces the current measurement map with the given snapshot. Marks + * layout dirty. Called from VL when restoring inner VL state from the + * parent state cache — no `_onChange` notification because the caller + * is responsible for the surrounding render flow. + * + * Also clears any in-flight dampening / batching state — pending + * entries from the previous content are no longer relevant under + * the restored measurement set. + */ + setMeasurements(measurements: Map): void { + this._measuredSizes = new Map(measurements); + this._batchedSizes.clear(); + this._pendingSizes.clear(); + + for (const timer of this._pendingTimers.values()) { + clearTimeout(timer); + } + + this._pendingTimers.clear(); + this._dirty = true; } - get averageItemSize(): number { - return this._averageWindow.currentValue || this._estimatedItemSize; + /** + * Toggle batching mode. While active, reports accumulate per `userKey` + * (latest wins) and the dampening path is skipped — animations are the + * caller's clear signal that intermediate measurements aren't worth + * committing. Returns `true` if disabling caused at least one stored + * size to change. + */ + setBatching(active: boolean): boolean { + if (this._batching === active) { + return false; + } + + this._batching = active; + + if (active) { + // Switching INTO batching: cancel any in-flight dampening timers. + // The animation will overwrite all relevant values via the batch, + // so old pending entries are irrelevant. + for (const timer of this._pendingTimers.values()) { + clearTimeout(timer); + } + + this._pendingTimers.clear(); + this._pendingSizes.clear(); + + return false; + } + + let changed = false; + + for (const [userKey, size] of this._batchedSizes) { + const existing = this._measuredSizes.get(userKey); + + if (existing != null && Math.abs(existing - size) < 1) { + continue; + } + + this._measuredSizes.set(userKey, size); + + if (this._firstMeasuredSize === 0 && size > 0) { + this._firstMeasuredSize = size; + } + + changed = true; + } + + this._batchedSizes.clear(); + + if (changed) { + this._dirty = true; + } + + return changed; } updateConfig(config: Partial>): boolean { @@ -105,6 +272,16 @@ export class LayoutManager { changed = true; } + if (config.cellCrossSize !== undefined && config.cellCrossSize !== this._cellCrossSize) { + this._cellCrossSize = config.cellCrossSize; + changed = true; + } + + if (config.keyExtractor !== undefined && config.keyExtractor !== this._keyExtractor) { + this._keyExtractor = config.keyExtractor; + changed = true; + } + if (changed) { this._dirty = true; } @@ -112,50 +289,198 @@ export class LayoutManager { return changed; } - getLayout(index: number): ComputedLayout | undefined { - if (this._dirty) { - this._recompute(); + /** + * Records the rendered main-axis size for an item, keyed by its stable + * `userKey`. Subsequent layouts use this size instead of + * `overrideItemLayout` / `estimatedItemSize` for that key. + * + * Returns `true` when the stored size changed synchronously (caller + * should bump layoutVersion). Returns `false` when the report was + * batched, dampened, or rejected. + * + * Rejects: + * - `size <= 0` — transient zero during recycle. Use `reportItemEmpty` + * for genuinely-empty rows. + * - non-finite values. + */ + reportItemSize(userKey: string, size: number): boolean { + if (!Number.isFinite(size) || size <= 0) { + return false; } - if (index < 0 || index >= this._layoutCount) { - return undefined; - } + if (this._batching) { + this._batchedSizes.set(userKey, size); - return this._layouts[index]; - } + return false; + } - reportItemSize(index: number, size: number, crossSize?: number): boolean { - const layout = this._layouts[index]; + const existing = this._measuredSizes.get(userKey); - if (!layout || index < 0 || index >= this._layoutCount) { + if (existing != null && Math.abs(existing - size) < 1) { + this._clearPending(userKey); return false; } - let changed = false; + // First measurement — apply immediately. There's no existing value + // to thrash against, and waiting would just delay layout settling. + if (existing == null) { + this._measuredSizes.set(userKey, size); - if (Math.abs(layout.size - size) >= 1) { - this._averageWindow.addValue(size); - this._measuredSizes.set(index, size); - layout.size = size; - changed = true; + if (this._firstMeasuredSize === 0) { + this._firstMeasuredSize = size; + } + + this._clearPending(userKey); + this._dirty = true; + + return true; } - if (crossSize != null && Math.abs(layout.crossSize - crossSize) >= 1) { - layout.crossSize = crossSize; - this._measuredCrossSizes.set(index, crossSize); + // Differs from existing — require stability before accepting it. + const now = Date.now(); + const pending = this._pendingSizes.get(userKey); - if (crossSize > this._maxCrossSize) { - this._maxCrossSize = crossSize; + if (pending != null && Math.abs(pending.size - size) < 1) { + // Same as already-pending. Don't reset the timer. If the window + // has already elapsed, commit synchronously. + if (now - pending.firstSeenAt >= LayoutManager._STABILITY_MS) { + this._measuredSizes.set(userKey, size); + this._clearPending(userKey); + this._dirty = true; + + return true; } - changed = true; + return false; } - if (changed) { + // New candidate. Replace prior pending (cancels its timer) and + // schedule a backstop that commits after the stability window even + // if no further reports arrive. + this._clearPending(userKey); + this._pendingSizes.set(userKey, { size, firstSeenAt: now }); + + const timer = setTimeout(() => { + const stillPending = this._pendingSizes.get(userKey); + + if (stillPending == null || Math.abs(stillPending.size - size) >= 1) { + return; + } + + this._measuredSizes.set(userKey, size); + this._pendingSizes.delete(userKey); + this._pendingTimers.delete(userKey); this._dirty = true; + this._onChange?.(); + }, LayoutManager._STABILITY_MS); + + this._pendingTimers.set(userKey, timer); + + return false; + } + + private _clearPending(userKey: string): void { + const timer = this._pendingTimers.get(userKey); + + if (timer != null) { + clearTimeout(timer); + this._pendingTimers.delete(userKey); } - return changed; + this._pendingSizes.delete(userKey); + } + + /** + * Drops every per-key measurement and resets the first-measured size. + * Not called automatically — VirtualList preserves measurements across + * data identity changes so recycled items use their cached size on + * first paint. Callers can invoke this imperatively when they truly + * want to invalidate (e.g. orientation change, theme swap that + * materially affects content sizing). + */ + clearMeasurements(): void { + if (this._measuredSizes.size === 0 && this._firstMeasuredSize === 0) { + return; + } + + this._measuredSizes.clear(); + this._batchedSizes.clear(); + this._pendingSizes.clear(); + + for (const timer of this._pendingTimers.values()) { + clearTimeout(timer); + } + + this._pendingTimers.clear(); + this._firstMeasuredSize = 0; + this._dirty = true; + } + + /** + * Records the item identified by `userKey` as logically empty. The row + * collapses to zero main-axis size and other items close ranks around + * it. + * + * Distinct from `reportItemSize(_, 0)` (which we reject as a transient + * FlexRoot zero during recycle): this is the *intentional* empty path, + * called from `VirtualListCell` when `renderItem` returns null. + */ + reportItemEmpty(userKey: string): boolean { + if (this._batching) { + this._batchedSizes.set(userKey, 0); + + return false; + } + + const existing = this._measuredSizes.get(userKey); + + if (existing === 0) { + return false; + } + + this._measuredSizes.set(userKey, 0); + this._clearPending(userKey); + this._dirty = true; + + return true; + } + + /** + * Returns the stored measurement for a userKey, or undefined if none. + * Mostly here so consumers can ask "have I measured this?" without + * exposing the internal Map. + */ + getMeasuredSize(userKey: string): number | undefined { + return this._measuredSizes.get(userKey); + } + + getLayout(index: number): ComputedLayout | undefined { + if (this._dirty) { + this._recompute(); + } + + if (index < 0 || index >= this._layoutCount) { + return undefined; + } + + return this._layouts[index]; + } + + /** + * Returns the layout index whose [offset, offset+size) range contains the + * given offset (in item-space). Used to map a focused descendant's + * position back to which item it lives in. + */ + findIndexAtOffset(offset: number): number { + if (this._dirty) { + this._recompute(); + } + + if (this._layoutCount === 0) { + return -1; + } + + return this._binarySearchStart(offset); } getVisibleRange( @@ -194,54 +519,6 @@ export class LayoutManager { }; } - clear(): void { - this._layoutCount = 0; - this._measuredSizes.clear(); - this._measuredCrossSizes.clear(); - this._averageWindow.clear(); - this._dirty = true; - this._totalSize = 0; - this._maxCrossSize = 0; - this._pending = []; - } - - get hasPendingMeasurements(): boolean { - return this._pending.length > 0; - } - - deferMeasurement(index: number, size: number, crossSize: number): void { - this._pending.push({ index, size, crossSize }); - } - - /** - * Apply all deferred measurements and return how much the anchor - * item shifted so the caller can adjust the scroll position. - */ - flushDeferred(anchorIndex?: number): { changed: boolean; anchorDelta: number } { - const pending = this._pending; - if (pending.length === 0) { - return { changed: false, anchorDelta: 0 }; - } - - this._pending = []; - const anchorBefore = - anchorIndex != null ? (this.getLayout(anchorIndex)?.offset ?? 0) : 0; - - let changed = false; - for (const { index, size, crossSize } of pending) { - if (this.reportItemSize(index, size, crossSize)) { - changed = true; - } - } - - if (!changed || anchorIndex == null) { - return { changed, anchorDelta: 0 }; - } - - const anchorAfter = this.getLayout(anchorIndex)?.offset ?? 0; - return { changed, anchorDelta: anchorAfter - anchorBefore }; - } - private _ensureCapacity(count: number): void { for (let i = this._layouts.length; i < count; i++) { this._layouts.push({ @@ -292,36 +569,66 @@ export class LayoutManager { this._ensureCapacity(count); - const estimatedSize = this._averageWindow.currentValue || this._estimatedItemSize; - if (this._numColumns <= 1) { - this._recomputeSingleColumn(count, estimatedSize); + this._recomputeSingleColumn(count); } else { - this._recomputeMultiColumn(count, estimatedSize); + this._recomputeMultiColumn(count); + } + } + + private _resolveSize(index: number, isEmpty: boolean, override: { size?: number }): number { + if (isEmpty) { + return 0; + } + + const item = this._data[index]; + + if (item != null && this._keyExtractor) { + const userKey = this._keyExtractor(item, index); + const measured = this._measuredSizes.get(userKey); + + if (measured != null) { + return measured; + } + } + + if (override.size != null) { + return override.size; } + + // Prefer the first-measured size over the caller's estimate once any + // cell has reported. Per-key measurements above still win for cells + // that have actually been seen — this is the fallback for unmeasured + // ones only. + return this._firstMeasuredSize > 0 ? this._firstMeasuredSize : this._estimatedItemSize; } - private _recomputeSingleColumn(count: number, estimatedSize: number): void { + private _recomputeSingleColumn(count: number): void { let offset = 0; for (let i = 0; i < count; i++) { - const override = this._getOverride(i); - const size = this._measuredSizes.get(i) ?? override.size ?? estimatedSize; - const crossSize = this._measuredCrossSizes.get(i) ?? this._estimatedItemSize; const layout = this._layouts[i]; if (!layout) { break; } + const item = this._data[i]; + // Null/undefined data is rendered as nothing (VirtualList returns + // null for the cell), so it must take zero main-axis space. + const isEmpty = item === undefined || item === null; + const override = this._getOverride(i); + const size = this._resolveSize(i, isEmpty, override); + layout.offset = offset; layout.size = size; layout.column = 0; layout.crossOffset = 0; - layout.crossSize = crossSize; + layout.crossSize = this._cellCrossSize; + offset += size; - if (i < count - 1) { + if (size > 0 && i < count - 1) { offset += this._separatorSize; } } @@ -330,8 +637,8 @@ export class LayoutManager { this._totalSize = offset; } - private _recomputeMultiColumn(count: number, estimatedSize: number): void { - const columnWidth = this._estimatedItemSize; + private _recomputeMultiColumn(count: number): void { + const cellCross = this._cellCrossSize; let offset = 0; let i = 0; @@ -340,21 +647,24 @@ export class LayoutManager { let columnsUsed = 0; while (i < count && columnsUsed < this._numColumns) { - const override = this._getOverride(i); - const span = Math.min(override.span ?? 1, this._numColumns - columnsUsed); - const size = this._measuredSizes.get(i) ?? override.size ?? estimatedSize; - const crossSize = this._measuredCrossSizes.get(i) ?? span * columnWidth; const layout = this._layouts[i]; if (!layout) { break; } + const item = this._data[i]; + const isEmpty = item === undefined || item === null; + const override = this._getOverride(i); + const span = Math.min(override.span ?? 1, this._numColumns - columnsUsed); + const size = this._resolveSize(i, isEmpty, override); + layout.offset = offset; layout.size = size; layout.column = columnsUsed; - layout.crossOffset = columnsUsed * columnWidth; - layout.crossSize = crossSize; + layout.crossOffset = columnsUsed * cellCross; + layout.crossSize = span * cellCross; + rowHeight = Math.max(rowHeight, size); columnsUsed += span; diff --git a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts index 83336d1..6e1706e 100644 --- a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts @@ -97,4 +97,36 @@ describe('RecyclerPool', () => { expect(pool.getSlotKey(5)).toBeDefined(); expect(pool.getSlotKey(99)).toBeUndefined(); }); + + it('getPooledSlots returns released slots paired with their last index', () => { + const pool = new RecyclerPool(); + + const slots1 = pool.reconcile([0, 1, 2], () => 'default'); + // oxlint-disable-next-line typescript/no-non-null-assertion -- key was just inserted + const keyForIndex0 = slots1.get(0)!; + + // 0 leaves visibility, 3 enters. Slot for 0 is now pooled but reused for 3. + pool.reconcile([1, 2, 3], () => 'default'); + + // No fully-released slots (keyForIndex0 was repurposed). + expect(pool.getPooledSlots()).toEqual([]); + + // Drop down to two visible items — slot for index 3 is released without reuse. + pool.reconcile([1, 2], () => 'default'); + + const pooled = pool.getPooledSlots(); + expect(pooled).toHaveLength(1); + expect(pooled[0]).toEqual({ slotKey: keyForIndex0, lastIndex: 3 }); + }); + + it('getPooledSlots is empty after clear', () => { + const pool = new RecyclerPool(); + + pool.reconcile([0, 1], () => 'default'); + pool.reconcile([], () => 'default'); + expect(pool.getPooledSlots()).toHaveLength(2); + + pool.clear(); + expect(pool.getPooledSlots()).toEqual([]); + }); }); diff --git a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts index 2f7e02c..d82ad85 100644 --- a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts @@ -1,13 +1,41 @@ export class RecyclerPool { + /** Debug-only label used in pool logging to disambiguate instances. */ + private _label: string; /** dataIndex -> slotKey */ private _active = new Map(); /** itemType -> available slotKeys */ private _available = new Map(); /** slotKey -> itemType */ private _slotTypes = new Map(); + /** + * Last-known slot assignment per data index. When an index leaves + * visibility and later comes back, we try to give it the same slot it + * had before — that keeps the cell's React identity stable across the + * round-trip, so descendant state (like nested VL measurements, + * focusables, transient component state) survives. Without this, every + * scroll-out-and-back churns the entire subtree. + * + * For this preservation to actually work, callers must keep pooled + * slots mounted in the React tree (typically positioned offscreen) — + * see `getPooledSlots`. If pooled cells are unmounted, this map only + * preserves the slot-key string, not the React subtree it was anchored + * to. + */ + private _lastSlotForIndex = new Map(); + /** + * Reverse of `_lastSlotForIndex` — for each known slot, the data index + * it most recently served. Used by `getPooledSlots` so the host can + * keep pooled slots mounted (rendered offscreen) with their last item, + * preserving the inner React subtree across release/reclaim cycles. + */ + private _slotToLastIndex = new Map(); private _visibleSet = new Set(); private _nextId = 0; + constructor(label = "pool") { + this._label = label; + } + get activeCount(): number { return this._active.size; } @@ -34,32 +62,125 @@ export class RecyclerPool { visibleSet.add(idx); } + let released = 0; + let preferredReused = 0; + let pooled = 0; + let created = 0; + for (const [index, key] of this._active) { if (!visibleSet.has(index)) { this._release(key); this._active.delete(index); + this._slotToLastIndex.set(key, index); + released++; } } for (const index of visibleIndices) { if (!this._active.has(index)) { const type = getType(index); - const key = this._acquire(type); + const preferred = this._lastSlotForIndex.get(index); + let key: string; + + if ( + preferred !== undefined && + this._tryClaimPreferred(type, preferred) + ) { + key = preferred; + preferredReused++; + } else { + const before = this._nextId; + + key = this._acquire(type); + + if (this._nextId > before) { + created++; + } else { + pooled++; + } + } + this._active.set(index, key); } } + for (const [index, key] of this._active) { + this._lastSlotForIndex.set(index, key); + } + + if (import.meta.env.DEV && (released > 0 || preferredReused > 0 || pooled > 0 || created > 0)) { + console.log( + `[Pool ${this._label}] released:${released} preferred:${preferredReused} pool:${pooled} new:${created} active:${this._active.size} pooled:${this.pooledCount} types:${this._available.size}`, + ); + } + return this._active; } + /** + * Try to pull `preferred` out of the available pool for the given type. + * Returns true on success (slot is now claimed for the caller); false + * if the slot isn't available (was reassigned to a different index, or + * has a different type now). + */ + private _tryClaimPreferred( + type: string | number, + preferred: string, + ): boolean { + if (this._slotTypes.get(preferred) !== type) { + return false; + } + + const available = this._available.get(type); + + if (!available) { + return false; + } + + const idx = available.indexOf(preferred); + + if (idx < 0) { + return false; + } + + available.splice(idx, 1); + + return true; + } + getSlotKey(index: number): string | undefined { return this._active.get(index); } + /** + * Enumerate currently-pooled slots paired with the data index they + * most recently served. Callers should render these slots in the React + * tree (typically positioned offscreen) so the React subtree — and any + * nested recycler pools inside it — survives the release/reclaim + * round-trip. + */ + getPooledSlots(): Array<{ slotKey: string; lastIndex: number }> { + const result: Array<{ slotKey: string; lastIndex: number }> = []; + + for (const slots of this._available.values()) { + for (const slotKey of slots) { + const lastIndex = this._slotToLastIndex.get(slotKey); + + if (lastIndex !== undefined) { + result.push({ slotKey, lastIndex }); + } + } + } + + return result; + } + clear(): void { this._active.clear(); this._available.clear(); this._slotTypes.clear(); + this._lastSlotForIndex.clear(); + this._slotToLastIndex.clear(); this._nextId = 0; } diff --git a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts index a4010be..f79fe10 100644 --- a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts @@ -9,6 +9,7 @@ const makeLayout = (offset: number, size: number): ComputedLayout => ({ column: 0, crossOffset: 0, crossSize: 100, + measured: false, }); function getCallArgs(fn: ReturnType, index: number) { diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.md b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md new file mode 100644 index 0000000..9342dc6 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md @@ -0,0 +1,405 @@ +# VirtualList + +A virtualized scroll list for Lightning, modeled on FlashList v1. The user is **Willson**; this doc is the authoritative spec for what the component does, why it does it that way, and what's deliberately out of scope. Edit this file to change requirements — Claude reads it when fixing bugs and adding features. + +--- + +## Model + +**Two modes, decided by the runtime environment:** + +### Pinned mode (no flex ancestor) + +When the VL is rendered outside any flex parent (`useIsInFlex() === false`), no yoga is running in this subtree. Cells are absolutely positioned with explicit `w` AND `h` from `LayoutManager` — VL is the *single, exclusive* source of cell positioning and sizing. No FlexRoot, no measurement, no `onResize`. The user's `renderItem` content sizes itself however it wants, but the cell does not adapt; the caller is responsible for accurate `estimatedItemSize` / `overrideItemLayout`. This is the FlashList v1 strict model and has no feedback loops by construction. + +### Measured mode (flex ancestor) + +When the VL is rendered inside a flex parent (`useIsInFlex() === true`), yoga is already laying out the surrounding tree. In this mode each cell wraps its content in a `FlexRoot`, which: + +- gives the user's `renderItem` real flex layout (children can `flexGrow`, `flexDirection`, etc.), +- pins its cross-axis to `cellCrossSize` (so flex-percentage layouts have a concrete cross dimension to compute against), +- leaves its main axis free so yoga shrinks-to-fit content, +- emits `onResize` whenever that natural main-axis size changes. + +The cell forwards the main-axis number — and only the main-axis number — to `LayoutManager.reportItemSize(userKey, size)`. Subsequent items reposition based on the measurement. + +The crucial discipline in measured mode is **measurement is one-directional and main-axis only** — cross-axis sizes are never aggregated back into VL's layout decisions, and viewport size never depends on cell reports. That's how the prior architecture's three-way feedback loop (cell-cross → `_maxCrossSize` → contentCross → cellCrossSize → cell-cross) is avoided. + +### What stays the same in both modes + +- The cell wrapper (``) is positioned exclusively by VL. It has no flex layout itself. +- Cross-axis size of every cell is `cellCrossSize`, derived from viewport, never from cell reports. +- Measurements are stored by `userKey` (the caller's `keyExtractor` output), not by index. Recycling preserves measurements; a cell rendering an item it has measured before uses the cached size on first render. + +**Three responsibilities:** + +| File | Responsibility | +|---|---| +| `LayoutManager.ts` | Pure layout math. Given data + sizes + cross-axis size, computes per-item offsets in O(n). | +| `VirtualListCell.tsx` | One `` per visible cell with explicit absolute position and dimensions. Wraps user content in `VLCellKeyContext` + `CellBoundsContext` providers. The renderItem subtree persists across slot recycles — the cell wrapper *and* its descendants survive userKey changes; nested VLs read the new userKey via `VLCellKeyContext` and run their cellKey-change branch instead of remounting. | +| `VirtualList.tsx` | Viewport derivation, scroll/focus state, recycling, the React glue. | + +Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven scroll), `useViewability.ts` (onViewableItemsChanged), `RecyclerPool.ts` (slot reuse by item type), `parseContentStyle.ts` (RN-style padding props), `VirtualListContext.ts` (the three React contexts). + +--- + +## Public API + +`VirtualListProps` (see `VirtualListTypes.ts` for full types): + +### Required +- **`data: ReadonlyArray`** — items to render. +- **`renderItem: (info: VirtualListRenderItemInfo) => ReactElement`** — render function. `info` carries `{ item, index, extraData, shouldFocus }`. + +### Sizing +- **`estimatedItemSize?: number`** (default `200`) — main-axis size used when no override is provided. **In this model, the estimate IS the size** for items lacking an override — it's not a guess that gets refined. Pick it carefully. +- **`overrideItemLayout?: (layout, item, index, numColumns, extraData) => void`** — set `layout.size` (main-axis) and optionally `layout.span` (multi-column). Called for every layout pass; must be fast. +- **`numColumns?: number`** (default `1`) — multi-column grid. Cells fill columns left-to-right, then advance main-axis by the tallest size in the row. + +### Layout +- **`horizontal?: boolean`** (default `false`) — scroll axis. +- **`style?: LightningViewElementStyle`** — applied to the outer FocusGroup. `style.w` / `style.h` set the viewport explicitly (highest priority over parent bounds and self-measure). +- **`contentContainerStyle?: ContentStyle`** — RN-style padding (`padding`, `paddingHorizontal`, `paddingVertical`, `paddingTop` etc) and `backgroundColor`. + +### Slots +- **`ListHeaderComponent` / `listHeaderSize`** — header rendered before items, takes `listHeaderSize` main-axis pixels. +- **`ListFooterComponent` / `listFooterSize`** — footer after items. +- **`ListEmptyComponent`** — replaces the entire list when `data.length === 0`. +- **`ItemSeparatorComponent`** — rendered between cells in single-column lists. Skipped after collapsed (size=0) rows. + +### Behavior +- **`drawDistance?: number`** (default `250`) — pixels beyond viewport to keep mounted. Larger = smoother scroll, more memory. +- **`keyExtractor?: (item, index) => string`** — used by recycler for slot identity AND by focus restoration. Falls back to `String(index)`. +- **`getItemType?: (item, index, extraData) => string | number`** — items of the same type share a recycler pool. +- **`extraData?: unknown`** — opaque value forwarded to `renderItem`. Changing it forces re-layout. +- **`initialScrollIndex` / `initialScrollIndexParams`** — scroll to a specific index on mount. +- **`snapToAlignment?: 'start' | 'center' | 'end'`** (default `'start'`) — where focused items land in the viewport. +- **`animationDuration?: number`** (default `300`) — scroll animation duration. +- **`onScroll` / `onEndReached` / `onEndReachedThreshold`** — scroll callbacks. +- **`onViewableItemsChanged` / `viewabilityConfig`** — viewability tracking. +- **`onLoad`** — fires once when first items render (with elapsed ms since mount). +- **`onLayout`** — fires when content dimensions change. +- **`autoFocus` / `trapFocus{Up,Right,Down,Left}`** — forwarded to the FocusGroup wrapping the list. + +### Imperative — `VirtualListRef` +- `scrollToIndex({ index, animated?, viewPosition?, viewOffset? })` +- `scrollToOffset({ offset, animated? })` +- `scrollToEnd({ animated? })` +- `getScrollOffset()` +- `getVisibleRange()` + +--- + +## Sizing contract + +**Main-axis size priority** (highest wins): + +1. **Measured size** *(measured mode only)* — the cell's last reported main-axis dimension, keyed by `userKey`. +2. **`overrideItemLayout` returns `layout.size`**. +3. **Data entry is `null` / `undefined`** — size is forced to `0`, cell is not rendered. +4. **First-measured size** *(measured mode only)* — once any cell has reported a measurement, that first value is used as the fallback for *unmeasured* cells in place of `estimatedItemSize`. Locked on first; later measurements update individual cells via the per-key path but do not change the implicit fallback. +5. **Fallback: `estimatedItemSize`** — used only before the first measurement lands (and always in pinned mode, where no measurements happen). + +The first-measured fallback is what makes `estimatedItemSize` matter less in measured mode: a slightly-off estimate produces visible reflow only on the very first cell. After that, the second and subsequent cells use the first cell's actual rendered size as their starting point — usually a much closer match to the real content size, so each cell's per-key measurement update moves things only a few pixels (or not at all). Locking on the *first* measurement (rather than tracking a running average or the most recent) is deliberate: a moving estimate would cascade-rerender every later unmeasured item every time someone new measured, defeating the goal. + +In **pinned mode** (no flex ancestor), step 1 doesn't apply: cells never report sizes, so the chain effectively skips to step 2/3/4. Every cell is exactly the size LayoutManager dictated. Get your `estimatedItemSize` / `overrideItemLayout` right or you'll see gaps/overflow. + +In **measured mode** (flex ancestor), measurement wins over override because real rendered size is authoritative — if the user's content renders to 250px and the override said 200px, going with 250 prevents overflow into the next item. If the caller wants to *force* a size and prevent measurement updates, they need to render content sized to that exact dimension (the cell measures whatever content actually renders). + +**Cross-axis size** is *never* measured in either mode. Every item's cross-axis size equals `cellCrossSize` (derived from viewport). Span is the one exception — `layout.span = 2` makes a multi-column item occupy 2 columns of `cellCrossSize` width. If a user's content overflows the cross-axis, it paints outside the cell wrapper but does not affect `cellCrossSize` for any item. + +**First-render behavior** *(measured mode)*. Cells whose `userKey` has never been measured fall through to override → estimate. They render at that estimated main-axis size; once yoga lays them out and `onResize` fires, the cell reports its actual size, LayoutManager updates, and following items reposition. So a list with accurate `estimatedItemSize` (or `overrideItemLayout`) renders correctly from frame 1; lists with bad estimates show a brief reflow on first paint. + +**Recycling and measurement.** Measurements are keyed by `userKey`, not index, so a recycled cell reuses its prior measurement immediately. A cell rendering an item it has measured before paints at the right size on first paint — no re-measure needed unless content actually changed. + +**Two-layer commit dampening.** Measurement reports go through one of two paths depending on whether a scroll/focus-snap animation is in flight: + +1. **Animation batching** (top layer). When `useScrollHandler` fires `onAnimationStart`, VL flips LM into batching mode (`setBatching(true)`). While batched, every report goes into `_batchedSizes` (latest wins per `userKey`) and bypasses the dampening path entirely — the animation is the consumer's clear signal that intermediate yoga measurements aren't worth committing. On `onAnimationEnd`, `setBatching(false)` drains the batch directly into `_measuredSizes` in a single step and bumps `layoutVersion` once. The layout stays frozen for the visible duration of the animation, then settles in one commit when it ends. + +2. **Per-key stability dampening** (below the batching layer). Outside of animations, first measurements apply immediately. Subsequent *changes* to an already-stored measurement go through a `_STABILITY_MS` (currently 120ms) wait: a different value starts a "pending" timer; further reports of the same value advance toward confirmation, and only after the value has been stable for the window does it commit to layout. A different intermediate value restarts the timer. A backstop `setTimeout` is also scheduled per pending entry so values commit even if the cell pushes once and goes quiet (props stable, no further re-renders). This filters transient oscillations from user content that re-measures asynchronously after a scroll has already settled — without the dampener, every spurious yoga measurement propagates to layout offsets and rows below jump on every scroll tick. + +The backstop wakes VL through `LayoutManager.setOnChange(cb)` — VL registers a callback at construction that calls `setLayoutVersion(v => v + 1)`. The synchronous-commit paths (immediate first-measurement, stable repeat-after-window) instead return `true` from `reportItemSize` so VL bumps inline. The two paths are mutually exclusive: while `_batching` is true, the dampening map is cleared and pending timers are cancelled (the upcoming flush will replace those values anyway). + +**Imperative scroll position is the single source of truth during scroll animations.** When `isScrollAnimating` is true, `contentStyle` deliberately omits `x`/`y` so React reconciliation can't clobber the running `el.node.animate(...)` interpolation. The `stopped` callback in `useScrollHandler` pins the final `node.x`/`node.y` synchronously; the subsequent render with `isScrollAnimating === false` writes the matching declarative value. `committedScrollOffset` is updated unconditionally on every `scrollToOffset` call so the post-animation render's declarative `x`/`y` matches the pinned position even when a hop lands inside the same visible range as the prior commit (e.g. a snap-to-edge from the second item to the first). Without that, the next render would re-apply a stale offset via style and snap content past the interpolated position. + +**Measurements persist across data identity changes.** A new `data` array reference (e.g. from a tab switch or a parent re-render that recomputes the array) does not wipe measurements. Cells whose `userKey` survives the change reuse their cached size — items don't shift on re-render. Callers who truly need to invalidate (orientation change, theme swap that materially affects content sizing) can call `layoutManager.clearMeasurements()` imperatively. If user-content's measured size genuinely fluctuates during a session (focus animations, async-loaded content), each reported value updates the layout — that's intentional, since the alternative (locking to max) leaves visible empty space when content is at smaller sizes. + +**Skipping cells when `renderItem` returns null.** If `renderItem` returns `null`, the cell renders nothing — no `FocusGroup` wrapper, no separator, no DOM — *and* the cell calls `LayoutManager.reportItemEmpty(userKey)` via `useLayoutEffect`, which collapses the row to zero main-axis size. Following items close ranks around the gap. This is for callers whose data shape doesn't permit a clean `null` entry but still has logically-empty rows (e.g. `{ id, label, items: [] }` with `items.length === 0` meaning "render nothing"). + +The `reportItemEmpty` path is deliberately separate from `reportItemSize`. The size path rejects 0 to filter transient FlexRoot reports during recycle (FlexRoot briefly measures 0 between unmount and remount of its children); accepting those would collapse legitimate rows. `reportItemEmpty` is the explicit, intent-bearing alternative — only called from the renderItem-null code path. + +--- + +## Viewport resolution + +`viewportSize` is the main-axis dimension (the scroll axis). `viewportCrossSize` is the cross-axis. They resolve via separate priority chains because the main axis is normally driven by the parent's flex (so `measuredSize` is reliable there) while the cross axis can be content-driven for unbounded layouts. + +**Main axis:** + +1. `style.w` / `style.h` (caller-provided) +2. `parentCellBounds.width` / `.height` (from an enclosing `VirtualListCell`) +3. `measuredSize` (FocusGroup `onResize`) + +**Cross axis** — order depends on orientation: + +**Vertical VL:** + +1. `style.w` (caller-provided) +2. `parentCellBounds.width` +3. `measuredSize.w` (FocusGroup `onResize` — reliable here because `outerStyle` has `flexGrow: 1` so the parent's flex allocates the cross dim) +4. `maxContentCross` (max cross dim reported by any cell's content) +5. `estimatedItemSize` + +**Horizontal VL:** + +1. `style.h` (caller-provided) +2. `maxContentCross` — max cross dim reported by any cell's content +3. `parentCellBounds.height` +4. `measuredSize.h` +5. `estimatedItemSize` + +The asymmetry is load-bearing. In a vertical VL nested inside a column-flex parent, `parentCellBounds.width` and `measuredSize.w` both report the full allocated column width — the right size for cells to fill. Cell-content cross sizes (per-row natural widths) are typically larger than the viewport in app-style rows that wrap inner horizontal scrollers, so trusting them would set `cellCrossSize` to the inner scroller's full `totalContentSize` instead of the viewport. + +In a horizontal VL nested inside a parent section that contains other siblings (a title, a status row, etc.), `parentCellBounds.height` is the *outer cell's* full height — `title + innerVL + other`. Using that as the inner VL's cross dim sizes the cells to include the chrome height too. Worse, when the outer cell's measurement bounces (as the user's section component re-measures during scroll/focus animations), the inner VL's `cellCrossSize` bounces with it, and cells are sometimes too tall (gap), sometimes too short (overflow). Trusting the cells' own measured cross instead — `maxContentCross` — gives `cellCrossSize` the cards' natural height regardless of what the outer section measures around them. Only when no cell has measured yet do we fall back to parent/measured/estimate. + +`maxContentCross` is monotonic per-dataset: once a cell reports cross=N, the VL stays at N or larger until `data` / `extraData` identity changes (at which point the value resets so a fresh, smaller dataset isn't stuck at the prior dataset's max). + +**`cellCrossSize = (viewportCrossSize - crossPadding) / numColumns`** + +For a horizontal VL, `viewportCrossSize` is content-driven once any cell has reported (per the priority chain above), so this formula naturally yields cells sized to their actual content. For a vertical VL it yields cells filling the parent-allocated column. + +For a list with no explicit cross AND no flex ancestor (pinned mode), no measurement happens — the chain falls through to `estimatedItemSize`. In that case the caller MUST pass an `estimatedItemSize` that makes sense as a cross-axis dim, OR set `style` explicitly. Pinned-mode horizontal VLs essentially require `style.h` to be useful. + +--- + +## Cell rendering + +`VirtualListCell` produces a fully-pinned `FocusGroup` cell wrapper, then either a plain or a flex-wrapped content subtree depending on `isInFlex`: + +```jsx + + {isInFlex ? ( + onItemSizeChange(userKey, horizontal ? e.w : e.h)} + > + + + {renderedItem} + + + + ) : ( + /* plain content — no flex, no measurement */ + + + {renderedItem} + + + )} + {/* optional separator, position:absolute */} + +``` + +The renderedItem subtree is **not** wrapped in a keyed Fragment. Slot recycle changes `userKey` but the React tree at and below the cell wrapper persists — including the user's section component and any nested VLs inside it. That preservation is what lets nested-VL `LayoutManager` and `RecyclerPool` instances survive the round-trip. + +**Why the cell wrapper has no flex.** The cell is positioned and sized exclusively by VL. Adding flex to it would either (a) drag it into the parent's flex flow (it shouldn't be — `position: 'absolute'` keeps it independent) or (b) start a flex subtree at the wrong layer. Either way, you'd be re-introducing the kinds of two-source-of-truth bugs the architecture is designed to prevent. Keep the cell wrapper a plain absolutely-positioned ``. + +**Why the cell wrapper is a `FocusGroup` (not just a focusable).** Each cell is its own focus group, nested inside the VL's outer FocusGroup. Three reasons: + +1. **Default focus target without effort from the caller.** A `renderItem` that doesn't wrap its content in a focusable still gets navigation behaviour — focus lands on the cell wrapper. Useful for display-only items, icon grids, etc. +2. **Stable focus home during recycle.** The cell wrapper persists across slot recycles. If the user's renderItem changes shape between content swaps (e.g. one row has an inner focusable and another renders a static label), focus has a guaranteed landing point on the cell wrapper itself rather than escaping to a sibling row. +3. **Containment for spatial nav.** When a cell has multiple focusables (e.g., a row with action buttons), arrow keys stay within the cell until there's no candidate, then bubble to the VL's outer FocusGroup for cross-cell navigation. Without the per-cell group, every focusable in every cell competes in one flat space and spatial nav can jump unexpectedly across cells. + +When the caller's `renderItem` includes an inner focusable, that inner is added to FocusManager as a child of the cell's group (not the VL's). The inner becomes the leaf-focused element; `onChildFocused` cascades up through the cell group to the VL's outer group, where `handleVLFocus` runs the snap-alignment scroll and writes to the state cache. Existing focus-driven behaviour is preserved — nesting is purely additive. + +**Why FlexRoot is conditional on `isInFlex`.** When the VL has no flex ancestor, no yoga is running in this subtree. Adding a FlexRoot just for measurement would force yoga to spin up — pure overhead with no benefit, since the user's content isn't using flex either. So we skip it; the cell is silent and pinned. + +**Why FlexRoot is unpinned on both axes.** Yoga shrinks the FlexRoot to fit content on both axes. The cell forwards both dimensions: main goes into `LayoutManager`'s per-key measurement store; cross feeds VL's `maxContentCross` fallback (used only when no explicit cross source is available). Pinning cross to `cellCrossSize` would create the prior architecture's feedback loop — cell echoes its own pinned size back to VL, which uses that to compute the pin, etc. Leaving both unpinned lets cell content drive sizing without a loop. Tradeoff: flex-percentage layouts on cross axis (e.g. `width: '100%'` inside a horizontal VL's cell) won't work because the parent has no fixed cross dim — callers needing those should set `style.h` (or `.w`) on the VL, which flips the chain into the explicit branch. + +**Why measure via `onResize`?** Lightning's universal `NodeResizeObserver` fires `onResize` whenever a node's size changes. The cell ignores the cross-axis dimension and only reports a positive main-axis number; cross-axis and zero/negative reports are filtered before they reach VL. + +**Why a one-shot RAF push on `userKey` change.** `NodeResizeObserver` (driving `onResize`) only fires when the FlexRoot's actual size changes. Two scenarios miss measurements when relying on it alone: + +1. **Same-size recycle.** A slot reassigns from item N to item M with identical dimensions. FlexRoot's size doesn't change → no observer event → item M's `userKey` never gets a measurement under the new key. +2. **Empty → non-empty transition.** The cell previously rendered null (no FlexRoot at all); now it renders content. There's nothing for the observer to compare against on the new mount. + +The fix: a `useLayoutEffect` keyed on `[userKey, isInFlex, isEmpty, horizontal, onItemSizeChange, onContentCrossLayout]` that schedules a RAF when the userKey (or its preconditions) changes, reads `flexRootRef.current.node.w/h` after yoga has laid out, and reports those dimensions. RAF defers until post-layout so the values reflect the post-render dimensions. Cleanup cancels the prior frame's RAF. + +This is intentionally **not** an every-render push. Once a cell has reported its size for a given `userKey`, ongoing size changes flow exclusively through `onResize` (which dedupes by definition). The recently-removed every-render variant was contributing to mid-scroll thrashing — every cell's mainOffset change triggered a re-render, every re-render scheduled a RAF, and every RAF pushed a possibly-transient yoga snapshot back into LM. Scoping to userKey changes plus `onResize` reduces the noise without losing the same-size-recycle path. + +**Why imperative `cellElementRef.current.focus()` on `shouldFocus` false → true.** `useFocus` claims focus through the focus manager only at `addElement` time (mount). On a persisting cell whose `autoFocus` prop flips from false to true, `setAutoFocus` only updates the property — it does not actively claim focus. Without an alternative trigger, focus restoration after a slot recycle silently fails: the cell now "wants" focus but nothing pulls it. + +The cell holds a ref to its outer FocusGroup's element (`cellElementRef`) and a `prevShouldFocusRef` to detect transitions. A `useLayoutEffect` keyed on `[shouldFocus]` calls `cellElementRef.current.focus()` exactly once per false → true transition. The mount-time claim still goes through `FocusGroup`'s `autoFocus` prop (via `useFocus → addElement`); the imperative path fires only for already-mounted cells. This replaces the prior `` mechanism, which forced the renderItem subtree to remount on every userKey change so the user's inner focusables would re-fire their own `autoFocus` — that approach also tore down nested-VL `LayoutManager`/`RecyclerPool` state on every recycle, which is what we now preserve. + +**Why `CellBoundsContext`?** Nested VLs need to know their bounded width and height without measurement of their own. The cell hands them down through context. Both numbers come straight from layout (estimate, override, or measurement) so a nested list's viewport is reasonable from frame 1. + +--- + +## Separators + +`ItemSeparatorComponent` renders between cells. The separator is positioned `position: absolute` inside the cell wrapper at the cell's main-axis trailing edge, so it doesn't participate in cell measurement. + +**One measurement, all cells.** Separators are a single component — every instance is the same shape. In flex (measured) mode, every cell that renders a separator reports its size up to VL, but VL keeps a single `separatorSize` state and dedupes; only the first non-zero report (or genuine subsequent change) hits state. That value flows into `LayoutManager.updateConfig({ separatorSize })`, which adds the gap into per-item offsets. + +In pinned (non-flex) mode there is no measurement: `separatorSize` stays at the last value VL knows about (initial: 0). If the caller wants a non-zero gap with no flex ancestor, they need to size the separator by other means and the gap will appear visually but won't be reflected in LM offsets — meaning following items overlap. In practice this is fine because non-flex consumers tend to use accurate `overrideItemLayout` that already includes the separator gap. + +LM only adds `separatorSize` between adjacent cells in single-column lists. Multi-column lists do not auto-insert separators between rows or columns. + +--- + +## Focus restoration + +`VirtualList` persists `focusedIndex` to the parent's state cache (`VLStateCacheContext`) so revisiting a row restores focus to the last item the user navigated to. There are two flows: in-list navigation (user moves focus around) and recycle entry (the cell holding this VL just got a new identity, restore prior state). + +### In-list navigation flow + +User presses arrow → FocusGroup fires `onChildFocused(child)` → `handleVLFocus(child)`: + +1. **Compute target index** from `child.getRelativePosition(contentRef)`. Child positions inside contentRef are scroll-independent (cells are absolutely positioned inside contentRef; only contentRef's own x/y changes for scroll), so this is safe to do before scrolling. +2. **Run `handleChildFocused(child)`** — that's the snap-alignment scroll inside `useScrollHandler`. It calls `scrollToOffset(target, animated)`, which synchronously sets `scrollOffsetRef.current = clamp(target)` and kicks off the animation. So even though the visual scroll is async, the ref is already updated. +3. **Write to cache** with `{ scrollOffset: scrollOffsetRef.current, focusedIndex: targetIdx }`. Because step 2 already updated the ref, this captures the *post-alignment* offset. +4. **Update `focusedIndexRef.current`**. + +The order matters. If you write before step 2, you capture the pre-scroll offset. On restore, `resetScroll` puts the row at that pre-scroll offset, autoFocus fires, `handleChildFocused` runs and scrolls to the *correct* offset — but the user sees the row land slightly off and then jump. Two visits are needed for the cache to converge. Doing alignment first and writing after is what makes the first visit land cleanly. + +### Recycle entry flow + +The enclosing cell's `userKey` (received via `VLCellKeyContext`) changes — meaning the parent VL recycled this row into a different item. The VL's React identity persists across this transition (no remount), so the branch fires on the persisting component instance: + +1. **Detect the change** via `prevCellKeyRef.current !== cellKey` (during render, not in an effect — we want the current render to use the new state, not flash the old). +2. **Save outgoing state** to the parent's cache under `prevCellKeyRef.current`, capturing `scrollOffsetRef.current`, `focusedIndexRef.current`, and **a snapshot of `LayoutManager.getMeasurements()`**. The measurement snapshot survives the recycle so the row's inner cells don't have to re-measure from estimate when the row becomes visible again — without it, every recycle-back would cascade first-measurement commits through every following item. +3. **Apply incoming config synchronously** via `layoutManager.updateConfig({ data, ..., separatorSize })`. Without this, LM's `_data` would still be the prior content (the `useLayoutEffect` that calls `updateConfig` hasn't fired yet), so `_resolveSize` would derive userKeys from the old data and miss every entry in the about-to-be-restored measurements map. The result would be one frame of estimate-based layout before the effect catches up — visible flicker. +4. **Read incoming state** from the cache under the new `cellKey`. +5. **`resetScroll(incoming.scrollOffset ?? 0)`** — synchronously updates `scrollOffsetRef`, applies the position to contentRef directly, resets the visible-range cache. +6. **`setFocusedIndex(incoming.focusedIndex ?? 0)`** — cells about to render will pick this up as `shouldFocus`. The `?? 0` fallback is load-bearing: the inner FG's `focusedElement` is React-element-stable across recycle (each cell at its slotKey persists the same Lightning element), so it carries stale memory pointing at whichever cell was last focused under the **previous** cellKey. With `focusedIndex=undefined` no cell flips `shouldFocus` false → true, `VirtualListCell`'s `setFocusedChild` layoutEffect never fires, and the next time the user navigates into this row the FG path traversal lands on the stale cell instead of cell 0. Defaulting to 0 makes cell 0 claim `setFocusedChild` on the next layoutEffect, matching the fresh-mount behavior where `addElement` sets cell 0 as the default `focusedElement` (first focusable child added wins). +7. **`layoutManager.setMeasurements(incoming.measurements)`** if the incoming entry has them, else `layoutManager.clearMeasurements()`. `setMeasurements` also clears any in-flight dampening (`_pendingSizes`, `_pendingTimers`) and batched (`_batchedSizes`) state — pending entries from the prior content are no longer relevant under the restored measurement set. +8. **`skipNextFocusRef.current = true`** — the next `onChildFocused` event is going to be the FocusGroup re-establishing focus on entry; we don't want that overwriting the restored index. We still call `handleChildFocused` so the snap-alignment runs (if the focused item is currently outside the snap window for some reason), but we skip the cache update. +9. **`prevCellKeyRef.current = cellKey`**. + +Because step 3 of the in-list flow captured the post-alignment offset, step 5 here restores to a value that — when autoFocus fires and `handleChildFocused` runs — produces a no-op scroll. No animation, no flicker. + +### State and invariants + +- **Refs, not state.** `focusedIndexRef`, `skipNextFocusRef`, `prevCellKeyRef`, `scrollOffsetRef`. State-based tracking caused setState-async timing bugs where the persistence useEffect captured stale focusedIndex. +- **`shouldFocus` is read at render time.** On initial cell mount, `FocusGroup`'s `autoFocus={shouldFocus}` triggers `useFocus → addElement` with `autoFocus: true`, which claims focus through the focus manager. On a persisting cell whose `shouldFocus` flips false → true, the imperative `cellElementRef.current.focus()` in `VirtualListCell`'s layoutEffect is what actually moves focus. +- **Top-level VLs (no `cellKey`) skip persistence entirely.** No outgoing save, no incoming restore, no cache writes from `handleVLFocus`. + +--- + +## State persistence + +`VirtualList` provides `VLStateCacheContext` to its descendants — a `Map` keyed by the parent VL's userKey for that row. Each entry holds: + +```ts +interface VLPersistedState { + scrollOffset: number; + focusedIndex?: number; + measurements?: Map; +} +``` + +Three write paths, with different timing characteristics: + +| When | Reads what | Why | +|---|---|---| +| `handleVLFocus`, after `handleChildFocused` | `scrollOffsetRef.current` (latest, synchronous) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Captures every focus-driven scroll precisely — including sub-range scrolls that don't commit a new visible range. | +| `useEffect` on `committedScrollOffset` | `committedScrollOffset` (state, updated when range changes) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Backstop for non-focus scrolls (touch/wheel/imperative `scrollToOffset`). Doesn't fire for sub-range scrolls. | +| Cell-key-change block (during render) | `scrollOffsetRef.current` + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Saves outgoing state right before restoring incoming, ensuring the most recent measurements survive even if no in-life save fired since the last update. | + +The focus-event write must run *after* `handleChildFocused` so that `scrollOffsetRef.current` reflects the snap-aligned offset, not the pre-scroll one. See [Focus restoration → In-list navigation flow](#in-list-navigation-flow) for the full rationale. + +The cell-key-change block runs as derived state (set during render), not in an effect, so the current render uses the new state — avoids a flash of stale scroll/focus. + +**Initial mount restoration.** When VL initializes its `LayoutManager`, it checks `parentStateCache.get(cellKey)` for an `initialRestoredState` and, if `measurements` is present, calls `layoutManager.setMeasurements(restored)` immediately — before the first cell render. The cell wrappers in this render get layout offsets computed against the restored measurements, so a recycled-into-existence VL paints at the right sizes from frame 1. Without this, the first frame would use estimates and then reflow once measurements re-pushed. + +`getMeasurements()` returns a copy (so the cache snapshot doesn't alias the live LM state). `setMeasurements(snapshot)` replaces `_measuredSizes`, clears any pending dampening / batched entries, and marks layout dirty. It deliberately does NOT call `_onChange` because the surrounding code (cell-key-change branch or LM init) is already inside a render that will re-render naturally. + +Top-level VLs (no `cellKey`) don't persist anything. + +--- + +## Recycling + +`RecyclerPool` reconciles visible indices to React-stable slot keys, grouped by `getItemType`. Items of the same type reuse each other's React subtrees. The pool's constructor takes an optional `label` (e.g. `"v"` for the outer vertical VL, `"h"` for an inner horizontal) used in debug logging — `[Pool v] released:1 preferred:1 ...`. + +**Subtree persists on content swap.** The cell's outer `FocusGroup` is React-keyed by slot, so the wrapper persists across recycles. There is **no keyed Fragment** under the wrapper — when `userKey` changes, the renderedItem subtree (and any nested VL inside it) reconciles in place rather than remounting. Nested VLs detect the new `userKey` via `VLCellKeyContext` and run their cellKey-change branch (save outgoing state, apply new config, restore incoming state) on the same component instance. Their `LayoutManager`, `RecyclerPool`, and any other useRef-backed state survives the round-trip. + +**Per-index slot stability.** When an index leaves visibility and later comes back, `RecyclerPool` tries to give it back its **previous slot** via `_lastSlotForIndex`, not whichever one happens to be at the top of the LIFO pool. Without this preference, the same item gets a different `slotKey` on round-trip, React unmounts the entire `VirtualListCell`, and any descendant state is destroyed and reconstructed. The reconstruction goes through transient measurement states that ripple up to the parent VL and cause visible thrashing on scroll-back. With the preference, the round-trip is a no-op for the React tree — same `slotKey`, same component instance, same descendant state. + +When the preferred slot isn't available — e.g. it's been reassigned to a different item during the absence — `_acquire` falls back to LIFO pop from the type pool. The cell at that slot was previously rendering some other index; React preserves the cell instance, the userKey changes, and the cell's renderedItem reconciles to the new content. State below the cell still persists (because the cell itself does), so the scroll-back to a stale-preferred index is still better than the no-preservation path. + +**Pooled-slot mounting hook.** `RecyclerPool.getPooledSlots()` returns `Array<{ slotKey, lastIndex }>` — every slot currently sitting in the available pool, paired with the data index it most recently served (tracked via `_slotToLastIndex`, the reverse of `_lastSlotForIndex`). Callers can render these slots in the React tree positioned offscreen, so the React subtree at each pooled slot — and any nested recycler pools inside it — stays mounted across release/reclaim cycles. The pool itself preserves only the slot-key string and last-served index; without the host rendering pooled slots, the React component instances at those slots are unmounted by reconciliation as soon as they leave `visibleIndices`. The wiring in `VirtualList.tsx` may render only currently-visible slots (so pooled cells unmount and re-mount on round-trip) or render pooled slots offscreen too (so the entire React tree below the pool persists end-to-end) — see the JSX for which mode is in effect. The `pooled` prop on `VirtualListCell` exists for the latter mode: when a cell is held in the pool, the host passes `pooled={true}` so the cell's outer `FocusGroup` is disabled and spatial navigation skips both the cell and its descendants. + +If you don't pass `getItemType`, all cells share one pool — fine for uniform lists. + +--- + +## What's not supported + +By design, this VL **does not**: + +- Measure or aggregate cross-axis sizes. Cross is always `cellCrossSize` from viewport. If your content needs more cross space, set `style.w/h` (top-level) or `numColumns`, or accept overflow. +- Auto-derive viewport size from content. Viewport flows: explicit `style` → `parentCellBounds` → self-measured FocusGroup. It never depends on cell reports. +- Offer per-cell cross-axis overrides. `overrideItemLayout.size` is main-axis only; `span` is the only multi-column knob. +- Compute a "running average" of measured sizes. Each measurement is stored verbatim per `userKey`. + +If you find yourself adding cross-axis aggregation or letting cell reports drive viewport, stop and reread [Why measurement is main-axis only](#why-measurement-is-main-axis-only). + +--- + +## Why measurement is main-axis only + +The pre-rewrite VL measured both axes. That spawned three coupled measurement systems: + +1. **Cell-level** (`FlexRoot` + `NodeResizeObserver`) — each cell's content was observed via yoga and reported on both axes to LayoutManager. +2. **Viewport-level** (`FocusGroup.onResize` → `measuredSize`) — VL watched its own container size for `viewportCrossSize`. +3. **Content-derived cross-size** (`_maxCrossSize` → `contentCross` → `finalCross` → `cellCrossSize`) — VL aggregated cell-reported cross sizes back into its own sizing decisions. + +The three formed loops: cells report cross → LayoutManager aggregates → VL sets contentRef.h → FocusGroup measures → viewportCrossSize updates → cellCrossSize re-derived → cells re-render at new cross size → cells report different cross size. Symptoms: backward-scroll jank, recycle flickers, sizes "stuck" at initial estimate. + +The current design measures only main-axis. Cross is unilaterally `cellCrossSize` from viewport — cells don't report it, LayoutManager doesn't aggregate it, VL doesn't derive viewport from it. That's the entire fix. + +--- + +## Canonical examples + +- **Top-level uniform list:** Storybook `VirtualList.stories.tsx` → `Vertical`, `Horizontal`. Pass `estimatedItemSize` matching the actual size, set `style.w/h`. +- **Multi-column grid:** Storybook → `Grid`, `OverrideItemLayout`. Set `numColumns`, optionally `overrideItemLayout` for per-item span/size. +- **Empty / collapsed rows:** Either set `data[i] = null` (auto-collapses), or have `overrideItemLayout` return `layout.size = 0` for some items. +- **Variable-height rows:** `apps/react-lightning-example/src/pages/NestedListPage.tsx` — outer VL uses `overrideItemLayout` to return `ROW_HEIGHT_NORMAL` / `ROW_HEIGHT_SMALL` / `0` per row. +- **Nested horizontal-in-vertical:** `NestedListPage.tsx` again. Inner VL has explicit `style.h` matching its item height; could also inherit from `parentCellBounds.height` if the cell is sized to fit. +- **Initial scroll & imperative scroll:** Storybook → `InitialScrollIndex`, `ImperativeScrolling`. +- **Item types & recycling:** Storybook → `ItemTypes` (mixed image/text rows reused per pool). +- **Plex production:** `react-native-client` consumers go through wrappers (`FlashList.lng.tsx` etc) — those wrappers are responsible for translating their callers' size hints into `estimatedItemSize` / `overrideItemLayout`. + +--- + +## Invariants and pitfalls + +- **`estimatedItemSize` is critical in pinned mode** (no flex ancestor) — it IS the size for items without an override. In measured mode, a bad estimate only affects items rendered *before the very first cell measures*; once any cell reports its size, that becomes the implicit fallback for the rest of the unmeasured items. +- **`overrideItemLayout` is hot.** It's called for every item on every layout. Don't allocate or do expensive work inside. +- **`keyExtractor` matters.** Measurements are keyed by it. Without one, indices are used (`String(index)`) — which means measurements don't survive data inserts/removes that shift indices. For dynamic lists, supply a stable `keyExtractor`. +- **The cell's main-axis is whatever yoga lays out.** If your `renderItem` returns content with explicit dimensions matching your `estimatedItemSize`/`overrideItemLayout`, the cell measures to that. If they disagree, measurement wins on subsequent renders. +- **The cell's cross-axis is fixed at `cellCrossSize`.** Content overflowing the cross axis paints outside the cell wrapper. Either fit content to `cellCrossSize` or pick a larger viewport / smaller `numColumns`. +- **Top-level VLs should set `style.w/h` explicitly** when known. Self-measurement via `FocusGroup.onResize` works but adds a render cycle. +- **Nested VLs without `style.h` (or `style.w`) inherit from `parentCellBounds`.** That's the cell wrapper's current size — which after measurement is the cell's *content* size (estimate before, measured after). For a horizontal inner VL inside a row, it's usually cleaner to set `style.h` on the inner VL to its actual item height rather than rely on the parent cell sizing. +- **`focusedIndexRef` is read at cell render time.** Mount-time autoFocus chains through `FocusGroup → useFocus → addElement` claim focus on first paint. For a persisting cell whose `shouldFocus` flips false → true (slot recycle to a new content that should be focused), the imperative `cellElementRef.current.focus()` in `VirtualListCell`'s layoutEffect is what actually moves focus. +- **Don't add focusedIndex to the persistence `useEffect` deps.** The ref-based direct write in `handleVLFocus` covers per-focus updates; adding the dep reintroduces the setState-async race. +- **In `handleVLFocus`, run `handleChildFocused` BEFORE writing to the cache.** `scrollToOffset` synchronously updates `scrollOffsetRef.current` to the snap-aligned target, so the subsequent cache write captures the post-alignment offset. Inverting this order forces a two-visit convergence on restore — the row lands at the wrong offset and only fixes itself on a second focus event. +- **The `useLayoutEffect` RAF size-push in `VirtualListCell` is keyed on `[userKey, isInFlex, isEmpty, horizontal, onItemSizeChange, onContentCrossLayout]`.** Don't widen to `[]` (every-render): that contributed to mid-scroll thrashing by pushing transient yoga snapshots whenever a sibling cell's update re-flowed this one. Don't narrow further than `userKey`: same-size recycles need at least the userKey transition to push the new key's measurement. +- **Cell wrappers are `FocusGroup`s; don't downgrade them to `useFocus` or to plain views.** Per-cell focus groups give every cell a baseline focus target, contain spatial nav for multi-focusable cells, and provide the ref target for the imperative `focus()` call on `shouldFocus` transitions. +- **Don't re-introduce a keyed `` around `renderedItem`.** The previous version did exactly that to make `useFocus.autoFocus` re-fire on recycle, but it tore down the entire renderItem subtree (including any nested VLs and their `LayoutManager`/`RecyclerPool` state) on every content swap. The current architecture relies on subtree persistence + nested-VL cellKey-change handling to preserve state. Replacing the Fragment with imperative focus is what enables that. +- **Don't call `setMeasurements` outside the cellKey-change branch or LM init.** It clears all in-flight dampening / batching state, which is correct as a "we're switching content, throw away pending observations" reset but would be a bug if called mid-flight on stable content. +- **Don't re-apply `contentStyle.x/y` while `isScrollAnimating` is true.** The imperative `node.animate()` is the source of truth for position during the animation; re-applying the target via React style on a mid-animation re-render snaps the content past the interpolated value. +- **Reject zero-size measurements.** A cell briefly measuring 0 (mid-recycle, content unmount) must not pollute the cache. `LayoutManager.reportItemSize` rejects `size <= 0`. Genuinely-empty rows go through `reportItemEmpty` instead. +- **Don't aggregate cross-axis from cells.** The single rule that prevents the prior architecture's loops. diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx index 95acdc9..0f92e32 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx @@ -1,35 +1,43 @@ -import type { ComponentType, Ref } from 'react'; +import type { ComponentType, Ref } from "react"; import { type ForwardedRef, forwardRef, isValidElement, type ReactElement, - useCallback, + useContext, useEffect, useImperativeHandle, useLayoutEffect, - useMemo, useRef, useState, -} from 'react'; +} from "react"; import { FocusGroup, type LightningElement, type LightningViewElementStyle, -} from '@plextv/react-lightning'; - -import { LayoutManager } from './LayoutManager'; -import { parseContentStyle } from './parseContentStyle'; -import { RecyclerPool } from './RecyclerPool'; -import { useScrollHandler } from './useScrollHandler'; -import { useViewability } from './useViewability'; -import { VirtualListCell } from './VirtualListCell'; -import { VirtualListContent } from './VirtualListContent'; -import type { VirtualListProps, VirtualListRef } from './VirtualListTypes'; +} from "@plextv/react-lightning"; +import { + FlexBoundary, + useIsInFlex, +} from "@plextv/react-lightning-plugin-flexbox"; + +import { LayoutManager } from "./LayoutManager"; +import { parseContentStyle } from "./parseContentStyle"; +import { RecyclerPool } from "./RecyclerPool"; +import { useScrollHandler } from "./useScrollHandler"; +import { useViewability } from "./useViewability"; +import { VirtualListCell } from "./VirtualListCell"; +import { + CellBoundsContext, + type VLPersistedState, + VLCellKeyContext, + VLStateCacheContext, +} from "./VirtualListContext"; +import type { VirtualListProps, VirtualListRef } from "./VirtualListTypes"; function renderListComponent( - component: VirtualListProps['ListHeaderComponent'], + component: VirtualListProps["ListHeaderComponent"], ): ReactElement | null { if (!component) { return null; @@ -44,9 +52,10 @@ function renderListComponent( return ; } -function VirtualListInner(props: VirtualListProps, ref: ForwardedRef) { - 'use no memo'; - +function VirtualListInner( + props: VirtualListProps, + ref: ForwardedRef, +) { const { data, renderItem, @@ -75,7 +84,7 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef(props: VirtualListProps, ref: ForwardedRef( + initialRestoredState?.focusedIndex, + ); + // After a cellKey change we restore the saved focused index. The + // FocusGroup may auto-pick its first focusable on entry, which fires + // onChildFocused and would otherwise overwrite the restored index. + // Skip exactly one onChildFocused after a cellKey change. + const [skipNextFocus, setSkipNextFocus] = useState(false); + + // State cache provided to nested VLs inside our cells. Lazy-init via + // useState so the Map's identity is stable without a render-phase + // ref-write pattern. + const [ownStateCache] = useState>( + () => new Map(), + ); + const [measuredSize, setMeasuredSize] = useState({ w: 0, h: 0 }); const [, setLayoutVersion] = useState(0); + // Separator size is measured once (any cell that renders a separator + // reports its size; we dedupe to a single sticky value). LayoutManager + // gets it via updateConfig and accounts for it in offsets between cells. const [separatorSize, setSeparatorSize] = useState(0); + const separatorSizeRef = useRef(0); + // Max cross-axis size reported by any cell's content. Used as a + // fallback when the caller hasn't given us an explicit cross via + // `style` or `parentCellBounds`. Monotonic by design — once a cell + // measures cross=N, VL stays at that size or larger. Resets when data + // identity changes (extraData / data ref) since fresh content can + // legitimately be smaller than what came before. + const [maxContentCross, setMaxContentCross] = useState(0); + const maxContentCrossRef = useRef(0); const padding = parseContentStyle(contentContainerStyle); - const viewportWidth = (style?.w as number) || measuredSize.w; - const viewportHeight = (style?.h as number) || measuredSize.h; - const viewportSize = horizontal ? viewportWidth : viewportHeight; - const viewportCrossSize = horizontal ? viewportHeight : viewportWidth; - const layoutManagerRef = useRef | null>(null); - - if (!layoutManagerRef.current) { - layoutManagerRef.current = new LayoutManager({ + + const paddingStart = horizontal ? padding.left : padding.top; + const paddingEnd = horizontal ? padding.right : padding.bottom; + const paddingCross = horizontal ? padding.top : padding.left; + const paddingCrossEnd = horizontal ? padding.bottom : padding.right; + const crossPadding = paddingCross + paddingCrossEnd; + const headerSize = ListHeaderComponent ? listHeaderSize : 0; + const footerSize = ListFooterComponent ? listFooterSize : 0; + const itemAreaOffset = paddingStart + headerSize; + + // Main-axis viewport size (the scroll dimension): explicit style > + // parent cell bounds > self-measured. We trust measuredSize on the + // main axis because it represents how much room the parent's flex + // gave us — orthogonal to anything cell content does. + const explicitMain = horizontal + ? (style?.w as number | undefined) + : (style?.h as number | undefined); + const parentMain = horizontal + ? parentCellBounds?.width + : parentCellBounds?.height; + const measuredOuterMain = horizontal ? measuredSize.w : measuredSize.h; + const viewportSize = + explicitMain ?? + parentMain ?? + (measuredOuterMain > 0 ? measuredOuterMain : 0); + + // Cross-axis viewport size (the bounded dim). Priority order depends on + // orientation because `measuredOuterCross` means different things in + // each case: + // + // - Vertical VL: outerStyle has `flexGrow: 1`, so the FocusGroup gets + // its width from the parent's flex layout. `measuredOuterCross` is + // that parent-allocated viewport width — reliable, orthogonal to + // anything the cells do. Trust it over content reports. + // + // - Horizontal VL: outerStyle has no flex behavior on either axis, so + // the FocusGroup's height shrinks to content. `measuredOuterCross` + // is itself content-driven (fed by cellCrossSize → contentRef → self + // measure) and would create the prior architecture's feedback loop. + // In this case use `maxContentCross` (the natural cross dim of the + // tallest cell) so the VL grows to fit content on the unbounded axis. + // + // Without this asymmetry, vertical VLs end up with `cellCrossSize` set + // to the natural width of the widest row content (i.e. an inner + // horizontal scroller's full `totalContentSize` — many thousands of + // pixels) instead of the viewport width, and every row oscillates + // around its measured height as inner-VL bounds churn. + const explicitCross = horizontal + ? (style?.h as number | undefined) + : (style?.w as number | undefined); + const parentCross = horizontal + ? parentCellBounds?.height + : parentCellBounds?.width; + const measuredOuterCross = horizontal ? measuredSize.h : measuredSize.w; + + let viewportCrossSize: number; + + // Vertical VLs: parent and measured cross are reliable — for a vertical + // VL nested inside a column-flex parent, both report the full allocated + // column width, which is exactly what cells should fill. Cell-content + // cross sizes (per-row natural widths) are typically larger than the + // viewport in app rows that wrap inner horizontal scrollers; using them + // would set cellCross to the scroller's full content width. Skip. + // + // Horizontal VLs: parent and measured cross are NOT reliable — for a + // horizontal VL nested inside a parent section that contains other + // siblings (a title, etc.), the outer cell's measured height is + // title+innerVL+other, and `parentCellBounds.height` propagates that + // total down. The inner VL's cards only need their own height, not the + // section's. So for horizontal we prefer content-driven cellCross when + // available and only fall back to parent/measured/estimate when no + // content has been measured yet. + if (explicitCross != null && explicitCross > 0) { + viewportCrossSize = explicitCross; + } else if (!horizontal && parentCross != null && parentCross > 0) { + viewportCrossSize = parentCross; + } else if (!horizontal && measuredOuterCross > 0) { + viewportCrossSize = measuredOuterCross; + } else if (maxContentCross > 0) { + viewportCrossSize = maxContentCross + crossPadding; + } else if (parentCross != null && parentCross > 0) { + viewportCrossSize = parentCross; + } else if (measuredOuterCross > 0) { + viewportCrossSize = measuredOuterCross; + } else { + viewportCrossSize = estimatedItemSize; + } + + const cellCrossSize = (viewportCrossSize - crossPadding) / numColumns; + + // Lazy-init the LayoutManager via useState — the previous + // `useRef(null) + if (!ref.current) ref.current = new ...` pattern is a + // render-phase ref read AND write, which causes React Compiler to bail + // on memoizing `VirtualListInner`. `useState(() => new ...)` does the + // same thing and is compiler-friendly. Initial-mount-only side effects + // (measurement restore, onChange wiring) live inside the initializer. + const [layoutManager] = useState>(() => { + const lm = new LayoutManager({ data, estimatedItemSize, numColumns, overrideItemLayout, extraData, - separatorSize: 0, + separatorSize, + cellCrossSize, + keyExtractor, + }); + + if (initialRestoredState?.measurements) { + lm.setMeasurements(initialRestoredState.measurements); + } + + lm.setOnChange(() => { + setLayoutVersion((v) => v + 1); }); - } - const layoutManager = layoutManagerRef.current; + return lm; + }); useLayoutEffect(() => { if ( @@ -115,56 +278,129 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef v + 1); } - }, [data, estimatedItemSize, numColumns, overrideItemLayout, extraData]); - - const poolRef = useRef(null); - - if (!poolRef.current) { - poolRef.current = new RecyclerPool(); - } - - const pool = poolRef.current; - const cellNodesRef = useRef(new Map()); + }, [ + data, + estimatedItemSize, + numColumns, + overrideItemLayout, + extraData, + cellCrossSize, + keyExtractor, + separatorSize, + ]); + + // Lazy-init via useState for the same reason as `layoutManager`: + // `useRef(null) + if (!ref.current) ref.current = ...` is a render-phase + // ref read+write that bails out React Compiler's whole-function + // memoization. + const [pool] = useState( + () => new RecyclerPool(horizontal ? "h" : "v"), + ); const getKey = (index: number): string => - keyExtractor && data[index] !== undefined ? keyExtractor(data[index], index) : String(index); + keyExtractor && data[index] !== undefined + ? keyExtractor(data[index], index) + : String(index); const getData = (i: number) => data[i]; const getLayout = (i: number) => layoutManager.getLayout(i); - const paddingStart = horizontal ? padding.left : padding.top; - const paddingEnd = horizontal ? padding.right : padding.bottom; - const paddingCross = horizontal ? padding.top : padding.left; - const paddingCrossEnd = horizontal ? padding.bottom : padding.right; - const headerSize = ListHeaderComponent ? listHeaderSize : 0; - const footerSize = ListFooterComponent ? listFooterSize : 0; - const itemAreaOffset = paddingStart + headerSize; const totalContentSize = - paddingStart + headerSize + layoutManager.totalSize + footerSize + paddingEnd; - const contentCross = layoutManager.maxCrossSize || estimatedItemSize; - const crossPadding = paddingCross + paddingCrossEnd; - const contentCrossWithPadding = contentCross + crossPadding; - const finalCross = horizontal - ? Math.max(viewportCrossSize, contentCrossWithPadding) - : viewportCrossSize || contentCrossWithPadding; - const separatorCross = numColumns > 1 ? separatorSize : 0; - const cellCrossSize = - ((viewportCrossSize || contentCross) - crossPadding - separatorCross * (numColumns - 1)) / - numColumns; + paddingStart + + headerSize + + layoutManager.totalSize + + footerSize + + paddingEnd; + // Cross size of the scrollable content area: viewport size when known + // (so content fills its container); otherwise the cells' computed cross + // size plus padding. + const finalCross = + viewportCrossSize > 0 + ? viewportCrossSize + : cellCrossSize * numColumns + crossPadding; + + // Two-layer commit dampening: + // + // 1. While a scroll/focus-snap animation is running, LM is in batching + // mode — reports accumulate per-userKey and only commit on animation + // end (skipping the dampening path entirely). This keeps the layout + // frozen for the visible duration of the animation. + // + // 2. Outside of animations, LM uses per-userKey stability dampening + // with a backstop timer. Reports that differ from the stored value + // sit pending until either a matching report arrives after the + // stability window, or the backstop fires. This absorbs the + // multi-frame cascade where a section's measured height keeps + // shifting as inner cells/async content settle after the scroll. + // While true, contentStyle omits x/y so React reconciliation can't + // clobber the imperative animation in flight. Flipped by the animation + // start/end hooks; the stopped handler in useScrollHandler pins the + // final node.x/y, and the subsequent render (false again) writes the + // matching declarative value. + const [isScrollAnimating, setIsScrollAnimating] = useState(false); + + const handleAnimationStart = () => { + layoutManager.setBatching(true); + setIsScrollAnimating(true); + }; + const handleAnimationEnd = () => { + setIsScrollAnimating(false); + + if (layoutManager.setBatching(false)) { + setLayoutVersion((v) => v + 1); + } + }; + + // Cell handlers are declared HERE, before `pool.reconcile(...)` further + // below, because that method-call appears to be React Compiler 1.0's + // optimization boundary in this function — anything declared after is + // emitted as a plain inline `const` and isn't memoized. Cells consume + // these handlers as props and rely on stable identities so their + // `memo`/`areCellPropsEqual` short-circuit holds across VL renders. + // Each handler captures only stable values (layoutManager from a lazy + // `useState`, stable setters, refs accessed inside the closure body), + // so the compiler-emitted `if ($[i] !== layoutManager) ...` cache check + // is enough to keep them referentially stable. + const handleItemSizeChange = (userKey: string, measuredSize: number) => { + if (layoutManager.reportItemSize(userKey, measuredSize)) { + setLayoutVersion((v) => v + 1); + } + }; - const flushWithAnchorRef = useRef<() => void>(() => {}); + const handleItemEmpty = (userKey: string) => { + if (layoutManager.reportItemEmpty(userKey)) { + setLayoutVersion((v) => v + 1); + } + }; + + const handleSeparatorLayout = (size: number) => { + if (size > 0 && Math.abs(size - separatorSizeRef.current) >= 1) { + separatorSizeRef.current = size; + setSeparatorSize(size); + } + }; + + const handleContentCrossLayout = (size: number) => { + if (size > maxContentCrossRef.current) { + maxContentCrossRef.current = size; + setMaxContentCross(size); + } + }; const { contentRef, scrollOffsetRef, - animatingRef, - computeVisibleRange, + committedScrollOffset, scrollToOffset, scrollToIndex, scrollToEnd, handleChildFocused, + resetScroll, } = useScrollHandler({ layoutManager: layoutManager as LayoutManager, horizontal, @@ -179,46 +415,152 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef flushWithAnchorRef.current(), - onBeforeScroll: () => flushWithAnchorRef.current(), paddingStart, paddingEnd, + initialScrollOffset, + onAnimationStart: handleAnimationStart, + onAnimationEnd: handleAnimationEnd, }); - // Assigned in a layout effect to avoid ref mutation during render. - useLayoutEffect(() => { - flushWithAnchorRef.current = () => { - if (!layoutManager.hasPendingMeasurements) { - return; - } + // Persist scroll position to the parent's cache when the visible range + // commits. Focus changes write to the cache directly via handleVLFocus. + useEffect(() => { + if (cellKey == null || !parentStateCache) { + return; + } - const range = computeVisibleRange(); - const anchorIndex = range.endIndex >= range.startIndex ? range.startIndex : undefined; - const { changed, anchorDelta } = layoutManager.flushDeferred(anchorIndex); + parentStateCache.set(cellKey, { + scrollOffset: committedScrollOffset, + focusedIndex, + measurements: layoutManager.getMeasurements(), + }); + }, [ + cellKey, + committedScrollOffset, + focusedIndex, + parentStateCache, + layoutManager, + ]); + + // Detect cellKey change: this VL was recycled into a different row. + // Save the old row's state, restore the new row's state. Done as a + // derived-state-from-props pattern (setState during render) so the + // current render uses the new state — avoids a flash of stale + // scroll/focus. Stored as state (not a ref) so the comparison and + // setState pair don't trigger React Compiler's render-phase ref bailout. + const [prevCellKey, setPrevCellKey] = useState(cellKey); + + if (prevCellKey !== cellKey) { + if (prevCellKey != null && parentStateCache) { + parentStateCache.set(prevCellKey, { + // `committedScrollOffset` is the latest committed scroll for this + // (about-to-leave) cellKey. Reading `scrollOffsetRef.current` + // here would be a render-phase ref read — bailout territory — + // and the inner VL hasn't been animating, so the committed value + // matches `scrollOffsetRef.current` for our purposes. + scrollOffset: committedScrollOffset, + focusedIndex, + measurements: layoutManager.getMeasurements(), + }); + } - if (!changed) { - return; - } + // Apply the incoming config synchronously so the cells in THIS render + // lay out against the new content with correctly-keyed measurements. + // Without this, LM's `_data` would still be the prior content (the + // useLayoutEffect that calls updateConfig hasn't fired yet), so + // `_resolveSize` would derive userKeys from the old data and miss + // every entry in the just-restored measurements map. + layoutManager.updateConfig({ + data, + estimatedItemSize, + numColumns, + overrideItemLayout, + extraData, + cellCrossSize, + keyExtractor, + separatorSize, + }); - if (anchorDelta !== 0) { - const adjusted = scrollOffsetRef.current + anchorDelta; - scrollOffsetRef.current = adjusted; - const el = contentRef.current; - if (el) { - const value = -adjusted; - if (horizontal) { - el.node.x = value; - } else { - el.node.y = value; - } - } - } + const incoming = + cellKey != null && parentStateCache + ? parentStateCache.get(cellKey) + : undefined; + + resetScroll(incoming?.scrollOffset ?? 0); + // The `?? 0` fallback is load-bearing. The outer FG's `focusedElement` + // is element-stable across recycle (cells at each slotKey persist), so + // it points at whichever cell the user last focused under the prior + // cellKey. With focusedIndex=undefined no cell flips shouldFocus + // false→true and VirtualListCell's layoutEffect never calls + // `setFocusedChild` to overwrite that stale entry — the next traversal + // into this row would land on the stale cell. Defaulting to 0 makes + // cell 0 claim it, matching fresh-mount behavior where addElement + // sets the first focusable as `focusedElement`. + setFocusedIndex(incoming?.focusedIndex ?? 0); + + if (incoming?.measurements) { + layoutManager.setMeasurements(incoming.measurements); + } else { + layoutManager.clearMeasurements(); + } - setLayoutVersion((v) => v + 1); - }; - }); + setSkipNextFocus(true); + setPrevCellKey(cellKey); + } + + // Focus tracking: when a focusable descendant is focused, find which + // item index it lives in via its position relative to contentRef. Write + // directly to the parent's cache so the latest focused index survives + // recycle even when no scroll-driven useEffect runs between focus moves. + // + // Order matters: child's position relative to contentRef is independent + // of scroll (cells are absolutely positioned inside contentRef), so we + // can compute the target index BEFORE running alignment. Then we run + // alignment — which synchronously updates scrollOffsetRef.current to + // the snap target — and finally write to the cache. Writing before + // alignment would capture the pre-scroll offset, which on restore + // requires a second focus event to converge (the user sees the row + // land slightly off, then jump to the right spot). + const handleVLFocus = (child: LightningElement) => { + if (skipNextFocus) { + setSkipNextFocus(false); + handleChildFocused(child); + + return; + } + + const el = contentRef.current; + let resolvedIdx = -1; + + if (el) { + const pos = child.getRelativePosition(el); + const offset = horizontal ? pos.x : pos.y; + const offsetInItemSpace = Math.max(0, offset - itemAreaOffset); + + resolvedIdx = layoutManager.findIndexAtOffset(offsetInItemSpace); + } + + handleChildFocused(child); - const visibleRange = computeVisibleRange(); + if (resolvedIdx >= 0) { + setFocusedIndex(resolvedIdx); + + if (cellKey != null && parentStateCache) { + parentStateCache.set(cellKey, { + scrollOffset: scrollOffsetRef.current, + focusedIndex: resolvedIdx, + measurements: layoutManager.getMeasurements(), + }); + } + } + }; + + const scrollInItemSpace = Math.max(0, committedScrollOffset - itemAreaOffset); + const visibleRange = layoutManager.getVisibleRange( + scrollInItemSpace, + viewportSize, + drawDistance, + ); const visibleIndices: number[] = []; if (data.length > 0 && visibleRange.endIndex >= visibleRange.startIndex) { @@ -227,7 +569,6 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef(props: VirtualListProps, ref: ForwardedRef { - if (animatingRef.current) { - const layout = layoutManager.getLayout(index); - if (layout) { - const itemStart = itemAreaOffset + layout.offset; - const itemEnd = itemStart + layout.size; - const viewStart = scrollOffsetRef.current; - const viewEnd = viewStart + viewportSize; - if (itemEnd < viewStart || itemStart > viewEnd) { - layoutManager.deferMeasurement(index, size, crossSize); - return; - } - } - } - - if (layoutManager.reportItemSize(index, size, crossSize)) { - setLayoutVersion((v) => v + 1); - } - }, - [itemAreaOffset, viewportSize], - ); - - const handleCellNodeRef = useCallback((key: string, node: LightningElement | null) => { - if (node) { - cellNodesRef.current.set(key, node); - } else { - cellNodesRef.current.delete(key); - } - }, []); - const handleSeparatorLayout = useCallback((size: number) => { - if (size > 0 && size !== separatorSizeRef.current) { - separatorSizeRef.current = size; - setSeparatorSize(size); - layoutManager.updateConfig({ separatorSize: size }); - } - }, []); - - const handleViewportLayout = useCallback( - (event: { w: number; h: number }) => { - setMeasuredSize((prev) => { - if (prev.w === event.w && prev.h === event.h) { - return prev; - } - - return event; - }); - - onLayout?.(event); - }, - [onLayout], - ); + useLayoutEffect(() => { + maxContentCrossRef.current = 0; + setMaxContentCross(0); + // Per-key measurements are NOT cleared here on purpose. Tab switches + // and other re-renders often produce a new `data` array reference + // even when the underlying items (and their userKeys) haven't + // changed. Clearing measurements would force every cell to re-measure + // from estimate, and any render-time variance (focus state, async + // content) would shift row positions visibly. If a caller truly + // needs to invalidate measurements, they can call + // `layoutManager.clearMeasurements()` imperatively. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- intentional reset on data identity change + }, [data, extraData]); + + const handleViewportResize = (event: { w: number; h: number }) => { + setMeasuredSize((prev) => + prev.w === event.w && prev.h === event.h ? prev : event, + ); + }; useImperativeHandle(ref, () => ({ scrollToIndex: (params) => - scrollToIndex(params.index, params.animated, params.viewPosition, params.viewOffset), + scrollToIndex( + params.index, + params.animated, + params.viewPosition, + params.viewOffset, + ), scrollToOffset: (params) => scrollToOffset(params.offset, params.animated), scrollToEnd: (params) => scrollToEnd(params?.animated), getScrollOffset: () => scrollOffsetRef.current, @@ -350,83 +661,52 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef { - for (const index of visibleIndices) { - const layout = layoutManager.getLayout(index); - - if (!layout) { - continue; - } - - const slotKey = slotAssignments.get(index); - - if (!slotKey) { - continue; - } - - const cellNode = cellNodesRef.current.get(slotKey); - - if (!cellNode) { - continue; - } - - const mainPos = itemAreaOffset + layout.offset; - const crossPos = - numColumns > 1 - ? paddingCross + layout.column * (cellCrossSize + separatorCross) - : paddingCross + layout.crossOffset; - const targetX = horizontal ? mainPos : crossPos; - const targetY = horizontal ? crossPos : mainPos; - - if (cellNode.node.x !== targetX || cellNode.node.y !== targetY) { - cellNode.node.x = targetX; - cellNode.node.y = targetY; - } - } - }); - - const outerStyle = useMemo(() => { - const boundsDistance = drawDistance * 2; - - return { - flexGrow: horizontal ? undefined : 1, - flexShrink: horizontal ? undefined : 1, - clipping: true, - boundsMargin: horizontal - ? [0, boundsDistance, 0, boundsDistance] - : [boundsDistance, 0, boundsDistance, 0], - ...style, - ...(padding.backgroundColor != null ? { color: padding.backgroundColor } : undefined), - }; - }, [horizontal, drawDistance, style, padding.backgroundColor]); + const outerStyle: LightningViewElementStyle = { + flexGrow: horizontal ? undefined : 1, + flexShrink: horizontal ? undefined : 1, + clipping: true, + boundsMargin: horizontal + ? [0, drawDistance * 2, 0, drawDistance * 2] + : [drawDistance * 2, 0, drawDistance * 2, 0], + ...style, + ...(padding.backgroundColor != null + ? { color: padding.backgroundColor } + : undefined), + }; if (data.length === 0 && ListEmptyComponent) { return ( - - {renderListComponent(ListEmptyComponent)} - + + + {renderListComponent(ListEmptyComponent)} + + ); } - const scrollPosition = -scrollOffsetRef.current; + const scrollPosition = -committedScrollOffset; + // Skip declarative x/y while a scroll animation is in flight — the + // imperative `el.node.animate(...)` in useScrollHandler is the source + // of truth for position during that window. Reapplying the target via + // contentStyle on a mid-animation re-render (e.g. when the visible + // range commits) snaps the content past the interpolated value. const contentStyle: LightningViewElementStyle = horizontal ? { w: totalContentSize, h: finalCross, - x: scrollPosition, + ...(isScrollAnimating ? null : { x: scrollPosition }), } : { w: finalCross, h: totalContentSize, - y: scrollPosition, + ...(isScrollAnimating ? null : { y: scrollPosition }), }; const cells = visibleIndices.map((index) => { @@ -438,14 +718,18 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef 1 - ? paddingCross + layout.column * (cellCrossSize + separatorCross) - : paddingCross + layout.crossOffset; - + const crossPos = paddingCross + layout.crossOffset; const isLastItem = numColumns > 1 ? layout.column >= numColumns - 1 || index >= data.length - 1 @@ -456,64 +740,74 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef ); }); return ( - - - {ListHeaderComponent && ( - - {renderListComponent(ListHeaderComponent)} - - )} - - {cells} - - {ListFooterComponent && ( - - {renderListComponent(ListFooterComponent)} - - )} - - + + + + + {ListHeaderComponent && ( + + {renderListComponent(ListHeaderComponent)} + + )} + + {cells} + + {ListFooterComponent && ( + + {renderListComponent(ListFooterComponent)} + + )} + + + + ); } @@ -521,4 +815,4 @@ export const VirtualList = forwardRef(VirtualListInner) as ( props: VirtualListProps & { ref?: Ref }, ) => ReactElement | null; -(VirtualList as { displayName?: string }).displayName = 'VirtualList'; +(VirtualList as { displayName?: string }).displayName = "VirtualList"; diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx index 6997127..7b41187 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx @@ -1,152 +1,395 @@ import type { ComponentType, ReactElement } from 'react'; -import { memo, useEffect, useLayoutEffect, useRef } from 'react'; +import { memo, useLayoutEffect, useRef } from 'react'; import type { LightningElement, LightningViewElementStyle } from '@plextv/react-lightning'; +import { FocusGroup, useFocusManager } from '@plextv/react-lightning'; +import { FlexRoot } from '@plextv/react-lightning-plugin-flexbox'; +import { CellBoundsContext, VLCellKeyContext } from './VirtualListContext'; import type { VirtualListRenderItemInfo } from './VirtualListTypes'; export interface VirtualListCellProps { + /** Main-axis position of the cell within the content container. */ mainOffset: number; + /** Cross-axis position of the cell within the content container. */ crossOffset: number; + /** + * Main-axis size dictated by LayoutManager (estimate / override / last + * measurement). The cell wrapper is pinned to this exactly — VL is the + * single source of cell positioning. Measurement happens on the inner + * FlexRoot, not on this element. + */ + size: number; + /** Cross-axis size of the cell (from LayoutManager, viewport-driven). */ + crossSize: number; renderItem: (info: VirtualListRenderItemInfo) => ReactElement; item: T; index: number; + /** + * Content-identity key from VL's keyExtractor. Two roles: + * 1. Stable identity for measurement: passed to onItemSizeChange so + * LayoutManager keys measurements by userKey, not index. + * 2. Provided to descendants via VLCellKeyContext for nested-VL state + * persistence — when this cell's slot recycles to different content, + * the nested VL's `cellKey` context value flips, which fires its + * cellKey-change branch (save old measurements/scroll/focus, restore + * incoming) so the nested VL component instance survives the recycle. + */ + userKey: string; + /** + * Forwarded to renderItem's info as `shouldFocus`. True when this cell's + * item should auto-focus on mount (focus restoration after a recycle). + */ + shouldFocus: boolean; extraData?: unknown; horizontal: boolean; - numColumns: number; - itemSize: number; - cellCrossSize: number; isLastItem: boolean; ItemSeparatorComponent?: ComponentType | null; - onItemSizeChange?: (index: number, size: number, crossSize: number) => void; + /** + * True when the VL has a flex ancestor — yoga is already running in + * this subtree. The cell wraps content in a FlexRoot so the user's + * content can use flex layout AND so the cell can measure its rendered + * main-axis size and report it. When false, no flex is involved at all + * — cells are fully pinned and the caller is responsible for accurate + * `estimatedItemSize` / `overrideItemLayout` (FlashList v1 strict mode). + */ + isInFlex: boolean; + /** + * Called whenever the inner FlexRoot reports a new main-axis size for + * this cell. Only fired in flex mode — see `isInFlex`. The handler + * always receives a positive main-axis number; zero/negative reports + * are filtered out here. + */ + onItemSizeChange?: (userKey: string, size: number) => void; + /** + * Called when `renderItem` returns null. The VL marks the row as + * logically empty so layout collapses it to zero main-axis size. + * Distinct from `onItemSizeChange(userKey, 0)` (which is rejected) — + * this is the explicit "no content" path. + */ + onItemEmpty?: (userKey: string) => void; + /** + * Called whenever this cell's content reports a cross-axis size (flex + * mode only). VL keeps a single max across all reporters as a fallback + * cross size when no explicit `style.h` (or `.w` for vertical) and no + * `parentCellBounds` are available. Same dedupe approach as + * `onSeparatorLayout`. + */ + onContentCrossLayout?: (size: number) => void; + /** + * Called when this cell's separator first measures (flex mode only). + * VL is expected to dedupe — every cell that has a separator reports + * its size, but they're all the same component so VL only acts on the + * first non-zero value (or genuine changes thereafter). Cross-axis is + * ignored; only the main-axis dimension is reported. + */ onSeparatorLayout?: (size: number) => void; - cellKey: string; - onNodeRef?: (key: string, node: LightningElement | null) => void; + /** + * True when this cell is currently being held in the recycler pool — + * i.e. it's mounted (so its React subtree and any nested recycler + * pools survive) but rendered offscreen and excluded from focus + * traversal. The cell's outer FocusGroup is disabled so spatial + * navigation skips both the cell and its descendants. + */ + pooled?: boolean; } const VirtualListCellInner = ({ mainOffset, crossOffset, + size, + crossSize, renderItem, item, index, + userKey, + shouldFocus, extraData, horizontal, - numColumns, - itemSize, - cellCrossSize, isLastItem, ItemSeparatorComponent, + isInFlex, onItemSizeChange, + onItemEmpty, + onContentCrossLayout, onSeparatorLayout, - cellKey, - onNodeRef, -}: VirtualListCellProps): ReactElement => { - const outerRef = useRef(null); + pooled = false, +}: VirtualListCellProps): ReactElement | null => { + // Cell wrapper is positioned and sized by VL exclusively. It uses + // absolute positioning with explicit width AND height — no flex on this + // element. VL is the single source of where and how big a cell is. + const cellStyle: LightningViewElementStyle = { + position: 'absolute', + x: horizontal ? mainOffset : crossOffset, + y: horizontal ? crossOffset : mainOffset, + w: horizontal ? size : crossSize, + h: horizontal ? crossSize : size, + }; + const cellBounds = { + width: horizontal ? size : crossSize, + height: horizontal ? crossSize : size, + }; + + const flexRootRef = useRef(null); + // The cell's outer FocusGroup element. We need a ref to it so we can + // imperatively claim focus when `shouldFocus` flips false → true on an + // already-mounted cell — `useFocus.setAutoFocus` only updates the + // property, it doesn't actively claim focus. Without this, focus + // restoration on slot recycle would silently fail. + const cellElementRef = useRef(null); + const prevShouldFocusRef = useRef(shouldFocus); + const focusManager = useFocusManager(); + + const renderedItem = renderItem({ item, index, extraData, shouldFocus }); + const isEmpty = renderedItem == null; + + // Imperative focus-tree update on shouldFocus transition. Only fires + // on a false → true flip for an existing cell — fresh mounts are + // handled by FocusGroup's autoFocus via useFocus → addElement. + // + // Why `setFocusedChild` and not `focus()` (either on the element or via + // FocusManager): when a parent VL recycles its slot back to this row's + // userKey, the inner VL hits its cellKey-change branch and restores + // `focusedIndex = N` from the cache. That happens during the parent's + // re-render, BEFORE the user has actually navigated to this row — at + // that moment the user is still focused on whichever row they pressed + // up from. `focusManager.focus(cellN)` walks up setting parent + // focusedElement at every level and runs _recalculateFocusPath, which + // would yank the user's focus across rows. We only want to record + // "when focus next traverses into this inner VL, land on cellN" — i.e. + // update the parent's focusedElement and let _recalculateFocusPath + // decide whether the current path actually intersects (it doesn't, in + // the off-screen restore case, so nothing visible changes). + // + // Calling `cellElementRef.current.focus()` directly is also wrong: it + // only flips `_focused` on the Lightning element and the manager's + // focusedElement chain stays pointing at whatever sibling slot the + // intermediate row's session left behind, so the next traversal lands + // on the wrong cell. useLayoutEffect(() => { - if (outerRef.current) { - onNodeRef?.(cellKey, outerRef.current); + if (shouldFocus && !prevShouldFocusRef.current && cellElementRef.current) { + focusManager.setFocusedChild(cellElementRef.current); } - return () => { - onNodeRef?.(cellKey, null); - }; - }, [cellKey, onNodeRef]); + prevShouldFocusRef.current = shouldFocus; + }, [shouldFocus, focusManager]); - const contentSizeRef = useRef({ w: 0, h: 0 }); - const prevIndexRef = useRef(index); + const handleResize = (event: { w: number; h: number }) => { + const main = horizontal ? event.w : event.h; + const cross = horizontal ? event.h : event.w; - useEffect(() => { - if (prevIndexRef.current !== index) { - const { w, h } = contentSizeRef.current; + if (main > 0) { + onItemSizeChange?.(userKey, main); + } - prevIndexRef.current = index; + if (cross > 0) { + onContentCrossLayout?.(cross); + } + }; - if (w > 0 || h > 0) { - const size = horizontal ? w : h; - const crossSize = horizontal ? h : w; + // If `renderItem` returns null, signal LM to collapse this row to zero + // main-axis size. Otherwise the row would still occupy `firstMeasured` + // / `estimatedItemSize` worth of space (and following items would be + // pushed down by a phantom gap). Effect fires after the render commits + // — order doesn't matter; `reportItemEmpty` is idempotent and dedupes. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemEmpty is intentionally + // omitted: it captures only stable values (LayoutManager from useState lazy init, stable + // setters), so a "stale" closure is functionally identical to a fresh one. Including it + // in deps would force the effect to re-fire on every VL render because React Compiler + // doesn't memoize VL's cell handlers (they live after pool.reconcile, the compiler's + // optimization boundary in VirtualListInner). + useLayoutEffect(() => { + if (isEmpty && onItemEmpty) { + onItemEmpty(userKey); + } + }, [isEmpty, userKey]); - onItemSizeChange?.(index, size, crossSize); - } + // One-shot push on mount and on `userKey` change (slot recycle). All + // ongoing size changes after that flow through `handleResize` → + // `onResize` (NodeResizeObserver). We need this hook because + // NodeResizeObserver only fires when the FlexRoot's own dimensions + // change — when a slot recycles to new content that happens to lay out + // at the same size as the previous occupant, the observer is silent + // but LM still needs the per-key measurement under the new userKey. + // RAF defers until after yoga's layout pass so node.w/h reflect the + // post-render dimensions. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemSizeChange / onContentCrossLayout + // are intentionally omitted: VL's compiler-generated output (current React Compiler 1.0) + // doesn't memoize cell handlers because they live after pool.reconcile (an apparent + // optimization boundary). Their closures only capture stable values (LayoutManager from + // useState lazy init, stable setters), so a "stale" reference is functionally equivalent + // to a fresh one. Including them in deps would re-fire this effect on every VL render + // and re-push every cell's measurement uselessly. + useLayoutEffect(() => { + if (!isInFlex || !onItemSizeChange || isEmpty) { + return; } - }); - const handleContentLayout = (event: { w: number; h: number }) => { - const size = horizontal ? event.w : event.h; - const crossSize = horizontal ? event.h : event.w; + const rafId = requestAnimationFrame(() => { + const node = flexRootRef.current?.node; - contentSizeRef.current = event; + if (!node) { + return; + } - onItemSizeChange?.(index, size, crossSize); - }; + const main = horizontal ? node.w : node.h; + const cross = horizontal ? node.h : node.w; - const crossAxisSeparator = numColumns > 1; + if (main > 0) { + onItemSizeChange(userKey, main); + } - const handleSeparatorLayout = (event: { w: number; h: number }) => { - const alongX = crossAxisSeparator ? !horizontal : horizontal; - const size = alongX ? event.w : event.h; + if (cross > 0) { + onContentCrossLayout?.(cross); + } + }); - onSeparatorLayout?.(size); - }; + return () => { + cancelAnimationFrame(rafId); + }; + }, [userKey, isInFlex, isEmpty, horizontal]); - const separatorPosition: { x: number } | { y: number } = crossAxisSeparator - ? horizontal - ? { y: cellCrossSize } - : { x: cellCrossSize } - : horizontal - ? { x: itemSize } - : { y: itemSize }; + const separatorPosition: { x: number } | { y: number } = horizontal + ? { x: size } + : { y: size }; - const cellStyle: LightningViewElementStyle = { - position: 'absolute', - x: horizontal ? mainOffset : crossOffset, - y: horizontal ? crossOffset : mainOffset, - w: horizontal ? undefined : cellCrossSize, - h: horizontal ? cellCrossSize : undefined, + // Return null AFTER the hooks so we don't paint an empty cell wrapper. + // The empty-row effect above signals LM via `onItemEmpty` so the row + // collapses to zero main-axis size in layout. + if (isEmpty) { + return null; + } + + const innerContent = ( + + {renderedItem} + + ); + + // Two paths: + // + // - In flex (`isInFlex`): yoga is already running in this subtree, so + // we wrap content in a FlexRoot. The FlexRoot has NO width/height + // pinning — yoga sizes it to fit content on both axes. The cell + // reports both axes back to VL via `handleResize`: main-axis goes + // into LayoutManager's per-key measurement store, cross-axis goes + // into VL's max-content-cross fallback. Leaving the cross axis + // unpinned is what lets VL size to content when no explicit cross + // source is available — pinning it would create the feedback loop + // the prior architecture had (cell reports its own pinned size back + // to VL, which uses that to compute the pin, etc.). + // + // Tradeoff: flex content using cross-axis percentages (e.g. + // `width: '100%'` inside a vertical VL's cell) won't work, because + // the FlexRoot has no fixed cross dim to compute the percentage + // against. Callers needing those layouts should set `style.h` (or + // `.w`) on the VL — that flips the chain into the explicit branch + // and `crossSize` becomes a real number the renderItem can target. + // + // - Not in flex: there is no yoga in this subtree. Adding a FlexRoot + // would force one to spin up just for measurement, which has cost + // and no benefit (the user's renderItem isn't using flex anyway). + // We render content plainly. No measurement is reported; the cell + // stays at exactly the size LayoutManager dictated, so the caller + // is responsible for accurate `estimatedItemSize` / + // `overrideItemLayout`. + const measuredContent = isInFlex ? ( + + {innerContent} + + ) : ( + innerContent + ); + + const handleSeparatorResize = (event: { w: number; h: number }) => { + const measured = horizontal ? event.w : event.h; + + if (measured > 0) { + onSeparatorLayout?.(measured); + } }; + // Separator: in flex mode, wrap in a FlexRoot so yoga measures it and + // we can report the size up to VL. VL dedupes — every cell reports the + // same size, but VL only acts on changes. In pinned mode there's no + // measurement, so the separator just renders at its own intrinsic size + // (which the caller is responsible for setting via explicit dims), and + // VL keeps `separatorSize` at whatever the caller passed in (default 0). + let separatorEl: ReactElement | null = null; + + if (ItemSeparatorComponent && !isLastItem) { + const SeparatorComponent = ItemSeparatorComponent; + const separatorContent = isInFlex ? ( + + + + ) : ( + + ); + + separatorEl = ( + + {separatorContent} + + ); + } + + // Each cell is its own FocusGroup. Spatial navigation within a cell + // (e.g., a row with multiple buttons) stays inside the cell until + // there's no candidate, then bubbles up to the VL's outer FocusGroup + // for cross-cell movement. autoFocus={shouldFocus} restores focus on + // initial mount; the imperative `cellElementRef.current.focus()` in + // the layoutEffect above handles subsequent recycle-time transitions. return ( - - {renderItem({ item, index, extraData })} - {ItemSeparatorComponent && !isLastItem && ( - - - - )} - + + {measuredContent} + {separatorEl} + ); }; -// Ignores mainOffset/crossOffset — positions are applied directly to nodes. function areCellPropsEqual( prev: VirtualListCellProps, next: VirtualListCellProps, ): boolean { + // The four `on*` callbacks below are intentionally skipped in this + // comparison. React Compiler doesn't memoize them inside VirtualList + // (they're inline closures defined after `pool.reconcile(...)`, which + // appears to be the compiler's optimization boundary in this function), + // so they're fresh references on every VL render. But each captures + // only stable values — `layoutManager` from a lazy `useState` (set + // once), the stable setState setters, and refs accessed inside the + // closure body. A "stale" callback from a prior render is functionally + // identical to a fresh one, so we don't need to bust memoization on + // identity. Comparing them would force every visible cell to re-render + // on every VL render, defeating the whole point of `memo`. return ( + prev.mainOffset === next.mainOffset && + prev.crossOffset === next.crossOffset && + prev.size === next.size && + prev.crossSize === next.crossSize && prev.renderItem === next.renderItem && prev.item === next.item && prev.index === next.index && + prev.userKey === next.userKey && + prev.shouldFocus === next.shouldFocus && prev.extraData === next.extraData && prev.horizontal === next.horizontal && - prev.numColumns === next.numColumns && - prev.itemSize === next.itemSize && - prev.cellCrossSize === next.cellCrossSize && prev.isLastItem === next.isLastItem && prev.ItemSeparatorComponent === next.ItemSeparatorComponent && - prev.onItemSizeChange === next.onItemSizeChange && - prev.onSeparatorLayout === next.onSeparatorLayout && - prev.cellKey === next.cellKey && - prev.onNodeRef === next.onNodeRef + prev.isInFlex === next.isInFlex && + prev.pooled === next.pooled ); } -export const VirtualListCell = memo(VirtualListCellInner, areCellPropsEqual) as ( +export const VirtualListCell = memo(VirtualListCellInner, areCellPropsEqual) as (( props: VirtualListCellProps, -) => ReactElement; +) => ReactElement | null) & { displayName?: string }; + +VirtualListCell.displayName = 'VirtualListCell'; diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListContent.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualListContent.tsx deleted file mode 100644 index 11d7147..0000000 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualListContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - type ForwardRefExoticComponent, - forwardRef, - type PropsWithChildren, - type RefAttributes, -} from 'react'; - -import type { LightningElement, LightningElementStyle } from '@plextv/react-lightning'; - -type Props = PropsWithChildren<{ - style: LightningElementStyle; -}> & - RefAttributes; - -export const VirtualListContent: ForwardRefExoticComponent = forwardRef< - LightningElement, - Props ->(({ style, children }, ref) => { - return ( - - {children} - - ); -}); - -VirtualListContent.displayName = 'VirtualListContent'; diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts b/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts new file mode 100644 index 0000000..7a5ec93 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts @@ -0,0 +1,51 @@ +import { type Context, createContext } from 'react'; + +export interface CellBounds { + /** Cell's width in pixels — always set, always positive when known. */ + width: number; + /** Cell's height in pixels — always set, always positive when known. */ + height: number; +} + +export const CellBoundsContext: Context = createContext(null); + +/** + * State that survives a cell recycle/remount so the next time a row with the + * same identity becomes visible, it picks up where it left off. + */ +export interface VLPersistedState { + scrollOffset: number; + /** + * Last item index that had focus inside this VL. On remount, the cell at + * this index gets `shouldFocus: true` so renderItem can opt into focusing + * the right item. + */ + focusedIndex?: number; + /** + * Snapshot of the VL's per-userKey measurement map at save time. On + * remount, the new VL seeds its LayoutManager with this so the row's + * content (inner cells, sections) doesn't have to re-measure from + * estimate — which would otherwise cascade through every following item + * via layoutVersion bumps as each cell pushes its first measurement. + * + * The cache holds a reference, not a copy. The producing VL is unmounted + * by the time this gets read, so no aliasing concern. + */ + measurements?: Map; +} + +/** + * Identity key of the enclosing cell (from the parent VL's keyExtractor). + * A nested VL reads this to know which slot in the parent VL's state cache + * belongs to it. + */ +export const VLCellKeyContext: Context = createContext(null); + +/** + * Per-VL state cache. The VL provides this to its descendants so a nested + * VL inside a cell can persist its scroll position + focused index across + * recycle remounts (keyed by the nested VL's enclosing cell key). + */ +export const VLStateCacheContext: Context | null> = createContext< + Map | null +>(null); diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts b/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts index 16bf4e6..daece40 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts @@ -6,6 +6,12 @@ export interface VirtualListRenderItemInfo { item: T; index: number; extraData?: unknown; + /** + * True when this item should receive focus on mount — set by VirtualList + * when restoring a previously-focused item after a recycle remount. Pass + * to the focusable element's autoFocus prop. + */ + shouldFocus?: boolean; } export interface OverrideItemLayout { diff --git a/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts index aa26a80..18b4fa9 100644 --- a/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts +++ b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts @@ -23,25 +23,39 @@ export interface UseScrollHandlerOptions { onScroll?: (event: ScrollEvent) => void; onEndReached?: () => void; onEndReachedThreshold: number; - /** Called when a scroll animation finishes. */ - onAnimationEnd?: () => void; - /** Called before a scroll begins (e.g. to flush deferred measurements). */ - onBeforeScroll?: () => void; /** Main-axis start padding (acts as scroll margin). */ paddingStart: number; /** Main-axis end padding (acts as scroll margin). */ paddingEnd: number; + /** + * Initial scroll offset to start at — used when restoring state for a + * recycled VL whose previous scroll position was cached. + */ + initialScrollOffset?: number; + /** + * Fired the moment a scroll/focus-snap animation begins — i.e. when + * `scrollToOffset(_, animated=true)` enters the animated branch and is + * not already in flight. VL uses this to enable LayoutManager batching + * so intermediate yoga measurements during the animation don't reflow + * the layout on every frame. + */ + onAnimationStart?: () => void; + /** + * Fired when the most recent scroll animation finishes (its `stopped` + * event fires while still being the current one) OR when `resetScroll` + * cancels an animation that was in flight. VL uses this to flush the + * batched measurements and bump layoutVersion in one go. + */ + onAnimationEnd?: () => void; } -export function useScrollHandler(options: UseScrollHandlerOptions): { +export interface UseScrollHandlerResult { contentRef: RefObject; + /** Live scroll offset — updated on every scroll, including mid-animation. */ scrollOffsetRef: RefObject; - animatingRef: RefObject; + /** Last scroll offset at which the visible range changed — safe to read during render. */ + committedScrollOffset: number; maxScroll: number; - computeVisibleRange: () => { - startIndex: number; - endIndex: number; - }; scrollToOffset: (offset: number, animated?: boolean) => void; scrollToIndex: ( index: number, @@ -51,7 +65,15 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { ) => void; scrollToEnd: (animated?: boolean) => void; handleChildFocused: (child: LightningElement) => void; -} { + /** + * Imperatively jump scroll state to an absolute offset without animation + * or onScroll/onEndReached side-effects. Used to restore cached scroll + * position when a recycled VL switches to a different cellKey. + */ + resetScroll: (offset: number) => void; +} + +export function useScrollHandler(options: UseScrollHandlerOptions): UseScrollHandlerResult { const { layoutManager, horizontal, @@ -66,26 +88,40 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { onScroll, onEndReached, onEndReachedThreshold, - onAnimationEnd, - onBeforeScroll, paddingStart, paddingEnd, + initialScrollOffset = 0, + onAnimationStart, + onAnimationEnd, } = options; const contentRef = useRef(null); - const scrollOffsetRef = useRef(0); + const scrollOffsetRef = useRef(initialScrollOffset); const endReachedRef = useRef(false); - const lastRangeRef = useRef({ startIndex: 0, endIndex: -1 }); - const animatingRef = useRef(false); const animationIdRef = useRef(0); - const onAnimationEndRef = useRef(onAnimationEnd); - const onBeforeScrollRef = useRef(onBeforeScroll); - const [, setScrollVersion] = useState(0); - + // True from the moment an animated scroll begins until the final + // `stopped` event fires (or `resetScroll` cancels). Guards both ends + // of the start/end notification so chained animations only fire one + // start and one end overall. + const isAnimatingRef = useRef(false); + const [committedScrollOffset, setCommittedScrollOffset] = useState(initialScrollOffset); + + // Apply the restored scroll offset to lightning on mount. useState's + // initializer keeps committedScrollOffset in sync, but the contentRef's + // node still needs node.x/y written so the scroll position is visually + // correct from the first frame. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- mount-only restore useEffect(() => { - onAnimationEndRef.current = onAnimationEnd; - onBeforeScrollRef.current = onBeforeScroll; - }); + if (initialScrollOffset > 0 && contentRef.current) { + const value = -initialScrollOffset; + + if (horizontal) { + contentRef.current.node.x = value; + } else { + contentRef.current.node.y = value; + } + } + }, []); const maxScroll = Math.max(0, totalContentSize - viewportSize); @@ -103,9 +139,13 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { const value = -offset; if (animated && animationDuration > 0) { - animatingRef.current = true; - const thisAnimId = ++animationIdRef.current; + + if (!isAnimatingRef.current) { + isAnimatingRef.current = true; + onAnimationStart?.(); + } + const anim = horizontal ? el.node.animate({ x: value }, { duration: animationDuration, easing: 'ease-out' }) : el.node.animate({ y: value }, { duration: animationDuration, easing: 'ease-out' }); @@ -115,8 +155,6 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { return; } - animatingRef.current = false; - // Pin the position so reconciliation doesn't reset it if (horizontal) { el.node.x = value; @@ -124,13 +162,12 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { el.node.y = value; } - onAnimationEndRef.current?.(); + isAnimatingRef.current = false; + onAnimationEnd?.(); }); anim.start(); } else { - animatingRef.current = false; - if (horizontal) { el.node.x = value; } else { @@ -139,31 +176,18 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { } } - function computeVisibleRange() { - const scrollInItemSpace = Math.max(0, scrollOffsetRef.current - itemAreaOffset); - - return layoutManager.getVisibleRange(scrollInItemSpace, viewportSize, drawDistance); - } - - function checkRangeChanged(): void { - const range = computeVisibleRange(); - const last = lastRangeRef.current; - - if (range.startIndex !== last.startIndex || range.endIndex !== last.endIndex) { - lastRangeRef.current = range; - setScrollVersion((v) => v + 1); - } - } - function scrollToOffset(offset: number, animated = true): void { - onBeforeScrollRef.current?.(); - const clamped = clamp(offset); scrollOffsetRef.current = clamped; applyPosition(clamped, animated); - checkRangeChanged(); + // Commit unconditionally. `committedScrollOffset` drives `contentStyle.x` + // on the post-animation render and the parent's state-cache write — both + // need the actual scroll, not just the offset of the last range change. + // Same-value setState bails out of rendering, so a no-op commit costs + // nothing. + setCommittedScrollOffset(clamped); if (onEndReached) { const distFromEnd = totalContentSize - clamped - viewportSize; @@ -244,15 +268,42 @@ export function useScrollHandler(options: UseScrollHandlerOptions): { scrollToOffset(target, true); } + function resetScroll(offset: number): void { + scrollOffsetRef.current = offset; + setCommittedScrollOffset(offset); + + // Cancel any in-flight scroll animation so it doesn't snap back. + animationIdRef.current++; + + if (isAnimatingRef.current) { + isAnimatingRef.current = false; + onAnimationEnd?.(); + } + + // Apply directly to lightning so the next paint reflects the new + // scroll without waiting for React's reconciliation to flush. + const el = contentRef.current; + + if (el) { + const value = -offset; + + if (horizontal) { + el.node.x = value; + } else { + el.node.y = value; + } + } + } + return { contentRef, scrollOffsetRef, - animatingRef, + committedScrollOffset, maxScroll, - computeVisibleRange, scrollToOffset, scrollToIndex, scrollToEnd, handleChildFocused, + resetScroll, }; } diff --git a/packages/react-lightning/package.json b/packages/react-lightning/package.json index d63e824..50bf858 100644 --- a/packages/react-lightning/package.json +++ b/packages/react-lightning/package.json @@ -54,7 +54,7 @@ }, "peerDependencies": { "@lightningjs/renderer": "catalog:", - "react": "catalog:peers" + "react": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning/src/element/LightningViewElement.ts b/packages/react-lightning/src/element/LightningViewElement.ts index 8244faf..60be67e 100644 --- a/packages/react-lightning/src/element/LightningViewElement.ts +++ b/packages/react-lightning/src/element/LightningViewElement.ts @@ -15,6 +15,7 @@ import type { import type { Fiber } from 'react-reconciler'; import { EventEmitter, type IEventEmitter } from 'tseep'; +import { getNodeResizeObserver, type NodeResizeObserver } from '../observer/NodeResizeObserver'; import type { Plugin } from '../render/Plugin'; import { type Focusable, @@ -100,6 +101,8 @@ export class LightningViewElement< private _eventEmitter = new EventEmitter(); private _deferTarget: LightningElement | null = null; private _deferNodeRemovalHandler: ((destroy: () => void) => void) | null = null; + private _resizeObserver: NodeResizeObserver | null = null; + private _isObservingResize = false; public get visible(): boolean { return this._visible; @@ -310,10 +313,19 @@ export class LightningViewElement< LightningViewElement.allElements[this.id] = this; + if (this.props.onResize) { + this._reconcileResizeObserving(); + } + this._eventEmitter.emit('initialized'); } public destroy(): void { + if (this._isObservingResize && this._resizeObserver) { + this._resizeObserver.unobserve(this); + this._isObservingResize = false; + } + this.node.off('inViewport', this._onInViewport); this.node.off('loaded', this._onTextureLoaded); this.node.off('failed', this._onTextureFailed); @@ -340,17 +352,34 @@ export class LightningViewElement< public on = (...args: Parameters['on']>): (() => void) => { this._eventEmitter.on(...args); - return () => this._eventEmitter.off(...args); + + if (args[0] === 'resized') { + this._reconcileResizeObserving(); + } + + return () => this.off(...args); }; public once = ( ...args: Parameters['once']> ): (() => void) => { this._eventEmitter.once(...args); - return () => this._eventEmitter.off(...args); + + if (args[0] === 'resized') { + this._reconcileResizeObserving(); + } + + return () => this.off(...args); }; - public off: IEventEmitter['off'] = (...args) => - this._eventEmitter.off(...args); + public off: IEventEmitter['off'] = (...args) => { + const result = this._eventEmitter.off(...args); + + if (args[0] === 'resized') { + this._reconcileResizeObserving(); + } + + return result; + }; public emit: IEventEmitter['emit'] = (...args) => this._eventEmitter.emit(...args); @@ -404,10 +433,18 @@ export class LightningViewElement< public removeChild(child: LightningElement): void { const index = this.children.indexOf(child); - if (index >= 0) { - this.children.splice(index, 1); + // Idempotent — React's reconciler hits this path twice per unmount: + // first via the host-config `removeChild`, then again from inside + // `destroy()` (`this.parent?.removeChild(this)`). The second call is + // a no-op for `this.children` but used to re-emit `childRemoved`, + // doubling downstream worker traffic in plugin-flexbox (each emit + // fires `removeNode` + `queueRender` etc.). + if (index < 0) { + return; } + this.children.splice(index, 1); + if (!child._deferTarget) { child.node.parent = null; } @@ -570,6 +607,17 @@ export class LightningViewElement< this._onLayout(dimensions); } + /** + * Invoked by {@link NodeResizeObserver} at the start of each frame when + * the observed node's width or height has changed since the prior frame. + * + * Not intended to be called by application code. + */ + public _emitResize(dimensions: { w: number; h: number }): void { + this._eventEmitter.emit('resized', this, dimensions); + this.props.onResize?.(dimensions); + } + public recalculateVisibility = (): void => { const prevFocusable = this.focusable; const prevVisible = this._visible; @@ -623,6 +671,27 @@ export class LightningViewElement< this._renderer.destroyNode(this.node); }; + private _reconcileResizeObserving(): void { + const shouldObserve = + this.props.onResize != null || this._eventEmitter.hasListeners('resized'); + + if (shouldObserve === this._isObservingResize) { + return; + } + + if (shouldObserve) { + if (this._resizeObserver === null) { + this._resizeObserver = getNodeResizeObserver(this._renderer); + } + + this._resizeObserver.observe(this); + this._isObservingResize = true; + } else if (this._resizeObserver) { + this._resizeObserver.unobserve(this); + this._isObservingResize = false; + } + } + // Don't pass down the `data` prop to the lightning node. private _createNode({ data: _data, ...props }: Partial): RendererNode { const node = this.isTextElement @@ -661,6 +730,7 @@ export class LightningViewElement< const transformedProps = this._transformProps(payload) ?? ({} as TProps); const previousOpacity = this.node.alpha; + const previousOnResize = this.props.onResize; let changed = false; @@ -671,6 +741,10 @@ export class LightningViewElement< } } + if (previousOnResize !== this.props.onResize) { + this._reconcileResizeObserving(); + } + const lngProps = this._toLightningNodeProps({ ...this.props, ...transformedProps, diff --git a/packages/react-lightning/src/focus/FocusManager.ts b/packages/react-lightning/src/focus/FocusManager.ts index 797efad..c919c41 100644 --- a/packages/react-lightning/src/focus/FocusManager.ts +++ b/packages/react-lightning/src/focus/FocusManager.ts @@ -292,6 +292,41 @@ export class FocusManager< } } + /** + * Mark `element` as the preferred focus target of its immediate parent + * without walking up the tree or stealing focus from elsewhere. + * + * Use case: a virtualised-list cell whose `shouldFocus` flips true on + * slot recycle while the user is focused on a different subtree. The + * parent's `focusedElement` may still point at a stale sibling slot + * from the row this slot served previously, so the next time focus + * actually traverses into this group it would land on the wrong cell. + * Setting the parent's `focusedElement` here updates the tree so that + * future traversal resolves correctly. `_recalculateFocusPath` is + * still invoked: if the parent is already in the active focus path + * (the user is on this group), focus moves from the old child to the + * new one as expected; otherwise the path is unchanged and the user's + * current focus is left alone. + */ + public setFocusedChild(element: T): void { + const node = this.activeLayer.elements.get(element); + + if (!node) { + return; + } + + if (!element.focusable || hasExternalRedirect(node)) { + return; + } + + if (node.parent.focusedElement === node) { + return; + } + + node.parent.focusedElement = node; + this._recalculateFocusPath(); + } + public pushLayer(): void { // Store the current layer before creating new one const previousLayer = this.activeLayer; diff --git a/packages/react-lightning/src/focus/useFocus.tsx b/packages/react-lightning/src/focus/useFocus.tsx index c6fdec0..3156674 100644 --- a/packages/react-lightning/src/focus/useFocus.tsx +++ b/packages/react-lightning/src/focus/useFocus.tsx @@ -63,12 +63,6 @@ export function useFocus( destinations, allowOffscreen, }); - } else if (import.meta.env.DEV && ref.current && !parentFocusable) { - console.warn( - 'useFocus: Element exists but no parent FocusGroup found. ' + - 'This element will not participate in focus management. ' + - 'Wrap it in a FocusGroup or ensure the parent FocusGroup has mounted.', - ); } return () => { diff --git a/packages/react-lightning/src/index.ts b/packages/react-lightning/src/index.ts index 188cfca..4f51de4 100644 --- a/packages/react-lightning/src/index.ts +++ b/packages/react-lightning/src/index.ts @@ -1,4 +1,5 @@ export { Canvas } from './components/Canvas'; +export type { CanvasProps } from './components/Canvas/CanvasProps'; export { AllStyleProps } from './element/AllStyleProps'; export { createLightningElement } from './element/createLightningElement'; export { LightningImageElement } from './element/LightningImageElement'; diff --git a/packages/react-lightning/src/observer/NodeResizeObserver.spec.ts b/packages/react-lightning/src/observer/NodeResizeObserver.spec.ts new file mode 100644 index 0000000..50b7e4f --- /dev/null +++ b/packages/react-lightning/src/observer/NodeResizeObserver.spec.ts @@ -0,0 +1,302 @@ +import type { RendererMain } from '@lightningjs/renderer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { LightningElement } from '../types'; +import { getNodeResizeObserver, NodeResizeObserver } from './NodeResizeObserver'; + +type FrameTickHandler = () => void; + +interface MockRenderer { + on: ReturnType; + off: ReturnType; + tick(): void; + handlerCount(): number; +} + +function createMockRenderer(): MockRenderer { + const handlers = new Set(); + + const renderer: MockRenderer = { + on: vi.fn((event: string, handler: FrameTickHandler) => { + if (event === 'frameTick') { + handlers.add(handler); + } + }), + off: vi.fn((event: string, handler: FrameTickHandler) => { + if (event === 'frameTick') { + handlers.delete(handler); + } + }), + tick() { + for (const handler of handlers) { + handler(); + } + }, + handlerCount() { + return handlers.size; + }, + }; + + return renderer; +} + +interface MockElement { + node: { w: number; h: number }; + _emitResize: ReturnType; +} + +function createMockElement(w = 0, h = 0): MockElement { + return { + node: { w, h }, + _emitResize: vi.fn(), + }; +} + +function asObserverArg(el: MockElement): LightningElement { + return el as unknown as LightningElement; +} + +function asRendererArg(r: MockRenderer): RendererMain { + return r as unknown as RendererMain; +} + +describe('NodeResizeObserver', () => { + let renderer: MockRenderer; + let observer: NodeResizeObserver; + + beforeEach(() => { + renderer = createMockRenderer(); + observer = new NodeResizeObserver(asRendererArg(renderer)); + }); + + describe('lazy frameTick subscription', () => { + it('does not subscribe to frameTick before any element is observed', () => { + expect(renderer.handlerCount()).toBe(0); + expect(renderer.on).not.toHaveBeenCalled(); + }); + + it('subscribes on the first observe', () => { + observer.observe(asObserverArg(createMockElement())); + + expect(renderer.handlerCount()).toBe(1); + expect(renderer.on).toHaveBeenCalledWith('frameTick', expect.any(Function)); + }); + + it('stays subscribed while at least one element is observed', () => { + const a = createMockElement(); + const b = createMockElement(); + + observer.observe(asObserverArg(a)); + observer.observe(asObserverArg(b)); + + expect(renderer.handlerCount()).toBe(1); + + observer.unobserve(asObserverArg(a)); + + expect(renderer.handlerCount()).toBe(1); + expect(renderer.off).not.toHaveBeenCalled(); + }); + + it('unsubscribes when the last observed element is removed', () => { + const a = createMockElement(); + + observer.observe(asObserverArg(a)); + observer.unobserve(asObserverArg(a)); + + expect(renderer.handlerCount()).toBe(0); + expect(renderer.off).toHaveBeenCalledWith('frameTick', expect.any(Function)); + }); + + it('re-subscribes after a full unobserve/observe cycle', () => { + const a = createMockElement(); + + observer.observe(asObserverArg(a)); + observer.unobserve(asObserverArg(a)); + observer.observe(asObserverArg(a)); + + expect(renderer.handlerCount()).toBe(1); + expect(renderer.on).toHaveBeenCalledTimes(2); + }); + }); + + describe('observe / unobserve idempotency', () => { + it('observing the same element twice does not double-register', () => { + const a = createMockElement(); + + observer.observe(asObserverArg(a)); + observer.observe(asObserverArg(a)); + + expect(observer.isObserving(asObserverArg(a))).toBe(true); + + observer.unobserve(asObserverArg(a)); + + expect(observer.isObserving(asObserverArg(a))).toBe(false); + expect(renderer.handlerCount()).toBe(0); + }); + + it('unobserving an element that was never observed is a no-op', () => { + const a = createMockElement(); + + observer.unobserve(asObserverArg(a)); + + expect(observer.isObserving(asObserverArg(a))).toBe(false); + expect(renderer.off).not.toHaveBeenCalled(); + }); + + it('isObserving reflects observation state', () => { + const a = createMockElement(); + + expect(observer.isObserving(asObserverArg(a))).toBe(false); + + observer.observe(asObserverArg(a)); + + expect(observer.isObserving(asObserverArg(a))).toBe(true); + + observer.unobserve(asObserverArg(a)); + + expect(observer.isObserving(asObserverArg(a))).toBe(false); + }); + }); + + describe('size-change emission', () => { + it('emits on the first tick with the current dimensions', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(1); + expect(a._emitResize).toHaveBeenCalledWith({ w: 100, h: 200 }); + }); + + it('emits the initial measurement even when dimensions are zero', () => { + const a = createMockElement(0, 0); + + observer.observe(asObserverArg(a)); + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(1); + expect(a._emitResize).toHaveBeenCalledWith({ w: 0, h: 0 }); + }); + + it('does not emit on subsequent ticks when dimensions are unchanged', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + renderer.tick(); + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(1); + }); + + it('emits when only width changes', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + + a.node.w = 150; + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(2); + expect(a._emitResize).toHaveBeenNthCalledWith(2, { w: 150, h: 200 }); + }); + + it('emits when only height changes', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + + a.node.h = 250; + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(2); + expect(a._emitResize).toHaveBeenNthCalledWith(2, { w: 100, h: 250 }); + }); + + it('emits when both dimensions change', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + + a.node.w = 150; + a.node.h = 250; + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(2); + expect(a._emitResize).toHaveBeenNthCalledWith(2, { w: 150, h: 250 }); + }); + + it('tracks per-element previous dimensions independently', () => { + const a = createMockElement(100, 100); + const b = createMockElement(200, 200); + + observer.observe(asObserverArg(a)); + observer.observe(asObserverArg(b)); + renderer.tick(); + + a._emitResize.mockClear(); + b._emitResize.mockClear(); + + a.node.w = 150; + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(1); + expect(b._emitResize).not.toHaveBeenCalled(); + }); + + it('does not emit for an element after it is unobserved', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + a._emitResize.mockClear(); + + observer.unobserve(asObserverArg(a)); + + a.node.w = 500; + renderer.tick(); + + expect(a._emitResize).not.toHaveBeenCalled(); + }); + + it('resets prev dimensions when an element is re-observed after unobserve', () => { + const a = createMockElement(100, 200); + + observer.observe(asObserverArg(a)); + renderer.tick(); + a._emitResize.mockClear(); + + observer.unobserve(asObserverArg(a)); + observer.observe(asObserverArg(a)); + renderer.tick(); + + expect(a._emitResize).toHaveBeenCalledTimes(1); + expect(a._emitResize).toHaveBeenCalledWith({ w: 100, h: 200 }); + }); + }); + + describe('getNodeResizeObserver', () => { + it('returns the same instance for the same renderer', () => { + const r = asRendererArg(createMockRenderer()); + + const first = getNodeResizeObserver(r); + const second = getNodeResizeObserver(r); + + expect(first).toBe(second); + }); + + it('returns different instances for different renderers', () => { + const r1 = asRendererArg(createMockRenderer()); + const r2 = asRendererArg(createMockRenderer()); + + const first = getNodeResizeObserver(r1); + const second = getNodeResizeObserver(r2); + + expect(first).not.toBe(second); + }); + }); +}); diff --git a/packages/react-lightning/src/observer/NodeResizeObserver.ts b/packages/react-lightning/src/observer/NodeResizeObserver.ts new file mode 100644 index 0000000..3ae86c2 --- /dev/null +++ b/packages/react-lightning/src/observer/NodeResizeObserver.ts @@ -0,0 +1,89 @@ +import type { RendererMain } from '@lightningjs/renderer'; + +import type { LightningElement } from '../types'; + +interface ObservedEntry { + element: LightningElement; + prevW: number; + prevH: number; +} + +/** + * Fires a `resized` event on observed elements whenever their rendered + * width or height changes. + * + * Hooks into the renderer's `frameTick` event to sample once per frame. + * This catches every size-change path — React prop updates, direct node + * writes, animations, Autosizer (children/texture), and text auto-measure. + * + * The observer is lazy: it only subscribes to `frameTick` while at least one + * element is being watched. With no observers, there is zero per-frame cost. + */ +export class NodeResizeObserver { + private _renderer: RendererMain; + private _observed = new Map(); + private _frameHandler: (() => void) | null = null; + + constructor(renderer: RendererMain) { + this._renderer = renderer; + } + + observe(element: LightningElement): void { + if (this._observed.has(element)) { + return; + } + + this._observed.set(element, { + element, + prevW: -1, + prevH: -1, + }); + + if (this._frameHandler === null) { + this._frameHandler = this._tick; + this._renderer.on('frameTick', this._frameHandler); + } + } + + unobserve(element: LightningElement): void { + if (!this._observed.delete(element)) { + return; + } + + if (this._observed.size === 0 && this._frameHandler !== null) { + this._renderer.off('frameTick', this._frameHandler); + this._frameHandler = null; + } + } + + isObserving(element: LightningElement): boolean { + return this._observed.has(element); + } + + private _tick = (): void => { + for (const entry of this._observed.values()) { + const node = entry.element.node; + const w = node.w; + const h = node.h; + + if (entry.prevW !== w || entry.prevH !== h) { + entry.prevW = w; + entry.prevH = h; + entry.element._emitResize({ w, h }); + } + } + }; +} + +const _observerByRenderer = new WeakMap(); + +export function getNodeResizeObserver(renderer: RendererMain): NodeResizeObserver { + let observer = _observerByRenderer.get(renderer); + + if (observer === undefined) { + observer = new NodeResizeObserver(renderer); + _observerByRenderer.set(renderer, observer); + } + + return observer; +} diff --git a/packages/react-lightning/src/types/LightningElementEvents.ts b/packages/react-lightning/src/types/LightningElementEvents.ts index 4fca165..c90523b 100644 --- a/packages/react-lightning/src/types/LightningElementEvents.ts +++ b/packages/react-lightning/src/types/LightningElementEvents.ts @@ -22,6 +22,7 @@ export interface LightningElementEvents extends FocusEvents { childRemoved: (child: LightningElement, index: number) => void; beforeRender: () => void; layout: (dimensions: Rect) => void; + resized: (element: LightningElement, dimensions: { w: number; h: number }) => void; inViewport: NodeRenderStateEventHandler; textureLoaded: NodeLoadedEventHandler; textureFailed: NodeFailedEventHandler; diff --git a/packages/react-lightning/src/types/Props.ts b/packages/react-lightning/src/types/Props.ts index 21aed36..793f3a1 100644 --- a/packages/react-lightning/src/types/Props.ts +++ b/packages/react-lightning/src/types/Props.ts @@ -15,6 +15,7 @@ import type { export interface LightningElementEventProps { onBeforeLayout?: (rect: Rect) => void; onLayout?: (rect: Rect) => void; + onResize?: (dimensions: { w: number; h: number }) => void; onRender?: () => void; onBeforeRender?: () => void; onTextureReady?: (dimensions: Dimensions) => void; diff --git a/packages/react-native-lightning-components/package.json b/packages/react-native-lightning-components/package.json index 7adde5f..033d471 100644 --- a/packages/react-native-lightning-components/package.json +++ b/packages/react-native-lightning-components/package.json @@ -59,8 +59,8 @@ "@plextv/react-lightning-components": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", "@plextv/react-native-lightning": "workspace:^", - "react": "catalog:peers", - "react-native": "catalog:peers" + "react": "catalog:", + "react-native": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-native-lightning/package.json b/packages/react-native-lightning/package.json index d91937c..9f3e97e 100644 --- a/packages/react-native-lightning/package.json +++ b/packages/react-native-lightning/package.json @@ -56,17 +56,12 @@ "@plextv/react-lightning-components": "workspace:^", "@plextv/react-lightning-plugin-css-transform": "workspace:^", "@plextv/react-lightning-plugin-flexbox": "workspace:^", - "react": "catalog:peers", - "react-native": "catalog:peers" + "react": "catalog:", + "react-native": "catalog:" }, "volta": { "extends": "../../package.json" }, - "peerDependencyRules": { - "allowedVersions": { - "react": "^19" - } - }, "inlinedDependencies": { "tseep": "1.3.1", "type-fest": "5.5.0" diff --git a/packages/react-native-lightning/src/exports/NativeCanvas.tsx b/packages/react-native-lightning/src/exports/NativeCanvas.tsx new file mode 100644 index 0000000..2f406e7 --- /dev/null +++ b/packages/react-native-lightning/src/exports/NativeCanvas.tsx @@ -0,0 +1,20 @@ +import type { FC } from 'react'; + +import { Canvas, type CanvasProps } from '@plextv/react-lightning'; +import { FlexRoot } from '@plextv/react-lightning-plugin-flexbox'; + +/** + * Drop-in replacement for {@link Canvas} that wraps its children in a + * {@link FlexRoot} sized to the canvas, so React Native components below it + * lay out via flex without any extra setup. Flex is opt-in for the flexbox + * plugin — using this canvas is the easiest way to opt the whole app in. + */ +export const NativeCanvas: FC = ({ children, options, ...rest }) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/react-native-lightning/src/index.ts b/packages/react-native-lightning/src/index.ts index 12078aa..d454adf 100644 --- a/packages/react-native-lightning/src/index.ts +++ b/packages/react-native-lightning/src/index.ts @@ -12,6 +12,7 @@ export { ActivityIndicator, type ActivityIndicatorProps } from './exports/Activi export { Button, type ButtonProps } from './exports/Button'; export { FocusGroup, type FocusGroupProps } from './exports/FocusGroup'; export { Image, type ImageProps } from './exports/Image'; +export { NativeCanvas } from './exports/NativeCanvas'; export * as Platform from './exports/Platform'; export { Pressable, type PressableProps } from './exports/Pressable'; export { ScrollView, type ScrollViewProps } from './exports/ScrollView'; diff --git a/packages/vite-plugin-msdf-fontgen/package.json b/packages/vite-plugin-msdf-fontgen/package.json index 112d92f..9a4c25f 100644 --- a/packages/vite-plugin-msdf-fontgen/package.json +++ b/packages/vite-plugin-msdf-fontgen/package.json @@ -44,6 +44,6 @@ "@repo/configs": "workspace:*" }, "peerDependencies": { - "vite": "catalog:peers" + "vite": "catalog:" } } diff --git a/packages/vite-plugin-react-native-lightning/package.json b/packages/vite-plugin-react-native-lightning/package.json index 7a37ad2..aa7ea31 100644 --- a/packages/vite-plugin-react-native-lightning/package.json +++ b/packages/vite-plugin-react-native-lightning/package.json @@ -42,7 +42,8 @@ }, "peerDependencies": { "@plextv/react-native-lightning": "workspace:^", - "@vitejs/plugin-react": "catalog:peers", - "vite": "catalog:peers" + "@rolldown/plugin-babel": "catalog:", + "@vitejs/plugin-react": "catalog:", + "vite": "catalog:" } } diff --git a/packages/vite-plugin-react-native-lightning/src/index.ts b/packages/vite-plugin-react-native-lightning/src/index.ts index 6597a51..0070883 100644 --- a/packages/vite-plugin-react-native-lightning/src/index.ts +++ b/packages/vite-plugin-react-native-lightning/src/index.ts @@ -1,4 +1,5 @@ -import react from '@vitejs/plugin-react'; +import babel from '@rolldown/plugin-babel'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; import type { PluginOption } from 'vite'; const extensions = [ @@ -20,11 +21,13 @@ const extensions = [ type Options = { cwd?: string; reactOptions?: Parameters[0]; + babelOptions?: Parameters[0]; }; const vitePlugin = (options?: Options): PluginOption => { return [ react(options?.reactOptions), + babel({ presets: [reactCompilerPreset()], ...options?.babelOptions }), { name: 'vite-react-native-lightning', enforce: 'pre', diff --git a/packages/vite-plugin-react-reanimated-lightning/package.json b/packages/vite-plugin-react-reanimated-lightning/package.json index 9514080..3dccf2d 100644 --- a/packages/vite-plugin-react-reanimated-lightning/package.json +++ b/packages/vite-plugin-react-reanimated-lightning/package.json @@ -43,7 +43,7 @@ }, "peerDependencies": { "@plextv/react-lightning-plugin-reanimated": "workspace:^", - "react-native-reanimated": "catalog:peers", - "vite": "catalog:peers" + "react-native-reanimated": "catalog:", + "vite": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db1be29..0a96958 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,29 @@ settings: excludeLinksFromLockfile: false catalogs: + apps: + '@lightningjs/renderer': + specifier: 3.0.1 + version: 3.0.1 + react: + specifier: 19.2.5 + version: 19.2.5 + react-dom: + specifier: 19.2.5 + version: 19.2.5 + react-native: + specifier: 0.85.1 + version: 0.85.1 + react-native-reanimated: + specifier: 4.3.0 + version: 4.3.0 default: '@lightningjs/renderer': specifier: 3.0.1 version: 3.0.1 + '@rolldown/plugin-babel': + specifier: ^0.2.3 + version: 0.2.3 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -22,22 +41,19 @@ catalogs: specifier: 8.0.1 version: 8.0.1 '@vitejs/plugin-react': - specifier: 6.0.1 + specifier: ^6.0.0 version: 6.0.1 babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 react: - specifier: 19.2.5 - version: 19.2.5 - react-dom: - specifier: 19.2.5 + specifier: ^19.2.0 version: 19.2.5 react-native: - specifier: 0.85.1 + specifier: ^0.85.1 version: 0.85.1 react-native-reanimated: - specifier: 4.3.0 + specifier: ^4.3.0 version: 4.3.0 tseep: specifier: 1.3.1 @@ -45,21 +61,8 @@ catalogs: type-fest: specifier: 5.5.0 version: 5.5.0 - peers: - '@vitejs/plugin-react': - specifier: ^6.0.0 - version: 6.0.1 - react: - specifier: ^19.2.5 - version: 19.2.5 - react-native: - specifier: ^0.85.1 - version: 0.85.1 - react-native-reanimated: - specifier: ^4.3.0 - version: 4.3.0 vite: - specifier: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + specifier: ^8.0.0 version: 8.0.8 importers: @@ -130,7 +133,7 @@ importers: apps/react-lightning-example: dependencies: '@lightningjs/renderer': - specifier: 'catalog:' + specifier: catalog:apps version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* @@ -145,10 +148,10 @@ importers: specifier: workspace:* version: link:../../packages/plugin-flexbox react: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5 react-dom: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5(react@19.2.5) react-router-dom: specifier: 7.14.1 @@ -163,6 +166,9 @@ importers: '@repo/configs': specifier: workspace:* version: link:../../packages/configs + '@rolldown/plugin-babel': + specifier: 'catalog:' + version: 0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/react': specifier: 'catalog:' version: 19.2.14 @@ -174,18 +180,15 @@ importers: version: 8.0.1(terser@5.46.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 - vite-tsconfig-paths: - specifier: 6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) apps/react-native-lightning-example: dependencies: '@lightningjs/renderer': - specifier: 'catalog:' + specifier: catalog:apps version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* @@ -210,22 +213,22 @@ importers: version: link:../../packages/react-native-lightning-components '@react-navigation/native': specifier: 7.2.2 - version: 7.2.2(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + version: 7.2.2(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5 react-dom: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5(react@19.2.5) react-native: - specifier: 'catalog:' - version: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + specifier: catalog:apps + version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: - specifier: 'catalog:' - version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + specifier: catalog:apps + version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react-native-worklets: specifier: 0.8.1 - version: 0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + version: 0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) devDependencies: '@plextv/vite-plugin-msdf-fontgen': specifier: workspace:* @@ -239,6 +242,9 @@ importers: '@repo/configs': specifier: workspace:* version: link:../../packages/configs + '@rolldown/plugin-babel': + specifier: 'catalog:' + version: 0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/react': specifier: 'catalog:' version: 19.2.14 @@ -250,7 +256,7 @@ importers: version: 8.0.1(terser@5.46.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 @@ -261,7 +267,7 @@ importers: apps/storybook: dependencies: '@lightningjs/renderer': - specifier: 'catalog:' + specifier: catalog:apps version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* @@ -297,16 +303,16 @@ importers: specifier: 10.3.5 version: 10.3.5(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.55.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) react: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5 react-dom: - specifier: 'catalog:' + specifier: catalog:apps version: 19.2.5(react@19.2.5) react-native: - specifier: 'catalog:' + specifier: catalog:apps version: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: - specifier: 'catalog:' + specifier: catalog:apps version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react-native-worklets: specifier: 0.8.1 @@ -327,12 +333,15 @@ importers: '@repo/configs': specifier: workspace:* version: link:../../packages/configs + '@rolldown/plugin-babel': + specifier: 'catalog:' + version: 0.2.3(@babel/core@7.28.6)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/react': specifier: 'catalog:' version: 19.2.14 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.28.6)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 @@ -358,7 +367,7 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react-native: - specifier: catalog:peers + specifier: 'catalog:' version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) devDependencies: '@repo/configs': @@ -376,6 +385,9 @@ importers: '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning + react: + specifier: 'catalog:' + version: 19.2.5 tseep: specifier: 'catalog:' version: 1.3.1 @@ -386,6 +398,9 @@ importers: '@repo/configs': specifier: workspace:* version: link:../configs + '@types/react': + specifier: 'catalog:' + version: 19.2.14 copyfiles: specifier: 2.4.1 version: 2.4.1 @@ -421,13 +436,13 @@ importers: specifier: workspace:^ version: link:../react-native-lightning react: - specifier: catalog:peers + specifier: 'catalog:' version: 19.2.5 react-native: - specifier: catalog:peers + specifier: 'catalog:' version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-reanimated: - specifier: catalog:peers + specifier: 'catalog:' version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) devDependencies: '@repo/configs': @@ -446,7 +461,7 @@ importers: specifier: 'catalog:' version: 3.0.1 react: - specifier: catalog:peers + specifier: 'catalog:' version: 19.2.5 react-reconciler: specifier: 0.33.0 @@ -477,7 +492,7 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react: - specifier: catalog:peers + specifier: 'catalog:' version: 19.2.5 devDependencies: '@repo/configs': @@ -508,10 +523,10 @@ importers: specifier: 2.0.0 version: 2.0.0(@types/react@19.2.14)(react@19.2.5) react: - specifier: catalog:peers + specifier: 'catalog:' version: 19.2.5 react-native: - specifier: catalog:peers + specifier: 'catalog:' version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-web: specifier: 0.21.2 @@ -545,10 +560,10 @@ importers: specifier: workspace:^ version: link:../react-native-lightning react: - specifier: catalog:peers + specifier: 'catalog:' version: 19.2.5 react-native: - specifier: catalog:peers + specifier: 'catalog:' version: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) devDependencies: '@repo/configs': @@ -570,7 +585,7 @@ importers: specifier: 13.0.6 version: 13.0.6 vite: - specifier: catalog:peers + specifier: 'catalog:' version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': @@ -582,11 +597,14 @@ importers: '@plextv/react-native-lightning': specifier: workspace:^ version: link:../react-native-lightning + '@rolldown/plugin-babel': + specifier: 'catalog:' + version: 0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': - specifier: catalog:peers - version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 'catalog:' + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) vite: - specifier: catalog:peers + specifier: 'catalog:' version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': @@ -599,10 +617,10 @@ importers: specifier: workspace:^ version: link:../plugin-reanimated react-native-reanimated: - specifier: catalog:peers + specifier: 'catalog:' version: 4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) vite: - specifier: catalog:peers + specifier: 'catalog:' version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) devDependencies: '@repo/configs': @@ -2129,6 +2147,23 @@ packages: cpu: [x64] os: [win32] + '@rolldown/plugin-babel@0.2.3': + resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} + engines: {node: '>=22.12.0 || ^24.0.0'} + peerDependencies: + '@babel/core': ^7.29.0 || ^8.0.0-rc.1 + '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1 + '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + '@babel/plugin-transform-runtime': + optional: true + '@babel/runtime': + optional: true + vite: + optional: true + '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} @@ -3310,9 +3345,6 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -4114,10 +4146,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.9: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} @@ -4739,16 +4767,6 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -4808,11 +4826,6 @@ packages: resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@6.0.2: resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} @@ -4918,11 +4931,6 @@ packages: peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5408,7 +5416,7 @@ snapshots: dependencies: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -5433,7 +5441,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5460,7 +5468,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5683,7 +5691,7 @@ snapshots: dependencies: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5691,7 +5699,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5768,7 +5776,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -5873,7 +5881,7 @@ snapshots: '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -6221,7 +6229,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 esutils: 2.0.3 '@babel/preset-typescript@7.27.1(@babel/core@7.28.6)': @@ -7074,7 +7082,7 @@ snapshots: hermes-parser: 0.33.3 invariant: 2.2.4 nullthrows: 1.1.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 yargs: 17.7.2 '@react-native/codegen@0.85.1(@babel/core@7.29.0)': @@ -7084,7 +7092,7 @@ snapshots: hermes-parser: 0.33.3 invariant: 2.2.4 nullthrows: 1.1.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 yargs: 17.7.2 '@react-native/community-cli-plugin@0.85.1(@react-native/metro-config@0.85.1(@babel/core@7.28.6))': @@ -7095,7 +7103,7 @@ snapshots: metro: 0.84.3 metro-config: 0.84.3 metro-core: 0.84.3 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: '@react-native/metro-config': 0.85.1(@babel/core@7.28.6) transitivePeerDependencies: @@ -7111,7 +7119,7 @@ snapshots: metro: 0.84.3 metro-config: 0.84.3 metro-core: 0.84.3 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: '@react-native/metro-config': 0.85.1(@babel/core@7.29.0) transitivePeerDependencies: @@ -7224,14 +7232,14 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.5) use-sync-external-store: 1.6.0(react@19.2.5) - '@react-navigation/native@7.2.2(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': + '@react-navigation/native@7.2.2(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': dependencies: '@react-navigation/core': 7.17.2(react@19.2.5) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.11 react: 19.2.5 - react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) + react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) use-latest-callback: 0.2.6(react@19.2.5) '@react-navigation/routers@7.5.3': @@ -7287,6 +7295,26 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true + '@rolldown/plugin-babel@0.2.3(@babel/core@7.28.6)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.28.6 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.15 + optionalDependencies: + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.28.6) + '@babel/runtime': 7.28.6 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.29.0 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.15 + optionalDependencies: + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/runtime': 7.28.6 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + '@rolldown/pluginutils@1.0.0-rc.15': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -7306,7 +7334,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.55.2 @@ -7636,11 +7664,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.28.6)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.28.6)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.28.6))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + babel-plugin-react-compiler: 1.0.0 + + '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + optionalDependencies: + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0))(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.15)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) babel-plugin-react-compiler: 1.0.0 '@vitest/expect@3.2.4': @@ -7728,7 +7765,7 @@ snapshots: '@vue/shared': 3.5.27 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.6 + postcss: 8.5.9 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.27': @@ -7880,7 +7917,7 @@ snapshots: babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 babel-plugin-syntax-hermes-parser@0.33.3: dependencies: @@ -8375,10 +8412,6 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -8524,8 +8557,6 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 - globrex@0.1.2: {} - graceful-fs@4.2.10: {} graceful-fs@4.2.11: {} @@ -8932,7 +8963,7 @@ snapshots: metro-babel-transformer@0.84.3: dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 hermes-parser: 0.35.0 metro-cache-key: 0.84.3 @@ -9029,7 +9060,7 @@ snapshots: metro-transform-plugins@0.84.3: dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 @@ -9040,7 +9071,7 @@ snapshots: metro-transform-worker@0.84.3: dependencies: - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.2 '@babel/types': 7.29.0 @@ -9061,7 +9092,7 @@ snapshots: metro@0.84.3: dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.28.6 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.2 '@babel/template': 7.28.6 @@ -9398,12 +9429,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.9: dependencies: nanoid: 3.3.11 @@ -9524,7 +9549,7 @@ snapshots: react-native: 0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5) react-native-is-edge-to-edge: 1.3.1(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react-native-worklets: 0.8.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(react-native@0.85.1(@babel/core@7.28.6)(@react-native/metro-config@0.85.1(@babel/core@7.28.6))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) - semver: 7.7.3 + semver: 7.7.4 react-native-reanimated@4.3.0(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5): dependencies: @@ -9532,7 +9557,7 @@ snapshots: react-native: 0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5) react-native-is-edge-to-edge: 1.3.1(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) react-native-worklets: 0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(react-native@0.85.1(@babel/core@7.29.0)(@react-native/metro-config@0.85.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) - semver: 7.7.3 + semver: 7.7.4 react-native-web@0.21.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: @@ -9618,9 +9643,9 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.27.0 - semver: 7.7.3 + semver: 7.7.4 stacktrace-parser: 0.1.11 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 whatwg-fetch: 3.6.20 ws: 7.5.10 yargs: 17.7.2 @@ -9663,9 +9688,9 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.27.0 - semver: 7.7.3 + semver: 7.7.4 stacktrace-parser: 0.1.11 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 whatwg-fetch: 3.6.20 ws: 7.5.10 yargs: 17.7.2 @@ -10146,8 +10171,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.16: dependencies: @@ -10181,10 +10206,6 @@ snapshots: ts-dedent@2.2.0: {} - tsconfck@3.1.6(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -10246,9 +10267,6 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typescript@5.9.3: - optional: true - typescript@6.0.2: {} ua-parser-js@1.0.41: {} @@ -10286,7 +10304,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 unrun@0.2.35: @@ -10334,23 +10352,13 @@ snapshots: dependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - - typescript - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.9 rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 esbuild: 0.27.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 548d8e4..5ecf2aa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,26 +4,29 @@ packages: catalog: '@lightningjs/renderer': 3.0.1 + '@rolldown/plugin-babel': ^0.2.3 '@types/react': 19.2.14 '@types/react-dom': 19.2.3 '@types/react-reconciler': 0.33.0 '@vitejs/plugin-legacy': 8.0.1 - '@vitejs/plugin-react': 6.0.1 + '@vitejs/plugin-react': ^6.0.0 babel-plugin-react-compiler: 1.0.0 - react: 19.2.5 - react-dom: 19.2.5 - react-native: 0.85.1 - react-native-reanimated: 4.3.0 + react: ^19.2.0 + react-dom: ^19.2.0 + react-native: ^0.85.1 + react-native-reanimated: ^4.3.0 tseep: 1.3.1 type-fest: 5.5.0 + vite: ^8.0.0 catalogs: - peers: - '@vitejs/plugin-react': ^6.0.0 - react: ^19.2.5 - react-native: ^0.85.1 - react-native-reanimated: ^4.3.0 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + apps: + '@lightningjs/renderer': 3.0.1 + '@vitejs/plugin-react': 6.0.1 + react: 19.2.5 + react-dom: 19.2.5 + react-native: 0.85.1 + react-native-reanimated: 4.3.0 onlyBuiltDependencies: - core-js From 4ea2028f3a1216ef956ef143a6a9b2a96cd3b5ef Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Fri, 1 May 2026 16:47:26 -0700 Subject: [PATCH 07/10] Add changesets --- .changeset/gentle-foxes-sing.md | 5 +++++ .changeset/swift-hawks-dive.md | 5 +++++ .changeset/warm-clouds-rise.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/gentle-foxes-sing.md create mode 100644 .changeset/swift-hawks-dive.md create mode 100644 .changeset/warm-clouds-rise.md diff --git a/.changeset/gentle-foxes-sing.md b/.changeset/gentle-foxes-sing.md new file mode 100644 index 0000000..7623732 --- /dev/null +++ b/.changeset/gentle-foxes-sing.md @@ -0,0 +1,5 @@ +--- +"@plextv/react-native-lightning": patch +--- + +feat: Export `NativeCanvas` for embedding a Lightning canvas inside a React Native Lightning tree. diff --git a/.changeset/swift-hawks-dive.md b/.changeset/swift-hawks-dive.md new file mode 100644 index 0000000..7704b52 --- /dev/null +++ b/.changeset/swift-hawks-dive.md @@ -0,0 +1,5 @@ +--- +"@plextv/react-lightning-plugin-flexbox": patch +--- + +refactor: Rework YogaManager, LightningManager, and the worker pipeline for faster prop translation and a non-flex fast path. diff --git a/.changeset/warm-clouds-rise.md b/.changeset/warm-clouds-rise.md new file mode 100644 index 0000000..c6763df --- /dev/null +++ b/.changeset/warm-clouds-rise.md @@ -0,0 +1,5 @@ +--- +"@plextv/react-lightning": patch +--- + +feat: Add NodeResizeObserver, FocusManager.setFocusedChild, the `resized` element event, and a CanvasProps type export. From e2244cb47875e7e330dc3eb64dcc2cfe76883dc2 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Sat, 2 May 2026 00:13:50 -0700 Subject: [PATCH 08/10] Cleanup --- .../src/utils/flattenStyles.ts | 1 + .../src/utils/fromCssUnit.ts | 1 + packages/plugin-flexbox-lite/src/index.ts | 2 + .../plugin-flexbox/src/LightningManager.ts | 117 ++----- packages/plugin-flexbox/src/YogaManager.ts | 48 +-- .../plugin-flexbox/src/YogaManagerWorker.ts | 104 ++---- .../plugin-flexbox/src/util/SimpleDataView.ts | 4 + packages/plugin-flexbox/src/wrappers.tsx | 37 +-- .../src/animation/springUtils.ts | 1 + .../src/exports/createAnimatedComponent.tsx | 2 + .../src/exports/useAnimatedScrollHandler.tsx | 1 + packages/plugin-reanimated/src/mergeRefs.ts | 3 + .../components/VirtualList/LayoutManager.ts | 158 ++------- .../components/VirtualList/RecyclerPool.ts | 12 +- .../src/components/VirtualList/VirtualList.md | 71 ++-- .../components/VirtualList/VirtualList.tsx | 312 +++++------------- .../VirtualList/VirtualListCell.tsx | 218 ++---------- .../VirtualList/VirtualListContext.ts | 7 +- .../VirtualList/useScrollHandler.ts | 40 +-- .../src/exports/text/StyledText.tsx | 1 + .../src/element/LightningViewElement.ts | 13 +- .../src/focus/FocusKeyManager.ts | 1 + .../react-lightning/src/focus/FocusManager.ts | 15 + .../src/render/mapReactPropsToLightning.ts | 2 + .../src/shim/resizeObserverShim.ts | 4 + .../react-lightning/src/utils/EventEmitter.ts | 1 + .../src/utils/findClosestElement.ts | 1 + .../react-lightning/src/utils/simpleDiff.ts | 5 + .../src/exports/Pressable.tsx | 1 + .../src/exports/StyleSheet.ts | 1 + .../src/plugins/reactNativePolyfillsPlugin.ts | 2 + .../vite-plugin-msdf-fontgen/src/configs.ts | 2 + .../src/generateFonts.ts | 1 + .../src/getFileChangeInfo.ts | 1 + .../src/sortByExtension.ts | 1 + 35 files changed, 354 insertions(+), 837 deletions(-) diff --git a/packages/plugin-css-transform/src/utils/flattenStyles.ts b/packages/plugin-css-transform/src/utils/flattenStyles.ts index 4d8c0e4..4a9f68d 100644 --- a/packages/plugin-css-transform/src/utils/flattenStyles.ts +++ b/packages/plugin-css-transform/src/utils/flattenStyles.ts @@ -12,6 +12,7 @@ export function flattenStyles(styles: AllStyleProps): T { return JSON.parse(styles); } catch (err) { console.warn('There was an error parsing the style: ', styles, '\n', err); + return {} as T; } } diff --git a/packages/plugin-css-transform/src/utils/fromCssUnit.ts b/packages/plugin-css-transform/src/utils/fromCssUnit.ts index 6fa3917..35332d4 100644 --- a/packages/plugin-css-transform/src/utils/fromCssUnit.ts +++ b/packages/plugin-css-transform/src/utils/fromCssUnit.ts @@ -17,6 +17,7 @@ export function fromCssUnit( if (typeof value === 'object') { // TODO: Support animated nodes console.warn('[fromCssUnit] Unsupported css unit:', value); + return; } diff --git a/packages/plugin-flexbox-lite/src/index.ts b/packages/plugin-flexbox-lite/src/index.ts index 2ff780f..9c43c29 100644 --- a/packages/plugin-flexbox-lite/src/index.ts +++ b/packages/plugin-flexbox-lite/src/index.ts @@ -194,8 +194,10 @@ export default function flexPlugin(): Plugin { window.setTimeout(() => { _isUpdateQueued = false; + while (updateQueue.length > 0) { const instance = updateQueue.shift(); + if (instance === null || instance === undefined) { continue; } diff --git a/packages/plugin-flexbox/src/LightningManager.ts b/packages/plugin-flexbox/src/LightningManager.ts index aaa0d74..2b0d299 100644 --- a/packages/plugin-flexbox/src/LightningManager.ts +++ b/packages/plugin-flexbox/src/LightningManager.ts @@ -11,28 +11,14 @@ import loadYoga from './yoga'; import type { YogaManager } from './YogaManager'; import type { Workerized } from './YogaManagerWorker'; -/** - * Manages the lifecycle of Yoga nodes for Lightning elements. This can only be - * done on the main thread and not the worker thread. - */ +/** Lifecycle of Yoga nodes for Lightning elements. Main-thread only. */ export class LightningManager { private _elements = new Map(); private _boundaries = new Set(); private _flexRoots = new Set(); - /** - * Tracks the yoga-side parent for every attached node (childId -> parentId). - * Used to make boundary/flex-root marking idempotent without a sync round-trip - * to a worker. - */ + /** childId -> yoga-side parentId. Lets boundary/flex-root marking stay sync without a worker round-trip. */ private _yogaParents = new Map(); - /** - * Per-parent count of children currently attached in yoga. Lets - * `_yogaIndexFor` short-circuit the O(n) sibling walk for the common - * append-at-end case (the new child's yoga index equals the parent's - * current attached count). Maintained alongside `_yogaParents`: any - * change to a child's yoga parent updates the old parent's count down - * and the new parent's count up. - */ + /** Per-parent attached-children count. Lets `_yogaIndexFor` skip the O(n) sibling walk on append-at-end. */ private _yogaChildCounts = new Map(); private _yogaManager: YogaManager | Workerized | undefined; @@ -42,13 +28,9 @@ export class LightningManager { } /** - * Marks an element as a flex boundary inside a flex tree. Its existing - * children are detached from yoga and any future children added to its - * subtree are not added to yoga either. A nested {@link markFlexRoot} - * restores yoga participation for everything below it. - * - * Note: flex is opt-in, so calling this outside a {@link markFlexRoot} - * subtree is a no-op — those elements are already excluded from yoga. + * Detaches the element's subtree from yoga (and excludes future + * descendants). A nested {@link markFlexRoot} re-enables flex below it. + * No-op outside a flex root since those elements are already excluded. */ public markBoundary(element: LightningElement): () => void { if (this._boundaries.has(element.id)) { @@ -77,12 +59,9 @@ export class LightningManager { } /** - * Opts an element and its subtree into flex layout. The element becomes an - * independent yoga root — its subtree is laid out on its own each render, - * separately from any other flex tree. - * - * Flex is opt-in for this plugin. Without a flex root somewhere above an - * element, that element is invisible to yoga and gets no flex behavior. + * Opts an element and its subtree into flex layout as an independent + * yoga root. Flex is opt-in — without a flex root above it, an element + * is invisible to yoga. */ public markFlexRoot(element: LightningElement): () => void { if (this._flexRoots.has(element.id)) { @@ -103,9 +82,8 @@ export class LightningManager { this._reattachChildren(element); - // Trigger the first layout pass for the freshly-attached subtree. - // Without this, the new independent root sits with default 0,0 sizes - // until something else happens to call applyStyle. + // First layout pass — without this the root sits at 0,0 until + // something else calls applyStyle. this._yogaManager.queueRender(element.id); } @@ -117,12 +95,7 @@ export class LightningManager { this._yogaManager?.removeIndependentRoot(elementId); } - /** - * Returns true when an element should NOT participate in yoga layout. Flex - * is opt-in: an element is in flex only when it has an ancestor (or is one) - * marked as a {@link markFlexRoot}. A nested {@link markBoundary} between - * the element and that flex root re-disables flex for the subtree. - */ + /** True when the element should NOT participate in yoga (no flex root ancestor, or a boundary intervenes). */ private _isInBoundary(parent: LightningElement): boolean { let curr: LightningElement | null = parent; @@ -142,15 +115,9 @@ export class LightningManager { } /** - * Counts how many preceding React siblings of `parent` are currently - * attached to `parent` in yoga. The result is the yoga-side index at which - * a child should be inserted to preserve relative order with React. - * - * Fast path: when the new child is being appended at the end of - * `parent.children` (the common case for React mounts and most list - * additions), the yoga-side index is exactly the parent's current - * attached-children count — no sibling walk needed. This turns the - * O(N²) cost of mass-mounting a list into O(N). + * Yoga-side index for inserting at React's `reactIndex`, accounting for + * skipped siblings (boundaries, flex roots). Append-at-end fast path + * uses the cached count — turns O(N²) mass-mount into O(N). */ private _yogaIndexFor(parent: LightningElement, reactIndex: number): number { if (reactIndex === parent.children.length - 1) { @@ -170,22 +137,13 @@ export class LightningManager { return yogaIndex; } - /** - * Marks `childId` as yoga-attached to `parentId`. Updates `_yogaParents` - * AND `_yogaChildCounts` together so `_yogaIndexFor`'s fast path stays - * accurate. - */ + /** Maintains `_yogaParents` and `_yogaChildCounts` together so the fast path stays accurate. */ private _setYogaParent(childId: number, parentId: number): void { this._yogaParents.set(childId, parentId); this._yogaChildCounts.set(parentId, (this._yogaChildCounts.get(parentId) ?? 0) + 1); } - /** - * Records that `childId` is no longer yoga-attached to `parentId`. - * Symmetric counterpart of `_setYogaParent`. The `parentId` argument is - * required because at call time `_yogaParents.get(childId)` may have - * already been deleted. - */ + /** Counterpart of `_setYogaParent`. `parentId` passed explicitly since `_yogaParents.get` may already be cleared. */ private _clearYogaParent(childId: number, parentId: number): void { this._yogaParents.delete(childId); @@ -245,6 +203,7 @@ export class LightningManager { public trackElement(element: LightningElement): void { if (this._elements.has(element.id)) { console.warn(`Yoga node is already attached to element #${element.id}.`); + return; } @@ -284,11 +243,8 @@ export class LightningManager { return; } - // Translate the React-side index to a yoga-side index by counting - // preceding siblings that are actually attached to this parent in - // yoga. Without this, skipped siblings (nested boundaries, flex - // roots) cause yoga's insertChild to walk off the end of its - // children array — which surfaces as "memory access out of bounds". + // Translate React index → yoga index. Skipped siblings (boundaries, + // flex roots) cause "memory access out of bounds" otherwise. const yogaIndex = this._yogaIndexFor(element, index); // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above @@ -296,18 +252,15 @@ export class LightningManager { this._setYogaParent(child.id, element.id); this.applyStyle(element.id, element.style); - // React mounts bottom-up: `child` may already have its own subtree - // that was inserted before `child` itself joined the flex tree. Those - // descendants were skipped at their own childAdded time (no flex - // ancestor existed then). Promote them now. + // React mounts bottom-up: `child`'s descendants were inserted + // before `child` joined the flex tree, so they were skipped at + // their own childAdded time. Promote them now. if (!this._boundaries.has(child.id)) { this._reattachChildren(child); } }), element.on('childRemoved', (child) => { - // This will remove any pending worker style updates that haven't been sent - const childYogaParent = this._yogaParents.get(child.id); if (childYogaParent !== undefined) { @@ -319,11 +272,9 @@ export class LightningManager { // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.removeNode(child.id); - // Schedule a yoga re-layout. Without this, a parent that shrink-fits - // its children (no explicit w/h) keeps the old size when a child is - // removed — its node.w/h stay at the last computed values, and - // NodeResizeObserver never fires the shrink event up to consumers - // (e.g., VirtualList's reportItemSize). + // Re-layout — without this a shrink-fit parent keeps the old size + // (node.w/h stay at last computed) and NodeResizeObserver never + // fires the shrink event to consumers like VL.reportItemSize. // oxlint-disable-next-line typescript/no-non-null-assertion -- Guaranteed to exist. See above this._yogaManager!.queueRender(element.id); }), @@ -376,12 +327,8 @@ export class LightningManager { } private _applyUpdates = (buffer: ArrayBuffer) => { - // Raw `DataView` instead of `SimpleDataView` here — this is a pure - // read loop fired per render frame, and `SimpleDataView` adds an - // outer object plus a layer of `_readInt` indirection that's pure - // overhead when we don't need overflow handling, write tracking, or - // auto-incrementing offsets across method calls. Manual offset - // arithmetic is the cheapest option for a hot per-frame path. + // Raw `DataView` over `SimpleDataView` — this is a per-frame hot path + // that doesn't need overflow handling or write tracking. const view = new DataView(buffer); const length = buffer.byteLength; let offset = 0; @@ -401,14 +348,12 @@ export class LightningManager { continue; } - // Apply layout directly to the node to prevent re-rendering, and the - // style retains the original value that was set. + // Apply directly to the node so style retains its original value. let skipX = false; let skipY = false; let dirty = false; let resize = false; - // `isTextElement` is a getter — cache once, read twice below. const isText = el.isTextElement; if (el.parent?.style.display !== 'flex') { @@ -430,9 +375,7 @@ export class LightningManager { dirty = el.setNodeProp('y', y) || dirty; } - // If width is 0, we should not set it on the node, as it will cause - // layout issues. We also ignore setting width/height for text elements, - // as their size is handled by lightning. + // Skip zero (causes layout issues) and text elements (Lightning sizes them). if (width !== 0 && !isText) { dirty = el.setNodeProp('w', width) || dirty; resize = true; diff --git a/packages/plugin-flexbox/src/YogaManager.ts b/packages/plugin-flexbox/src/YogaManager.ts index 4131d8e..794ab3a 100644 --- a/packages/plugin-flexbox/src/YogaManager.ts +++ b/packages/plugin-flexbox/src/YogaManager.ts @@ -184,12 +184,9 @@ export class YogaManager { this._isRenderQueued = true; - // Microtask instead of setTimeout(_, 1): we want the layout pass to - // run AFTER the current synchronous batch of style/node operations - // (which arrive in succession from postMessage handlers and - // worker-side updates), and a microtask achieves that with no timer - // overhead. setTimeout enforces a 1ms minimum (4ms after nesting) and - // fragments large batches into many separate render passes. + // Microtask runs AFTER the current synchronous batch of style/node + // ops (arriving from postMessage handlers); setTimeout's 1ms+ minimum + // would fragment a batch into many render passes. queueMicrotask(() => { this._isRenderQueued = false; @@ -200,11 +197,9 @@ export class YogaManager { this._initializeArrayBuffer(); for (const independentRoot of this._independentRoots) { - // Pass undefined for available size so yoga uses the root's own - // explicit w/h if set, and shrinks-to-fit otherwise. Hardcoding - // 1920×1080 here makes every root with an unset axis stretch to the - // canvas dimensions — which breaks measurement-driven roots like - // VirtualList cells. + // undefined available size → yoga uses the root's own w/h (or + // shrink-to-fit). Passing 1920×1080 would stretch any unset axis + // and break measurement-driven roots like VirtualList cells. independentRoot.node.calculateLayout( undefined, undefined, @@ -226,10 +221,8 @@ export class YogaManager { throw new Error('Yoga was not initialized! Did you call `init()`?'); } - // `for...in` instead of `Object.entries(styles)` to skip the per-call - // [key, value] tuple allocation. This iterates every batched style - // update on the worker side per `'flushBoth'` / `'applyStyles'` - // message. + // `for...in` skips the [key, value] tuple allocation of Object.entries — + // this is a hot path on every flushBoth/applyStyles message. for (const elementId in styles) { // oxlint-disable-next-line typescript/no-non-null-assertion -- key from for..in iteration of own props this.applyStyle(+elementId, styles[elementId as unknown as number]!, skipRender); @@ -253,6 +246,7 @@ export class YogaManager { if (!yogaNode) { console.warn(`Yoga node with ID ${elementId} not found.`); + return; } @@ -324,33 +318,25 @@ export class YogaManager { return yogaNode; } - // Walks the yoga subtree and emits an update for every node with a fresh - // layout. Critically, recursion is unconditional — yoga's hasNewLayout is - // per-node, so a child's layout may have changed even when its parent's - // didn't (e.g. absolute children laid out independently of flow siblings, - // or a subtree just attached via _reattachChildren). + // Recursion is unconditional — yoga's hasNewLayout is per-node, so a + // child's layout can change even when the parent's didn't (absolute + // children, just-attached subtrees from _reattachChildren). private _getUpdatedStyles(yogaNode: ManagerNode, force = false) { const skipHiddenNode = !this._yogaOptions.processHiddenNodes && this._hiddenElements.has(yogaNode.id); if (!skipHiddenNode && (force || yogaNode.node.hasNewLayout())) { - // We want to keep chunks together, so check the size to ensure we have enough space if (!this._dataView.hasSpace(APPROX_SIZEOF_UPDATE)) { - // If we don't have enough space, flush the current buffer this._flushArrayBuffer(this._dataView.buffer); } - // Read layout via individual getters instead of `getComputedLayout()`, - // which allocates a fresh `{ left, top, width, height }` object per - // node. This function recurses through every yoga descendant on every - // layout pass, so the saved allocations add up quickly on big trees. + // Individual getters instead of getComputedLayout() — that allocates + // a {left, top, width, height} object per node, and we recurse the + // entire yoga tree every layout pass. const node = yogaNode.node; - // Direct `DataView` writes — `hasSpace(APPROX_SIZEOF_UPDATE)` above - // already validated the full 12-byte run, so the per-call overflow - // check and `_writeInt` switch dispatch inside `writeUint32`/ - // `writeInt16` are pure overhead here. This fires for every - // freshly-laid-out yoga descendant on every layout pass. + // Direct DataView writes — hasSpace above already validated the full + // 12-byte run, so per-call overflow checks are pure overhead here. const view = this._dataView.dataView; const offset = this._dataView.offset; diff --git a/packages/plugin-flexbox/src/YogaManagerWorker.ts b/packages/plugin-flexbox/src/YogaManagerWorker.ts index a6a566f..6dd3628 100644 --- a/packages/plugin-flexbox/src/YogaManagerWorker.ts +++ b/packages/plugin-flexbox/src/YogaManagerWorker.ts @@ -21,15 +21,9 @@ export type Workerized = { }; /** - * Coalesces calls within a single synchronous task. The first call schedules - * a microtask; subsequent calls before that microtask fires are no-ops (they - * just keep `latestArgs` updated). At the end of the current synchronous - * code, the microtask runs `fn` with the latest args. - * - * We use a microtask instead of `setTimeout(_, 1)` because the timer's - * minimum 1ms (and 4ms after nesting) breaks coalescing into many small - * postMessage flushes during a React commit pass. A microtask collapses a - * whole commit's worth of writes into one flush. + * Coalesces calls within a sync task — runs `fn` once at the end of the + * current sync code with the latest args. Uses a microtask, not setTimeout, + * because the timer's 1ms+ minimum breaks coalescing during a React commit. */ function debounceMicrotask void | Promise>(fn: T): T { let scheduled = false; @@ -61,14 +55,9 @@ function wrapWorker(worker: Worker): Workerized { const _childOperations = new SimpleDataView(undefined, undefined, _onChildOpsOverflow); /** - * Overflow handler for `_childOperations`. Deliberately distinct from - * `flushChildOperations` (which combines with pending styles): an - * overflow happens mid-write, so the buffer being flushed is a *partial* - * batch of node operations. Combining pending styles with this partial - * batch would land them on the worker before the rest of the nodeOps - * arrive — styles would target nodes that don't exist yet, causing - * "node not found" warnings and silently-skipped layout. nodeOps-only - * here is the only safe choice. + * Overflow flush is nodeOps-only — combining pending styles with a + * partial nodeOps batch would land styles before the remaining nodeOps, + * targeting nodes that don't exist yet ("node not found" warnings). */ function _onChildOpsOverflow(filledBuffer: ArrayBuffer) { worker.postMessage( @@ -87,12 +76,10 @@ function wrapWorker(worker: Worker): Workerized { return; } - // Combine with pending nodeOps if any. See `_flushBothInternal` for - // the full reasoning — the short version: this collapses two separate - // postMessages into one when both flush types fire in the same - // microtask cycle. + // Combine with pending nodeOps — collapses two postMessages into one. if (_childOperations.offset > 0) { _flushBothInternal(); + return; } @@ -122,19 +109,9 @@ function wrapWorker(worker: Worker): Workerized { _stylesToSend[elementId] = styleToSend; } - // `for...in` instead of `Object.entries(style)` — avoids the per-call - // [key, value] tuple array allocation. This loop runs on every - // applyStyle, which fires hundreds of times during a busy commit. - // - // We also short-circuit non-flex keys here. LightningManager.applyStyle - // is called with the element's full `props.style` from event handlers - // (`stylesChanged`, `inViewport`, `childAdded`'s parent applyStyle), - // so the input often contains color/alpha/font/etc. — keys yoga - // doesn't care about. Filtering here saves: a `toSerializableValue` - // call per skipped key, the bytes those keys would add to the - // postMessage payload (structured-clone cost), and the - // `isFlexStyleProp` check the worker's `applyReactPropsToYoga` - // would do on the same key anyway. + // `for...in` skips Object.entries' tuple allocation — hot path on + // every applyStyle. Filter non-flex keys here so we don't serialize + // them, ship them across postMessage, and let the worker re-filter. for (const key in style) { if (!isFlexStyleProp(key)) { continue; @@ -149,13 +126,9 @@ function wrapWorker(worker: Worker): Workerized { } } } else { - // Drop a pending entry if one exists. Without the existence check the - // counter underflows whenever `applyStyle(id, null)` runs for an id - // with nothing pending — the common case for `childRemoved`, which - // calls `applyStyle(child.id, null, true)` regardless of whether any - // style was buffered for that child. A negative counter then breaks - // the `> 50` early-flush threshold and the `=== 0` short-circuit in - // `flushSendStyles`. + // Existence check is required — `applyStyle(id, null)` fires from + // childRemoved regardless of whether anything was buffered, and a + // counter underflow breaks the > 50 / === 0 thresholds below. if (!_stylesToSend[elementId]) { return; } @@ -167,7 +140,6 @@ function wrapWorker(worker: Worker): Workerized { _needsRender ||= !skipRender; if (_numStylesToSend > 50) { - // Flush early if the object gets too large flushSendStyles(); } else { queueSendStyles(); @@ -184,6 +156,7 @@ function wrapWorker(worker: Worker): Workerized { // Combine with pending styles if any. See `_flushBothInternal`. if (_numStylesToSend > 0) { _flushBothInternal(); + return; } @@ -199,17 +172,9 @@ function wrapWorker(worker: Worker): Workerized { } /** - * Drain both pending node operations and pending styles into a single - * `'flushBoth'` postMessage. The worker handler applies node operations - * first, then styles — preserving the causal ordering the two-message - * setup enforced by hand (`flushChildOperations()` before - * `flushSendStyles()` posted `applyStyles`). - * - * Caller must have verified that BOTH queues have data; this function - * blindly transfers/clears both. The overflow handler - * (`_onChildOpsOverflow`) intentionally bypasses this and posts - * nodeOps-only, since combining at overflow would land pre-overflow - * styles on the worker before the rest of the nodeOps catch up. + * Single 'flushBoth' postMessage — worker applies nodeOps then styles, + * preserving the causal ordering. Caller must have verified BOTH queues + * have data; this function blindly transfers and clears. */ function _flushBothInternal() { const buffer = _childOperations.buffer; @@ -230,11 +195,8 @@ function wrapWorker(worker: Worker): Workerized { const queueSendNodeOperations = debounceMicrotask(flushChildOperations); - // Coalesce N synchronous `queueRender` calls into a single postMessage. - // During React unmount cascades the prior fire-immediate path produced - // ~2 postMessages per destroyed node (a flush + a queueRender). With - // these state vars + the debounced drain below, all `queueRender`s in a - // sync block collapse into one message at end-of-microtask. + // Coalesce N synchronous queueRender calls into one postMessage — + // unmount cascades otherwise produce ~2 messages per destroyed node. let _wantsRender = false; let _renderElementId = 0; let _renderForce = false; @@ -244,10 +206,9 @@ function wrapWorker(worker: Worker): Workerized { return; } - // Capture before flushSendStyles resets `_needsRender`. When pending - // styles are flushed with skipRender=false (i.e. `_needsRender` was - // true), `applyStyles` on the worker auto-triggers `queueRender` - // already — so the explicit message below is redundant and we skip it. + // Capture before flushSendStyles resets _needsRender. When applyStyles + // ships with skipRender=false the worker auto-renders, so the explicit + // queueRender below would be redundant. const willAutoRender = _numStylesToSend > 0 && _needsRender; flushChildOperations(); @@ -279,7 +240,6 @@ function wrapWorker(worker: Worker): Workerized { childId?: number, index?: number, ) { - // Batch operations into a buffer for quick transfers and less postMessage calls switch (method) { case 'addNode': _childOperations.writeUint8(NodeOperations.AddNode); @@ -334,6 +294,7 @@ function wrapWorker(worker: Worker): Workerized { if (id === 'render') { // Special case for render updates _eventEmitter.emit('render', result as ArrayBuffer); + return; } @@ -341,6 +302,7 @@ function wrapWorker(worker: Worker): Workerized { if (!callee) { console.error(`No handler found for worker message id: ${id}`); + return; } @@ -355,11 +317,8 @@ function wrapWorker(worker: Worker): Workerized { } }; - // Awaitable: flush pending ops/styles for causal ordering, then post - // the call and register a callee so the caller can `await` the worker's - // response. Used only by `init` — every other call from - // LightningManager is fire-and-forget and rides the buffered nodeOps - // pipeline (see `nodeOperation`) or the debounced render drain. + // Used by `init` only — every other call is fire-and-forget on the + // buffered pipeline. Flushes pending ops/styles for causal ordering. function _awaitable(method: string, args: unknown[]): Promise { return new Promise((resolve, reject) => { const id = getId(); @@ -373,10 +332,8 @@ function wrapWorker(worker: Worker): Workerized { }); } - // Plain object with pre-bound methods instead of a `Proxy`. The Proxy - // version did `Proxy.get` + a string-compare chain + a fresh - // `(...args) => nodeOperation(prop, ...args)` closure allocation per - // node-op call — measurable self-time during VL recycle bursts. + // Pre-bound methods instead of a Proxy — Proxy.get + closure allocation + // per node-op call was measurable self-time in VL recycle bursts. const proxy = { on: _eventEmitter.on.bind(_eventEmitter), off: _eventEmitter.off.bind(_eventEmitter), @@ -394,8 +351,7 @@ function wrapWorker(worker: Worker): Workerized { queueRenderDrain(); }, addIndependentRoot: (elementId: number) => nodeOperation('addIndependentRoot', elementId), - removeIndependentRoot: (elementId: number) => - nodeOperation('removeIndependentRoot', elementId), + removeIndependentRoot: (elementId: number) => nodeOperation('removeIndependentRoot', elementId), init: (yogaOptions?: unknown) => _awaitable('init', [yogaOptions]), }; diff --git a/packages/plugin-flexbox/src/util/SimpleDataView.ts b/packages/plugin-flexbox/src/util/SimpleDataView.ts index 7651418..250fd22 100644 --- a/packages/plugin-flexbox/src/util/SimpleDataView.ts +++ b/packages/plugin-flexbox/src/util/SimpleDataView.ts @@ -205,7 +205,9 @@ export class SimpleDataView { private _readInt(size: 1 | 2 | 4 | 8, unsigned: boolean): number | bigint { this._checkOverflow(size); const value = this._readIntAt(this._offset, size, unsigned); + this._offset += size; + return value; } @@ -286,7 +288,9 @@ export class SimpleDataView { private _readFloat(size: 4 | 8): number { this._checkOverflow(size); const value = this._readFloatAt(this._offset, size); + this._offset += size; + return value; } diff --git a/packages/plugin-flexbox/src/wrappers.tsx b/packages/plugin-flexbox/src/wrappers.tsx index 1f6dcee..8af05aa 100644 --- a/packages/plugin-flexbox/src/wrappers.tsx +++ b/packages/plugin-flexbox/src/wrappers.tsx @@ -72,29 +72,28 @@ export interface FlexRootProps { * elements get no flex behavior. Wrap your app's root (or any subtree that * should use flexbox) with this component. */ -export const FlexRoot: ForwardRefExoticComponent< - FlexRootProps & RefAttributes -> = forwardRef(({ children, style, onResize }, forwardedRef) => { - const ref = useRef(null); +export const FlexRoot: ForwardRefExoticComponent> = + forwardRef(({ children, style, onResize }, forwardedRef) => { + const ref = useRef(null); - useImperativeHandle(forwardedRef, () => ref.current as LightningElement, []); + useImperativeHandle(forwardedRef, () => ref.current as LightningElement, []); - useLayoutEffect(() => { - const manager = getFlexboxManager(); - const element = ref.current; + useLayoutEffect(() => { + const manager = getFlexboxManager(); + const element = ref.current; - if (!manager || !element) { - return; - } + if (!manager || !element) { + return; + } - return manager.markFlexRoot(element); - }, []); + return manager.markFlexRoot(element); + }, []); - return ( - - {children} - - ); -}); + return ( + + {children} + + ); + }); FlexRoot.displayName = 'FlexRoot'; diff --git a/packages/plugin-reanimated/src/animation/springUtils.ts b/packages/plugin-reanimated/src/animation/springUtils.ts index 13cfebd..e0a3791 100644 --- a/packages/plugin-reanimated/src/animation/springUtils.ts +++ b/packages/plugin-reanimated/src/animation/springUtils.ts @@ -12,6 +12,7 @@ export function checkIfConfigIsValid(config: DefaultSpringConfig): boolean { let errorMessage = ''; (['stiffness', 'damping', 'dampingRatio', 'mass', 'energyThreshold'] as const).forEach((prop) => { const value = config[prop]; + if (value <= 0) { errorMessage += `, ${prop} must be grater than zero but got ${value}`; } diff --git a/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx b/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx index df21569..bafa3a9 100644 --- a/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx +++ b/packages/plugin-reanimated/src/exports/createAnimatedComponent.tsx @@ -266,6 +266,7 @@ export function createAnimatedComponent( private _runAnimation(builder: LayoutAnimationFunction | null, callback?: () => void) { if (!this._ref || !builder) { callback?.(); + return; } @@ -285,6 +286,7 @@ export function createAnimatedComponent( if (!layoutAnimation) { callback?.(); + return; } diff --git a/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx b/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx index c3cbfe9..61dd57e 100644 --- a/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx +++ b/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx @@ -30,6 +30,7 @@ export const useAnimatedScrollHandler: UseAnimatedScrollHandlerFn = ( if (typeof scrollHandlers === 'function') { scrollHandlers(reanimatedEvent, context); + return; } diff --git a/packages/plugin-reanimated/src/mergeRefs.ts b/packages/plugin-reanimated/src/mergeRefs.ts index c74d409..53d7bb8 100644 --- a/packages/plugin-reanimated/src/mergeRefs.ts +++ b/packages/plugin-reanimated/src/mergeRefs.ts @@ -21,14 +21,17 @@ export default function mergeRefs(...refs: any[]) { if (ref == null) { continue; } + if (typeof ref === 'function') { ref(node); continue; } + if (typeof ref === 'object') { ref.current = node; continue; } + console.error( `mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`, ); diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts index 5e6e34d..9e16f7c 100644 --- a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts @@ -20,33 +20,17 @@ export interface LayoutManagerConfig { overrideItemLayout?: OverrideItemLayoutFn; extraData?: unknown; separatorSize?: number; - /** - * Cross-axis size of a single column, computed by VirtualList from its - * viewport. The LayoutManager treats this as ground truth — no aggregation - * from cell-reported sizes. When 0/unset, layouts still resolve their - * main-axis offsets correctly so visibility math works during the first - * render. - */ + /** Ground truth from VL viewport; never aggregated from cell reports. */ cellCrossSize: number; - /** - * Stable identity function for items, mirroring `VirtualList.keyExtractor`. - * Used to key measurements so they survive recycling and data shifts. - * When not provided, the index is used as the key — measurements still - * work but don't survive inserts/removes that shift indices. - */ + /** When omitted, index is used — measurements then don't survive insert/remove that shifts indices. */ keyExtractor?: (item: T, index: number) => string; } /** - * Computes per-item offsets in O(n). Sizes come from (in priority order) - * a measurement reported via `reportItemSize`, then `overrideItemLayout`, - * then `estimatedItemSize`. Cross-axis size is unilateral: every item's - * `crossSize` equals the configured `cellCrossSize` (× span for grids). - * Cross-axis is never measured or aggregated — that's the load-bearing - * rule that keeps the layout free of feedback loops. - * - * Measurements are stored by `userKey` (from `keyExtractor`) so they - * survive recycling and data inserts/removes that shift indices. + * Computes per-item offsets in O(n). Main-axis size is the per-userKey + * measurement, then `overrideItemLayout`, then `estimatedItemSize`. Cross + * is always `cellCrossSize` (× span); never measured or aggregated — + * that's the rule that keeps the layout loop-free. */ export class LayoutManager { private static _overrideScratch: { size?: number; span?: number } = {}; @@ -63,52 +47,24 @@ export class LayoutManager { private _cellCrossSize: number; private _keyExtractor?: (item: T, index: number) => string; private _measuredSizes: Map = new Map(); - /** - * While `_batching` is on (VL flips it during a scroll/focus-snap - * animation), reports accumulate per-`userKey` here instead of running - * through dampening — the animation is the consumer's clear signal that - * intermediate yoga measurements aren't worth committing. - * `setBatching(false)` drains this directly into `_measuredSizes` at - * animation end. - */ + /** While true, reports accumulate per-userKey and skip dampening. Drained on `setBatching(false)`. */ private _batching = false; private _batchedSizes: Map = new Map(); /** - * Per-`userKey` stability window. When a cell's reported size differs - * from the currently-stored one, the new value sits here until either - * (a) the same value is re-reported after `_STABILITY_MS` elapses, or - * (b) the backstop timer fires `_STABILITY_MS` after `firstSeenAt`. A - * different incoming value cancels the timer and replaces the entry. - * - * This dampens the multi-frame cascade where a user's section component - * re-measures during focus/scroll animations or async content settling - * — without dampening, every intermediate measurement reflows the - * layout for every following item. + * Per-userKey stability window. A different incoming value sits pending + * until either matched after `_STABILITY_MS` or the backstop timer + * fires. Filters multi-frame measurement cascades during scroll/focus + * animations and async content settling. */ private _pendingSizes: Map = new Map(); - /** - * Backstop timers per `userKey`. Required because a cell may push - * exactly once for a given size and then go quiet (props stable, no - * further re-renders) — without the timer, the pending value would - * sit forever and the layout would paint at the old size. - */ + /** Backstop timers — required because a cell can push once and go quiet (props stable). */ private _pendingTimers: Map> = new Map(); private _onChange?: () => void; private static readonly _STABILITY_MS = 120; /** - * The very first non-zero size reported via `reportItemSize` for this - * list. Used as the implicit fallback for unmeasured items in place of - * the caller-provided `estimatedItemSize` once at least one cell has - * been seen — empirically that's a much better predictor than a generic - * estimate, and it cuts the visible reflow when subsequent cells turn - * out to be roughly the same size. - * - * Locked on first measurement; never updates. If we tracked the most - * recent measurement instead, every measured cell would shift the - * implicit estimate and cascade-rerender every later unmeasured item — - * which is the opposite of "less jank". The first measurement is - * usually representative and the remaining error gets corrected only - * when each individual cell measures. + * Implicit fallback for unmeasured items once any cell has measured — + * usually a much better predictor than the caller's estimate. Locked on + * first measurement so subsequent cells don't cascade-shift the fallback. */ private _firstMeasuredSize = 0; @@ -131,35 +87,20 @@ export class LayoutManager { return this._totalSize; } - /** - * Register a "layout dirtied" callback. Fires when a pending - * measurement matures via the stability backstop timer — there's no - * incoming report at that moment to bump layoutVersion synchronously, - * so LM has to wake the caller itself. - */ + /** Fires when the stability backstop timer commits a pending measurement (no incoming report to bump layoutVersion synchronously). */ setOnChange(cb: () => void): void { this._onChange = cb; } - /** - * Returns a copy of the current per-`userKey` measurement map. Used - * by VL to snapshot measurements into the parent state cache so a - * recycled cell's inner VL can restore them on remount instead of - * having to re-measure from estimate. - */ + /** Copy so the cache snapshot doesn't alias live state. */ getMeasurements(): Map { return new Map(this._measuredSizes); } /** - * Replaces the current measurement map with the given snapshot. Marks - * layout dirty. Called from VL when restoring inner VL state from the - * parent state cache — no `_onChange` notification because the caller - * is responsible for the surrounding render flow. - * - * Also clears any in-flight dampening / batching state — pending - * entries from the previous content are no longer relevant under - * the restored measurement set. + * Replace measurements wholesale and clear in-flight dampening/batching + * (entries from prior content are stale under the new set). No + * `_onChange` — caller owns the surrounding render flow. */ setMeasurements(measurements: Map): void { this._measuredSizes = new Map(measurements); @@ -174,13 +115,7 @@ export class LayoutManager { this._dirty = true; } - /** - * Toggle batching mode. While active, reports accumulate per `userKey` - * (latest wins) and the dampening path is skipped — animations are the - * caller's clear signal that intermediate measurements aren't worth - * committing. Returns `true` if disabling caused at least one stored - * size to change. - */ + /** Returns `true` if disabling drained at least one batched size into measurements. */ setBatching(active: boolean): boolean { if (this._batching === active) { return false; @@ -189,9 +124,7 @@ export class LayoutManager { this._batching = active; if (active) { - // Switching INTO batching: cancel any in-flight dampening timers. - // The animation will overwrite all relevant values via the batch, - // so old pending entries are irrelevant. + // Cancel pending dampening — the upcoming batch will overwrite anyway. for (const timer of this._pendingTimers.values()) { clearTimeout(timer); } @@ -290,18 +223,10 @@ export class LayoutManager { } /** - * Records the rendered main-axis size for an item, keyed by its stable - * `userKey`. Subsequent layouts use this size instead of - * `overrideItemLayout` / `estimatedItemSize` for that key. - * - * Returns `true` when the stored size changed synchronously (caller - * should bump layoutVersion). Returns `false` when the report was - * batched, dampened, or rejected. - * - * Rejects: - * - `size <= 0` — transient zero during recycle. Use `reportItemEmpty` - * for genuinely-empty rows. - * - non-finite values. + * Records the rendered main-axis size keyed by `userKey`. Returns `true` + * when the size committed synchronously (caller should bump + * layoutVersion). Rejects size ≤ 0 / non-finite — use `reportItemEmpty` + * for genuinely-empty rows. */ reportItemSize(userKey: string, size: number): boolean { if (!Number.isFinite(size) || size <= 0) { @@ -318,11 +243,11 @@ export class LayoutManager { if (existing != null && Math.abs(existing - size) < 1) { this._clearPending(userKey); + return false; } - // First measurement — apply immediately. There's no existing value - // to thrash against, and waiting would just delay layout settling. + // First measurement — apply immediately, nothing to thrash against. if (existing == null) { this._measuredSizes.set(userKey, size); @@ -341,8 +266,7 @@ export class LayoutManager { const pending = this._pendingSizes.get(userKey); if (pending != null && Math.abs(pending.size - size) < 1) { - // Same as already-pending. Don't reset the timer. If the window - // has already elapsed, commit synchronously. + // Same as pending — don't reset the timer; commit if window elapsed. if (now - pending.firstSeenAt >= LayoutManager._STABILITY_MS) { this._measuredSizes.set(userKey, size); this._clearPending(userKey); @@ -390,14 +314,7 @@ export class LayoutManager { this._pendingSizes.delete(userKey); } - /** - * Drops every per-key measurement and resets the first-measured size. - * Not called automatically — VirtualList preserves measurements across - * data identity changes so recycled items use their cached size on - * first paint. Callers can invoke this imperatively when they truly - * want to invalidate (e.g. orientation change, theme swap that - * materially affects content sizing). - */ + /** Imperative invalidation — VL itself preserves measurements across data identity changes. */ clearMeasurements(): void { if (this._measuredSizes.size === 0 && this._firstMeasuredSize === 0) { return; @@ -417,13 +334,9 @@ export class LayoutManager { } /** - * Records the item identified by `userKey` as logically empty. The row - * collapses to zero main-axis size and other items close ranks around - * it. - * - * Distinct from `reportItemSize(_, 0)` (which we reject as a transient - * FlexRoot zero during recycle): this is the *intentional* empty path, - * called from `VirtualListCell` when `renderItem` returns null. + * Collapses the row to zero main-axis size. Distinct from + * `reportItemSize(_, 0)` (rejected as transient): this is the + * intentional empty path, fired when `renderItem` returns null. */ reportItemEmpty(userKey: string): boolean { if (this._batching) { @@ -445,11 +358,6 @@ export class LayoutManager { return true; } - /** - * Returns the stored measurement for a userKey, or undefined if none. - * Mostly here so consumers can ask "have I measured this?" without - * exposing the internal Map. - */ getMeasuredSize(userKey: string): number | undefined { return this._measuredSizes.get(userKey); } diff --git a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts index d82ad85..25f7847 100644 --- a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts @@ -32,7 +32,7 @@ export class RecyclerPool { private _visibleSet = new Set(); private _nextId = 0; - constructor(label = "pool") { + constructor(label = 'pool') { this._label = label; } @@ -82,10 +82,7 @@ export class RecyclerPool { const preferred = this._lastSlotForIndex.get(index); let key: string; - if ( - preferred !== undefined && - this._tryClaimPreferred(type, preferred) - ) { + if (preferred !== undefined && this._tryClaimPreferred(type, preferred)) { key = preferred; preferredReused++; } else { @@ -123,10 +120,7 @@ export class RecyclerPool { * if the slot isn't available (was reassigned to a different index, or * has a different type now). */ - private _tryClaimPreferred( - type: string | number, - preferred: string, - ): boolean { + private _tryClaimPreferred(type: string | number, preferred: string): boolean { if (this._slotTypes.get(preferred) !== type) { return false; } diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.md b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md index 9342dc6..5001368 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualList.md +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md @@ -10,7 +10,7 @@ A virtualized scroll list for Lightning, modeled on FlashList v1. The user is ** ### Pinned mode (no flex ancestor) -When the VL is rendered outside any flex parent (`useIsInFlex() === false`), no yoga is running in this subtree. Cells are absolutely positioned with explicit `w` AND `h` from `LayoutManager` — VL is the *single, exclusive* source of cell positioning and sizing. No FlexRoot, no measurement, no `onResize`. The user's `renderItem` content sizes itself however it wants, but the cell does not adapt; the caller is responsible for accurate `estimatedItemSize` / `overrideItemLayout`. This is the FlashList v1 strict model and has no feedback loops by construction. +When the VL is rendered outside any flex parent (`useIsInFlex() === false`), no yoga is running in this subtree. Cells are absolutely positioned with explicit `w` AND `h` from `LayoutManager` — VL is the _single, exclusive_ source of cell positioning and sizing. No FlexRoot, no measurement, no `onResize`. The user's `renderItem` content sizes itself however it wants, but the cell does not adapt; the caller is responsible for accurate `estimatedItemSize` / `overrideItemLayout`. This is the FlashList v1 strict model and has no feedback loops by construction. ### Measured mode (flex ancestor) @@ -33,11 +33,11 @@ The crucial discipline in measured mode is **measurement is one-directional and **Three responsibilities:** -| File | Responsibility | -|---|---| -| `LayoutManager.ts` | Pure layout math. Given data + sizes + cross-axis size, computes per-item offsets in O(n). | -| `VirtualListCell.tsx` | One `` per visible cell with explicit absolute position and dimensions. Wraps user content in `VLCellKeyContext` + `CellBoundsContext` providers. The renderItem subtree persists across slot recycles — the cell wrapper *and* its descendants survive userKey changes; nested VLs read the new userKey via `VLCellKeyContext` and run their cellKey-change branch instead of remounting. | -| `VirtualList.tsx` | Viewport derivation, scroll/focus state, recycling, the React glue. | +| File | Responsibility | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LayoutManager.ts` | Pure layout math. Given data + sizes + cross-axis size, computes per-item offsets in O(n). | +| `VirtualListCell.tsx` | One `` per visible cell with explicit absolute position and dimensions. Wraps user content in `VLCellKeyContext` + `CellBoundsContext` providers. The renderItem subtree persists across slot recycles — the cell wrapper _and_ its descendants survive userKey changes; nested VLs read the new userKey via `VLCellKeyContext` and run their cellKey-change branch instead of remounting. | +| `VirtualList.tsx` | Viewport derivation, scroll/focus state, recycling, the React glue. | Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven scroll), `useViewability.ts` (onViewableItemsChanged), `RecyclerPool.ts` (slot reuse by item type), `parseContentStyle.ts` (RN-style padding props), `VirtualListContext.ts` (the three React contexts). @@ -48,26 +48,31 @@ Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven `VirtualListProps` (see `VirtualListTypes.ts` for full types): ### Required + - **`data: ReadonlyArray`** — items to render. - **`renderItem: (info: VirtualListRenderItemInfo) => ReactElement`** — render function. `info` carries `{ item, index, extraData, shouldFocus }`. ### Sizing + - **`estimatedItemSize?: number`** (default `200`) — main-axis size used when no override is provided. **In this model, the estimate IS the size** for items lacking an override — it's not a guess that gets refined. Pick it carefully. - **`overrideItemLayout?: (layout, item, index, numColumns, extraData) => void`** — set `layout.size` (main-axis) and optionally `layout.span` (multi-column). Called for every layout pass; must be fast. - **`numColumns?: number`** (default `1`) — multi-column grid. Cells fill columns left-to-right, then advance main-axis by the tallest size in the row. ### Layout + - **`horizontal?: boolean`** (default `false`) — scroll axis. - **`style?: LightningViewElementStyle`** — applied to the outer FocusGroup. `style.w` / `style.h` set the viewport explicitly (highest priority over parent bounds and self-measure). - **`contentContainerStyle?: ContentStyle`** — RN-style padding (`padding`, `paddingHorizontal`, `paddingVertical`, `paddingTop` etc) and `backgroundColor`. ### Slots + - **`ListHeaderComponent` / `listHeaderSize`** — header rendered before items, takes `listHeaderSize` main-axis pixels. - **`ListFooterComponent` / `listFooterSize`** — footer after items. - **`ListEmptyComponent`** — replaces the entire list when `data.length === 0`. - **`ItemSeparatorComponent`** — rendered between cells in single-column lists. Skipped after collapsed (size=0) rows. ### Behavior + - **`drawDistance?: number`** (default `250`) — pixels beyond viewport to keep mounted. Larger = smoother scroll, more memory. - **`keyExtractor?: (item, index) => string`** — used by recycler for slot identity AND by focus restoration. Falls back to `String(index)`. - **`getItemType?: (item, index, extraData) => string | number`** — items of the same type share a recycler pool. @@ -82,6 +87,7 @@ Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven - **`autoFocus` / `trapFocus{Up,Right,Down,Left}`** — forwarded to the FocusGroup wrapping the list. ### Imperative — `VirtualListRef` + - `scrollToIndex({ index, animated?, viewPosition?, viewOffset? })` - `scrollToOffset({ offset, animated? })` - `scrollToEnd({ animated? })` @@ -94,21 +100,21 @@ Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven **Main-axis size priority** (highest wins): -1. **Measured size** *(measured mode only)* — the cell's last reported main-axis dimension, keyed by `userKey`. +1. **Measured size** _(measured mode only)_ — the cell's last reported main-axis dimension, keyed by `userKey`. 2. **`overrideItemLayout` returns `layout.size`**. 3. **Data entry is `null` / `undefined`** — size is forced to `0`, cell is not rendered. -4. **First-measured size** *(measured mode only)* — once any cell has reported a measurement, that first value is used as the fallback for *unmeasured* cells in place of `estimatedItemSize`. Locked on first; later measurements update individual cells via the per-key path but do not change the implicit fallback. +4. **First-measured size** _(measured mode only)_ — once any cell has reported a measurement, that first value is used as the fallback for _unmeasured_ cells in place of `estimatedItemSize`. Locked on first; later measurements update individual cells via the per-key path but do not change the implicit fallback. 5. **Fallback: `estimatedItemSize`** — used only before the first measurement lands (and always in pinned mode, where no measurements happen). -The first-measured fallback is what makes `estimatedItemSize` matter less in measured mode: a slightly-off estimate produces visible reflow only on the very first cell. After that, the second and subsequent cells use the first cell's actual rendered size as their starting point — usually a much closer match to the real content size, so each cell's per-key measurement update moves things only a few pixels (or not at all). Locking on the *first* measurement (rather than tracking a running average or the most recent) is deliberate: a moving estimate would cascade-rerender every later unmeasured item every time someone new measured, defeating the goal. +The first-measured fallback is what makes `estimatedItemSize` matter less in measured mode: a slightly-off estimate produces visible reflow only on the very first cell. After that, the second and subsequent cells use the first cell's actual rendered size as their starting point — usually a much closer match to the real content size, so each cell's per-key measurement update moves things only a few pixels (or not at all). Locking on the _first_ measurement (rather than tracking a running average or the most recent) is deliberate: a moving estimate would cascade-rerender every later unmeasured item every time someone new measured, defeating the goal. In **pinned mode** (no flex ancestor), step 1 doesn't apply: cells never report sizes, so the chain effectively skips to step 2/3/4. Every cell is exactly the size LayoutManager dictated. Get your `estimatedItemSize` / `overrideItemLayout` right or you'll see gaps/overflow. -In **measured mode** (flex ancestor), measurement wins over override because real rendered size is authoritative — if the user's content renders to 250px and the override said 200px, going with 250 prevents overflow into the next item. If the caller wants to *force* a size and prevent measurement updates, they need to render content sized to that exact dimension (the cell measures whatever content actually renders). +In **measured mode** (flex ancestor), measurement wins over override because real rendered size is authoritative — if the user's content renders to 250px and the override said 200px, going with 250 prevents overflow into the next item. If the caller wants to _force_ a size and prevent measurement updates, they need to render content sized to that exact dimension (the cell measures whatever content actually renders). -**Cross-axis size** is *never* measured in either mode. Every item's cross-axis size equals `cellCrossSize` (derived from viewport). Span is the one exception — `layout.span = 2` makes a multi-column item occupy 2 columns of `cellCrossSize` width. If a user's content overflows the cross-axis, it paints outside the cell wrapper but does not affect `cellCrossSize` for any item. +**Cross-axis size** is _never_ measured in either mode. Every item's cross-axis size equals `cellCrossSize` (derived from viewport). Span is the one exception — `layout.span = 2` makes a multi-column item occupy 2 columns of `cellCrossSize` width. If a user's content overflows the cross-axis, it paints outside the cell wrapper but does not affect `cellCrossSize` for any item. -**First-render behavior** *(measured mode)*. Cells whose `userKey` has never been measured fall through to override → estimate. They render at that estimated main-axis size; once yoga lays them out and `onResize` fires, the cell reports its actual size, LayoutManager updates, and following items reposition. So a list with accurate `estimatedItemSize` (or `overrideItemLayout`) renders correctly from frame 1; lists with bad estimates show a brief reflow on first paint. +**First-render behavior** _(measured mode)_. Cells whose `userKey` has never been measured fall through to override → estimate. They render at that estimated main-axis size; once yoga lays them out and `onResize` fires, the cell reports its actual size, LayoutManager updates, and following items reposition. So a list with accurate `estimatedItemSize` (or `overrideItemLayout`) renders correctly from frame 1; lists with bad estimates show a brief reflow on first paint. **Recycling and measurement.** Measurements are keyed by `userKey`, not index, so a recycled cell reuses its prior measurement immediately. A cell rendering an item it has measured before paints at the right size on first paint — no re-measure needed unless content actually changed. @@ -116,7 +122,7 @@ In **measured mode** (flex ancestor), measurement wins over override because rea 1. **Animation batching** (top layer). When `useScrollHandler` fires `onAnimationStart`, VL flips LM into batching mode (`setBatching(true)`). While batched, every report goes into `_batchedSizes` (latest wins per `userKey`) and bypasses the dampening path entirely — the animation is the consumer's clear signal that intermediate yoga measurements aren't worth committing. On `onAnimationEnd`, `setBatching(false)` drains the batch directly into `_measuredSizes` in a single step and bumps `layoutVersion` once. The layout stays frozen for the visible duration of the animation, then settles in one commit when it ends. -2. **Per-key stability dampening** (below the batching layer). Outside of animations, first measurements apply immediately. Subsequent *changes* to an already-stored measurement go through a `_STABILITY_MS` (currently 120ms) wait: a different value starts a "pending" timer; further reports of the same value advance toward confirmation, and only after the value has been stable for the window does it commit to layout. A different intermediate value restarts the timer. A backstop `setTimeout` is also scheduled per pending entry so values commit even if the cell pushes once and goes quiet (props stable, no further re-renders). This filters transient oscillations from user content that re-measures asynchronously after a scroll has already settled — without the dampener, every spurious yoga measurement propagates to layout offsets and rows below jump on every scroll tick. +2. **Per-key stability dampening** (below the batching layer). Outside of animations, first measurements apply immediately. Subsequent _changes_ to an already-stored measurement go through a `_STABILITY_MS` (currently 120ms) wait: a different value starts a "pending" timer; further reports of the same value advance toward confirmation, and only after the value has been stable for the window does it commit to layout. A different intermediate value restarts the timer. A backstop `setTimeout` is also scheduled per pending entry so values commit even if the cell pushes once and goes quiet (props stable, no further re-renders). This filters transient oscillations from user content that re-measures asynchronously after a scroll has already settled — without the dampener, every spurious yoga measurement propagates to layout offsets and rows below jump on every scroll tick. The backstop wakes VL through `LayoutManager.setOnChange(cb)` — VL registers a callback at construction that calls `setLayoutVersion(v => v + 1)`. The synchronous-commit paths (immediate first-measurement, stable repeat-after-window) instead return `true` from `reportItemSize` so VL bumps inline. The two paths are mutually exclusive: while `_batching` is true, the dampening map is cleared and pending timers are cancelled (the upcoming flush will replace those values anyway). @@ -124,7 +130,7 @@ The backstop wakes VL through `LayoutManager.setOnChange(cb)` — VL registers a **Measurements persist across data identity changes.** A new `data` array reference (e.g. from a tab switch or a parent re-render that recomputes the array) does not wipe measurements. Cells whose `userKey` survives the change reuse their cached size — items don't shift on re-render. Callers who truly need to invalidate (orientation change, theme swap that materially affects content sizing) can call `layoutManager.clearMeasurements()` imperatively. If user-content's measured size genuinely fluctuates during a session (focus animations, async-loaded content), each reported value updates the layout — that's intentional, since the alternative (locking to max) leaves visible empty space when content is at smaller sizes. -**Skipping cells when `renderItem` returns null.** If `renderItem` returns `null`, the cell renders nothing — no `FocusGroup` wrapper, no separator, no DOM — *and* the cell calls `LayoutManager.reportItemEmpty(userKey)` via `useLayoutEffect`, which collapses the row to zero main-axis size. Following items close ranks around the gap. This is for callers whose data shape doesn't permit a clean `null` entry but still has logically-empty rows (e.g. `{ id, label, items: [] }` with `items.length === 0` meaning "render nothing"). +**Skipping cells when `renderItem` returns null.** If `renderItem` returns `null`, the cell renders nothing — no `FocusGroup` wrapper, no separator, no DOM — _and_ the cell calls `LayoutManager.reportItemEmpty(userKey)` via `useLayoutEffect`, which collapses the row to zero main-axis size. Following items close ranks around the gap. This is for callers whose data shape doesn't permit a clean `null` entry but still has logically-empty rows (e.g. `{ id, label, items: [] }` with `items.length === 0` meaning "render nothing"). The `reportItemEmpty` path is deliberately separate from `reportItemSize`. The size path rejects 0 to filter transient FlexRoot reports during recycle (FlexRoot briefly measures 0 between unmount and remount of its children); accepting those would collapse legitimate rows. `reportItemEmpty` is the explicit, intent-bearing alternative — only called from the renderItem-null code path. @@ -160,7 +166,7 @@ The `reportItemEmpty` path is deliberately separate from `reportItemSize`. The s The asymmetry is load-bearing. In a vertical VL nested inside a column-flex parent, `parentCellBounds.width` and `measuredSize.w` both report the full allocated column width — the right size for cells to fill. Cell-content cross sizes (per-row natural widths) are typically larger than the viewport in app-style rows that wrap inner horizontal scrollers, so trusting them would set `cellCrossSize` to the inner scroller's full `totalContentSize` instead of the viewport. -In a horizontal VL nested inside a parent section that contains other siblings (a title, a status row, etc.), `parentCellBounds.height` is the *outer cell's* full height — `title + innerVL + other`. Using that as the inner VL's cross dim sizes the cells to include the chrome height too. Worse, when the outer cell's measurement bounces (as the user's section component re-measures during scroll/focus animations), the inner VL's `cellCrossSize` bounces with it, and cells are sometimes too tall (gap), sometimes too short (overflow). Trusting the cells' own measured cross instead — `maxContentCross` — gives `cellCrossSize` the cards' natural height regardless of what the outer section measures around them. Only when no cell has measured yet do we fall back to parent/measured/estimate. +In a horizontal VL nested inside a parent section that contains other siblings (a title, a status row, etc.), `parentCellBounds.height` is the _outer cell's_ full height — `title + innerVL + other`. Using that as the inner VL's cross dim sizes the cells to include the chrome height too. Worse, when the outer cell's measurement bounces (as the user's section component re-measures during scroll/focus animations), the inner VL's `cellCrossSize` bounces with it, and cells are sometimes too tall (gap), sometimes too short (overflow). Trusting the cells' own measured cross instead — `maxContentCross` — gives `cellCrossSize` the cards' natural height regardless of what the outer section measures around them. Only when no cell has measured yet do we fall back to parent/measured/estimate. `maxContentCross` is monotonic per-dataset: once a cell reports cross=N, the VL stays at N or larger until `data` / `extraData` identity changes (at which point the value resets so a fresh, smaller dataset isn't stuck at the prior dataset's max). @@ -182,28 +188,23 @@ For a list with no explicit cross AND no flex ancestor (pinned mode), no measure autoFocus={shouldFocus} style={{ position: 'absolute', - x, y, + x, + y, // Both axes pinned by VL — cell wrapper has NO flex of its own. w: horizontal ? size : crossSize, h: horizontal ? crossSize : size, }} > {isInFlex ? ( - onItemSizeChange(userKey, horizontal ? e.w : e.h)} - > + onItemSizeChange(userKey, horizontal ? e.w : e.h)}> - - {renderedItem} - + {renderedItem} ) : ( /* plain content — no flex, no measurement */ - - {renderedItem} - + {renderedItem} )} {/* optional separator, position:absolute */} @@ -267,10 +268,10 @@ User presses arrow → FocusGroup fires `onChildFocused(child)` → `handleVLFoc 1. **Compute target index** from `child.getRelativePosition(contentRef)`. Child positions inside contentRef are scroll-independent (cells are absolutely positioned inside contentRef; only contentRef's own x/y changes for scroll), so this is safe to do before scrolling. 2. **Run `handleChildFocused(child)`** — that's the snap-alignment scroll inside `useScrollHandler`. It calls `scrollToOffset(target, animated)`, which synchronously sets `scrollOffsetRef.current = clamp(target)` and kicks off the animation. So even though the visual scroll is async, the ref is already updated. -3. **Write to cache** with `{ scrollOffset: scrollOffsetRef.current, focusedIndex: targetIdx }`. Because step 2 already updated the ref, this captures the *post-alignment* offset. +3. **Write to cache** with `{ scrollOffset: scrollOffsetRef.current, focusedIndex: targetIdx }`. Because step 2 already updated the ref, this captures the _post-alignment_ offset. 4. **Update `focusedIndexRef.current`**. -The order matters. If you write before step 2, you capture the pre-scroll offset. On restore, `resetScroll` puts the row at that pre-scroll offset, autoFocus fires, `handleChildFocused` runs and scrolls to the *correct* offset — but the user sees the row land slightly off and then jump. Two visits are needed for the cache to converge. Doing alignment first and writing after is what makes the first visit land cleanly. +The order matters. If you write before step 2, you capture the pre-scroll offset. On restore, `resetScroll` puts the row at that pre-scroll offset, autoFocus fires, `handleChildFocused` runs and scrolls to the _correct_ offset — but the user sees the row land slightly off and then jump. Two visits are needed for the cache to converge. Doing alignment first and writing after is what makes the first visit land cleanly. ### Recycle entry flow @@ -310,13 +311,13 @@ interface VLPersistedState { Three write paths, with different timing characteristics: -| When | Reads what | Why | -|---|---|---| -| `handleVLFocus`, after `handleChildFocused` | `scrollOffsetRef.current` (latest, synchronous) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Captures every focus-driven scroll precisely — including sub-range scrolls that don't commit a new visible range. | -| `useEffect` on `committedScrollOffset` | `committedScrollOffset` (state, updated when range changes) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Backstop for non-focus scrolls (touch/wheel/imperative `scrollToOffset`). Doesn't fire for sub-range scrolls. | -| Cell-key-change block (during render) | `scrollOffsetRef.current` + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Saves outgoing state right before restoring incoming, ensuring the most recent measurements survive even if no in-life save fired since the last update. | +| When | Reads what | Why | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `handleVLFocus`, after `handleChildFocused` | `scrollOffsetRef.current` (latest, synchronous) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Captures every focus-driven scroll precisely — including sub-range scrolls that don't commit a new visible range. | +| `useEffect` on `committedScrollOffset` | `committedScrollOffset` (state, updated when range changes) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Backstop for non-focus scrolls (touch/wheel/imperative `scrollToOffset`). Doesn't fire for sub-range scrolls. | +| Cell-key-change block (during render) | `scrollOffsetRef.current` + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Saves outgoing state right before restoring incoming, ensuring the most recent measurements survive even if no in-life save fired since the last update. | -The focus-event write must run *after* `handleChildFocused` so that `scrollOffsetRef.current` reflects the snap-aligned offset, not the pre-scroll one. See [Focus restoration → In-list navigation flow](#in-list-navigation-flow) for the full rationale. +The focus-event write must run _after_ `handleChildFocused` so that `scrollOffsetRef.current` reflects the snap-aligned offset, not the pre-scroll one. See [Focus restoration → In-list navigation flow](#in-list-navigation-flow) for the full rationale. The cell-key-change block runs as derived state (set during render), not in an effect, so the current render uses the new state — avoids a flash of stale scroll/focus. @@ -386,13 +387,13 @@ The current design measures only main-axis. Cross is unilaterally `cellCrossSize ## Invariants and pitfalls -- **`estimatedItemSize` is critical in pinned mode** (no flex ancestor) — it IS the size for items without an override. In measured mode, a bad estimate only affects items rendered *before the very first cell measures*; once any cell reports its size, that becomes the implicit fallback for the rest of the unmeasured items. +- **`estimatedItemSize` is critical in pinned mode** (no flex ancestor) — it IS the size for items without an override. In measured mode, a bad estimate only affects items rendered _before the very first cell measures_; once any cell reports its size, that becomes the implicit fallback for the rest of the unmeasured items. - **`overrideItemLayout` is hot.** It's called for every item on every layout. Don't allocate or do expensive work inside. - **`keyExtractor` matters.** Measurements are keyed by it. Without one, indices are used (`String(index)`) — which means measurements don't survive data inserts/removes that shift indices. For dynamic lists, supply a stable `keyExtractor`. - **The cell's main-axis is whatever yoga lays out.** If your `renderItem` returns content with explicit dimensions matching your `estimatedItemSize`/`overrideItemLayout`, the cell measures to that. If they disagree, measurement wins on subsequent renders. - **The cell's cross-axis is fixed at `cellCrossSize`.** Content overflowing the cross axis paints outside the cell wrapper. Either fit content to `cellCrossSize` or pick a larger viewport / smaller `numColumns`. - **Top-level VLs should set `style.w/h` explicitly** when known. Self-measurement via `FocusGroup.onResize` works but adds a render cycle. -- **Nested VLs without `style.h` (or `style.w`) inherit from `parentCellBounds`.** That's the cell wrapper's current size — which after measurement is the cell's *content* size (estimate before, measured after). For a horizontal inner VL inside a row, it's usually cleaner to set `style.h` on the inner VL to its actual item height rather than rely on the parent cell sizing. +- **Nested VLs without `style.h` (or `style.w`) inherit from `parentCellBounds`.** That's the cell wrapper's current size — which after measurement is the cell's _content_ size (estimate before, measured after). For a horizontal inner VL inside a row, it's usually cleaner to set `style.h` on the inner VL to its actual item height rather than rely on the parent cell sizing. - **`focusedIndexRef` is read at cell render time.** Mount-time autoFocus chains through `FocusGroup → useFocus → addElement` claim focus on first paint. For a persisting cell whose `shouldFocus` flips false → true (slot recycle to a new content that should be focused), the imperative `cellElementRef.current.focus()` in `VirtualListCell`'s layoutEffect is what actually moves focus. - **Don't add focusedIndex to the persistence `useEffect` deps.** The ref-based direct write in `handleVLFocus` covers per-focus updates; adding the dep reintroduces the setState-async race. - **In `handleVLFocus`, run `handleChildFocused` BEFORE writing to the cache.** `scrollToOffset` synchronously updates `scrollOffsetRef.current` to the snap-aligned target, so the subsequent cache write captures the post-alignment offset. Inverting this order forces a two-visit convergence on restore — the row lands at the wrong offset and only fixes itself on a second focus event. diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx index 0f92e32..0ef1d87 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx @@ -1,4 +1,4 @@ -import type { ComponentType, Ref } from "react"; +import type { ComponentType, Ref } from 'react'; import { type ForwardedRef, forwardRef, @@ -10,34 +10,31 @@ import { useLayoutEffect, useRef, useState, -} from "react"; +} from 'react'; import { FocusGroup, type LightningElement, type LightningViewElementStyle, -} from "@plextv/react-lightning"; -import { - FlexBoundary, - useIsInFlex, -} from "@plextv/react-lightning-plugin-flexbox"; - -import { LayoutManager } from "./LayoutManager"; -import { parseContentStyle } from "./parseContentStyle"; -import { RecyclerPool } from "./RecyclerPool"; -import { useScrollHandler } from "./useScrollHandler"; -import { useViewability } from "./useViewability"; -import { VirtualListCell } from "./VirtualListCell"; +} from '@plextv/react-lightning'; +import { FlexBoundary, useIsInFlex } from '@plextv/react-lightning-plugin-flexbox'; + +import { LayoutManager } from './LayoutManager'; +import { parseContentStyle } from './parseContentStyle'; +import { RecyclerPool } from './RecyclerPool'; +import { useScrollHandler } from './useScrollHandler'; +import { useViewability } from './useViewability'; +import { VirtualListCell } from './VirtualListCell'; import { CellBoundsContext, type VLPersistedState, VLCellKeyContext, VLStateCacheContext, -} from "./VirtualListContext"; -import type { VirtualListProps, VirtualListRef } from "./VirtualListTypes"; +} from './VirtualListContext'; +import type { VirtualListProps, VirtualListRef } from './VirtualListTypes'; function renderListComponent( - component: VirtualListProps["ListHeaderComponent"], + component: VirtualListProps['ListHeaderComponent'], ): ReactElement | null { if (!component) { return null; @@ -52,10 +49,7 @@ function renderListComponent( return ; } -function VirtualListInner( - props: VirtualListProps, - ref: ForwardedRef, -) { +function VirtualListInner(props: VirtualListProps, ref: ForwardedRef) { const { data, renderItem, @@ -84,7 +78,7 @@ function VirtualListInner( viewabilityConfig, onLoad, onLayout, - snapToAlignment = "start", + snapToAlignment = 'start', animationDuration = 300, autoFocus, trapFocusUp, @@ -94,60 +88,31 @@ function VirtualListInner( } = props; const parentCellBounds = useContext(CellBoundsContext); - // True when this VL is rendered inside a flex parent. Determines whether - // cells get a FlexRoot wrapper (which both supports flex content and - // measures content size). When false, cells are pinned and silent. const isInFlex = useIsInFlex(); - // State persistence across cell recycles. The enclosing cell of a parent - // VL provides our identity (cellKey) and the parent VL provides a cache; - // when cellKey changes (recycle into a different row), we save the old - // row's state to the cache and restore the new row's. Top-level VLs (no - // parent VL) skip this entirely. + // Top-level VLs (no parent VL) skip persistence entirely. const cellKey = useContext(VLCellKeyContext); const parentStateCache = useContext(VLStateCacheContext); const initialRestoredState = - cellKey != null && parentStateCache - ? parentStateCache.get(cellKey) - : undefined; + cellKey != null && parentStateCache ? parentStateCache.get(cellKey) : undefined; const initialScrollOffset = initialRestoredState?.scrollOffset ?? 0; - // Focus tracking is state (not a ref) so React Compiler can memoize - // the rest of VirtualListInner — render-phase ref reads (e.g. - // `shouldFocus={focusedIndex === index}` in cells.map) cause the - // compiler to bail on the entire function, which then leaves every - // inline callback and JSX value unmemoized. Cells re-render on every - // VL render because their `areCellPropsEqual` compares fresh closures. - // setState during render (the cellKey-change branch) is the - // React-canonical "derived state from props" pattern. + // State (not ref) — render-phase ref reads bail React Compiler on the + // whole function, leaving every cell prop closure unmemoized. const [focusedIndex, setFocusedIndex] = useState( initialRestoredState?.focusedIndex, ); - // After a cellKey change we restore the saved focused index. The - // FocusGroup may auto-pick its first focusable on entry, which fires - // onChildFocused and would otherwise overwrite the restored index. - // Skip exactly one onChildFocused after a cellKey change. + // Consumed on the next onChildFocused after a cellKey change so the FG's + // auto-pick on row entry doesn't overwrite the restored index. const [skipNextFocus, setSkipNextFocus] = useState(false); - // State cache provided to nested VLs inside our cells. Lazy-init via - // useState so the Map's identity is stable without a render-phase - // ref-write pattern. - const [ownStateCache] = useState>( - () => new Map(), - ); + const [ownStateCache] = useState>(() => new Map()); const [measuredSize, setMeasuredSize] = useState({ w: 0, h: 0 }); const [, setLayoutVersion] = useState(0); - // Separator size is measured once (any cell that renders a separator - // reports its size; we dedupe to a single sticky value). LayoutManager - // gets it via updateConfig and accounts for it in offsets between cells. const [separatorSize, setSeparatorSize] = useState(0); const separatorSizeRef = useRef(0); - // Max cross-axis size reported by any cell's content. Used as a - // fallback when the caller hasn't given us an explicit cross via - // `style` or `parentCellBounds`. Monotonic by design — once a cell - // measures cross=N, VL stays at that size or larger. Resets when data - // identity changes (extraData / data ref) since fresh content can - // legitimately be smaller than what came before. + // Monotonic per-dataset: once a cell reports cross=N, stays at N or + // larger until data/extraData identity changes. const [maxContentCross, setMaxContentCross] = useState(0); const maxContentCrossRef = useRef(0); const padding = parseContentStyle(contentContainerStyle); @@ -161,68 +126,30 @@ function VirtualListInner( const footerSize = ListFooterComponent ? listFooterSize : 0; const itemAreaOffset = paddingStart + headerSize; - // Main-axis viewport size (the scroll dimension): explicit style > - // parent cell bounds > self-measured. We trust measuredSize on the - // main axis because it represents how much room the parent's flex - // gave us — orthogonal to anything cell content does. + // Main axis: explicit style > parent cell bounds > self-measured. const explicitMain = horizontal ? (style?.w as number | undefined) : (style?.h as number | undefined); - const parentMain = horizontal - ? parentCellBounds?.width - : parentCellBounds?.height; + const parentMain = horizontal ? parentCellBounds?.width : parentCellBounds?.height; const measuredOuterMain = horizontal ? measuredSize.w : measuredSize.h; const viewportSize = - explicitMain ?? - parentMain ?? - (measuredOuterMain > 0 ? measuredOuterMain : 0); - - // Cross-axis viewport size (the bounded dim). Priority order depends on - // orientation because `measuredOuterCross` means different things in - // each case: - // - // - Vertical VL: outerStyle has `flexGrow: 1`, so the FocusGroup gets - // its width from the parent's flex layout. `measuredOuterCross` is - // that parent-allocated viewport width — reliable, orthogonal to - // anything the cells do. Trust it over content reports. - // - // - Horizontal VL: outerStyle has no flex behavior on either axis, so - // the FocusGroup's height shrinks to content. `measuredOuterCross` - // is itself content-driven (fed by cellCrossSize → contentRef → self - // measure) and would create the prior architecture's feedback loop. - // In this case use `maxContentCross` (the natural cross dim of the - // tallest cell) so the VL grows to fit content on the unbounded axis. - // - // Without this asymmetry, vertical VLs end up with `cellCrossSize` set - // to the natural width of the widest row content (i.e. an inner - // horizontal scroller's full `totalContentSize` — many thousands of - // pixels) instead of the viewport width, and every row oscillates - // around its measured height as inner-VL bounds churn. + explicitMain ?? parentMain ?? (measuredOuterMain > 0 ? measuredOuterMain : 0); + const explicitCross = horizontal ? (style?.h as number | undefined) : (style?.w as number | undefined); - const parentCross = horizontal - ? parentCellBounds?.height - : parentCellBounds?.width; + const parentCross = horizontal ? parentCellBounds?.height : parentCellBounds?.width; const measuredOuterCross = horizontal ? measuredSize.h : measuredSize.w; let viewportCrossSize: number; - // Vertical VLs: parent and measured cross are reliable — for a vertical - // VL nested inside a column-flex parent, both report the full allocated - // column width, which is exactly what cells should fill. Cell-content - // cross sizes (per-row natural widths) are typically larger than the - // viewport in app rows that wrap inner horizontal scrollers; using them - // would set cellCross to the scroller's full content width. Skip. - // - // Horizontal VLs: parent and measured cross are NOT reliable — for a - // horizontal VL nested inside a parent section that contains other - // siblings (a title, etc.), the outer cell's measured height is - // title+innerVL+other, and `parentCellBounds.height` propagates that - // total down. The inner VL's cards only need their own height, not the - // section's. So for horizontal we prefer content-driven cellCross when - // available and only fall back to parent/measured/estimate when no - // content has been measured yet. + // Cross-axis priority differs by orientation. Vertical: parent/measured + // cross is reliable (parent flex allocates column width). Horizontal: + // parent/measured cross is the OUTER cell's full height (title + this VL + // + siblings) which is bigger than the cards themselves — prefer + // content-driven `maxContentCross` and only fall back when no content + // has measured yet. Without the asymmetry the cells oscillate as the + // outer cell's measured height churns during scroll/focus animations. if (explicitCross != null && explicitCross > 0) { viewportCrossSize = explicitCross; } else if (!horizontal && parentCross != null && parentCross > 0) { @@ -296,52 +223,19 @@ function VirtualListInner( separatorSize, ]); - // Lazy-init via useState for the same reason as `layoutManager`: - // `useRef(null) + if (!ref.current) ref.current = ...` is a render-phase - // ref read+write that bails out React Compiler's whole-function - // memoization. - const [pool] = useState( - () => new RecyclerPool(horizontal ? "h" : "v"), - ); + const [pool] = useState(() => new RecyclerPool(horizontal ? 'h' : 'v')); const getKey = (index: number): string => - keyExtractor && data[index] !== undefined - ? keyExtractor(data[index], index) - : String(index); + keyExtractor && data[index] !== undefined ? keyExtractor(data[index], index) : String(index); const getData = (i: number) => data[i]; const getLayout = (i: number) => layoutManager.getLayout(i); const totalContentSize = - paddingStart + - headerSize + - layoutManager.totalSize + - footerSize + - paddingEnd; - // Cross size of the scrollable content area: viewport size when known - // (so content fills its container); otherwise the cells' computed cross - // size plus padding. + paddingStart + headerSize + layoutManager.totalSize + footerSize + paddingEnd; const finalCross = - viewportCrossSize > 0 - ? viewportCrossSize - : cellCrossSize * numColumns + crossPadding; - - // Two-layer commit dampening: - // - // 1. While a scroll/focus-snap animation is running, LM is in batching - // mode — reports accumulate per-userKey and only commit on animation - // end (skipping the dampening path entirely). This keeps the layout - // frozen for the visible duration of the animation. - // - // 2. Outside of animations, LM uses per-userKey stability dampening - // with a backstop timer. Reports that differ from the stored value - // sit pending until either a matching report arrives after the - // stability window, or the backstop fires. This absorbs the - // multi-frame cascade where a section's measured height keeps - // shifting as inner cells/async content settle after the scroll. - // While true, contentStyle omits x/y so React reconciliation can't - // clobber the imperative animation in flight. Flipped by the animation - // start/end hooks; the stopped handler in useScrollHandler pins the - // final node.x/y, and the subsequent render (false again) writes the - // matching declarative value. + viewportCrossSize > 0 ? viewportCrossSize : cellCrossSize * numColumns + crossPadding; + + // While true, contentStyle omits x/y so reconciliation can't clobber the + // imperative scroll animation in flight. const [isScrollAnimating, setIsScrollAnimating] = useState(false); const handleAnimationStart = () => { @@ -356,16 +250,9 @@ function VirtualListInner( } }; - // Cell handlers are declared HERE, before `pool.reconcile(...)` further - // below, because that method-call appears to be React Compiler 1.0's - // optimization boundary in this function — anything declared after is - // emitted as a plain inline `const` and isn't memoized. Cells consume - // these handlers as props and rely on stable identities so their - // `memo`/`areCellPropsEqual` short-circuit holds across VL renders. - // Each handler captures only stable values (layoutManager from a lazy - // `useState`, stable setters, refs accessed inside the closure body), - // so the compiler-emitted `if ($[i] !== layoutManager) ...` cache check - // is enough to keep them referentially stable. + // Declared BEFORE `pool.reconcile(...)` below — that call is React + // Compiler 1.0's optimization boundary in this function, so anything + // declared after it is emitted unmemoized. const handleItemSizeChange = (userKey: string, measuredSize: number) => { if (layoutManager.reportItemSize(userKey, measuredSize)) { setLayoutVersion((v) => v + 1); @@ -405,7 +292,6 @@ function VirtualListInner( layoutManager: layoutManager as LayoutManager, horizontal, viewportSize, - drawDistance, itemAreaOffset, totalContentSize, viewportCrossSize, @@ -422,8 +308,8 @@ function VirtualListInner( onAnimationEnd: handleAnimationEnd, }); - // Persist scroll position to the parent's cache when the visible range - // commits. Focus changes write to the cache directly via handleVLFocus. + // Backstop for non-focus scrolls (touch/wheel/imperative scrollToOffset). + // Focus-driven scrolls write to the cache directly via handleVLFocus. useEffect(() => { if (cellKey == null || !parentStateCache) { return; @@ -434,42 +320,25 @@ function VirtualListInner( focusedIndex, measurements: layoutManager.getMeasurements(), }); - }, [ - cellKey, - committedScrollOffset, - focusedIndex, - parentStateCache, - layoutManager, - ]); + }, [cellKey, committedScrollOffset, focusedIndex, parentStateCache, layoutManager]); - // Detect cellKey change: this VL was recycled into a different row. - // Save the old row's state, restore the new row's state. Done as a - // derived-state-from-props pattern (setState during render) so the - // current render uses the new state — avoids a flash of stale - // scroll/focus. Stored as state (not a ref) so the comparison and - // setState pair don't trigger React Compiler's render-phase ref bailout. + // Recycle into a different row: save outgoing state and restore incoming. + // setState during render so this render uses the new state — avoids a + // flash of stale scroll/focus. const [prevCellKey, setPrevCellKey] = useState(cellKey); if (prevCellKey !== cellKey) { if (prevCellKey != null && parentStateCache) { parentStateCache.set(prevCellKey, { - // `committedScrollOffset` is the latest committed scroll for this - // (about-to-leave) cellKey. Reading `scrollOffsetRef.current` - // here would be a render-phase ref read — bailout territory — - // and the inner VL hasn't been animating, so the committed value - // matches `scrollOffsetRef.current` for our purposes. scrollOffset: committedScrollOffset, focusedIndex, measurements: layoutManager.getMeasurements(), }); } - // Apply the incoming config synchronously so the cells in THIS render - // lay out against the new content with correctly-keyed measurements. - // Without this, LM's `_data` would still be the prior content (the - // useLayoutEffect that calls updateConfig hasn't fired yet), so - // `_resolveSize` would derive userKeys from the old data and miss - // every entry in the just-restored measurements map. + // Synchronous config update so cells in THIS render lay out against + // the new data with correctly-keyed measurements (the useLayoutEffect + // that calls updateConfig hasn't fired yet). layoutManager.updateConfig({ data, estimatedItemSize, @@ -482,9 +351,7 @@ function VirtualListInner( }); const incoming = - cellKey != null && parentStateCache - ? parentStateCache.get(cellKey) - : undefined; + cellKey != null && parentStateCache ? parentStateCache.get(cellKey) : undefined; resetScroll(incoming?.scrollOffset ?? 0); // The `?? 0` fallback is load-bearing. The outer FG's `focusedElement` @@ -508,19 +375,11 @@ function VirtualListInner( setPrevCellKey(cellKey); } - // Focus tracking: when a focusable descendant is focused, find which - // item index it lives in via its position relative to contentRef. Write - // directly to the parent's cache so the latest focused index survives - // recycle even when no scroll-driven useEffect runs between focus moves. - // - // Order matters: child's position relative to contentRef is independent - // of scroll (cells are absolutely positioned inside contentRef), so we - // can compute the target index BEFORE running alignment. Then we run - // alignment — which synchronously updates scrollOffsetRef.current to - // the snap target — and finally write to the cache. Writing before - // alignment would capture the pre-scroll offset, which on restore - // requires a second focus event to converge (the user sees the row - // land slightly off, then jump to the right spot). + // Order matters: resolve target index FIRST (child position relative to + // contentRef is scroll-independent), then run alignment (which updates + // scrollOffsetRef.current synchronously to the snap target), THEN write + // to the cache. Writing before alignment captures the pre-scroll offset + // and restore requires a second focus event to converge. const handleVLFocus = (child: LightningElement) => { if (skipNextFocus) { setSkipNextFocus(false); @@ -556,11 +415,7 @@ function VirtualListInner( }; const scrollInItemSpace = Math.max(0, committedScrollOffset - itemAreaOffset); - const visibleRange = layoutManager.getVisibleRange( - scrollInItemSpace, - viewportSize, - drawDistance, - ); + const visibleRange = layoutManager.getVisibleRange(scrollInItemSpace, viewportSize, drawDistance); const visibleIndices: number[] = []; if (data.length > 0 && visibleRange.endIndex >= visibleRange.startIndex) { @@ -587,34 +442,23 @@ function VirtualListInner( const slotAssignments = pool.reconcile(visibleIndices, getType); + // Per-key measurements deliberately NOT cleared on data identity change — + // tab switches re-create the array even when userKeys are identical, and + // re-measuring from estimate would shift rows visibly. Callers can call + // `layoutManager.clearMeasurements()` imperatively when they need it. useLayoutEffect(() => { maxContentCrossRef.current = 0; setMaxContentCross(0); - // Per-key measurements are NOT cleared here on purpose. Tab switches - // and other re-renders often produce a new `data` array reference - // even when the underlying items (and their userKeys) haven't - // changed. Clearing measurements would force every cell to re-measure - // from estimate, and any render-time variance (focus state, async - // content) would shift row positions visibly. If a caller truly - // needs to invalidate measurements, they can call - // `layoutManager.clearMeasurements()` imperatively. // oxlint-disable-next-line react-hooks/exhaustive-deps -- intentional reset on data identity change }, [data, extraData]); const handleViewportResize = (event: { w: number; h: number }) => { - setMeasuredSize((prev) => - prev.w === event.w && prev.h === event.h ? prev : event, - ); + setMeasuredSize((prev) => (prev.w === event.w && prev.h === event.h ? prev : event)); }; useImperativeHandle(ref, () => ({ scrollToIndex: (params) => - scrollToIndex( - params.index, - params.animated, - params.viewPosition, - params.viewOffset, - ), + scrollToIndex(params.index, params.animated, params.viewPosition, params.viewOffset), scrollToOffset: (params) => scrollToOffset(params.offset, params.animated), scrollToEnd: (params) => scrollToEnd(params?.animated), getScrollOffset: () => scrollOffsetRef.current, @@ -669,9 +513,7 @@ function VirtualListInner( ? [0, drawDistance * 2, 0, drawDistance * 2] : [drawDistance * 2, 0, drawDistance * 2, 0], ...style, - ...(padding.backgroundColor != null - ? { color: padding.backgroundColor } - : undefined), + ...(padding.backgroundColor != null ? { color: padding.backgroundColor } : undefined), }; if (data.length === 0 && ListEmptyComponent) { @@ -692,11 +534,9 @@ function VirtualListInner( } const scrollPosition = -committedScrollOffset; - // Skip declarative x/y while a scroll animation is in flight — the - // imperative `el.node.animate(...)` in useScrollHandler is the source - // of truth for position during that window. Reapplying the target via - // contentStyle on a mid-animation re-render (e.g. when the visible - // range commits) snaps the content past the interpolated value. + // Skip x/y while a scroll animation is in flight; the imperative + // node.animate() owns position during that window. Re-applying the + // target on a mid-animation render snaps past the interpolated value. const contentStyle: LightningViewElementStyle = horizontal ? { w: totalContentSize, @@ -778,7 +618,7 @@ function VirtualListInner( {ListHeaderComponent && ( ( {ListFooterComponent && ( ( props: VirtualListProps & { ref?: Ref }, ) => ReactElement | null; -(VirtualList as { displayName?: string }).displayName = "VirtualList"; +(VirtualList as { displayName?: string }).displayName = 'VirtualList'; diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx index 7b41187..d9e1504 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx @@ -9,88 +9,28 @@ import { CellBoundsContext, VLCellKeyContext } from './VirtualListContext'; import type { VirtualListRenderItemInfo } from './VirtualListTypes'; export interface VirtualListCellProps { - /** Main-axis position of the cell within the content container. */ mainOffset: number; - /** Cross-axis position of the cell within the content container. */ crossOffset: number; - /** - * Main-axis size dictated by LayoutManager (estimate / override / last - * measurement). The cell wrapper is pinned to this exactly — VL is the - * single source of cell positioning. Measurement happens on the inner - * FlexRoot, not on this element. - */ size: number; - /** Cross-axis size of the cell (from LayoutManager, viewport-driven). */ crossSize: number; renderItem: (info: VirtualListRenderItemInfo) => ReactElement; item: T; index: number; - /** - * Content-identity key from VL's keyExtractor. Two roles: - * 1. Stable identity for measurement: passed to onItemSizeChange so - * LayoutManager keys measurements by userKey, not index. - * 2. Provided to descendants via VLCellKeyContext for nested-VL state - * persistence — when this cell's slot recycles to different content, - * the nested VL's `cellKey` context value flips, which fires its - * cellKey-change branch (save old measurements/scroll/focus, restore - * incoming) so the nested VL component instance survives the recycle. - */ + /** Stable identity from VL's keyExtractor; keys measurements and provides VLCellKeyContext to descendants. */ userKey: string; - /** - * Forwarded to renderItem's info as `shouldFocus`. True when this cell's - * item should auto-focus on mount (focus restoration after a recycle). - */ shouldFocus: boolean; extraData?: unknown; horizontal: boolean; isLastItem: boolean; ItemSeparatorComponent?: ComponentType | null; - /** - * True when the VL has a flex ancestor — yoga is already running in - * this subtree. The cell wraps content in a FlexRoot so the user's - * content can use flex layout AND so the cell can measure its rendered - * main-axis size and report it. When false, no flex is involved at all - * — cells are fully pinned and the caller is responsible for accurate - * `estimatedItemSize` / `overrideItemLayout` (FlashList v1 strict mode). - */ + /** True when a flex ancestor exists; cells wrap in FlexRoot for layout + measurement. False means pinned/silent. */ isInFlex: boolean; - /** - * Called whenever the inner FlexRoot reports a new main-axis size for - * this cell. Only fired in flex mode — see `isInFlex`. The handler - * always receives a positive main-axis number; zero/negative reports - * are filtered out here. - */ onItemSizeChange?: (userKey: string, size: number) => void; - /** - * Called when `renderItem` returns null. The VL marks the row as - * logically empty so layout collapses it to zero main-axis size. - * Distinct from `onItemSizeChange(userKey, 0)` (which is rejected) — - * this is the explicit "no content" path. - */ + /** Distinct from `onItemSizeChange(_, 0)` (rejected) — this is the explicit empty-row path. */ onItemEmpty?: (userKey: string) => void; - /** - * Called whenever this cell's content reports a cross-axis size (flex - * mode only). VL keeps a single max across all reporters as a fallback - * cross size when no explicit `style.h` (or `.w` for vertical) and no - * `parentCellBounds` are available. Same dedupe approach as - * `onSeparatorLayout`. - */ onContentCrossLayout?: (size: number) => void; - /** - * Called when this cell's separator first measures (flex mode only). - * VL is expected to dedupe — every cell that has a separator reports - * its size, but they're all the same component so VL only acts on the - * first non-zero value (or genuine changes thereafter). Cross-axis is - * ignored; only the main-axis dimension is reported. - */ onSeparatorLayout?: (size: number) => void; - /** - * True when this cell is currently being held in the recycler pool — - * i.e. it's mounted (so its React subtree and any nested recycler - * pools survive) but rendered offscreen and excluded from focus - * traversal. The cell's outer FocusGroup is disabled so spatial - * navigation skips both the cell and its descendants. - */ + /** Mounted offscreen for state preservation; outer FG is disabled so spatial nav skips it. */ pooled?: boolean; } @@ -115,9 +55,6 @@ const VirtualListCellInner = ({ onSeparatorLayout, pooled = false, }: VirtualListCellProps): ReactElement | null => { - // Cell wrapper is positioned and sized by VL exclusively. It uses - // absolute positioning with explicit width AND height — no flex on this - // element. VL is the single source of where and how big a cell is. const cellStyle: LightningViewElementStyle = { position: 'absolute', x: horizontal ? mainOffset : crossOffset, @@ -132,11 +69,6 @@ const VirtualListCellInner = ({ }; const flexRootRef = useRef(null); - // The cell's outer FocusGroup element. We need a ref to it so we can - // imperatively claim focus when `shouldFocus` flips false → true on an - // already-mounted cell — `useFocus.setAutoFocus` only updates the - // property, it doesn't actively claim focus. Without this, focus - // restoration on slot recycle would silently fail. const cellElementRef = useRef(null); const prevShouldFocusRef = useRef(shouldFocus); const focusManager = useFocusManager(); @@ -144,29 +76,15 @@ const VirtualListCellInner = ({ const renderedItem = renderItem({ item, index, extraData, shouldFocus }); const isEmpty = renderedItem == null; - // Imperative focus-tree update on shouldFocus transition. Only fires - // on a false → true flip for an existing cell — fresh mounts are - // handled by FocusGroup's autoFocus via useFocus → addElement. + // Imperative focus claim on shouldFocus false → true. Mount-time claims + // go through FocusGroup's autoFocus → useFocus.addElement instead. // - // Why `setFocusedChild` and not `focus()` (either on the element or via - // FocusManager): when a parent VL recycles its slot back to this row's - // userKey, the inner VL hits its cellKey-change branch and restores - // `focusedIndex = N` from the cache. That happens during the parent's - // re-render, BEFORE the user has actually navigated to this row — at - // that moment the user is still focused on whichever row they pressed - // up from. `focusManager.focus(cellN)` walks up setting parent - // focusedElement at every level and runs _recalculateFocusPath, which - // would yank the user's focus across rows. We only want to record - // "when focus next traverses into this inner VL, land on cellN" — i.e. - // update the parent's focusedElement and let _recalculateFocusPath - // decide whether the current path actually intersects (it doesn't, in - // the off-screen restore case, so nothing visible changes). - // - // Calling `cellElementRef.current.focus()` directly is also wrong: it - // only flips `_focused` on the Lightning element and the manager's - // focusedElement chain stays pointing at whatever sibling slot the - // intermediate row's session left behind, so the next traversal lands - // on the wrong cell. + // Use `setFocusedChild`, NOT `focusManager.focus()` or + // `cellElementRef.current.focus()`. The recycle-restore path runs while + // the user is focused on a different row; `focus()` would yank focus + // across rows via `_recalculateFocusPath`, and the element's own + // `.focus()` only flips `_focused` without updating the parent's + // focusedElement so the next traversal lands on a stale sibling. useLayoutEffect(() => { if (shouldFocus && !prevShouldFocusRef.current && cellElementRef.current) { focusManager.setFocusedChild(cellElementRef.current); @@ -188,39 +106,23 @@ const VirtualListCellInner = ({ } }; - // If `renderItem` returns null, signal LM to collapse this row to zero - // main-axis size. Otherwise the row would still occupy `firstMeasured` - // / `estimatedItemSize` worth of space (and following items would be - // pushed down by a phantom gap). Effect fires after the render commits - // — order doesn't matter; `reportItemEmpty` is idempotent and dedupes. - // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemEmpty is intentionally - // omitted: it captures only stable values (LayoutManager from useState lazy init, stable - // setters), so a "stale" closure is functionally identical to a fresh one. Including it - // in deps would force the effect to re-fire on every VL render because React Compiler - // doesn't memoize VL's cell handlers (they live after pool.reconcile, the compiler's - // optimization boundary in VirtualListInner). + // Collapse the row to zero in LM when renderItem returns null. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemEmpty omitted: captures + // only stable values (lazy-init LM, stable setters); compiler doesn't memoize VL's cell + // handlers (they live after pool.reconcile), so including the dep would re-fire on every + // VL render with no behavior change. useLayoutEffect(() => { if (isEmpty && onItemEmpty) { onItemEmpty(userKey); } }, [isEmpty, userKey]); - // One-shot push on mount and on `userKey` change (slot recycle). All - // ongoing size changes after that flow through `handleResize` → - // `onResize` (NodeResizeObserver). We need this hook because - // NodeResizeObserver only fires when the FlexRoot's own dimensions - // change — when a slot recycles to new content that happens to lay out - // at the same size as the previous occupant, the observer is silent - // but LM still needs the per-key measurement under the new userKey. - // RAF defers until after yoga's layout pass so node.w/h reflect the - // post-render dimensions. - // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemSizeChange / onContentCrossLayout - // are intentionally omitted: VL's compiler-generated output (current React Compiler 1.0) - // doesn't memoize cell handlers because they live after pool.reconcile (an apparent - // optimization boundary). Their closures only capture stable values (LayoutManager from - // useState lazy init, stable setters), so a "stale" reference is functionally equivalent - // to a fresh one. Including them in deps would re-fire this effect on every VL render - // and re-push every cell's measurement uselessly. + // One-shot push on userKey change for the same-size-recycle case: + // NodeResizeObserver stays silent when content lays out at the previous + // occupant's size, but LM still needs the new userKey's measurement. + // RAF defers past yoga's layout pass so node.w/h are post-render. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- onItemSizeChange/onContentCrossLayout + // omitted: see the previous effect for the rationale. useLayoutEffect(() => { if (!isInFlex || !onItemSizeChange || isEmpty) { return; @@ -250,9 +152,7 @@ const VirtualListCellInner = ({ }; }, [userKey, isInFlex, isEmpty, horizontal]); - const separatorPosition: { x: number } | { y: number } = horizontal - ? { x: size } - : { y: size }; + const separatorPosition: { x: number } | { y: number } = horizontal ? { x: size } : { y: size }; // Return null AFTER the hooks so we don't paint an empty cell wrapper. // The empty-row effect above signals LM via `onItemEmpty` so the row @@ -267,33 +167,10 @@ const VirtualListCellInner = ({ ); - // Two paths: - // - // - In flex (`isInFlex`): yoga is already running in this subtree, so - // we wrap content in a FlexRoot. The FlexRoot has NO width/height - // pinning — yoga sizes it to fit content on both axes. The cell - // reports both axes back to VL via `handleResize`: main-axis goes - // into LayoutManager's per-key measurement store, cross-axis goes - // into VL's max-content-cross fallback. Leaving the cross axis - // unpinned is what lets VL size to content when no explicit cross - // source is available — pinning it would create the feedback loop - // the prior architecture had (cell reports its own pinned size back - // to VL, which uses that to compute the pin, etc.). - // - // Tradeoff: flex content using cross-axis percentages (e.g. - // `width: '100%'` inside a vertical VL's cell) won't work, because - // the FlexRoot has no fixed cross dim to compute the percentage - // against. Callers needing those layouts should set `style.h` (or - // `.w`) on the VL — that flips the chain into the explicit branch - // and `crossSize` becomes a real number the renderItem can target. - // - // - Not in flex: there is no yoga in this subtree. Adding a FlexRoot - // would force one to spin up just for measurement, which has cost - // and no benefit (the user's renderItem isn't using flex anyway). - // We render content plainly. No measurement is reported; the cell - // stays at exactly the size LayoutManager dictated, so the caller - // is responsible for accurate `estimatedItemSize` / - // `overrideItemLayout`. + // FlexRoot is unpinned on both axes so yoga shrinks-to-fit content; + // pinning the cross axis would create a cell→VL→cell feedback loop. + // Tradeoff: cross-axis percentages (`width: '100%'` in a vertical VL + // cell) need the caller to set `style.h`/`.w` on the VL. const measuredContent = isInFlex ? ( {innerContent} @@ -310,12 +187,6 @@ const VirtualListCellInner = ({ } }; - // Separator: in flex mode, wrap in a FlexRoot so yoga measures it and - // we can report the size up to VL. VL dedupes — every cell reports the - // same size, but VL only acts on changes. In pinned mode there's no - // measurement, so the separator just renders at its own intrinsic size - // (which the caller is responsible for setting via explicit dims), and - // VL keeps `separatorSize` at whatever the caller passed in (default 0). let separatorEl: ReactElement | null = null; if (ItemSeparatorComponent && !isLastItem) { @@ -329,25 +200,12 @@ const VirtualListCellInner = ({ ); separatorEl = ( - - {separatorContent} - + {separatorContent} ); } - // Each cell is its own FocusGroup. Spatial navigation within a cell - // (e.g., a row with multiple buttons) stays inside the cell until - // there's no candidate, then bubbles up to the VL's outer FocusGroup - // for cross-cell movement. autoFocus={shouldFocus} restores focus on - // initial mount; the imperative `cellElementRef.current.focus()` in - // the layoutEffect above handles subsequent recycle-time transitions. return ( - + {measuredContent} {separatorEl} @@ -358,17 +216,11 @@ function areCellPropsEqual( prev: VirtualListCellProps, next: VirtualListCellProps, ): boolean { - // The four `on*` callbacks below are intentionally skipped in this - // comparison. React Compiler doesn't memoize them inside VirtualList - // (they're inline closures defined after `pool.reconcile(...)`, which - // appears to be the compiler's optimization boundary in this function), - // so they're fresh references on every VL render. But each captures - // only stable values — `layoutManager` from a lazy `useState` (set - // once), the stable setState setters, and refs accessed inside the - // closure body. A "stale" callback from a prior render is functionally - // identical to a fresh one, so we don't need to bust memoization on - // identity. Comparing them would force every visible cell to re-render - // on every VL render, defeating the whole point of `memo`. + // The four `on*` callbacks are intentionally skipped — React Compiler + // doesn't memoize them in VL (they sit after pool.reconcile, the + // optimization boundary), and they capture only stable values, so a + // "stale" reference is identical to a fresh one. Comparing identity + // would re-render every cell on every VL render. return ( prev.mainOffset === next.mainOffset && prev.crossOffset === next.crossOffset && diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts b/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts index 7a5ec93..1a4018b 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts @@ -46,6 +46,7 @@ export const VLCellKeyContext: Context = createContext | null> = createContext< - Map | null ->(null); +export const VLStateCacheContext: Context | null> = createContext | null>(null); diff --git a/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts index 18b4fa9..abe6a9f 100644 --- a/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts +++ b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts @@ -9,7 +9,6 @@ export interface UseScrollHandlerOptions { layoutManager: LayoutManager; horizontal: boolean; viewportSize: number; - drawDistance: number; /** Offset from the top of the content container to the first item. */ itemAreaOffset: number; /** Total size of the scrollable content (including padding, header, footer). */ @@ -27,25 +26,10 @@ export interface UseScrollHandlerOptions { paddingStart: number; /** Main-axis end padding (acts as scroll margin). */ paddingEnd: number; - /** - * Initial scroll offset to start at — used when restoring state for a - * recycled VL whose previous scroll position was cached. - */ initialScrollOffset?: number; - /** - * Fired the moment a scroll/focus-snap animation begins — i.e. when - * `scrollToOffset(_, animated=true)` enters the animated branch and is - * not already in flight. VL uses this to enable LayoutManager batching - * so intermediate yoga measurements during the animation don't reflow - * the layout on every frame. - */ + /** Fires once when an animated scroll begins; VL flips LM into batching. */ onAnimationStart?: () => void; - /** - * Fired when the most recent scroll animation finishes (its `stopped` - * event fires while still being the current one) OR when `resetScroll` - * cancels an animation that was in flight. VL uses this to flush the - * batched measurements and bump layoutVersion in one go. - */ + /** Fires once when the in-flight animation ends or `resetScroll` cancels it; VL drains the batch. */ onAnimationEnd?: () => void; } @@ -65,11 +49,7 @@ export interface UseScrollHandlerResult { ) => void; scrollToEnd: (animated?: boolean) => void; handleChildFocused: (child: LightningElement) => void; - /** - * Imperatively jump scroll state to an absolute offset without animation - * or onScroll/onEndReached side-effects. Used to restore cached scroll - * position when a recycled VL switches to a different cellKey. - */ + /** Jump to an offset with no animation or onScroll/onEndReached side-effects (cellKey-restore path). */ resetScroll: (offset: number) => void; } @@ -78,7 +58,6 @@ export function useScrollHandler(options: UseScrollHandlerOptions): UseScrollHan layoutManager, horizontal, viewportSize, - drawDistance, itemAreaOffset, totalContentSize, viewportCrossSize, @@ -106,10 +85,8 @@ export function useScrollHandler(options: UseScrollHandlerOptions): UseScrollHan const isAnimatingRef = useRef(false); const [committedScrollOffset, setCommittedScrollOffset] = useState(initialScrollOffset); - // Apply the restored scroll offset to lightning on mount. useState's - // initializer keeps committedScrollOffset in sync, but the contentRef's - // node still needs node.x/y written so the scroll position is visually - // correct from the first frame. + // Pin restored scroll offset to node.x/y on mount so first paint matches + // committedScrollOffset (which useState already initialized). // oxlint-disable-next-line react-hooks/exhaustive-deps -- mount-only restore useEffect(() => { if (initialScrollOffset > 0 && contentRef.current) { @@ -182,11 +159,8 @@ export function useScrollHandler(options: UseScrollHandlerOptions): UseScrollHan scrollOffsetRef.current = clamped; applyPosition(clamped, animated); - // Commit unconditionally. `committedScrollOffset` drives `contentStyle.x` - // on the post-animation render and the parent's state-cache write — both - // need the actual scroll, not just the offset of the last range change. - // Same-value setState bails out of rendering, so a no-op commit costs - // nothing. + // Commit unconditionally — drives contentStyle.x on the post-animation + // render and the parent's cache write. Same-value setState is free. setCommittedScrollOffset(clamped); if (onEndReached) { diff --git a/packages/react-lightning-components/src/exports/text/StyledText.tsx b/packages/react-lightning-components/src/exports/text/StyledText.tsx index 33a2ccf..c6588a4 100644 --- a/packages/react-lightning-components/src/exports/text/StyledText.tsx +++ b/packages/react-lightning-components/src/exports/text/StyledText.tsx @@ -159,6 +159,7 @@ const StyledText: FC = ({ // oxlint-disable-next-line react-hooks/exhaustive-deps -- text/values/tagStyles force re-layout when content changes useEffect(() => { const container = containerRef.current; + if (!container) { return; } diff --git a/packages/react-lightning/src/element/LightningViewElement.ts b/packages/react-lightning/src/element/LightningViewElement.ts index 60be67e..9d617dc 100644 --- a/packages/react-lightning/src/element/LightningViewElement.ts +++ b/packages/react-lightning/src/element/LightningViewElement.ts @@ -223,6 +223,7 @@ export class LightningViewElement< // Optimize child iteration for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; + if (child) { child.deferTarget = value; } @@ -398,6 +399,7 @@ export class LightningViewElement< for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; + if (child) { child.node.parent = node; } @@ -672,8 +674,7 @@ export class LightningViewElement< }; private _reconcileResizeObserving(): void { - const shouldObserve = - this.props.onResize != null || this._eventEmitter.hasListeners('resized'); + const shouldObserve = this.props.onResize != null || this._eventEmitter.hasListeners('resized'); if (shouldObserve === this._isObservingResize) { return; @@ -782,6 +783,7 @@ export class LightningViewElement< // Check for style changes without allocating an array let hasStyleChanges = false; + if (payload.style) { for (const _ in payload.style) { hasStyleChanges = true; @@ -979,15 +981,19 @@ export class LightningViewElement< if (borderTop) { props[hasRounded ? 'border-top' : 'top'] = borderTop; } + if (borderLeft) { props[hasRounded ? 'border-left' : 'left'] = borderLeft; } + if (borderRight) { props[hasRounded ? 'border-right' : 'right'] = borderRight; } + if (borderBottom) { props[hasRounded ? 'border-bottom' : 'bottom'] = borderBottom; } + if (borderColor) { props[hasRounded ? 'border-color' : 'color'] = borderColor; } @@ -1039,12 +1045,15 @@ export class LightningViewElement< if (!style.w) { finalStyle.w = rect.w; } + if (!style.h) { finalStyle.h = rect.h; } + if (!style.x) { finalStyle.x = rect.x; } + if (!style.y) { finalStyle.y = rect.y; } diff --git a/packages/react-lightning/src/focus/FocusKeyManager.ts b/packages/react-lightning/src/focus/FocusKeyManager.ts index 97f3158..429218d 100644 --- a/packages/react-lightning/src/focus/FocusKeyManager.ts +++ b/packages/react-lightning/src/focus/FocusKeyManager.ts @@ -81,6 +81,7 @@ export class FocusKeyManager { if (closestElement) { this._focusManager.focus(closestElement as T); + return false; } diff --git a/packages/react-lightning/src/focus/FocusManager.ts b/packages/react-lightning/src/focus/FocusManager.ts index c919c41..1502ba1 100644 --- a/packages/react-lightning/src/focus/FocusManager.ts +++ b/packages/react-lightning/src/focus/FocusManager.ts @@ -335,6 +335,7 @@ export class FocusManager< for (let i = previousLayer.focusPath.length - 1; i >= 0; i--) { // oxlint-disable-next-line typescript/no-non-null-assertion -- bounds-checked loop const element = previousLayer.focusPath[i]!; + if (element.focused) { element.blur(); this._eventEmitter.emit('blurred', element); @@ -373,6 +374,7 @@ export class FocusManager< for (let i = currentLayer.focusPath.length - 1; i >= 0; i--) { // oxlint-disable-next-line typescript/no-non-null-assertion -- bounds-checked loop const element = currentLayer.focusPath[i]!; + if (element.focused) { element.blur(); this._eventEmitter.emit('blurred', element); @@ -520,6 +522,7 @@ export class FocusManager< // Look up the current node to avoid stale closure references // after re-parenting const currentNode = this.activeLayer.elements.get(element); + if (!currentNode) { return; } @@ -532,13 +535,16 @@ export class FocusManager< currentNode, ); } + this._checkFocusableChildren(currentNode.parent); this._recalculateFocusPath(); }), element.on('focusChanged', (_, isFocused) => { if (isFocused && !element.focused) { const currentNode = this.activeLayer.elements.get(element); + this.focus(element); + if (currentNode) { this._tryEmitChildFocusedEvent(currentNode); } @@ -551,6 +557,7 @@ export class FocusManager< const { element } = node; const disposers = this._disposers.get(element); + if (disposers) { for (const dispose of disposers) { dispose(); @@ -579,18 +586,23 @@ export class FocusManager< if (!focusNode) { console.warn('FocusManager: No focus node found for destination', destination); + return; } // Detect redirect cycles const visited = visitedRedirects ?? new Set(); + if (visited.has(destination)) { console.warn('FocusManager: Focus redirect cycle detected, aborting'); + return; } + visited.add(destination); this._focusNode(focusNode, visited); + return; } } @@ -651,6 +663,7 @@ export class FocusManager< if (childrenLength === 0) { parentNode.hasFocusableChildren = false; + return; } @@ -772,7 +785,9 @@ export class FocusManager< // Build new path only when we know it changed // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated array filled in the loop below const newPath: T[] = new Array(newLength); + curr = layer.root.focusedElement; + for (let i = 0; i < newLength; i++) { // oxlint-disable-next-line typescript/no-non-null-assertion -- curr is non-null for newLength iterations newPath[i] = curr!.element; diff --git a/packages/react-lightning/src/render/mapReactPropsToLightning.ts b/packages/react-lightning/src/render/mapReactPropsToLightning.ts index 5941b01..37d7ff3 100644 --- a/packages/react-lightning/src/render/mapReactPropsToLightning.ts +++ b/packages/react-lightning/src/render/mapReactPropsToLightning.ts @@ -44,6 +44,7 @@ export function mapReactPropsToLightning( // Single-pass: validate and concatenate simultaneously let text = ''; let allValid = true; + for (let i = 0; i < children.length; i++) { if (isValidTextChild(children[i])) { text += String(children[i]); @@ -52,6 +53,7 @@ export function mapReactPropsToLightning( break; } } + if (allValid) { textProps.text = text; } diff --git a/packages/react-lightning/src/shim/resizeObserverShim.ts b/packages/react-lightning/src/shim/resizeObserverShim.ts index c1edfa0..c273430 100644 --- a/packages/react-lightning/src/shim/resizeObserverShim.ts +++ b/packages/react-lightning/src/shim/resizeObserverShim.ts @@ -15,8 +15,10 @@ class LightningResizeObserver extends window.ResizeObserver { if (target instanceof LightningViewElement) { this._targets.add(target); target.on('layout', this._fireCallbacks); + return; } + super.observe(target, options); } @@ -24,8 +26,10 @@ class LightningResizeObserver extends window.ResizeObserver { if (target instanceof LightningViewElement) { this._targets.delete(target); target.off('layout', this._fireCallbacks); + return; } + super.unobserve(target); } diff --git a/packages/react-lightning/src/utils/EventEmitter.ts b/packages/react-lightning/src/utils/EventEmitter.ts index b444560..c652195 100644 --- a/packages/react-lightning/src/utils/EventEmitter.ts +++ b/packages/react-lightning/src/utils/EventEmitter.ts @@ -12,6 +12,7 @@ export class EventEmitter< public on(name: K, listener: T[K]): () => void { if (!listener) { console.warn('[EventEmitter] Invalid argument specified as a listener'); + return () => { /* no-op */ }; diff --git a/packages/react-lightning/src/utils/findClosestElement.ts b/packages/react-lightning/src/utils/findClosestElement.ts index b3e1a57..247af1d 100644 --- a/packages/react-lightning/src/utils/findClosestElement.ts +++ b/packages/react-lightning/src/utils/findClosestElement.ts @@ -184,6 +184,7 @@ function fillDimensions( out.y = y; out.centerX = x + w / 2; out.centerY = y + h / 2; + return out; } diff --git a/packages/react-lightning/src/utils/simpleDiff.ts b/packages/react-lightning/src/utils/simpleDiff.ts index 8c10cf9..1fd0145 100644 --- a/packages/react-lightning/src/utils/simpleDiff.ts +++ b/packages/react-lightning/src/utils/simpleDiff.ts @@ -68,6 +68,7 @@ function areValuesEqual(first: unknown, second: unknown): boolean { // Count keys and compare in a single pass without allocating arrays let firstKeyCount = 0; + for (const key in first as Record) { firstKeyCount++; const firstValue = (first as Record)[key]; @@ -86,8 +87,10 @@ function areValuesEqual(first: unknown, second: unknown): boolean { // Ensure second doesn't have extra keys let secondKeyCount = 0; + for (const _ in second as Record) { secondKeyCount++; + if (secondKeyCount > firstKeyCount) { return false; } @@ -152,8 +155,10 @@ export function simpleDiff(first: T, second: T): Partial | // Check for keys that are only in the second object if (!hasDiffs) { let secondKeyCount = 0; + for (const _ in second) { secondKeyCount++; + if (secondKeyCount > firstKeyCount) { hasDiffs = true; break; diff --git a/packages/react-native-lightning/src/exports/Pressable.tsx b/packages/react-native-lightning/src/exports/Pressable.tsx index 498a73c..5bcda76 100644 --- a/packages/react-native-lightning/src/exports/Pressable.tsx +++ b/packages/react-native-lightning/src/exports/Pressable.tsx @@ -16,6 +16,7 @@ function useEnterKeyHandler(handler: (e: KeyEvent) => void): (e: KeyEvent) => bo return (e) => { if (e.remoteKey === Keys.Enter) { handler(e); + return false; } diff --git a/packages/react-native-lightning/src/exports/StyleSheet.ts b/packages/react-native-lightning/src/exports/StyleSheet.ts index 8ebe89e..e3e8b0a 100644 --- a/packages/react-native-lightning/src/exports/StyleSheet.ts +++ b/packages/react-native-lightning/src/exports/StyleSheet.ts @@ -15,6 +15,7 @@ export function compose(style1: T, style2: T): T | NonNullable[] { if (style1 && style2) { return [style1, style2]; } + return style1 || style2; } diff --git a/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts b/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts index 6d1282e..de9de31 100644 --- a/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts +++ b/packages/react-native-lightning/src/plugins/reactNativePolyfillsPlugin.ts @@ -101,6 +101,7 @@ export const reactNativePolyfillsPlugin = (): Plugin => { console.warn( '[react-native-lightning polyfills] FocusManager not found, cannot set destinations', ); + return; } } @@ -115,6 +116,7 @@ export const reactNativePolyfillsPlugin = (): Plugin => { console.warn( '[react-native-lightning polyfills] FocusManager not found, failed to request focus', ); + return; } } diff --git a/packages/vite-plugin-msdf-fontgen/src/configs.ts b/packages/vite-plugin-msdf-fontgen/src/configs.ts index 6625ea7..0ed0b95 100644 --- a/packages/vite-plugin-msdf-fontgen/src/configs.ts +++ b/packages/vite-plugin-msdf-fontgen/src/configs.ts @@ -41,9 +41,11 @@ export async function ensureConfigsExist({ return () => { console.log('Cleaning up...'); + return Promise.all( cleanupFiles.map((file) => { console.info(` Removing ${file}`); + return unlink(file); }), ); diff --git a/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts b/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts index e8c9d62..aa1d108 100644 --- a/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts +++ b/packages/vite-plugin-msdf-fontgen/src/generateFonts.ts @@ -24,6 +24,7 @@ export default async function generateFonts( if (files.length === 0) { console.log('No font files found'); + return; } diff --git a/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts b/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts index 8e9f27d..b232a4e 100644 --- a/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts +++ b/packages/vite-plugin-msdf-fontgen/src/getFileChangeInfo.ts @@ -19,6 +19,7 @@ export async function getFileChangeInfo( }; } catch (err) { console.error('Error reading file:', err); + return { needsUpdate: true, checksum: null }; } } diff --git a/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts b/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts index 1c5de75..66321d4 100644 --- a/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts +++ b/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts @@ -2,6 +2,7 @@ export function sortByExtension(extensions: string[]): (a: string, b: string) => return (a: string, b: string): number => { const extA = a.split('.').pop() as string; const extB = b.split('.').pop() as string; + return extensions.indexOf(extA) - extensions.indexOf(extB); }; } From df1e9b8d8a3500bc82ab3b406757179081dfbad6 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Sat, 2 May 2026 01:37:59 -0700 Subject: [PATCH 09/10] Fix breaking tests --- .../plugin-flexbox/src/YogaManager.spec.ts | 22 +++++- .../VirtualList/LayoutManager.spec.ts | 63 ++++++++++++--- .../components/VirtualList/LayoutManager.ts | 7 +- .../VirtualList/ViewabilityTracker.spec.ts | 1 - .../src/focus/FocusManager.spec.ts | 79 +++++++++++++++++++ 5 files changed, 158 insertions(+), 14 deletions(-) diff --git a/packages/plugin-flexbox/src/YogaManager.spec.ts b/packages/plugin-flexbox/src/YogaManager.spec.ts index c4e75ce..b0b4a54 100644 --- a/packages/plugin-flexbox/src/YogaManager.spec.ts +++ b/packages/plugin-flexbox/src/YogaManager.spec.ts @@ -11,10 +11,14 @@ const mockNode = { create: vi.fn(), free: vi.fn(), insertChild: vi.fn(), + removeChild: vi.fn(), calculateLayout: vi.fn(), hasNewLayout: vi.fn(), getComputedLayout: vi.fn(), + getComputedLeft: vi.fn(), + getComputedTop: vi.fn(), getComputedWidth: vi.fn(), + getComputedHeight: vi.fn(), getMaxWidth: vi.fn(), getParent: vi.fn(), markLayoutSeen: vi.fn(), @@ -69,7 +73,10 @@ describe('YogaManager', () => { beforeEach(() => { yogaManager = new YogaManager(); - // Reset mock implementations + // Reset mock implementations. The render path uses individual getters + // (getComputedLeft/Top/Width/Height) instead of getComputedLayout, so + // each must be stubbed independently for `_getUpdatedStyles` to write + // a valid update record. mockNode.hasNewLayout.mockReturnValue(true); mockNode.getComputedLayout.mockReturnValue({ left: 10, @@ -77,7 +84,10 @@ describe('YogaManager', () => { width: 100, height: 50, }); + mockNode.getComputedLeft.mockReturnValue(10); + mockNode.getComputedTop.mockReturnValue(20); mockNode.getComputedWidth.mockReturnValue(100); + mockNode.getComputedHeight.mockReturnValue(50); mockNode.getMaxWidth.mockReturnValue({ value: NaN, unit: mockYoga.UNIT_UNDEFINED, @@ -260,7 +270,15 @@ describe('YogaManager', () => { yogaManager.on('render', (buffer) => { expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(mockNode.calculateLayout).toHaveBeenCalledWith(1920, 1080, mockYoga.DIRECTION_LTR); + // YogaManager passes `undefined` for both available dimensions + // so yoga uses each root's own w/h (or shrinks-to-fit). Hard- + // coding 1920×1080 here would stretch unset axes and break + // measurement-driven roots like VirtualList cells. + expect(mockNode.calculateLayout).toHaveBeenCalledWith( + undefined, + undefined, + mockYoga.DIRECTION_LTR, + ); resolve(); }); diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts index b1a565f..c581c6d 100644 --- a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts @@ -310,8 +310,13 @@ describe('LayoutManager', () => { const changed = lm.reportItemSize('0', 150); expect(changed).toBe(true); + expect(lm.getLayout(0)?.size).toBe(150); expect(lm.getLayout(1)?.offset).toBe(150); - expect(lm.totalSize).toBe(350); + // Items 1 and 2 are unmeasured but inherit `_firstMeasuredSize` (150) + // as their implicit fallback now that any cell has reported. + expect(lm.getLayout(1)?.size).toBe(150); + expect(lm.getLayout(2)?.size).toBe(150); + expect(lm.totalSize).toBe(450); }); it('measurement wins over override', () => { @@ -347,7 +352,7 @@ describe('LayoutManager', () => { expect(lm.getLayout(0)?.size).toBe(100); }); - it('returns false when reported size matches stored value', () => { + it('returns false on no-op reports and defers different values via dampening', () => { const lm = new LayoutManager({ data: makeData(2), estimatedItemSize: 100, @@ -356,10 +361,17 @@ describe('LayoutManager', () => { keyExtractor: (item) => String(item.id), }); - lm.reportItemSize('0', 150); + // First measurement applies immediately and returns true. + expect(lm.reportItemSize('0', 150)).toBe(true); + // Exact match on stored value: no-op, returns false. expect(lm.reportItemSize('0', 150)).toBe(false); + // Within 1px of stored value: treated as a match, returns false. expect(lm.reportItemSize('0', 150.4)).toBe(false); - expect(lm.reportItemSize('0', 152)).toBe(true); + // A meaningfully different value goes through stability dampening — + // it returns false synchronously and only commits later via the + // backstop timer or a confirming report after the stability window. + expect(lm.reportItemSize('0', 152)).toBe(false); + expect(lm.getLayout(0)?.size).toBe(150); }); it('measurements survive index shifts (keyed by userKey)', () => { @@ -379,12 +391,19 @@ describe('LayoutManager', () => { const newData = [{ id: 99 }, ...data]; lm.updateConfig({ data: newData }); - expect(lm.getLayout(0)?.size).toBe(100); - expect(lm.getLayout(1)?.size).toBe(100); + // The measurement for id=1 follows the userKey across the index + // shift. Unmeasured items (id=99 and id=0) fall back to the + // first-measured implicit estimate (150), not `estimatedItemSize`. + expect(lm.getLayout(0)?.size).toBe(150); + expect(lm.getLayout(1)?.size).toBe(150); expect(lm.getLayout(2)?.size).toBe(150); }); - it('falls back to estimate when no keyExtractor is provided', () => { + it('without keyExtractor, measurements apply via String(index) keys', () => { + // The cell calls reportItemSize with `String(index)` as the userKey + // when no keyExtractor is configured (see VirtualList.getKey). The + // LayoutManager must use the same fallback or measurements would be + // stored but never found, leaving cells stuck at the estimate. const lm = new LayoutManager({ data: makeData(2), estimatedItemSize: 100, @@ -393,7 +412,33 @@ describe('LayoutManager', () => { }); lm.reportItemSize('0', 150); - expect(lm.getLayout(0)?.size).toBe(100); + expect(lm.getLayout(0)?.size).toBe(150); + }); + + it('without keyExtractor, measurements do NOT survive index shifts', () => { + const data = makeData(3); + const lm = new LayoutManager({ + data, + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + // Measure index 1 — the userKey is the index string '1'. + lm.reportItemSize('1', 150); + expect(lm.getLayout(1)?.size).toBe(150); + + // Inserting at the front shifts every index down. With no + // keyExtractor, the measurement stays under userKey '1' and now + // applies to whatever item happens to be at index 1 in the new + // data, not to the original item that was measured. + const newData = [{ id: 99 }, ...data]; + lm.updateConfig({ data: newData }); + + // Index 1 still resolves to the measurement (now applied to id=0, + // which was originally at index 0). This is the documented gotcha + // — supply a keyExtractor for dynamic lists. + expect(lm.getLayout(1)?.size).toBe(150); }); it('null/undefined data overrides measurement (collapses to 0)', () => { @@ -403,7 +448,7 @@ describe('LayoutManager', () => { estimatedItemSize: 100, numColumns: 1, cellCrossSize: 200, - keyExtractor: (item) => String(item.id), + keyExtractor: (item) => String(item?.id), }); lm.reportItemSize('1', 150); diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts index 9e16f7c..767c6b6 100644 --- a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts @@ -491,8 +491,11 @@ export class LayoutManager { const item = this._data[index]; - if (item != null && this._keyExtractor) { - const userKey = this._keyExtractor(item, index); + if (item != null) { + // Match VirtualListCell: it reports with String(index) when no + // keyExtractor is configured, so per-item lookup must use the same + // key. Without this, measurements would be stored but never found. + const userKey = this._keyExtractor ? this._keyExtractor(item, index) : String(index); const measured = this._measuredSizes.get(userKey); if (measured != null) { diff --git a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts index f79fe10..a4010be 100644 --- a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts +++ b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts @@ -9,7 +9,6 @@ const makeLayout = (offset: number, size: number): ComputedLayout => ({ column: 0, crossOffset: 0, crossSize: 100, - measured: false, }); function getCallArgs(fn: ReturnType, index: number) { diff --git a/packages/react-lightning/src/focus/FocusManager.spec.ts b/packages/react-lightning/src/focus/FocusManager.spec.ts index 486c556..f27c8b0 100644 --- a/packages/react-lightning/src/focus/FocusManager.spec.ts +++ b/packages/react-lightning/src/focus/FocusManager.spec.ts @@ -288,6 +288,85 @@ describe('FocusManager', () => { expect(focusManager.focusPath).toEqual([parent, modal, modalChild, modalGrandChild]); }); + describe('setFocusedChild', () => { + it("updates the parent's preferred child without moving the active focus path", () => { + const root = createMockElement(1, 'root'); + const groupA = createMockElement(2, 'groupA'); + const groupB = createMockElement(3, 'groupB'); + const a1 = createMockElement(4, 'a1'); + const b1 = createMockElement(5, 'b1'); + const b2 = createMockElement(6, 'b2'); + + focusManager.addElement(root, null); + focusManager.addElement(groupA, root); + focusManager.addElement(groupB, root); + focusManager.addElement(a1, groupA, { autoFocus: true }); + focusManager.addElement(b1, groupB, { autoFocus: true }); + focusManager.addElement(b2, groupB); + + // User is focused inside groupA's subtree (root → groupA → a1). + focusManager.focus(a1); + expect(focusManager.focusPath).toEqual([root, groupA, a1]); + + // Mark b2 as the preferred entry into groupB. Since groupB isn't in + // the active focus path, the user's focus should not move. + focusManager.setFocusedChild(b2); + expect(focusManager.focusPath).toEqual([root, groupA, a1]); + + // When focus next traverses into groupB, it lands on b2 (the new + // preferred child) rather than b1 (the original autoFocus pick). + focusManager.focus(groupB); + expect(focusManager.focusPath).toEqual([root, groupB, b2]); + }); + + it('moves focus when the parent IS in the active focus path', () => { + const root = createMockElement(1, 'root'); + const child1 = createMockElement(2, 'child1'); + const child2 = createMockElement(3, 'child2'); + + focusManager.addElement(root, null); + focusManager.addElement(child1, root, { autoFocus: true }); + focusManager.addElement(child2, root); + + expect(focusManager.focusPath).toEqual([root, child1]); + + // root is in the active focus path; setFocusedChild(child2) replaces + // child1 with child2 in the path and triggers blur/focus events. + focusManager.setFocusedChild(child2); + expect(focusManager.focusPath).toEqual([root, child2]); + expect(child1.focused).toBe(false); + expect(child2.focused).toBe(true); + }); + + it('is a no-op when the element is already the preferred child', () => { + const root = createMockElement(1, 'root'); + const child = createMockElement(2, 'child'); + + focusManager.addElement(root, null); + focusManager.addElement(child, root, { autoFocus: true }); + + const focusedSpy = vi.fn(); + focusManager.on('focused', focusedSpy); + + // Already the focusedElement — should not re-fire any focus events. + focusManager.setFocusedChild(child); + expect(focusedSpy).not.toHaveBeenCalled(); + }); + + it('is a no-op for an unknown element', () => { + const root = createMockElement(1, 'root'); + const child = createMockElement(2, 'child'); + const stranger = createMockElement(3, 'stranger'); + + focusManager.addElement(root, null); + focusManager.addElement(child, root, { autoFocus: true }); + + // Stranger was never added — call should silently return. + focusManager.setFocusedChild(stranger); + expect(focusManager.focusPath).toEqual([root, child]); + }); + }); + describe('Layer Management (Modal Support)', () => { it('should create a new layer when pushLayer is called', () => { const mainElement = createMockElement(1, 'main'); From 3e8c17b5841ee8476ccc4fd51af2e58ac6add516 Mon Sep 17 00:00:00 2001 From: Willson Haw Date: Mon, 4 May 2026 14:32:46 -0700 Subject: [PATCH 10/10] Some more cleanup --- .../components/VirtualList/RecyclerPool.ts | 12 -- .../src/components/VirtualList/VirtualList.md | 130 +++++++++--------- .../components/VirtualList/VirtualList.tsx | 2 +- 3 files changed, 69 insertions(+), 75 deletions(-) diff --git a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts index 25f7847..f1ef71e 100644 --- a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts @@ -1,6 +1,4 @@ export class RecyclerPool { - /** Debug-only label used in pool logging to disambiguate instances. */ - private _label: string; /** dataIndex -> slotKey */ private _active = new Map(); /** itemType -> available slotKeys */ @@ -32,10 +30,6 @@ export class RecyclerPool { private _visibleSet = new Set(); private _nextId = 0; - constructor(label = 'pool') { - this._label = label; - } - get activeCount(): number { return this._active.size; } @@ -105,12 +99,6 @@ export class RecyclerPool { this._lastSlotForIndex.set(index, key); } - if (import.meta.env.DEV && (released > 0 || preferredReused > 0 || pooled > 0 || created > 0)) { - console.log( - `[Pool ${this._label}] released:${released} preferred:${preferredReused} pool:${pooled} new:${created} active:${this._active.size} pooled:${this.pooledCount} types:${this._available.size}`, - ); - } - return this._active; } diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.md b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md index 5001368..52efd5b 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualList.md +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md @@ -1,6 +1,6 @@ # VirtualList -A virtualized scroll list for Lightning, modeled on FlashList v1. The user is **Willson**; this doc is the authoritative spec for what the component does, why it does it that way, and what's deliberately out of scope. Edit this file to change requirements — Claude reads it when fixing bugs and adding features. +A virtualized scroll list for Lightning, modeled on FlashList v1. This doc is the authoritative spec for what the component does, why it does it that way, and what's deliberately out of scope. Edit this file to change requirements — Claude reads it when fixing bugs and adding features. --- @@ -17,27 +17,26 @@ When the VL is rendered outside any flex parent (`useIsInFlex() === false`), no When the VL is rendered inside a flex parent (`useIsInFlex() === true`), yoga is already laying out the surrounding tree. In this mode each cell wraps its content in a `FlexRoot`, which: - gives the user's `renderItem` real flex layout (children can `flexGrow`, `flexDirection`, etc.), -- pins its cross-axis to `cellCrossSize` (so flex-percentage layouts have a concrete cross dimension to compute against), -- leaves its main axis free so yoga shrinks-to-fit content, -- emits `onResize` whenever that natural main-axis size changes. +- is **unpinned on both axes** so yoga shrinks-to-fit content (see [Cell rendering](#cell-rendering) for why), +- emits `onResize` whenever its natural main-axis or cross-axis size changes. -The cell forwards the main-axis number — and only the main-axis number — to `LayoutManager.reportItemSize(userKey, size)`. Subsequent items reposition based on the measurement. +The cell forwards the main-axis size to `LayoutManager.reportItemSize(userKey, size)` (drives per-item layout offsets). The cross-axis size is forwarded separately to VL's `maxContentCross` aggregator (a monotonic, reset-on-data-change fallback for `viewportCrossSize` — see [Viewport resolution](#viewport-resolution)). -The crucial discipline in measured mode is **measurement is one-directional and main-axis only** — cross-axis sizes are never aggregated back into VL's layout decisions, and viewport size never depends on cell reports. That's how the prior architecture's three-way feedback loop (cell-cross → `_maxCrossSize` → contentCross → cellCrossSize → cell-cross) is avoided. +The crucial discipline is that the cross-axis aggregation is **monotonic** (only grows) and **resets on data identity change** — that's how the prior architecture's three-way feedback loop (cell-cross → `_maxCrossSize` → contentCross → cellCrossSize → cell-cross) is avoided. `LayoutManager` itself never sees cross-axis cell reports. ### What stays the same in both modes -- The cell wrapper (``) is positioned exclusively by VL. It has no flex layout itself. -- Cross-axis size of every cell is `cellCrossSize`, derived from viewport, never from cell reports. -- Measurements are stored by `userKey` (the caller's `keyExtractor` output), not by index. Recycling preserves measurements; a cell rendering an item it has measured before uses the cached size on first render. +- The cell wrapper is positioned exclusively by VL. It has no flex layout itself. +- Cross-axis size of every cell is `cellCrossSize`, derived from viewport. Cell-reported cross sizes only contribute to `maxContentCross` (a viewport-resolution fallback), never to a per-cell cross dim. +- Measurements are stored by `userKey` (the caller's `keyExtractor` output if provided, else `String(index)`). Recycling preserves measurements; a cell rendering an item it has measured before uses the cached size on first render. Without a `keyExtractor`, measurements don't survive data inserts/removes that shift indices. **Three responsibilities:** -| File | Responsibility | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LayoutManager.ts` | Pure layout math. Given data + sizes + cross-axis size, computes per-item offsets in O(n). | +| File | Responsibility | +| - | - | +| `LayoutManager.ts` | Pure layout math. Given data + sizes + cross-axis size, computes per-item offsets in O(n). | | `VirtualListCell.tsx` | One `` per visible cell with explicit absolute position and dimensions. Wraps user content in `VLCellKeyContext` + `CellBoundsContext` providers. The renderItem subtree persists across slot recycles — the cell wrapper _and_ its descendants survive userKey changes; nested VLs read the new userKey via `VLCellKeyContext` and run their cellKey-change branch instead of remounting. | -| `VirtualList.tsx` | Viewport derivation, scroll/focus state, recycling, the React glue. | +| `VirtualList.tsx` | Viewport derivation, scroll/focus state, recycling, the React glue. | Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven scroll), `useViewability.ts` (onViewableItemsChanged), `RecyclerPool.ts` (slot reuse by item type), `parseContentStyle.ts` (RN-style padding props), `VirtualListContext.ts` (the three React contexts). @@ -74,7 +73,7 @@ Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven ### Behavior - **`drawDistance?: number`** (default `250`) — pixels beyond viewport to keep mounted. Larger = smoother scroll, more memory. -- **`keyExtractor?: (item, index) => string`** — used by recycler for slot identity AND by focus restoration. Falls back to `String(index)`. +- **`keyExtractor?: (item, index) => string`** — produces stable per-item `userKey`s used by `LayoutManager` for measurement keys (so measurements survive recycling and index shifts) and by nested VLs as their `cellKey` for state-persistence cache lookups. Falls back to `String(index)` — measurements still apply within a stable list but don't survive inserts/removes that shift indices. - **`getItemType?: (item, index, extraData) => string | number`** — items of the same type share a recycler pool. - **`extraData?: unknown`** — opaque value forwarded to `renderItem`. Changing it forces re-layout. - **`initialScrollIndex` / `initialScrollIndexParams`** — scroll to a specific index on mount. @@ -98,21 +97,21 @@ Supporting modules: `useScrollHandler.ts` (scroll math, animation, focus-driven ## Sizing contract -**Main-axis size priority** (highest wins): +**Main-axis size priority** (in `_resolveSize` order, first match wins): -1. **Measured size** _(measured mode only)_ — the cell's last reported main-axis dimension, keyed by `userKey`. -2. **`overrideItemLayout` returns `layout.size`**. -3. **Data entry is `null` / `undefined`** — size is forced to `0`, cell is not rendered. -4. **First-measured size** _(measured mode only)_ — once any cell has reported a measurement, that first value is used as the fallback for _unmeasured_ cells in place of `estimatedItemSize`. Locked on first; later measurements update individual cells via the per-key path but do not change the implicit fallback. -5. **Fallback: `estimatedItemSize`** — used only before the first measurement lands (and always in pinned mode, where no measurements happen). +1. **Data entry is `null` / `undefined`** — size is forced to `0`, cell is not rendered. Wins over everything else, including a stale measurement under the same key. +2. **Measured size** _(populated only in measured mode)_ — `_measuredSizes.get(userKey)`. The cell reports under `keyExtractor(item, index)` if provided, else `String(index)`; lookup uses the same key so measurements always apply when the cell has reported. +3. **`overrideItemLayout` returns `layout.size`**. +4. **First-measured size** _(populated only in measured mode)_ — once any cell has reported a non-zero measurement, that first value is used as the fallback for _unmeasured_ cells in place of `estimatedItemSize`. Locked on first; later measurements update individual cells via the per-key path but do not change the implicit fallback. +5. **Fallback: `estimatedItemSize`** — used before any cell has measured (and always in pinned mode, where no measurements happen). The first-measured fallback is what makes `estimatedItemSize` matter less in measured mode: a slightly-off estimate produces visible reflow only on the very first cell. After that, the second and subsequent cells use the first cell's actual rendered size as their starting point — usually a much closer match to the real content size, so each cell's per-key measurement update moves things only a few pixels (or not at all). Locking on the _first_ measurement (rather than tracking a running average or the most recent) is deliberate: a moving estimate would cascade-rerender every later unmeasured item every time someone new measured, defeating the goal. -In **pinned mode** (no flex ancestor), step 1 doesn't apply: cells never report sizes, so the chain effectively skips to step 2/3/4. Every cell is exactly the size LayoutManager dictated. Get your `estimatedItemSize` / `overrideItemLayout` right or you'll see gaps/overflow. +In **pinned mode** (no flex ancestor), step 2 never populates: cells never report sizes, so the chain effectively skips to step 3/4/5. Every cell is exactly the size LayoutManager dictated. Get your `estimatedItemSize` / `overrideItemLayout` right or you'll see gaps/overflow. In **measured mode** (flex ancestor), measurement wins over override because real rendered size is authoritative — if the user's content renders to 250px and the override said 200px, going with 250 prevents overflow into the next item. If the caller wants to _force_ a size and prevent measurement updates, they need to render content sized to that exact dimension (the cell measures whatever content actually renders). -**Cross-axis size** is _never_ measured in either mode. Every item's cross-axis size equals `cellCrossSize` (derived from viewport). Span is the one exception — `layout.span = 2` makes a multi-column item occupy 2 columns of `cellCrossSize` width. If a user's content overflows the cross-axis, it paints outside the cell wrapper but does not affect `cellCrossSize` for any item. +**Cross-axis per-cell size.** `LayoutManager` never sees per-cell cross-axis measurements; every item's `crossSize` is `cellCrossSize × span` (where `cellCrossSize` is derived by VL from `viewportCrossSize`). Cross-axis cell reports flow only into VL's `maxContentCross` aggregator (a viewport-resolution fallback — see [Viewport resolution](#viewport-resolution)). If a user's content overflows the cross-axis, it paints outside the cell wrapper but does not affect `cellCrossSize` for any item. **First-render behavior** _(measured mode)_. Cells whose `userKey` has never been measured fall through to override → estimate. They render at that estimated main-axis size; once yoga lays them out and `onResize` fires, the cell reports its actual size, LayoutManager updates, and following items reposition. So a list with accurate `estimatedItemSize` (or `overrideItemLayout`) renders correctly from frame 1; lists with bad estimates show a brief reflow on first paint. @@ -196,7 +195,10 @@ For a list with no explicit cross AND no flex ancestor (pinned mode), no measure }} > {isInFlex ? ( - onItemSizeChange(userKey, horizontal ? e.w : e.h)}> + /* FlexRoot is unpinned on both axes — yoga shrinks-to-fit content. + handleResize forwards main-axis to onItemSizeChange (LM per-key + store) and cross-axis to onContentCrossLayout (VL maxContentCross). */ + {renderedItem} @@ -221,26 +223,31 @@ The renderedItem subtree is **not** wrapped in a keyed Fragment. Slot recycle ch 2. **Stable focus home during recycle.** The cell wrapper persists across slot recycles. If the user's renderItem changes shape between content swaps (e.g. one row has an inner focusable and another renders a static label), focus has a guaranteed landing point on the cell wrapper itself rather than escaping to a sibling row. 3. **Containment for spatial nav.** When a cell has multiple focusables (e.g., a row with action buttons), arrow keys stay within the cell until there's no candidate, then bubble to the VL's outer FocusGroup for cross-cell navigation. Without the per-cell group, every focusable in every cell competes in one flat space and spatial nav can jump unexpectedly across cells. -When the caller's `renderItem` includes an inner focusable, that inner is added to FocusManager as a child of the cell's group (not the VL's). The inner becomes the leaf-focused element; `onChildFocused` cascades up through the cell group to the VL's outer group, where `handleVLFocus` runs the snap-alignment scroll and writes to the state cache. Existing focus-driven behaviour is preserved — nesting is purely additive. +When the caller's `renderItem` includes an inner focusable, that inner is added to FocusManager as a child of the cell's group (not the VL's). The inner becomes the leaf-focused element. `handleVLFocus` (the inner VL's `onChildFocused` handler) fires only when focus crosses a cell boundary — `onChildFocused` is dispatched on the IMMEDIATE parent of the focused node, so spatial nav from one cell wrapper to another at the inner-VL level is the trigger. Movement between focusables _within_ a cell stays scoped to the cell's own focus group and doesn't fire the snap-alignment scroll. **Why FlexRoot is conditional on `isInFlex`.** When the VL has no flex ancestor, no yoga is running in this subtree. Adding a FlexRoot just for measurement would force yoga to spin up — pure overhead with no benefit, since the user's content isn't using flex either. So we skip it; the cell is silent and pinned. **Why FlexRoot is unpinned on both axes.** Yoga shrinks the FlexRoot to fit content on both axes. The cell forwards both dimensions: main goes into `LayoutManager`'s per-key measurement store; cross feeds VL's `maxContentCross` fallback (used only when no explicit cross source is available). Pinning cross to `cellCrossSize` would create the prior architecture's feedback loop — cell echoes its own pinned size back to VL, which uses that to compute the pin, etc. Leaving both unpinned lets cell content drive sizing without a loop. Tradeoff: flex-percentage layouts on cross axis (e.g. `width: '100%'` inside a horizontal VL's cell) won't work because the parent has no fixed cross dim — callers needing those should set `style.h` (or `.w`) on the VL, which flips the chain into the explicit branch. -**Why measure via `onResize`?** Lightning's universal `NodeResizeObserver` fires `onResize` whenever a node's size changes. The cell ignores the cross-axis dimension and only reports a positive main-axis number; cross-axis and zero/negative reports are filtered before they reach VL. +**Why measure via `onResize`?** Lightning's universal `NodeResizeObserver` fires `onResize` whenever a node's size changes. The cell reports the main-axis number to `onItemSizeChange` (LayoutManager's per-key store) and the cross-axis number to `onContentCrossLayout` (VL's `maxContentCross` aggregator). Zero/negative reports are filtered before they reach VL — a transient FlexRoot zero during recycle would otherwise pollute the cache. **Why a one-shot RAF push on `userKey` change.** `NodeResizeObserver` (driving `onResize`) only fires when the FlexRoot's actual size changes. Two scenarios miss measurements when relying on it alone: 1. **Same-size recycle.** A slot reassigns from item N to item M with identical dimensions. FlexRoot's size doesn't change → no observer event → item M's `userKey` never gets a measurement under the new key. 2. **Empty → non-empty transition.** The cell previously rendered null (no FlexRoot at all); now it renders content. There's nothing for the observer to compare against on the new mount. -The fix: a `useLayoutEffect` keyed on `[userKey, isInFlex, isEmpty, horizontal, onItemSizeChange, onContentCrossLayout]` that schedules a RAF when the userKey (or its preconditions) changes, reads `flexRootRef.current.node.w/h` after yoga has laid out, and reports those dimensions. RAF defers until post-layout so the values reflect the post-render dimensions. Cleanup cancels the prior frame's RAF. +The fix: a `useLayoutEffect` keyed on `[userKey, isInFlex, isEmpty, horizontal]` that schedules a RAF when the userKey (or its preconditions) changes, reads `flexRootRef.current.node.w/h` after yoga has laid out, and reports those dimensions. RAF defers until post-layout so the values reflect the post-render dimensions. Cleanup cancels the prior frame's RAF. `onItemSizeChange` / `onContentCrossLayout` are intentionally omitted from the deps with an `oxlint-disable-next-line` — those callbacks are inline closures defined after `pool.reconcile(...)` in `VirtualList.tsx` (React Compiler's optimization boundary in this function), so they're fresh references on every VL render but capture only stable values. Including them as deps would re-fire the effect on every VL render with no behavior change. This is intentionally **not** an every-render push. Once a cell has reported its size for a given `userKey`, ongoing size changes flow exclusively through `onResize` (which dedupes by definition). The recently-removed every-render variant was contributing to mid-scroll thrashing — every cell's mainOffset change triggered a re-render, every re-render scheduled a RAF, and every RAF pushed a possibly-transient yoga snapshot back into LM. Scoping to userKey changes plus `onResize` reduces the noise without losing the same-size-recycle path. -**Why imperative `cellElementRef.current.focus()` on `shouldFocus` false → true.** `useFocus` claims focus through the focus manager only at `addElement` time (mount). On a persisting cell whose `autoFocus` prop flips from false to true, `setAutoFocus` only updates the property — it does not actively claim focus. Without an alternative trigger, focus restoration after a slot recycle silently fails: the cell now "wants" focus but nothing pulls it. +**Why imperative `focusManager.setFocusedChild(cellElementRef.current)` on `shouldFocus` false → true.** `useFocus` claims focus through the focus manager only at `addElement` time (mount). On a persisting cell whose `autoFocus` prop flips from false to true, `setAutoFocus` only updates the property — it does not actively claim focus. Without an alternative trigger, focus restoration after a slot recycle silently fails: the cell now "wants" focus but nothing pulls it. -The cell holds a ref to its outer FocusGroup's element (`cellElementRef`) and a `prevShouldFocusRef` to detect transitions. A `useLayoutEffect` keyed on `[shouldFocus]` calls `cellElementRef.current.focus()` exactly once per false → true transition. The mount-time claim still goes through `FocusGroup`'s `autoFocus` prop (via `useFocus → addElement`); the imperative path fires only for already-mounted cells. This replaces the prior `` mechanism, which forced the renderItem subtree to remount on every userKey change so the user's inner focusables would re-fire their own `autoFocus` — that approach also tore down nested-VL `LayoutManager`/`RecyclerPool` state on every recycle, which is what we now preserve. +The cell holds a ref to its outer FocusGroup's element (`cellElementRef`) and a `prevShouldFocusRef` to detect transitions. A `useLayoutEffect` keyed on `[shouldFocus, focusManager]` calls `focusManager.setFocusedChild(cellElementRef.current)` exactly once per false → true transition. **`setFocusedChild` specifically — NOT `focusManager.focus()` and NOT `cellElementRef.current.focus()`.** The recycle-restore path runs while the user is focused on a different row (the inner VL is reconciling new content offscreen), and: + +- `focusManager.focus(cell)` walks up setting parent `focusedElement` at every level and runs `_recalculateFocusPath`, which would yank the user's focus across rows. +- `cellElementRef.current.focus()` only flips `_focused` on the Lightning element; the FocusManager's parent `focusedElement` chain still points at whatever sibling slot the prior content left behind, so the next traversal lands on the wrong cell. + +`setFocusedChild` updates only the parent's `focusedElement` and triggers `_recalculateFocusPath` — which is a no-op if the parent isn't already in the active focus path (the off-screen restore case), and otherwise moves focus from the old child to the new one. This replaces the prior `` mechanism, which forced the renderItem subtree to remount on every userKey change to re-fire inner `autoFocus` — that approach tore down nested-VL `LayoutManager`/`RecyclerPool` state on every recycle, which is what we now preserve. **Why `CellBoundsContext`?** Nested VLs need to know their bounded width and height without measurement of their own. The cell hands them down through context. Both numbers come straight from layout (estimate, override, or measurement) so a nested list's viewport is reasonable from frame 1. @@ -266,33 +273,32 @@ LM only adds `separatorSize` between adjacent cells in single-column lists. Mult User presses arrow → FocusGroup fires `onChildFocused(child)` → `handleVLFocus(child)`: -1. **Compute target index** from `child.getRelativePosition(contentRef)`. Child positions inside contentRef are scroll-independent (cells are absolutely positioned inside contentRef; only contentRef's own x/y changes for scroll), so this is safe to do before scrolling. +1. **Compute target index** from `child.getRelativePosition(contentRef)`, then `layoutManager.findIndexAtOffset(offset - itemAreaOffset)`. Child positions inside contentRef are scroll-independent (cells are absolutely positioned inside contentRef; only contentRef's own x/y changes for scroll), so this is safe to do before scrolling. 2. **Run `handleChildFocused(child)`** — that's the snap-alignment scroll inside `useScrollHandler`. It calls `scrollToOffset(target, animated)`, which synchronously sets `scrollOffsetRef.current = clamp(target)` and kicks off the animation. So even though the visual scroll is async, the ref is already updated. -3. **Write to cache** with `{ scrollOffset: scrollOffsetRef.current, focusedIndex: targetIdx }`. Because step 2 already updated the ref, this captures the _post-alignment_ offset. -4. **Update `focusedIndexRef.current`**. +3. **`setFocusedIndex(resolvedIdx)`** + write to cache with `{ scrollOffset: scrollOffsetRef.current, focusedIndex: resolvedIdx, measurements: layoutManager.getMeasurements() }`. Because step 2 already updated `scrollOffsetRef.current`, this captures the _post-alignment_ offset. The order matters. If you write before step 2, you capture the pre-scroll offset. On restore, `resetScroll` puts the row at that pre-scroll offset, autoFocus fires, `handleChildFocused` runs and scrolls to the _correct_ offset — but the user sees the row land slightly off and then jump. Two visits are needed for the cache to converge. Doing alignment first and writing after is what makes the first visit land cleanly. ### Recycle entry flow -The enclosing cell's `userKey` (received via `VLCellKeyContext`) changes — meaning the parent VL recycled this row into a different item. The VL's React identity persists across this transition (no remount), so the branch fires on the persisting component instance: +The enclosing cell's `userKey` (received via `VLCellKeyContext`) changes — meaning the parent VL recycled this row into a different item. The VL's React identity persists across this transition (no remount), so the branch fires on the persisting component instance. The branch runs **during render** as a "derived state from props" pattern (setState-during-render), so the current render uses the restored state — no flash of stale scroll/focus. -1. **Detect the change** via `prevCellKeyRef.current !== cellKey` (during render, not in an effect — we want the current render to use the new state, not flash the old). -2. **Save outgoing state** to the parent's cache under `prevCellKeyRef.current`, capturing `scrollOffsetRef.current`, `focusedIndexRef.current`, and **a snapshot of `LayoutManager.getMeasurements()`**. The measurement snapshot survives the recycle so the row's inner cells don't have to re-measure from estimate when the row becomes visible again — without it, every recycle-back would cascade first-measurement commits through every following item. +1. **Detect the change** via `prevCellKey !== cellKey` (state, not a ref — `useState` is the React-Compiler-friendly equivalent of a ref-write-during-render and avoids the bailout that would unmemoize the whole component). +2. **Save outgoing state** to the parent's cache under `prevCellKey`, capturing `committedScrollOffset` (the latest committed scroll for this about-to-leave cellKey — render-phase ref reads of `scrollOffsetRef.current` would also bail React Compiler, and the inner VL hasn't been animating so the committed value matches), `focusedIndex`, and **a snapshot of `LayoutManager.getMeasurements()`**. The measurement snapshot survives the recycle so the row's inner cells don't have to re-measure from estimate when the row becomes visible again — without it, every recycle-back would cascade first-measurement commits through every following item. 3. **Apply incoming config synchronously** via `layoutManager.updateConfig({ data, ..., separatorSize })`. Without this, LM's `_data` would still be the prior content (the `useLayoutEffect` that calls `updateConfig` hasn't fired yet), so `_resolveSize` would derive userKeys from the old data and miss every entry in the about-to-be-restored measurements map. The result would be one frame of estimate-based layout before the effect catches up — visible flicker. 4. **Read incoming state** from the cache under the new `cellKey`. 5. **`resetScroll(incoming.scrollOffset ?? 0)`** — synchronously updates `scrollOffsetRef`, applies the position to contentRef directly, resets the visible-range cache. 6. **`setFocusedIndex(incoming.focusedIndex ?? 0)`** — cells about to render will pick this up as `shouldFocus`. The `?? 0` fallback is load-bearing: the inner FG's `focusedElement` is React-element-stable across recycle (each cell at its slotKey persists the same Lightning element), so it carries stale memory pointing at whichever cell was last focused under the **previous** cellKey. With `focusedIndex=undefined` no cell flips `shouldFocus` false → true, `VirtualListCell`'s `setFocusedChild` layoutEffect never fires, and the next time the user navigates into this row the FG path traversal lands on the stale cell instead of cell 0. Defaulting to 0 makes cell 0 claim `setFocusedChild` on the next layoutEffect, matching the fresh-mount behavior where `addElement` sets cell 0 as the default `focusedElement` (first focusable child added wins). 7. **`layoutManager.setMeasurements(incoming.measurements)`** if the incoming entry has them, else `layoutManager.clearMeasurements()`. `setMeasurements` also clears any in-flight dampening (`_pendingSizes`, `_pendingTimers`) and batched (`_batchedSizes`) state — pending entries from the prior content are no longer relevant under the restored measurement set. -8. **`skipNextFocusRef.current = true`** — the next `onChildFocused` event is going to be the FocusGroup re-establishing focus on entry; we don't want that overwriting the restored index. We still call `handleChildFocused` so the snap-alignment runs (if the focused item is currently outside the snap window for some reason), but we skip the cache update. -9. **`prevCellKeyRef.current = cellKey`**. +8. **`setSkipNextFocus(true)`** — the next `handleVLFocus` call after the restore should consume this flag and skip the cache write (it's the FocusGroup re-establishing focus on entry; we don't want that overwriting the restored index). We still call `handleChildFocused` so the snap-alignment runs. +9. **`setPrevCellKey(cellKey)`** — completes the derived-state pattern, so the next render's diff compares against the new cellKey. -Because step 3 of the in-list flow captured the post-alignment offset, step 5 here restores to a value that — when autoFocus fires and `handleChildFocused` runs — produces a no-op scroll. No animation, no flicker. +Because step 3 of the in-list flow captured the post-alignment offset, step 5 here restores to a value that — when focus re-enters and `handleChildFocused` runs — produces a no-op scroll. No animation, no flicker. ### State and invariants -- **Refs, not state.** `focusedIndexRef`, `skipNextFocusRef`, `prevCellKeyRef`, `scrollOffsetRef`. State-based tracking caused setState-async timing bugs where the persistence useEffect captured stale focusedIndex. -- **`shouldFocus` is read at render time.** On initial cell mount, `FocusGroup`'s `autoFocus={shouldFocus}` triggers `useFocus → addElement` with `autoFocus: true`, which claims focus through the focus manager. On a persisting cell whose `shouldFocus` flips false → true, the imperative `cellElementRef.current.focus()` in `VirtualListCell`'s layoutEffect is what actually moves focus. +- **State, not refs (post-React-Compiler-1.0 refactor).** `focusedIndex`, `skipNextFocus`, `prevCellKey` are all `useState` in `VirtualList.tsx`. The earlier ref-based version caused render-phase ref reads/writes that bailed React Compiler on the entire `VirtualListInner`, leaving every inline callback and cell prop closure unmemoized — every cell re-rendered on every VL render. `scrollOffsetRef` (inside `useScrollHandler`) is still a ref because it must update synchronously inside `scrollToOffset` so the immediately-following cache write captures the post-alignment offset. The persistence `useEffect` includes `focusedIndex` in its deps; the setState-async-race the prior comment warned about was specific to the ref-based design and doesn't apply to the current state-based one. +- **`shouldFocus` is read at render time.** ``. On initial cell mount, `FocusGroup`'s `autoFocus={shouldFocus}` triggers `useFocus → addElement` with `autoFocus: true`, which claims focus through the focus manager. On a persisting cell whose `shouldFocus` flips false → true, the imperative `focusManager.setFocusedChild(cellElementRef.current)` in `VirtualListCell`'s layoutEffect is what actually moves focus. - **Top-level VLs (no `cellKey`) skip persistence entirely.** No outgoing save, no incoming restore, no cache writes from `handleVLFocus`. --- @@ -311,15 +317,15 @@ interface VLPersistedState { Three write paths, with different timing characteristics: -| When | Reads what | Why | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `handleVLFocus`, after `handleChildFocused` | `scrollOffsetRef.current` (latest, synchronous) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Captures every focus-driven scroll precisely — including sub-range scrolls that don't commit a new visible range. | -| `useEffect` on `committedScrollOffset` | `committedScrollOffset` (state, updated when range changes) + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Backstop for non-focus scrolls (touch/wheel/imperative `scrollToOffset`). Doesn't fire for sub-range scrolls. | -| Cell-key-change block (during render) | `scrollOffsetRef.current` + `focusedIndexRef.current` + `layoutManager.getMeasurements()` | Saves outgoing state right before restoring incoming, ensuring the most recent measurements survive even if no in-life save fired since the last update. | +| When | Reads what | Why | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `handleVLFocus`, after `handleChildFocused` | `scrollOffsetRef.current` (synchronously updated by `scrollToOffset`) + `resolvedIdx` + `layoutManager.getMeasurements()` | Captures every focus-driven scroll precisely — including sub-range scrolls that don't commit a new visible range. | +| `useEffect` on `committedScrollOffset` | `committedScrollOffset` (state, updated when range changes) + `focusedIndex` (state) + `layoutManager.getMeasurements()` | Backstop for non-focus scrolls (touch/wheel/imperative `scrollToOffset`). Doesn't fire for sub-range scrolls. | +| Cell-key-change block (during render) | `committedScrollOffset` + `focusedIndex` + `layoutManager.getMeasurements()` | Saves outgoing state right before restoring incoming. Reads state (not refs) because render-phase ref reads bail React Compiler; the inner VL hasn't been animating, so committed values match `scrollOffsetRef.current` for our purposes here. | The focus-event write must run _after_ `handleChildFocused` so that `scrollOffsetRef.current` reflects the snap-aligned offset, not the pre-scroll one. See [Focus restoration → In-list navigation flow](#in-list-navigation-flow) for the full rationale. -The cell-key-change block runs as derived state (set during render), not in an effect, so the current render uses the new state — avoids a flash of stale scroll/focus. +The cell-key-change block runs as derived state (set during render via `setFocusedIndex` / `setSkipNextFocus` / `setPrevCellKey`), not in an effect, so the current render uses the new state — avoids a flash of stale scroll/focus. **Initial mount restoration.** When VL initializes its `LayoutManager`, it checks `parentStateCache.get(cellKey)` for an `initialRestoredState` and, if `measurements` is present, calls `layoutManager.setMeasurements(restored)` immediately — before the first cell render. The cell wrappers in this render get layout offsets computed against the restored measurements, so a recycled-into-existence VL paints at the right sizes from frame 1. Without this, the first frame would use estimates and then reflow once measurements re-pushed. @@ -331,15 +337,14 @@ Top-level VLs (no `cellKey`) don't persist anything. ## Recycling -`RecyclerPool` reconciles visible indices to React-stable slot keys, grouped by `getItemType`. Items of the same type reuse each other's React subtrees. The pool's constructor takes an optional `label` (e.g. `"v"` for the outer vertical VL, `"h"` for an inner horizontal) used in debug logging — `[Pool v] released:1 preferred:1 ...`. - +`RecyclerPool` reconciles visible indices to React-stable slot keys, grouped by `getItemType`. Items of the same type reuse each other's React subtrees. **Subtree persists on content swap.** The cell's outer `FocusGroup` is React-keyed by slot, so the wrapper persists across recycles. There is **no keyed Fragment** under the wrapper — when `userKey` changes, the renderedItem subtree (and any nested VL inside it) reconciles in place rather than remounting. Nested VLs detect the new `userKey` via `VLCellKeyContext` and run their cellKey-change branch (save outgoing state, apply new config, restore incoming state) on the same component instance. Their `LayoutManager`, `RecyclerPool`, and any other useRef-backed state survives the round-trip. **Per-index slot stability.** When an index leaves visibility and later comes back, `RecyclerPool` tries to give it back its **previous slot** via `_lastSlotForIndex`, not whichever one happens to be at the top of the LIFO pool. Without this preference, the same item gets a different `slotKey` on round-trip, React unmounts the entire `VirtualListCell`, and any descendant state is destroyed and reconstructed. The reconstruction goes through transient measurement states that ripple up to the parent VL and cause visible thrashing on scroll-back. With the preference, the round-trip is a no-op for the React tree — same `slotKey`, same component instance, same descendant state. When the preferred slot isn't available — e.g. it's been reassigned to a different item during the absence — `_acquire` falls back to LIFO pop from the type pool. The cell at that slot was previously rendering some other index; React preserves the cell instance, the userKey changes, and the cell's renderedItem reconciles to the new content. State below the cell still persists (because the cell itself does), so the scroll-back to a stale-preferred index is still better than the no-preservation path. -**Pooled-slot mounting hook.** `RecyclerPool.getPooledSlots()` returns `Array<{ slotKey, lastIndex }>` — every slot currently sitting in the available pool, paired with the data index it most recently served (tracked via `_slotToLastIndex`, the reverse of `_lastSlotForIndex`). Callers can render these slots in the React tree positioned offscreen, so the React subtree at each pooled slot — and any nested recycler pools inside it — stays mounted across release/reclaim cycles. The pool itself preserves only the slot-key string and last-served index; without the host rendering pooled slots, the React component instances at those slots are unmounted by reconciliation as soon as they leave `visibleIndices`. The wiring in `VirtualList.tsx` may render only currently-visible slots (so pooled cells unmount and re-mount on round-trip) or render pooled slots offscreen too (so the entire React tree below the pool persists end-to-end) — see the JSX for which mode is in effect. The `pooled` prop on `VirtualListCell` exists for the latter mode: when a cell is held in the pool, the host passes `pooled={true}` so the cell's outer `FocusGroup` is disabled and spatial navigation skips both the cell and its descendants. +**Pooled-slot mounting hook.** `RecyclerPool.getPooledSlots()` returns `Array<{ slotKey, lastIndex }>` — every slot currently sitting in the available pool, paired with the data index it most recently served (tracked via `_slotToLastIndex`, the reverse of `_lastSlotForIndex`). It exists so a host could render pooled slots in the React tree positioned offscreen and preserve the React subtree at each pooled slot end-to-end across release/reclaim cycles. **Currently `VirtualList.tsx` does NOT use it** — only `visibleIndices` are rendered, so pooled cells unmount and remount on round-trip; per-index slot stability still gives the same `slotKey` back when the index returns, but any state below the cell wrapper that didn't survive the unmount is gone. The `pooled` prop on `VirtualListCell` and `getPooledSlots` are reserved infrastructure for a future opt-in mode where the host renders pooled cells offscreen with `pooled={true}` to disable their outer `FocusGroup` and skip them in spatial navigation. If you don't pass `getItemType`, all cells share one pool — fine for uniform lists. @@ -349,26 +354,26 @@ If you don't pass `getItemType`, all cells share one pool — fine for uniform l By design, this VL **does not**: -- Measure or aggregate cross-axis sizes. Cross is always `cellCrossSize` from viewport. If your content needs more cross space, set `style.w/h` (top-level) or `numColumns`, or accept overflow. -- Auto-derive viewport size from content. Viewport flows: explicit `style` → `parentCellBounds` → self-measured FocusGroup. It never depends on cell reports. +- Aggregate cross-axis cell reports into per-cell cross sizes. Every cell's `crossSize` is `cellCrossSize` (× span); `LayoutManager` never sees cross-axis cell reports. Cell-cross _does_ feed `maxContentCross` (a viewport-resolution fallback) — see the next bullet. +- Let cell-cross reports drive a feedback loop. The `maxContentCross` aggregation is **monotonic** (only grows) and **resets on `data` / `extraData` identity change** so a cell that uses `cellCrossSize` for its own dimensions can't push `cellCrossSize` larger on subsequent reports. - Offer per-cell cross-axis overrides. `overrideItemLayout.size` is main-axis only; `span` is the only multi-column knob. -- Compute a "running average" of measured sizes. Each measurement is stored verbatim per `userKey`. +- Compute a "running average" of measured main-axis sizes. Each measurement is stored verbatim per `userKey`. The first non-zero measurement seeds an _implicit estimate_ for unmeasured items but is locked on first; later measurements update individual cells via the per-key path only. -If you find yourself adding cross-axis aggregation or letting cell reports drive viewport, stop and reread [Why measurement is main-axis only](#why-measurement-is-main-axis-only). +If you find yourself letting cell reports drive viewport _bidirectionally_ (i.e. without the monotonic+reset discipline), stop and reread [Why LayoutManager only sees main-axis measurements](#why-layoutmanager-only-sees-main-axis-measurements). --- -## Why measurement is main-axis only +## Why LayoutManager only sees main-axis measurements -The pre-rewrite VL measured both axes. That spawned three coupled measurement systems: +The pre-rewrite VL fed cross-axis cell reports back into `LayoutManager`. That spawned three coupled measurement systems: 1. **Cell-level** (`FlexRoot` + `NodeResizeObserver`) — each cell's content was observed via yoga and reported on both axes to LayoutManager. 2. **Viewport-level** (`FocusGroup.onResize` → `measuredSize`) — VL watched its own container size for `viewportCrossSize`. -3. **Content-derived cross-size** (`_maxCrossSize` → `contentCross` → `finalCross` → `cellCrossSize`) — VL aggregated cell-reported cross sizes back into its own sizing decisions. +3. **Content-derived cross-size** (`_maxCrossSize` → `contentCross` → `finalCross` → `cellCrossSize`) — VL aggregated cell-reported cross sizes _without monotonicity or per-dataset reset_ back into its own sizing decisions. The three formed loops: cells report cross → LayoutManager aggregates → VL sets contentRef.h → FocusGroup measures → viewportCrossSize updates → cellCrossSize re-derived → cells re-render at new cross size → cells report different cross size. Symptoms: backward-scroll jank, recycle flickers, sizes "stuck" at initial estimate. -The current design measures only main-axis. Cross is unilaterally `cellCrossSize` from viewport — cells don't report it, LayoutManager doesn't aggregate it, VL doesn't derive viewport from it. That's the entire fix. +The current design keeps `LayoutManager` pure on the cross axis — cells don't report cross to LM, LM doesn't aggregate it, every cell's `crossSize` is `cellCrossSize` from VL's viewport math. Cross is still reported (via `onContentCrossLayout`) but only to VL's `maxContentCross` aggregator, which is **monotonic** and **resets on data identity change**. A cell that sizes itself off `cellCrossSize` can't push `cellCrossSize` larger on subsequent reports because the max only ever grows for the current dataset, and it starts fresh when the dataset changes. That's what breaks the loop. --- @@ -394,13 +399,14 @@ The current design measures only main-axis. Cross is unilaterally `cellCrossSize - **The cell's cross-axis is fixed at `cellCrossSize`.** Content overflowing the cross axis paints outside the cell wrapper. Either fit content to `cellCrossSize` or pick a larger viewport / smaller `numColumns`. - **Top-level VLs should set `style.w/h` explicitly** when known. Self-measurement via `FocusGroup.onResize` works but adds a render cycle. - **Nested VLs without `style.h` (or `style.w`) inherit from `parentCellBounds`.** That's the cell wrapper's current size — which after measurement is the cell's _content_ size (estimate before, measured after). For a horizontal inner VL inside a row, it's usually cleaner to set `style.h` on the inner VL to its actual item height rather than rely on the parent cell sizing. -- **`focusedIndexRef` is read at cell render time.** Mount-time autoFocus chains through `FocusGroup → useFocus → addElement` claim focus on first paint. For a persisting cell whose `shouldFocus` flips false → true (slot recycle to a new content that should be focused), the imperative `cellElementRef.current.focus()` in `VirtualListCell`'s layoutEffect is what actually moves focus. -- **Don't add focusedIndex to the persistence `useEffect` deps.** The ref-based direct write in `handleVLFocus` covers per-focus updates; adding the dep reintroduces the setState-async race. +- **`focusedIndex` is `useState`, read at cell render time.** Mount-time autoFocus chains through `FocusGroup → useFocus → addElement` to claim focus on first paint. For a persisting cell whose `shouldFocus` flips false → true (slot recycle to content that should be focused), the imperative `focusManager.setFocusedChild(cellElementRef.current)` in `VirtualListCell`'s layoutEffect is what actually moves focus. +- **The cellKey-restore path uses `setFocusedIndex(... ?? 0)`, never undefined.** The inner FG's `focusedElement` is element-stable across recycle and carries stale memory. Defaulting to 0 makes cell 0 trigger `setFocusedChild` on the next layoutEffect; with `undefined` no cell flips false→true and the next focus traversal lands on a stale cell. - **In `handleVLFocus`, run `handleChildFocused` BEFORE writing to the cache.** `scrollToOffset` synchronously updates `scrollOffsetRef.current` to the snap-aligned target, so the subsequent cache write captures the post-alignment offset. Inverting this order forces a two-visit convergence on restore — the row lands at the wrong offset and only fixes itself on a second focus event. -- **The `useLayoutEffect` RAF size-push in `VirtualListCell` is keyed on `[userKey, isInFlex, isEmpty, horizontal, onItemSizeChange, onContentCrossLayout]`.** Don't widen to `[]` (every-render): that contributed to mid-scroll thrashing by pushing transient yoga snapshots whenever a sibling cell's update re-flowed this one. Don't narrow further than `userKey`: same-size recycles need at least the userKey transition to push the new key's measurement. -- **Cell wrappers are `FocusGroup`s; don't downgrade them to `useFocus` or to plain views.** Per-cell focus groups give every cell a baseline focus target, contain spatial nav for multi-focusable cells, and provide the ref target for the imperative `focus()` call on `shouldFocus` transitions. -- **Don't re-introduce a keyed `` around `renderedItem`.** The previous version did exactly that to make `useFocus.autoFocus` re-fire on recycle, but it tore down the entire renderItem subtree (including any nested VLs and their `LayoutManager`/`RecyclerPool` state) on every content swap. The current architecture relies on subtree persistence + nested-VL cellKey-change handling to preserve state. Replacing the Fragment with imperative focus is what enables that. +- **The `useLayoutEffect` RAF size-push in `VirtualListCell` is keyed on `[userKey, isInFlex, isEmpty, horizontal]`** with `onItemSizeChange`/`onContentCrossLayout` intentionally omitted (they're VL-side closures fresh on every render but capturing only stable values; including them re-fires the effect every VL render with no behavior change). Don't widen to `[]` (every-render): that contributed to mid-scroll thrashing. Don't narrow further than `userKey`: same-size recycles need at least the userKey transition to push the new key's measurement. +- **Cell wrappers are `FocusGroup`s; don't downgrade them to `useFocus` or to plain views.** Per-cell focus groups give every cell a baseline focus target, contain spatial nav for multi-focusable cells, and provide the ref target for `setFocusedChild` on `shouldFocus` transitions. +- **Don't re-introduce a keyed `` around `renderedItem`.** The previous version did exactly that to make `useFocus.autoFocus` re-fire on recycle, but it tore down the entire renderItem subtree (including any nested VLs and their `LayoutManager`/`RecyclerPool` state) on every content swap. The current architecture relies on subtree persistence + nested-VL cellKey-change handling to preserve state. Replacing the Fragment with imperative `setFocusedChild` is what enables that. - **Don't call `setMeasurements` outside the cellKey-change branch or LM init.** It clears all in-flight dampening / batching state, which is correct as a "we're switching content, throw away pending observations" reset but would be a bug if called mid-flight on stable content. - **Don't re-apply `contentStyle.x/y` while `isScrollAnimating` is true.** The imperative `node.animate()` is the source of truth for position during the animation; re-applying the target via React style on a mid-animation re-render snaps the content past the interpolated value. - **Reject zero-size measurements.** A cell briefly measuring 0 (mid-recycle, content unmount) must not pollute the cache. `LayoutManager.reportItemSize` rejects `size <= 0`. Genuinely-empty rows go through `reportItemEmpty` instead. -- **Don't aggregate cross-axis from cells.** The single rule that prevents the prior architecture's loops. +- **Don't feed cross-axis cell reports into `LayoutManager`.** Cross stays at `cellCrossSize` per cell. Cross-axis cell reports are allowed only into `maxContentCross` (a monotonic, reset-on-data-change viewport-resolution fallback). Anything else re-introduces the prior architecture's feedback loop. +- **Don't use `cellElementRef.current.focus()` or `focusManager.focus(cell)` for the recycle-restore claim.** `setFocusedChild` is the only correct API: `.focus()` flips `_focused` without updating the parent's `focusedElement` chain (next traversal lands on a stale sibling); `focusManager.focus()` walks up and runs `_recalculateFocusPath` aggressively, yanking the user's focus across rows. diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx index 0ef1d87..98c5248 100644 --- a/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx @@ -223,7 +223,7 @@ function VirtualListInner(props: VirtualListProps, ref: ForwardedRef(() => new RecyclerPool(horizontal ? 'h' : 'v')); + const [pool] = useState(() => new RecyclerPool()); const getKey = (index: number): string => keyExtractor && data[index] !== undefined ? keyExtractor(data[index], index) : String(index); const getData = (i: number) => data[i];