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 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. diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml index 4f32613..1bd73db 100644 --- a/.github/workflows/deploy-storybook.yml +++ b/.github/workflows/deploy-storybook.yml @@ -34,7 +34,7 @@ jobs: - uses: actions/setup-node@v6.0.0 with: node-version-file: package.json - cache: "pnpm" + cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index d1d6189..8069f0d 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -3,7 +3,7 @@ name: Release Packages on: workflow_dispatch: workflow_run: - workflows: ["Run tests"] + workflows: ['Run tests'] branches: [main] types: - completed @@ -34,8 +34,8 @@ jobs: - uses: actions/setup-node@v6.0.0 with: node-version-file: package.json - cache: "pnpm" - registry-url: "https://registry.npmjs.org" + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -46,8 +46,8 @@ jobs: uses: changesets/action@v1.5.3 with: version: pnpm run ci:version - commit: "chore: Update versions" - title: "chore: Update versions" + commit: 'chore: Update versions' + title: 'chore: Update versions' publish: pnpm run ci:publish env: # When you use the repository's GITHUB_TOKEN to perform tasks, events diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6becc02..3308b8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: package.json - cache: "pnpm" + cache: 'pnpm' - run: pnpm install --frozen-lockfile diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..5652732 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "singleQuote": true, + "insertFinalNewline": true, + "trailingComma": "all", + "experimentalSortImports": { + "internalPattern": ["@plextv/", "@repo/"], + "partitionByNewline": false + }, + "ignorePatterns": [".changeset", "**/public", "**/dist"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..ec29658 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "eslint", + "typescript", + "unicorn", + "oxc", + "react", + "react-perf", + "jsx-a11y", + "import" + ], + "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", + "jsx-a11y/alt-text": "off", + "jsx-a11y/mouse-events-have-key-events": "off", + "jsx-a11y/click-events-have-key-events": "off" + }, + "ignorePatterns": ["dist", ".changeset", "**/public"] +} 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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d31c2eb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,282 @@ +# CLAUDE.md — react-lightning + +## Repository Overview + +**react-lightning** is a monorepo providing a React reconciler for [Lightning.js](https://lightningjs.io/), enabling developers to build Lightning.js apps using React and React Native patterns. + +- **Package manager:** pnpm@10.12.3 (workspace monorepo) +- **Build orchestrator:** Turbo 2.7.5 +- **Node requirement:** >=22 (Volta pins: node 24.11.1, pnpm 10.12.3) + +--- + +## Monorepo Structure + +``` +apps/ + react-lightning-example/ # Main React + Lightning.js example app + react-native-lightning-example/ # React Native + Lightning example app + storybook/ # Component documentation (Storybook 9) + +packages/ + react-lightning/ # Core React reconciler for Lightning.js + react-lightning-components/ # Pre-built UI components (Column, Row, VirtualList, etc.) + react-native-lightning/ # React Native API layer on Lightning + react-native-lightning-components/ + configs/ # Shared tsconfig, vite, tsdown configs + plugin-flexbox/ # Yoga-based flexbox layout plugin + plugin-flexbox-lite/ # Lightweight flexbox alternative + plugin-css-transform/ # CSS property → Lightning property transforms + plugin-reanimated/ # Reanimated animation integration + vite-plugin-msdf-fontgen/ # Vite plugin for MSDF font generation + vite-plugin-react-native-lightning/ + vite-plugin-react-reanimated-lightning/ + +templates/ + app-template/ # Starter template for new projects + +types/ # Global TypeScript type definitions +scripts/ # Build and maintenance scripts +``` + +--- + +## Key Commands + +### Development + +```bash +pnpm dev # Run all dev servers +pnpm dev --filter=@plextv/react-lightning-example # Run a specific app +``` + +### Testing + +```bash +pnpm test # Run all unit tests (vitest) +``` + +### Linting & Formatting + +```bash +pnpm lint # Lint with oxlint +pnpm lint:format # Lint + format with oxlint and oxfmt +``` + +### Type Checking + +```bash +pnpm check:types # TypeScript type checking across all packages +``` + +### Building + +```bash +pnpm build # Build all packages (turbo) +pnpm build:tsdown # Build library packages with tsdown +pnpm build:vite # Build app/vite packages +pnpm build:types # Generate type declarations only +pnpm build:storybook # Build Storybook docs +``` + +### Maintenance + +```bash +pnpm clean # Remove dist directories +pnpm nuke # Deep clean: clean + remove node_modules + reinstall +pnpm unused # Check for unused dependencies +``` + +### Versioning & Release + +```bash +pnpm changeset # Create a changeset (required for releases) +pnpm ci:version # Bump versions (CI only) +pnpm ci:publish # Publish to npm (CI only) +``` + +--- + +## Build System + +### Library packages (tsdown) + +Core packages (`react-lightning`, `react-lightning-components`, plugins, etc.) are bundled with **tsdown 0.19.0**. + +- Shared config: `packages/configs/tsdown.config.ts` +- Output: ESM + CJS with `dist/es/` and `dist/cjs/` directories +- Dual dev/prod builds: `index.development.js` and `index.production.js` +- CJS entry auto-selects dev vs. prod based on `NODE_ENV` +- Type declarations emitted to `dist/types/` +- React Compiler applied via `@rollup/plugin-babel` + `babel-plugin-react-compiler` + +### App packages (Vite) + +Example apps and Storybook are built with **Vite 7.3.1**. + +- Shared config base: `packages/configs/` +- Key plugins: `@vitejs/plugin-react`, `@vitejs/plugin-legacy` (Chrome 69+), `vite-tsconfig-paths`, `vite-plugin-msdf-fontgen` + +--- + +## TypeScript + +- **Base config:** `packages/configs/tsconfig.json` +- **React library config:** `packages/configs/tsconfig.react-library.json` +- **Key settings:** `target: ES2022`, `module: ESNext`, `moduleResolution: Bundler`, `strict: true`, `isolatedModules: true`, `isolatedDeclarations: true`, `jsx: react-jsx` +- All packages extend `@repo/configs/tsconfig.react-library.json` +- Use `workspace:*` protocol for internal package dependencies + +--- + +## Code Style + +- **Formatter:** oxfmt 0.35 — run `pnpm lint:format` to fix +- **Linter:** oxlint 1.50 — run `pnpm lint` to check +- **Indentation:** 2 spaces +- **Quotes:** Single quotes in JS/TS +- **No class components** — functional components + hooks only +- **No default exports on hooks or utilities** — prefer named exports +- Pre-commit hook via husky runs `oxlint` +- `.npmrc` enforces exact versions (`save-exact=true`) + +--- + +## Architecture + +### Core Layer: `@plextv/react-lightning` + +The reconciler bridges React's virtual DOM to Lightning.js node tree. + +**Element system** (`src/element/`): + +- `LightningViewElement` — container (like `
`) +- `LightningImageElement` — image rendering +- `LightningTextElement` — text rendering +- All wrap Lightning's `INode` with React-specific prop handling + +**Plugin system:** +Plugins extend renderer behavior via three hooks: + +```typescript +type Plugin = { + handledStyleProps?: Set; + init?(renderer, reconciler): Promise; + onCreateInstance?(instance, props, fiber): void; + transformProps?(instance, props): object | null; // return null to stop pipeline +}; +``` + +The `handledStyleProps` property defines the properties that the plugin will act on. This +will allow us to skip plugin processing. + +Built-in plugins: `plugin-flexbox`, `plugin-css-transform`, `plugin-reanimated` + +**Focus management** (`src/focus/`): + +- Tree-based focus tracking with support for layers (modals) +- `FocusManager`, `FocusGroup`, `focusable()` HOC +- Hooks: `useFocus()`, `useFocusManager()` +- Features: auto-focus, focus traps, focus redirection, custom navigation + +**Input handling** (`src/input/`): + +- `Keys` constants, `KeyPressHandler` component +- Full event bubbling, capture, and `preventDefault` support + +**Renderer** (`src/render/`): + +- `createRoot()` — main entry point; accepts `RenderOptions` (renderer, fonts, shaders, plugins) +- Lightning DevTools integration in dev builds +- `import.meta.env.DEV` guards dev-only code + +### Component Library: `@plextv/react-lightning-components` + +Pre-built Lightning-optimized components: + +- **Layout:** `Column`, `Row` (flexbox via `plugin-flexbox`) +- **Lists:** `VirtualList` (virtualized list with view recycling, optimized for low-powered devices) +- **Text:** `StyledText` +- **Dev:** `FPSMonitor` + +### React Native Layer: `@plextv/react-native-lightning` + +Provides a React Native-compatible API on top of Lightning: + +- RN-compatible components: `View`, `Text`, `Image`, `ScrollView`, etc. +- Built on `react-native-web` polyfills + `its-fine` for context bridging +- RN styling patterns supported + +### React Native Components: `@plextv/react-native-lightning-components` + +RN-style wrappers for Lightning components: + +- **Layout:** `Column`, `Row` + +--- + +## Naming Conventions + +| Thing | Convention | Example | +| ------------------ | ------------------------------ | --------------------------------------- | +| React components | PascalCase | `LightningViewElement`, `FocusGroup` | +| Hooks | `use` prefix + camelCase | `useFocus`, `useCombinedRef` | +| Types / interfaces | PascalCase + descriptor suffix | `LightningElementProps`, `FocusNode` | +| Plugin files | kebab-case directory | `plugin-flexbox/` | +| Utility functions | camelCase | `bubbleEvent`, `traceWrap` | + +--- + +## Testing + +- **Test runner:** vitest 4.0.17 +- **Config:** `vitest.workspace.ts` at root +- Tests are colocated with source files (`.spec.ts` or `.test.ts` suffix) +- `passWithNoTests: true` — packages without tests do not fail CI +- Mock utilities live in `src/mocks/` + +--- + +## CI/CD + +All workflows are in `.github/workflows/`: + +| Workflow | Trigger | What it does | +| ---------------------- | ----------------------------------- | ---------------------------------------------- | +| `test.yml` | PRs to `main`, pushes to `main` | Runs `pnpm lint` then `pnpm test` | +| `release-packages.yml` | After `test.yml` succeeds on `main` | Changesets version bump + npm publish via OIDC | +| `deploy-storybook.yml` | Pushes to `main` | Builds and deploys Storybook to GitHub Pages | + +CI uses `--frozen-lockfile` installs. Never modify `pnpm-lock.yaml` manually. + +--- + +## Releases & Versioning + +- Releases are managed by **Changesets** (`@changesets/cli`) +- Every PR that changes user-facing behavior needs a changeset: `pnpm changeset` +- Changeset config: `.changeset/config.json` +- Packages are published to npm under the `@plextv` scope +- CI uses npm OIDC trust (no token required in secrets) + +--- + +## Key Dependencies + +| Package | Version | Purpose | +| ----------------------------- | ------------ | --------------------------------------- | +| `@lightningjs/renderer` | 3.0.0-beta20 | Underlying Lightning.js renderer | +| `react-reconciler` | 0.33.0 | React custom reconciler API | +| `yoga-layout` | 3.2.1 | Flexbox layout engine (plugin-flexbox) | +| `react-native-web` | 0.21.2 | RN polyfills (react-native-lightning) | +| `its-fine` | 2.0.0 | Context bridge for concurrent renderers | +| `tseep` | 1.3.1 | Typed event emitter | +| `babel-plugin-react-compiler` | 1.0.0 | React Compiler for auto-memoization | + +--- + +## Docs & Resources + +- Official docs: https://plexinc.github.io/react-lightning/ +- Storybook (deployed from `main`): GitHub Pages +- GitHub: https://github.com/plexinc/react-lightning diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27082ca..9a79b93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,7 +69,6 @@ $ pnpm lint Reviewers should use the following questions to evaluate the implementation for correctness/completeness and ensure all housekeeping items have been addressed prior to merging the code. - Correctness/completeness - 1. Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?) 1. Is the intention of the code captured in relevant tests? - Does the description of each test accurately represent the assertions? @@ -83,7 +82,6 @@ Reviewers should use the following questions to evaluate the implementation for - easier to maintain (easier to change, harder to accidentally break) - Housekeeping - 1. Does the title and description of the PR reference the correct issue and does it use the correct conventional commit type (e.g., fix, feat, test, breaking change etc)? 1. If there are new TODOs, has a related issue been created? 1. Should any documentation be updated? diff --git a/apps/react-lightning-example/index.html b/apps/react-lightning-example/index.html index 331e0f5..5c52a8b 100644 --- a/apps/react-lightning-example/index.html +++ b/apps/react-lightning-example/index.html @@ -1,4 +1,4 @@ - + React-Lightning Sample App diff --git a/apps/react-lightning-example/package.json b/apps/react-lightning-example/package.json index 2ec5bcf..249f8db 100644 --- a/apps/react-lightning-example/package.json +++ b/apps/react-lightning-example/package.json @@ -1,19 +1,18 @@ { "name": "@plextv/react-lightning-example", - "description": "Sample implementation of @plextv/react-lightning in a React app", "version": "0.4.0", - "author": "Plex Inc.", + "private": true, + "description": "Sample implementation of @plextv/react-lightning in a React 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", @@ -23,26 +22,33 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@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": "19.2.3", - "react-dom": "19.2.3", - "react-router-dom": "7.12.0", - "swr": "2.3.8" + "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:*", - "@types/react": "19.2.8", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-legacy": "7.2.1", - "@vitejs/plugin-react": "5.1.2", - "vite-tsconfig-paths": "6.0.4" + "@rolldown/plugin-babel": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-legacy": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:" }, "volta": { "extends": "../../package.json" + }, + "packageManager": "pnpm@10.12.3", + "depcheck": { + "ignoreMatches": [ + "babel-plugin-react-compiler" + ] } } diff --git a/apps/react-lightning-example/src/api/useHubItemsData.ts b/apps/react-lightning-example/src/api/useHubItemsData.ts index 929b938..451052f 100644 --- a/apps/react-lightning-example/src/api/useHubItemsData.ts +++ b/apps/react-lightning-example/src/api/useHubItemsData.ts @@ -1,4 +1,5 @@ import useSWR, { type SWRResponse } from 'swr'; + import { getHeaders } from './getHeaders'; import { getToken } from './getToken'; import type { HubItemsRoot } from './types/HubItems'; @@ -7,8 +8,7 @@ const baseUrl = 'https://vod.provider.plex.tv'; const args = { contentDirectoryID: 'movies', - excludeElements: - 'Actor,Collection,Country,Label,Mood,Part,Producer,Similar,Photo,Vast,Topic', + excludeElements: 'Actor,Collection,Country,Label,Mood,Part,Producer,Similar,Photo,Vast,Topic', excludeFields: 'file,tagline', includeDetails: '1', }; diff --git a/apps/react-lightning-example/src/api/useHubsData.ts b/apps/react-lightning-example/src/api/useHubsData.ts index 8cd4d7a..fe58629 100644 --- a/apps/react-lightning-example/src/api/useHubsData.ts +++ b/apps/react-lightning-example/src/api/useHubsData.ts @@ -1,4 +1,5 @@ import useSWR, { type SWRResponse } from 'swr'; + import { getHeaders } from './getHeaders'; import { getToken } from './getToken'; import type { HubRoot } from './types/Hubs'; diff --git a/apps/react-lightning-example/src/components/AnimatedImage.tsx b/apps/react-lightning-example/src/components/AnimatedImage.tsx index 7b8fdac..58cd5d1 100644 --- a/apps/react-lightning-example/src/components/AnimatedImage.tsx +++ b/apps/react-lightning-example/src/components/AnimatedImage.tsx @@ -1,6 +1,7 @@ -import type { LightningImageElement } from '@plextv/react-lightning'; import { type FC, useEffect, useMemo, useRef, useState } from 'react'; +import type { LightningImageElement } from '@plextv/react-lightning'; + function randomInt(max: number): number { return Math.round(Math.random() * max); } diff --git a/apps/react-lightning-example/src/components/Button.tsx b/apps/react-lightning-example/src/components/Button.tsx index f5b5932..e3a9a73 100644 --- a/apps/react-lightning-example/src/components/Button.tsx +++ b/apps/react-lightning-example/src/components/Button.tsx @@ -1,3 +1,5 @@ +import { type FC, useCallback } from 'react'; + import type { KeyEvent, LightningElement, @@ -5,7 +7,6 @@ import type { LightningViewElementProps, } from '@plextv/react-lightning'; import { Keys, useFocus } from '@plextv/react-lightning'; -import { type FC, useCallback } from 'react'; const containerStyles: LightningElementStyle = { w: 330, @@ -47,12 +48,7 @@ const Button: FC = (props) => { const color = focused ? 0xcccc44ff : 0xcccc44aa; return ( - + {props.children} ); diff --git a/apps/react-lightning-example/src/components/HubItem.tsx b/apps/react-lightning-example/src/components/HubItem.tsx index 7a9843d..c26f657 100644 --- a/apps/react-lightning-example/src/components/HubItem.tsx +++ b/apps/react-lightning-example/src/components/HubItem.tsx @@ -1,3 +1,5 @@ +import type { FC } from 'react'; + import { type LightningViewElement, type LightningViewElementProps, @@ -5,7 +7,7 @@ import { useFocus, } from '@plextv/react-lightning'; import { Column } from '@plextv/react-lightning-components'; -import type { FC } from 'react'; + import { getImageUrl } from '../api/getImageUrl'; import type { Metadata } from '../api/types/Metadata'; @@ -25,10 +27,7 @@ export const HubItem: FC = ({ metadata, style, ...rest }) => { style={{ ...style, scale: focused ? 1.2 : 1 }} transition={{ scale: { duration: 250 } }} > - + = (props) => { const [horizontalOffset, setHorizontalOffset] = useState(0); const handleFocus = useCallback((element: LightningElement) => { - setHorizontalOffset( - Math.min(0, -element.node.x - element.node.w / 2 + 1920 / 2), - ); + setHorizontalOffset(Math.min(0, -element.node.x - element.node.w / 2 + 1920 / 2)); }, []); if (isLoading) { diff --git a/apps/react-lightning-example/src/components/PosterCollection.tsx b/apps/react-lightning-example/src/components/PosterCollection.tsx index b5e4a5d..f7ece62 100644 --- a/apps/react-lightning-example/src/components/PosterCollection.tsx +++ b/apps/react-lightning-example/src/components/PosterCollection.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react'; + import AnimatedImage from './AnimatedImage'; interface Props { @@ -12,11 +13,7 @@ const PosterCollection: FC = ({ posterCount }) => { posters.push(); } - return ( - - {posters} - - ); + return {posters}; }; export { PosterCollection }; diff --git a/apps/react-lightning-example/src/components/ScrollItem.tsx b/apps/react-lightning-example/src/components/ScrollItem.tsx new file mode 100644 index 0000000..36d1068 --- /dev/null +++ b/apps/react-lightning-example/src/components/ScrollItem.tsx @@ -0,0 +1,76 @@ +import { type ReactNode, useRef } from 'react'; + +import { focusable } from '@plextv/react-lightning'; + +export type ScrollItemProps = { + children: ReactNode; + index: number; + width?: number; + height?: number; + focused?: boolean; + horizontal?: boolean; + color: number; + altColor: number; +}; + +export const ScrollItem = focusable( + ({ color, altColor, index, focused, horizontal, width = 200, height = 75, children }, ref) => { + const isImage = useRef(Math.random() < 0.5).current; + const multiplier = index % 3 === 0 ? (horizontal ? 1.25 : 1.5) : 1; + const finalColor = index % 3 === 0 ? altColor : color; + const finalWidth = Math.round(horizontal ? width * multiplier : width); + const finalHeight = Math.round(horizontal ? height : height * multiplier); + const imageUrl = isImage + ? `https://picsum.photos/${horizontal ? finalWidth : finalWidth + 50}/${horizontal ? finalHeight + 25 : finalHeight}?seed=${index}` + : null; + + return ( + + {imageUrl ? ( + + ) : ( + + {children} + + )} + + ); + }, +); + +ScrollItem.displayName = 'ScrollItem'; diff --git a/apps/react-lightning-example/src/index.tsx b/apps/react-lightning-example/src/index.tsx index f4e012c..1879f8a 100644 --- a/apps/react-lightning-example/src/index.tsx +++ b/apps/react-lightning-example/src/index.tsx @@ -1,17 +1,21 @@ -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 { Canvas, type RenderOptions } from '@plextv/react-lightning'; +import { plugin as cssTransformPlugin } from '@plextv/react-lightning-plugin-css-transform'; +import { FlexRoot, plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox'; + import { keyMap } from './keyMap'; import { AnimationPage } from './pages/AnimationPage'; import { BrowsePage } from './pages/BrowsePage'; import { LayoutPage } from './pages/LayoutPage'; +import { NestedListPage } from './pages/NestedListPage'; import { Page60 } from './pages/Page60'; 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 +28,10 @@ const router = createBrowserRouter([ path: '/flex-test', element: , }, + { + path: '/virtual-list', + element: , + }, { path: '/poster', element: , @@ -44,6 +52,10 @@ const router = createBrowserRouter([ path: '/transforms', element: , }, + { + path: '/nested-list', + element: , + }, { path: '/page60', element: , @@ -83,7 +95,9 @@ const options: RenderOptions = { const App = () => ( - + + + ); diff --git a/apps/react-lightning-example/src/pages/AnimationPage.tsx b/apps/react-lightning-example/src/pages/AnimationPage.tsx index a1e32a1..6390ddd 100644 --- a/apps/react-lightning-example/src/pages/AnimationPage.tsx +++ b/apps/react-lightning-example/src/pages/AnimationPage.tsx @@ -1,5 +1,7 @@ -import { Column } from '@plextv/react-lightning-components'; import { type FC, useCallback, useState } from 'react'; + +import { Column } from '@plextv/react-lightning-components'; + import Button from '../components/Button'; import { PosterCollection } from '../components/PosterCollection'; @@ -16,9 +18,7 @@ export const AnimationPage: FC = () => { return ( <> - - Poster Count: {numPosters} - + Poster Count: {numPosters} 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/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/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 ( { ((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 ( + + { + layout.size = Math.round(index % 3 === 0 ? 75 * 1.25 : 75); + }} + renderItem={({ index, item }) => ( + + {item} + + )} + /> + + { + layout.size = Math.round(index % 3 === 0 ? 50 * 1.5 : 50); + }} + renderItem={({ index, item }) => ( + + {item} + + )} + /> + + ); +}; diff --git a/apps/react-lightning-example/src/shaders/MyCustomTexture.ts b/apps/react-lightning-example/src/shaders/MyCustomTexture.ts index c412315..d70c373 100644 --- a/apps/react-lightning-example/src/shaders/MyCustomTexture.ts +++ b/apps/react-lightning-example/src/shaders/MyCustomTexture.ts @@ -1,8 +1,4 @@ -import { - type CoreTextureManager, - Texture, - type TextureData, -} from '@lightningjs/renderer'; +import { type CoreTextureManager, Texture, type TextureData } from '@lightningjs/renderer'; /** * Augment the EffectMap interface to include the CustomEffect @@ -68,9 +64,7 @@ export class MyCustomTexture extends Texture { return false; // <-- Don't cache at all } - static override resolveDefaults( - props: MyCustomTextureProps, - ): Required { + static override resolveDefaults(props: MyCustomTextureProps): Required { return { percent: props.percent ?? 20, w: props.w, 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-lightning-example/vite.config.mjs b/apps/react-lightning-example/vite.config.mjs index 8a4cd7d..35c79e7 100644 --- a/apps/react-lightning-example/vite.config.mjs +++ b/apps/react-lightning-example/vite.config.mjs @@ -1,17 +1,20 @@ -import fontGen from '@plextv/vite-plugin-msdf-fontgen'; +import babel from '@rolldown/plugin-babel'; import legacy from '@vitejs/plugin-legacy'; -import react from '@vitejs/plugin-react'; -import tsconfigPaths from 'vite-tsconfig-paths'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; + +import fontGen from '@plextv/vite-plugin-msdf-fontgen'; /** * @type {import('vite').InlineConfig} */ const config = { plugins: [ - tsconfigPaths({ - skip: (dir) => dir.includes('app-template'), - }), 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 4439a15..bc07e2f 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", @@ -23,38 +22,44 @@ "test:unit": "vitest run --passWithNoTests" }, "dependencies": { - "@lightningjs/renderer": "3.0.0-beta20", + "@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:*", "@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-navigation/native": "7.2.2", + "react": "catalog:apps", + "react-dom": "catalog:apps", + "react-native": "catalog:apps", + "react-native-reanimated": "catalog:apps", + "react-native-worklets": "0.8.1" }, "devDependencies": { "@plextv/vite-plugin-msdf-fontgen": "workspace:*", "@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", - "typescript": "5.9.3" + "@rolldown/plugin-babel": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-legacy": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:", + "typescript": "6.0.2" }, "volta": { "extends": "../../package.json" }, + "packageManager": "pnpm@10.12.3", "//": "unsure why react-dom needed to be ignored...", "depcheck": { "ignoreMatches": [ "typescript", - "react-dom" + "react-dom", + "babel-plugin-react-compiler" ] } } 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/index.tsx b/apps/react-native-lightning-example/src/index.tsx index 3b8d157..32a4a68 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,26 @@ 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 { Column, Row } from '@plextv/react-lightning-components'; +import { getReactNativePlugins, NativeCanvas } 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 +58,6 @@ const screens = { Components: 'components', NestedLayouts: 'nestedLayouts', VirtualizedList: 'virtualizedList', - FlashList: 'flashList', }; const linking: LinkingOptions = { @@ -122,17 +119,9 @@ const MainApp = () => { color={'rgba(55, 55, 22, 1)'} onPress={() => nav.navigate('VirtualizedList')} /> - - {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 ( ; - -export const FlashListExample = () => { - const buttons = new Array(50).fill(null).map((_, i) => `Flash Button ${i}`); - const verticalRef = useRef>(null); - const horizontalRef = useRef>(null); - - const handleVerticalFocus = useCallback((index: number) => { - verticalRef.current?.scrollToIndex({ index, viewPosition: 0.5 }); - }, []); - - const handleHorizontalFocus = useCallback((index: number) => { - horizontalRef.current?.scrollToIndex({ index, viewPosition: 0.5 }); - }, []); - - return ( - - - ( - - {item} - - )} - drawDistance={50} - /> - - - - ( - - {item} - - )} - drawDistance={50} - /> - - - ); -}; diff --git a/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx b/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx index 0333e1b..8cc8467 100644 --- a/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx +++ b/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx @@ -1,6 +1,7 @@ import type { Meta } from '@storybook/react-vite'; -import { createRef, useCallback } from 'react'; -import { View, VirtualizedList } from 'react-native'; +import { createRef } from 'react'; +import { VirtualizedList } from 'react-native'; + import ScrollItem from '../../components/ScrollItem'; export default { @@ -9,49 +10,27 @@ export default { tags: ['reactNative'], } as Meta; -const getItem = (_: string[], index: number) => `Button ${index}`; -const ITEM_WIDTH = 100; -const ITEM_HEIGHT = 50; - export const VirtualizedListTest = () => { - const ref = createRef>(); - - const handleFocus = useCallback( - (index: number) => { - ref.current?.scrollToIndex({ index, viewPosition: 0.5 }); - }, - [ref.current], - ); + const ref = createRef>(); + const data = Array.from({ length: 5000 }, (_, i) => ({ + text: `Button ${i}`, + isImage: Math.random() < 0.5, + })); return ( - - 5000} - getItemLayout={(_, index) => ({ - index, - length: ITEM_HEIGHT, - offset: index * ITEM_HEIGHT, - })} - keyExtractor={(item) => item} - windowSize={2} - renderItem={({ index, item }) => ( - - {item} - - )} - /> - + + ref={ref} + data={data} + removeClippedSubviews={true} + snapToAlignment="center" + initialNumToRender={20} + keyExtractor={(item) => item.text} + windowSize={2} + renderItem={({ index, item }) => ( + + {item.text} + + )} + /> ); }; diff --git a/apps/storybook/vite.config.mjs b/apps/storybook/vite.config.mjs index 55b0f49..c32f0f4 100644 --- a/apps/storybook/vite.config.mjs +++ b/apps/storybook/vite.config.mjs @@ -1,7 +1,10 @@ +import babel from '@rolldown/plugin-babel'; +import { reactCompilerPreset } from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + import fontGen from '@plextv/vite-plugin-msdf-fontgen'; import reactNativeLightningPlugin from '@plextv/vite-plugin-react-native-lightning'; import reactReanimatedLightningPlugin from '@plextv/vite-plugin-react-reanimated-lightning'; -import { defineConfig } from 'vite'; /** * @type {import('vite').InlineConfig} @@ -10,14 +13,16 @@ const config = defineConfig((env) => ({ 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(), + // 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: [ @@ -31,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/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..fa1dc5e 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,26 +32,27 @@ "prepare": "husky" }, "devDependencies": { - "@biomejs/biome": "2.3.11", - "@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", - "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" }, - "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..7138774 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,7 +15,16 @@ "./vite.config": "./vite.config.mjs" }, "scripts": {}, + "devDependencies": { + "@rollup/plugin-babel": "7.0.0", + "babel-plugin-react-compiler": "catalog:" + }, "volta": { "extends": "../../package.json" + }, + "depcheck": { + "ignoreMatches": [ + "babel-plugin-react-compiler" + ] } } 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..06bafb0 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'; @@ -7,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/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..7636101 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,16 +24,16 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "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" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,21 +42,21 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "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:" }, "volta": { "extends": "../../package.json" + }, + "inlinedDependencies": { + "csstype": "3.2.3" } } 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..4a9f68d 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 { @@ -11,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 6c97f1d..35332d4 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; /** @@ -16,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-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..f0eb957 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,16 +24,16 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "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" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,11 +42,9 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { - "@repo/configs": "workspace:*" + "@repo/configs": "workspace:*", + "type-fest": "catalog:" }, "peerDependencies": { "@plextv/react-lightning": "workspace:^" diff --git a/packages/plugin-flexbox-lite/src/index.ts b/packages/plugin-flexbox-lite/src/index.ts index 3147315..9c43c29 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; } @@ -213,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; } @@ -226,11 +209,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 +235,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..1d86098 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,20 +48,18 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { - "tseep": "1.3.1", + "tseep": "catalog:", "yoga-layout": "3.2.1" }, "devDependencies": { "@repo/configs": "workspace:*", + "@types/react": "catalog:", "copyfiles": "2.4.1" }, "peerDependencies": { "@plextv/react-lightning": "workspace:^", - "react": "^19.2.3" + "react": "catalog:" }, "volta": { "extends": "../../package.json" diff --git a/packages/plugin-flexbox/src/LightningManager.ts b/packages/plugin-flexbox/src/LightningManager.ts index 5c2cc02..2b0d299 100644 --- a/packages/plugin-flexbox/src/LightningManager.ts +++ b/packages/plugin-flexbox/src/LightningManager.ts @@ -5,18 +5,21 @@ 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 - * 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(); + /** childId -> yoga-side parentId. Lets boundary/flex-root marking stay sync without a worker round-trip. */ + private _yogaParents = new Map(); + /** 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; public async init(yogaOptions?: YogaOptions): Promise { @@ -24,16 +27,188 @@ export class LightningManager { this._yogaManager.on('render', this._applyUpdates); } + /** + * 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)) { + 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 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)) { + 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); + + // First layout pass — without this the root sits at 0,0 until + // something else calls applyStyle. + this._yogaManager.queueRender(element.id); + } + + return () => this.unmarkFlexRoot(element.id); + } + + public unmarkFlexRoot(elementId: number): void { + this._flexRoots.delete(elementId); + this._yogaManager?.removeIndependentRoot(elementId); + } + + /** 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; + + while (curr) { + if (this._flexRoots.has(curr.id)) { + return false; + } + + if (this._boundaries.has(curr.id)) { + return true; + } + + curr = curr.parent; + } + + return true; + } + + /** + * 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) { + 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; + } + + /** 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); + } + + /** Counterpart of `_setYogaParent`. `parentId` passed explicitly since `_yogaParents.get` may already be cleared. */ + 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}.`); + return; } 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); @@ -45,26 +220,63 @@ export class LightningManager { dispose(); } + const yogaParent = this._yogaParents.get(element.id); + + if (yogaParent !== undefined) { + this._clearYogaParent(element.id, yogaParent); + } + this._elements.delete(element.id); - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. But avoiding the nullish operator for perf reasons + 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); - // 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!.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) => { - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above - this._yogaManager!.addChildNode(element.id, child.id, index); + if (this._isInBoundary(element)) { + return; + } + + // 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 + 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`'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); - // biome-ignore lint/style/noNonNullAssertion: Guaranteed to exist. See above + 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); - // 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); + + // 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); }), element.on('inViewport', () => { @@ -80,9 +292,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,21 +321,26 @@ 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); } } private _applyUpdates = (buffer: ArrayBuffer) => { - const dataView = new SimpleDataView(buffer); + // 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; - // 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); @@ -133,11 +348,13 @@ 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; + + const isText = el.isTextElement; if (el.parent?.style.display !== 'flex') { skipX = @@ -158,15 +375,19 @@ 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. - if (width !== 0 && !el.isTextElement) { + // Skip zero (causes layout issues) and text elements (Lightning sizes them). + 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 b430299..b0b4a54 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'; @@ -9,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(), @@ -67,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, @@ -75,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, @@ -124,8 +136,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 }, ]; @@ -250,17 +261,22 @@ 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); + // 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( - 1920, - 1080, + undefined, + undefined, mockYoga.DIRECTION_LTR, ); resolve(); @@ -270,18 +286,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); @@ -293,6 +309,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 @@ -309,6 +327,7 @@ describe('YogaManager', () => { const numNodes = 100; yogaManager.addNode(rootId); + yogaManager.addIndependentRoot(rootId); for (let i = 1; i < numNodes; i++) { yogaManager.addNode(i); @@ -335,6 +354,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', () => { @@ -360,9 +456,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 +471,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 +493,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,89 +520,11 @@ 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); }); }); - 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 049d806..794ab3a 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>; @@ -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', @@ -48,22 +47,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 +78,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 +93,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 +128,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; @@ -150,47 +138,79 @@ 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; + } + + this._isRenderQueued = true; + + // 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; - // biome-ignore lint/style/noNonNullAssertion: Already checked this._yoga above - root.node.calculateLayout(1920, 1080, this._yoga!.DIRECTION_LTR); + 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) { + // 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, + // 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( @@ -201,10 +221,11 @@ 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` 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); } } @@ -225,6 +246,7 @@ export class YogaManager { if (!yogaNode) { console.warn(`Yoga node with ID ${elementId} not found.`); + return; } @@ -268,49 +290,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); @@ -339,34 +318,43 @@ export class YogaManager { return yogaNode; } - // returns the new offset in the dataView + // 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); + !this._yogaOptions.processHiddenNodes && this._hiddenElements.has(yogaNode.id); - if (!force && (skipHiddenNode || !yogaNode.node.hasNewLayout())) { - return; - } + if (!skipHiddenNode && (force || yogaNode.node.hasNewLayout())) { + if (!this._dataView.hasSpace(APPROX_SIZEOF_UPDATE)) { + 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); - } + // 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; - const layout = yogaNode.node.getComputedLayout(); + // 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; - 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); + 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(); + } - 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 83793e5..6dd3628 100644 --- a/packages/plugin-flexbox/src/YogaManagerWorker.ts +++ b/packages/plugin-flexbox/src/YogaManagerWorker.ts @@ -1,19 +1,16 @@ -import type { LightningElementStyle } from '@plextv/react-lightning'; 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; - -// 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; export type Workerized = { [K in keyof T]: T[K] extends AnyFunc @@ -23,27 +20,30 @@ export type Workerized = { : never; }; -function delay void | Promise>( - fn: T, - delay: number, -): T { - let timeout: ReturnType | null = null; +/** + * 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; 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 { @@ -52,26 +52,36 @@ 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 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( + { + 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 — collapses two postMessages into one. + if (_childOperations.offset > 0) { + _flushBothInternal(); + + return; + } worker.postMessage({ method: 'applyStyles', @@ -83,7 +93,7 @@ function wrapWorker(worker: Worker): Workerized { _numStylesToSend = 0; } - const queueSendStyles = delay(flushSendStyles, DELAY_DURATION); + const queueSendStyles = debounceMicrotask(flushSendStyles); function applyStyle( elementId: number, @@ -99,9 +109,16 @@ 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` 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; + } + + // 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 @@ -109,6 +126,13 @@ function wrapWorker(worker: Worker): Workerized { } } } else { + // 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; + } + delete _stylesToSend[elementId]; _numStylesToSend--; } @@ -116,7 +140,6 @@ function wrapWorker(worker: Worker): Workerized { _needsRender ||= !skipRender; if (_numStylesToSend > 50) { - // Flush early if the object gets too large flushSendStyles(); } else { queueSendStyles(); @@ -128,28 +151,95 @@ 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); + /** + * 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; + + 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 one postMessage — + // unmount cascades otherwise produce ~2 messages per destroyed node. + let _wantsRender = false; + let _renderElementId = 0; + let _renderForce = false; + + function flushRender() { + if (!_wantsRender) { + return; + } + + // 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(); + 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, ) { - // Batch operations into a buffer for quick transfers and less postMessage calls switch (method) { case 'addNode': _childOperations.writeUint8(NodeOperations.AddNode); @@ -159,17 +249,21 @@ 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', - ); + 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); @@ -178,6 +272,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}`); } @@ -185,93 +288,13 @@ 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 }>, - ) => { + worker.onmessage = (event: MessageEvent<{ id: string; result?: unknown; error?: string }>) => { const { id, result, error } = event.data; if (id === 'render') { // Special case for render updates _eventEmitter.emit('render', result as ArrayBuffer); + return; } @@ -279,6 +302,7 @@ function wrapWorker(worker: Worker): Workerized { if (!callee) { console.error(`No handler found for worker message id: ${id}`); + return; } @@ -293,47 +317,45 @@ function wrapWorker(worker: Worker): Workerized { } }; - // @ts-expect-error - const proxy: Workerized = new Proxy(() => {}, { - get(_, prop) { - if (typeof prop !== 'string') { - return undefined; - } + // 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(); - // 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 }); - }); + // 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), + 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; @@ -341,5 +363,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..4bfa000 100644 --- a/packages/plugin-flexbox/src/index.ts +++ b/packages/plugin-flexbox/src/index.ts @@ -1,16 +1,18 @@ -import type { - LightningElement, - LightningElementStyle, - Plugin, -} from '@plextv/react-lightning'; +import type { LightningElement, LightningElementStyle, Plugin } from '@plextv/react-lightning'; + import { LightningManager } from './LightningManager'; +import { setFlexboxManager } from './manager'; 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(); + setFlexboxManager(lightningManager); + return { + handledStyleProps: new Set(Object.keys(flexProps)), + init() { return lightningManager.init(yogaOptions); }, @@ -26,39 +28,49 @@ export function plugin(yogaOptions?: YogaOptions): Plugin { return props; } - const flexStyles: Record = {}; - const remainingStyles: Record = {}; + // 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; - // Direct property iteration is faster than Object.entries + reduce for (const key in styles) { - const value = styles[key as keyof LightningElementStyle]; - if ( key === 'w' || key === 'h' || key === 'maxWidth' || - key === 'maxHeight' + key === 'maxHeight' || + isFlexStyleProp(key) ) { + hasFlexStyles = true; + break; + } + } + + if (!hasFlexStyles) { + return props; + } + + const flexStyles: Record = {}; + const remainingStyles: Record = {}; + + for (const key in styles) { + const value = styles[key as keyof LightningElementStyle]; + + 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; - 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, @@ -68,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/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/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/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..250fd22 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; } @@ -53,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; } @@ -65,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) { @@ -91,11 +107,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 +118,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 +129,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 +140,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 +172,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) @@ -215,22 +205,14 @@ 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; } - 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 +265,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; @@ -310,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/util/applyReactPropsToYoga.ts b/packages/plugin-flexbox/src/util/applyReactPropsToYoga.ts index e972817..c0f4b4f 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; } @@ -163,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], + ); + } } } } @@ -190,16 +195,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 +223,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 +308,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..7bb83df 100644 --- a/packages/plugin-flexbox/src/util/isFlexStyleProp.ts +++ b/packages/plugin-flexbox/src/util/isFlexStyleProp.ts @@ -59,8 +59,11 @@ flexProps satisfies Partial>; export type FlexProps = keyof typeof flexProps; -export function isFlexStyleProp( - prop: number | string | symbol, -): prop is FlexProps { - return prop in 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 typeof prop === 'string' && _flexPropsSet.has(prop); } 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..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,61 +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[]; }>, ) => { @@ -76,23 +91,27 @@ 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') { // @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 +122,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/wrappers.tsx b/packages/plugin-flexbox/src/wrappers.tsx new file mode 100644 index 0000000..8af05aa --- /dev/null +++ b/packages/plugin-flexbox/src/wrappers.tsx @@ -0,0 +1,99 @@ +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> = + 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-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/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-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..ff186d6 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,15 +23,15 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -37,22 +40,20 @@ "check:types": "tsc --noEmit -p tsconfig.json", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "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:", + "react-native": "catalog:", + "react-native-reanimated": "catalog:" }, "volta": { "extends": "../../package.json" @@ -61,5 +62,8 @@ "ignoreMatches": [ "react-native-reanimated-original" ] + }, + "inlinedDependencies": { + "type-fest": "5.5.0" } } 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..e0a3791 100644 --- a/packages/plugin-reanimated/src/animation/springUtils.ts +++ b/packages/plugin-reanimated/src/animation/springUtils.ts @@ -10,10 +10,9 @@ 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 +22,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 +138,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 +179,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 +207,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 +235,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..bafa3a9 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,12 +263,10 @@ 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; } @@ -303,12 +286,11 @@ export function createAnimatedComponent( if (!layoutAnimation) { callback?.(); + return; } - const lightningAnimation = toLightningAnimationAndStyles( - layoutAnimation.animations, - ); + const lightningAnimation = toLightningAnimationAndStyles(layoutAnimation.animations); el.once('animationFinished', () => { if (layoutAnimation.callback) { @@ -317,14 +299,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 +311,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..61dd57e 100644 --- a/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx +++ b/packages/plugin-reanimated/src/exports/useAnimatedScrollHandler.tsx @@ -12,31 +12,30 @@ 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); - if (typeof scrollHandlers === 'function') { - scrollHandlers(reanimatedEvent, context); - return; - } + 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..53d7bb8 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; @@ -23,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)}`, ); @@ -39,10 +40,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/package.json b/packages/react-lightning-components/package.json index 2a1ddc4..b615900 100644 --- a/packages/react-lightning-components/package.json +++ b/packages/react-lightning-components/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-lightning-components", - "description": "React components for react-lightning", "version": "0.4.0", - "author": "Plex Inc.", + "description": "React components for 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", @@ -19,36 +22,41 @@ ".": "./src/index.ts", "./layout/Column": "./src/exports/layout/Column.tsx", "./layout/Row": "./src/exports/layout/Row.tsx", + "./lists/VirtualList": "./src/exports/lists/VirtualList.tsx", "./text/StyledText": "./src/exports/text/StyledText.tsx", "./util/FPSMonitor": "./src/exports/util/FPSMonitor.tsx", "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "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": { + "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" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -57,17 +65,14 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "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:" }, "volta": { "extends": "../../package.json" diff --git a/packages/react-lightning-components/src/components/VirtualList/AverageWindow.spec.ts b/packages/react-lightning-components/src/components/VirtualList/AverageWindow.spec.ts new file mode 100644 index 0000000..264e6a0 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/AverageWindow.spec.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { AverageWindow } from './AverageWindow'; + +describe('AverageWindow', () => { + it('returns 0 when empty', () => { + const w = new AverageWindow(); + expect(w.currentValue).toBe(0); + expect(w.count).toBe(0); + }); + + it('computes average of added values', () => { + const w = new AverageWindow(5); + w.addValue(10); + w.addValue(20); + expect(w.currentValue).toBe(15); + expect(w.count).toBe(2); + }); + + it('evicts oldest values when window is full', () => { + const w = new AverageWindow(3); + w.addValue(10); + w.addValue(20); + w.addValue(30); + expect(w.currentValue).toBe(20); // (10+20+30)/3 + + w.addValue(40); + expect(w.currentValue).toBe(30); // (20+30+40)/3 + expect(w.count).toBe(3); + }); + + it('clears all values', () => { + const w = new AverageWindow(5); + w.addValue(10); + w.addValue(20); + w.clear(); + expect(w.currentValue).toBe(0); + expect(w.count).toBe(0); + }); + + it('handles window size of 1', () => { + const w = new AverageWindow(1); + w.addValue(10); + expect(w.currentValue).toBe(10); + w.addValue(20); + expect(w.currentValue).toBe(20); + expect(w.count).toBe(1); + }); + + it('enforces minimum window size of 1', () => { + const w = new AverageWindow(0); + w.addValue(42); + expect(w.currentValue).toBe(42); + expect(w.count).toBe(1); + }); +}); diff --git a/packages/react-lightning-components/src/components/VirtualList/AverageWindow.ts b/packages/react-lightning-components/src/components/VirtualList/AverageWindow.ts new file mode 100644 index 0000000..b1ff3b9 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/AverageWindow.ts @@ -0,0 +1,43 @@ +export class AverageWindow { + private _values: number[]; + private _head = 0; + private _count = 0; + private _size: number; + private _sum = 0; + + constructor(size = 10) { + this._size = Math.max(1, size); + this._values = Array.from({ length: this._size }).fill(0); + } + + get currentValue(): number { + if (this._count === 0) { + return 0; + } + + return this._sum / this._count; + } + + get count(): number { + return this._count; + } + + addValue(value: number): void { + if (this._count >= this._size) { + this._sum -= this._values[this._head] ?? 0; + this._values[this._head] = value; + this._head = (this._head + 1) % this._size; + } else { + this._values[(this._head + this._count) % this._size] = value; + this._count++; + } + + this._sum += value; + } + + clear(): void { + this._head = 0; + this._count = 0; + this._sum = 0; + } +} diff --git a/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts new file mode 100644 index 0000000..c581c6d --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.spec.ts @@ -0,0 +1,588 @@ +import { describe, expect, it } from 'vitest'; + +import { LayoutManager } from './LayoutManager'; + +const makeData = (n: number) => Array.from({ length: n }, (_, i) => ({ id: i })); + +describe('LayoutManager', () => { + describe('single column', () => { + it('positions items sequentially using estimatedItemSize', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + 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); + }); + + it('uses overrideItemLayout for custom sizes', () => { + const sizes = [50, 100, 75]; + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + overrideItemLayout: (layout, _item, index) => { + layout.size = sizes[index]; + }, + }); + + expect(lm.getLayout(0)?.size).toBe(50); + expect(lm.getLayout(1)?.offset).toBe(50); + expect(lm.getLayout(1)?.size).toBe(100); + expect(lm.getLayout(2)?.offset).toBe(150); + expect(lm.totalSize).toBe(225); + }); + + it('returns undefined for out-of-range index', () => { + const lm = new 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 using cellCrossSize', () => { + const lm = new LayoutManager({ + data: makeData(5), + estimatedItemSize: 100, + numColumns: 2, + cellCrossSize: 100, + }); + + expect(lm.getLayout(0)).toEqual( + expect.objectContaining({ + offset: 0, + column: 0, + crossOffset: 0, + crossSize: 100, + }), + ); + expect(lm.getLayout(1)).toEqual( + expect.objectContaining({ + offset: 0, + column: 1, + crossOffset: 100, + crossSize: 100, + }), + ); + expect(lm.getLayout(2)).toEqual(expect.objectContaining({ offset: 100, column: 0 })); + expect(lm.getLayout(3)).toEqual(expect.objectContaining({ offset: 100, column: 1 })); + expect(lm.getLayout(4)).toEqual(expect.objectContaining({ offset: 200, column: 0 })); + expect(lm.totalSize).toBe(300); + }); + + 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; + } + }, + }); + + expect(lm.getLayout(0)?.crossSize).toBe(200); + expect(lm.getLayout(0)?.column).toBe(0); + expect(lm.getLayout(1)?.column).toBe(2); + expect(lm.getLayout(1)?.crossSize).toBe(100); + }); + + it('clamps span to available columns', () => { + const lm = new LayoutManager({ + data: makeData(2), + estimatedItemSize: 100, + numColumns: 2, + cellCrossSize: 100, + overrideItemLayout: (layout) => { + layout.span = 5; + }, + }); + + expect(lm.getLayout(0)?.crossSize).toBe(200); + }); + }); + + describe('separator size', () => { + it('adds separator gap between items in single column', () => { + const lm = new 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 })); + expect(lm.totalSize).toBe(320); + }); + + it('does not add separator gap between rows in multi column', () => { + const lm = new LayoutManager({ + data: makeData(5), + estimatedItemSize: 100, + numColumns: 2, + cellCrossSize: 100, + separatorSize: 20, + }); + + expect(lm.getLayout(0)?.offset).toBe(0); + expect(lm.getLayout(1)?.offset).toBe(0); + expect(lm.getLayout(2)?.offset).toBe(100); + expect(lm.getLayout(3)?.offset).toBe(100); + expect(lm.getLayout(4)?.offset).toBe(200); + 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', () => { + it('returns correct range for a window in the middle', () => { + const lm = new LayoutManager({ + data: makeData(20), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + const range = lm.getVisibleRange(500, 300, 100); + expect(range.startIndex).toBe(4); + expect(range.endIndex).toBe(8); + }); + + it('returns empty range for empty data', () => { + const lm = new LayoutManager({ + data: [], + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + expect(lm.getVisibleRange(0, 300, 100)).toEqual({ + startIndex: 0, + endIndex: -1, + }); + }); + + it('clamps to data bounds', () => { + const lm = new LayoutManager({ + data: makeData(5), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + const range = lm.getVisibleRange(0, 1000, 500); + expect(range.startIndex).toBe(0); + expect(range.endIndex).toBe(4); + }); + + it('handles scroll at the very end', () => { + const lm = new LayoutManager({ + data: makeData(10), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + const range = lm.getVisibleRange(800, 200, 0); + expect(range.startIndex).toBe(8); + expect(range.endIndex).toBe(9); + }); + }); + + 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('uses measured size in subsequent layouts', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + }); + + expect(lm.getLayout(1)?.offset).toBe(100); + + const changed = lm.reportItemSize('0', 150); + expect(changed).toBe(true); + expect(lm.getLayout(0)?.size).toBe(150); + expect(lm.getLayout(1)?.offset).toBe(150); + // 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', () => { + 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), + }); + + expect(lm.reportItemSize('0', 0)).toBe(false); + expect(lm.reportItemSize('0', -5)).toBe(false); + expect(lm.getLayout(0)?.size).toBe(100); + }); + + it('returns false on no-op reports and defers different values via dampening', () => { + const lm = new LayoutManager({ + data: makeData(2), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + }); + + // 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); + // 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)', () => { + const data = makeData(3); + const lm = new LayoutManager({ + data, + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + keyExtractor: (item) => String(item.id), + }); + + 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 }); + + // 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('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, + numColumns: 1, + cellCrossSize: 200, + }); + + lm.reportItemSize('0', 150); + 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)', () => { + 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.reportItemSize('1', 150); + expect(lm.getLayout(1)?.size).toBe(0); + }); + + 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(150); + expect(lm.getLayout(2)?.size).toBe(150); + }); + + 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.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('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), + }); + + 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('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.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 + }); + }); + + describe('updateConfig', () => { + it('recomputes layouts after data change', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + expect(lm.totalSize).toBe(300); + + expect(lm.updateConfig({ data: makeData(5) })).toBe(true); + expect(lm.totalSize).toBe(500); + }); + + it('recomputes when cellCrossSize changes', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + expect(lm.updateConfig({ cellCrossSize: 300 })).toBe(true); + expect(lm.getLayout(0)?.crossSize).toBe(300); + }); + + it('returns false when nothing changed', () => { + const lm = new LayoutManager({ + data: makeData(3), + estimatedItemSize: 100, + numColumns: 1, + cellCrossSize: 200, + }); + + 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 new file mode 100644 index 0000000..767c6b6 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/LayoutManager.ts @@ -0,0 +1,612 @@ +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; +} + +export interface LayoutManagerConfig { + data: ReadonlyArray; + estimatedItemSize: number; + numColumns: number; + overrideItemLayout?: OverrideItemLayoutFn; + extraData?: unknown; + separatorSize?: number; + /** Ground truth from VL viewport; never aggregated from cell reports. */ + cellCrossSize: number; + /** 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). 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 } = {}; + private _layouts: ComputedLayout[] = []; + private _layoutCount = 0; + private _totalSize = 0; + private _dirty = true; + private _data: ReadonlyArray; + private _estimatedItemSize: number; + private _numColumns: number; + private _overrideItemLayout?: OverrideItemLayoutFn; + private _extraData?: unknown; + private _separatorSize: number; + private _cellCrossSize: number; + private _keyExtractor?: (item: T, index: number) => string; + private _measuredSizes: Map = new Map(); + /** 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. 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 — 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; + /** + * 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; + + constructor(config: LayoutManagerConfig) { + this._data = config.data; + this._estimatedItemSize = config.estimatedItemSize; + this._numColumns = Math.max(1, config.numColumns); + this._overrideItemLayout = config.overrideItemLayout; + this._extraData = config.extraData; + this._separatorSize = config.separatorSize ?? 0; + this._cellCrossSize = config.cellCrossSize; + this._keyExtractor = config.keyExtractor; + } + + get totalSize(): number { + if (this._dirty) { + this._recompute(); + } + + return this._totalSize; + } + + /** Fires when the stability backstop timer commits a pending measurement (no incoming report to bump layoutVersion synchronously). */ + setOnChange(cb: () => void): void { + this._onChange = cb; + } + + /** Copy so the cache snapshot doesn't alias live state. */ + getMeasurements(): Map { + return new Map(this._measuredSizes); + } + + /** + * 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); + this._batchedSizes.clear(); + this._pendingSizes.clear(); + + for (const timer of this._pendingTimers.values()) { + clearTimeout(timer); + } + + this._pendingTimers.clear(); + this._dirty = true; + } + + /** Returns `true` if disabling drained at least one batched size into measurements. */ + setBatching(active: boolean): boolean { + if (this._batching === active) { + return false; + } + + this._batching = active; + + if (active) { + // Cancel pending dampening — the upcoming batch will overwrite anyway. + 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 { + let changed = false; + + if (config.data !== undefined && config.data !== this._data) { + this._data = config.data; + changed = true; + } + + if ( + config.estimatedItemSize !== undefined && + config.estimatedItemSize !== this._estimatedItemSize + ) { + this._estimatedItemSize = config.estimatedItemSize; + changed = true; + } + + if (config.numColumns !== undefined) { + const nc = Math.max(1, config.numColumns); + + if (nc !== this._numColumns) { + this._numColumns = nc; + changed = true; + } + } + + if ( + config.overrideItemLayout !== undefined && + config.overrideItemLayout !== this._overrideItemLayout + ) { + this._overrideItemLayout = config.overrideItemLayout; + changed = true; + } + + if (config.extraData !== undefined && config.extraData !== this._extraData) { + this._extraData = config.extraData; + changed = true; + } + + if (config.separatorSize !== undefined && config.separatorSize !== this._separatorSize) { + this._separatorSize = config.separatorSize; + 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; + } + + return changed; + } + + /** + * 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) { + return false; + } + + if (this._batching) { + this._batchedSizes.set(userKey, size); + + return false; + } + + const existing = this._measuredSizes.get(userKey); + + if (existing != null && Math.abs(existing - size) < 1) { + this._clearPending(userKey); + + return false; + } + + // First measurement — apply immediately, nothing to thrash against. + if (existing == null) { + this._measuredSizes.set(userKey, size); + + if (this._firstMeasuredSize === 0) { + this._firstMeasuredSize = size; + } + + this._clearPending(userKey); + this._dirty = true; + + return true; + } + + // Differs from existing — require stability before accepting it. + const now = Date.now(); + const pending = this._pendingSizes.get(userKey); + + if (pending != null && Math.abs(pending.size - size) < 1) { + // 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); + this._dirty = true; + + return true; + } + + return false; + } + + // 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); + } + + this._pendingSizes.delete(userKey); + } + + /** Imperative invalidation — VL itself preserves measurements across data identity changes. */ + 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; + } + + /** + * 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) { + 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; + } + + 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( + scrollOffset: number, + viewportSize: number, + drawDistance: number, + ): { startIndex: number; endIndex: number } { + if (this._dirty) { + this._recompute(); + } + + const count = this._layoutCount; + + if (count === 0) { + return { startIndex: 0, endIndex: -1 }; + } + + const rangeStart = scrollOffset - drawDistance; + const rangeEnd = scrollOffset + viewportSize + drawDistance; + const startIndex = this._binarySearchStart(rangeStart); + let endIndex = startIndex; + + for (let i = startIndex; i < count; i++) { + const layout = this._layouts[i]; + + if (!layout || layout.offset >= rangeEnd) { + break; + } + + endIndex = i; + } + + return { + startIndex: Math.max(0, startIndex), + endIndex: Math.min(count - 1, endIndex), + }; + } + + private _ensureCapacity(count: number): void { + for (let i = this._layouts.length; i < count; i++) { + this._layouts.push({ + offset: 0, + size: 0, + column: 0, + crossOffset: 0, + crossSize: 0, + }); + } + } + + private _binarySearchStart(targetOffset: number): number { + let low = 0; + let high = this._layoutCount - 1; + let result = 0; + + while (low <= high) { + const mid = (low + high) >>> 1; + const layout = this._layouts[mid]; + + if (!layout) { + break; + } + + if (layout.offset + layout.size <= targetOffset) { + low = mid + 1; + } else { + result = mid; + high = mid - 1; + } + } + + return result; + } + + private _recompute(): void { + this._dirty = false; + + const count = this._data.length; + + if (count === 0) { + this._layoutCount = 0; + this._totalSize = 0; + + return; + } + + this._ensureCapacity(count); + + if (this._numColumns <= 1) { + this._recomputeSingleColumn(count); + } else { + 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) { + // 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) { + 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): void { + let offset = 0; + + for (let i = 0; i < count; i++) { + 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 = this._cellCrossSize; + + offset += size; + + if (size > 0 && i < count - 1) { + offset += this._separatorSize; + } + } + + this._layoutCount = count; + this._totalSize = offset; + } + + private _recomputeMultiColumn(count: number): void { + const cellCross = this._cellCrossSize; + let offset = 0; + let i = 0; + + while (i < count) { + let rowHeight = 0; + let columnsUsed = 0; + + while (i < count && columnsUsed < this._numColumns) { + 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 * cellCross; + layout.crossSize = span * cellCross; + + rowHeight = Math.max(rowHeight, size); + columnsUsed += span; + + i++; + } + + offset += rowHeight; + } + + this._layoutCount = count; + this._totalSize = offset; + } + + private _getOverride(index: number): { size?: number; span?: number } { + LayoutManager._overrideScratch.size = undefined; + LayoutManager._overrideScratch.span = undefined; + + const item = this._data[index]; + + if (!this._overrideItemLayout || item === undefined) { + return LayoutManager._overrideScratch; + } + + this._overrideItemLayout( + LayoutManager._overrideScratch, + item, + index, + this._numColumns, + this._extraData, + ); + + return LayoutManager._overrideScratch; + } +} diff --git a/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts new file mode 100644 index 0000000..6e1706e --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.spec.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest'; + +import { RecyclerPool } from './RecyclerPool'; + +describe('RecyclerPool', () => { + it('assigns unique slot keys to visible items', () => { + const pool = new RecyclerPool(); + const slots = pool.reconcile([0, 1, 2], () => 'default'); + + expect(slots.size).toBe(3); + const keys = [...slots.values()]; + expect(new Set(keys).size).toBe(3); + }); + + it('reuses slots when items scroll out and new ones enter', () => { + const pool = new RecyclerPool(); + + pool.reconcile([0, 1, 2], () => 'default'); + expect(pool.activeCount).toBe(3); + + // Scroll: 0 and 1 leave, 3 and 4 enter + const slots = pool.reconcile([2, 3, 4], () => 'default'); + expect(pool.activeCount).toBe(3); + expect(pool.pooledCount).toBe(0); // all reused + expect(slots.size).toBe(3); + }); + + it('keeps stable keys for items that remain visible', () => { + 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 key1 = slots1.get(1)!; + + const slots2 = pool.reconcile([1, 2, 3], () => 'default'); + expect(slots2.get(1)).toBe(key1); + }); + + it('separates pools by item type', () => { + const pool = new RecyclerPool(); + const getType = (index: number) => (index % 2 === 0 ? 'even' : 'odd'); + + const slots1 = pool.reconcile([0, 1], getType); + // oxlint-disable-next-line typescript/no-non-null-assertion -- keys were just inserted + const evenKey = slots1.get(0)!; + // oxlint-disable-next-line typescript/no-non-null-assertion -- keys were just inserted + const oddKey = slots1.get(1)!; + + // Replace both: even→even, odd→odd + const slots2 = pool.reconcile([2, 3], getType); + expect(slots2.get(2)).toBe(evenKey); + expect(slots2.get(3)).toBe(oddKey); + }); + + it('does not reuse slots across types', () => { + const pool = new RecyclerPool(); + const getType = (index: number) => (index < 2 ? 'a' : 'b'); + + const slots1 = pool.reconcile([0, 1], getType); + // oxlint-disable-next-line typescript/no-non-null-assertion -- keys were just inserted + const keyA0 = slots1.get(0)!; + // oxlint-disable-next-line typescript/no-non-null-assertion -- keys were just inserted + const keyA1 = slots1.get(1)!; + + // Both old slots are type 'a', new items are type 'b' + const slots2 = pool.reconcile([2, 3], getType); + // New keys should be created, not reused from type 'a' + expect(slots2.get(2)).not.toBe(keyA0); + expect(slots2.get(2)).not.toBe(keyA1); + expect(slots2.get(3)).not.toBe(keyA0); + expect(slots2.get(3)).not.toBe(keyA1); + }); + + it('clears all state', () => { + const pool = new RecyclerPool(); + pool.reconcile([0, 1, 2], () => 'default'); + pool.clear(); + + expect(pool.activeCount).toBe(0); + expect(pool.pooledCount).toBe(0); + }); + + it('handles empty visible list', () => { + const pool = new RecyclerPool(); + pool.reconcile([0, 1], () => 'default'); + + const slots = pool.reconcile([], () => 'default'); + expect(slots.size).toBe(0); + expect(pool.activeCount).toBe(0); + expect(pool.pooledCount).toBe(2); + }); + + it('getSlotKey returns key for active items', () => { + const pool = new RecyclerPool(); + pool.reconcile([5, 6], () => 'default'); + + 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 new file mode 100644 index 0000000..f1ef71e --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/RecyclerPool.ts @@ -0,0 +1,196 @@ +export class RecyclerPool { + /** 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; + + get activeCount(): number { + return this._active.size; + } + + get pooledCount(): number { + let count = 0; + + for (const keys of this._available.values()) { + count += keys.length; + } + + return count; + } + + reconcile( + visibleIndices: number[], + getType: (index: number) => string | number, + ): Map { + const visibleSet = this._visibleSet; + + visibleSet.clear(); + + for (const idx of visibleIndices) { + 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 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); + } + + 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; + } + + private _acquire(type: string | number): string { + const available = this._available.get(type); + + if (available && available.length > 0) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- length > 0 checked above + return available.pop()!; + } + + const key = `slot-${this._nextId++}`; + + this._slotTypes.set(key, type); + + return key; + } + + private _release(key: string): void { + const type = this._slotTypes.get(key); + + if (type === undefined) { + return; + } + + const available = this._available.get(type) ?? []; + + available.push(key); + this._available.set(type, available); + } +} diff --git a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts new file mode 100644 index 0000000..a4010be --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.spec.ts @@ -0,0 +1,283 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { ComputedLayout } from './LayoutManager'; +import { ViewabilityTracker } from './ViewabilityTracker'; + +const makeLayout = (offset: number, size: number): ComputedLayout => ({ + offset, + size, + column: 0, + crossOffset: 0, + 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()); + + it('reports newly viewable items', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100), makeLayout(200, 100)]; + + const tracker = new ViewabilityTracker({ + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0, 1, 2], 0, 250, false); + + expect(onChange).toHaveBeenCalledTimes(1); + const { viewableItems, changed } = getCallArgs(onChange, 0); + expect(viewableItems).toHaveLength(3); + expect(changed).toHaveLength(3); + }); + + it('does not fire when nothing changes', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100)]; + + const tracker = new ViewabilityTracker({ + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0, 1], 0, 300, false); + onChange.mockClear(); + + tracker.update([0, 1], 0, 300, false); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('respects itemVisiblePercentThreshold', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { itemVisiblePercentThreshold: 50 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + // Viewport 0–120: item 0 fully visible, item 1 only 20% visible + tracker.update([0, 1], 0, 120, false); + + const items = getCallArgs(onChange, 0).viewableItems; + expect(items).toHaveLength(1); + expect(items[0].index).toBe(0); + }); + + it('respects viewAreaCoveragePercentThreshold', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 50), makeLayout(50, 50)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { viewAreaCoveragePercentThreshold: 20 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + // Viewport 0–100, both items are 50px → each covers 50% + tracker.update([0, 1], 0, 100, false); + expect(getCallArgs(onChange, 0).viewableItems).toHaveLength(2); + }); + + it('respects waitForInteraction', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { waitForInteraction: true }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0], 0, 200, false); + expect(onChange).not.toHaveBeenCalled(); + + tracker.recordInteraction(); + tracker.update([0], 0, 200, false); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('respects minimumViewTime', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0], 0, 200, false); + expect(onChange).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(500); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('cancels pending timer if item leaves before minimumViewTime', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0], 0, 200, false); + // Item leaves before timer fires + tracker.update([], 200, 200, false); + + vi.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('reports items that left the viewport as not viewable', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100)]; + + const tracker = new ViewabilityTracker({ + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0, 1], 0, 300, false); + onChange.mockClear(); + + // Only item 1 visible now + tracker.update([1], 100, 100, false); + const { changed } = getCallArgs(onChange, 0); + const left = changed.find( + (t: { index: number; isViewable: boolean }) => t.index === 0 && !t.isViewable, + ); + expect(left).toBeDefined(); + }); + + it('graduates each item individually when timers fire simultaneously', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100), makeLayout(200, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + // Both items enter at t=0 — both timers start simultaneously + tracker.update([0], 0, 200, false); + tracker.update([0, 1], 0, 300, false); + + // Both timers fire at t=500; each graduates its own item + vi.advanceTimersByTime(500); + expect(onChange).toHaveBeenCalledTimes(2); + // Final state includes both items + const lastCall = getCallArgs(onChange, onChange.mock.calls.length - 1); + expect(lastCall.viewableItems).toHaveLength(2); + }); + + it('does not graduate items that have not met minimumViewTime', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100), makeLayout(100, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + // Item 0 enters at t=0 + tracker.update([0], 0, 200, false); + + // Item 1 enters at t=300 + vi.advanceTimersByTime(300); + tracker.update([0, 1], 0, 300, false); + + // At t=500, only T0 fires (item 0 visible 500ms, item 1 only 200ms) + vi.advanceTimersByTime(200); + expect(onChange).toHaveBeenCalledTimes(1); + 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(getCallArgs(onChange, 1).viewableItems).toHaveLength(2); + }); + + it('immediately reports committed items leaving with minimumViewTime', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0], 0, 200, false); + vi.advanceTimersByTime(500); + expect(onChange).toHaveBeenCalledTimes(1); + onChange.mockClear(); + + // Item 0 leaves — should be reported immediately + tracker.update([], 200, 200, false); + expect(onChange).toHaveBeenCalledTimes(1); + const { changed, viewableItems } = getCallArgs(onChange, 0); + expect(viewableItems).toHaveLength(0); + expect(changed).toHaveLength(1); + expect(changed[0].index).toBe(0); + expect(changed[0].isViewable).toBe(false); + }); + + it('disposes all timers', () => { + const onChange = vi.fn(); + const layouts = [makeLayout(0, 100)]; + + const tracker = new ViewabilityTracker({ + viewabilityConfig: { minimumViewTime: 500 }, + onViewableItemsChanged: onChange, + getLayout: (i) => layouts[i], + getData: (i) => ({ id: i }), + getKey: (i) => String(i), + }); + + tracker.update([0], 0, 200, false); + tracker.dispose(); + + vi.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.ts b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.ts new file mode 100644 index 0000000..d7e0760 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/ViewabilityTracker.ts @@ -0,0 +1,249 @@ +import type { ComputedLayout } from './LayoutManager'; +import type { ViewabilityConfig, ViewToken } from './VirtualListTypes'; + +export interface ViewabilityTrackerConfig { + viewabilityConfig?: ViewabilityConfig; + onViewableItemsChanged?: (info: { + viewableItems: ViewToken[]; + changed: ViewToken[]; + }) => void; + getLayout: (index: number) => ComputedLayout | undefined; + getData: (index: number) => T | undefined; + getKey: (index: number) => string; +} + +export class ViewabilityTracker { + private _config: ViewabilityTrackerConfig; + private _viewableIndices = new Set(); + private _latestViewable = new Set(); + private _pendingTimers = new Map>(); + private _hasInteracted = false; + + constructor(config: ViewabilityTrackerConfig) { + this._config = config; + } + + updateConfig(config: Partial>): void { + Object.assign(this._config, config); + } + + recordInteraction(): void { + this._hasInteracted = true; + } + + update( + visibleIndices: number[], + scrollOffset: number, + viewportSize: number, + _horizontal: boolean, + ): void { + const { viewabilityConfig, onViewableItemsChanged } = this._config; + + if (!onViewableItemsChanged) { + return; + } + + if (viewabilityConfig?.waitForInteraction && !this._hasInteracted) { + return; + } + + const minimumViewTime = viewabilityConfig?.minimumViewTime ?? 0; + const newViewable = new Set(); + + for (const index of visibleIndices) { + if (this._isItemViewable(index, scrollOffset, viewportSize)) { + newViewable.add(index); + } + } + + // Cancel pending timers for items that left the visible set entirely + for (const [index, timer] of this._pendingTimers) { + if (!newViewable.has(index)) { + clearTimeout(timer); + this._pendingTimers.delete(index); + } + } + + const nowViewable: number[] = []; + const noLongerViewable: number[] = []; + + for (const index of newViewable) { + if (!this._viewableIndices.has(index)) { + nowViewable.push(index); + } + } + + for (const index of this._viewableIndices) { + if (!newViewable.has(index)) { + noLongerViewable.push(index); + } + } + + if (nowViewable.length === 0 && noLongerViewable.length === 0) { + return; + } + + if (minimumViewTime > 0) { + this._handleWithDelay(nowViewable, noLongerViewable, newViewable, minimumViewTime); + } else { + this._commitChange(newViewable); + } + } + + dispose(): void { + for (const timer of this._pendingTimers.values()) { + clearTimeout(timer); + } + + this._pendingTimers.clear(); + this._viewableIndices.clear(); + } + + private _handleWithDelay( + nowViewable: number[], + noLongerViewable: number[], + newViewable: Set, + delay: number, + ): void { + // Store the latest viewable set so timers always read + // up-to-date state instead of a stale closure capture. + this._latestViewable = newViewable; + + for (const index of nowViewable) { + if (!this._pendingTimers.has(index)) { + this._pendingTimers.set( + index, + setTimeout(() => { + this._pendingTimers.delete(index); + // Only graduate this specific item — do not drag along + // items whose timers have not yet fired. + if (this._latestViewable.has(index)) { + const graduated = new Set(this._viewableIndices); + + for (const idx of graduated) { + if (!this._latestViewable.has(idx)) { + graduated.delete(idx); + } + } + + graduated.add(index); + this._commitChange(graduated); + } + }, delay), + ); + } + } + + for (const index of noLongerViewable) { + const timer = this._pendingTimers.get(index); + + if (timer) { + clearTimeout(timer); + this._pendingTimers.delete(index); + } + } + + // Immediately report committed items that are no longer viewable + let hasCommittedLeaving = false; + + for (const index of noLongerViewable) { + if (this._viewableIndices.has(index)) { + hasCommittedLeaving = true; + break; + } + } + + if (hasCommittedLeaving) { + const updated = new Set(this._viewableIndices); + + for (const index of noLongerViewable) { + updated.delete(index); + } + + this._commitChange(updated); + } + } + + private _commitChange(newViewable: Set): void { + const changed: ViewToken[] = []; + const viewableItems: ViewToken[] = []; + + for (const index of newViewable) { + const token = this._createToken(index, true); + + if (token) { + viewableItems.push(token); + + if (!this._viewableIndices.has(index)) { + changed.push(token); + } + } + } + + for (const index of this._viewableIndices) { + if (!newViewable.has(index)) { + const token = this._createToken(index, false); + + if (token) { + changed.push(token); + } + } + } + + this._viewableIndices = newViewable; + + if (changed.length > 0) { + this._config.onViewableItemsChanged?.({ viewableItems, changed }); + } + } + + private _isItemViewable(index: number, scrollOffset: number, viewportSize: number): boolean { + const layout = this._config.getLayout(index); + + if (!layout || layout.size === 0) { + return false; + } + + const { viewabilityConfig } = this._config; + const viewStart = scrollOffset; + const viewEnd = scrollOffset + viewportSize; + const itemStart = layout.offset; + const itemEnd = layout.offset + layout.size; + + if (itemEnd <= viewStart || itemStart >= viewEnd) { + return false; + } + + if (itemStart >= viewStart && itemEnd <= viewEnd) { + return true; + } + + const pixelsVisible = Math.min(itemEnd, viewEnd) - Math.max(itemStart, viewStart); + + if (viewabilityConfig?.viewAreaCoveragePercentThreshold != null) { + return ( + (pixelsVisible / viewportSize) * 100 >= viewabilityConfig.viewAreaCoveragePercentThreshold + ); + } + + const threshold = viewabilityConfig?.itemVisiblePercentThreshold ?? 0; + + return (pixelsVisible / layout.size) * 100 >= threshold; + } + + private _createToken(index: number, isViewable: boolean): ViewToken | null { + const item = this._config.getData(index); + + if (item === undefined) { + return null; + } + + return { + item, + key: this._config.getKey(index), + index, + isViewable, + timestamp: Date.now(), + }; + } +} 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..52efd5b --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.md @@ -0,0 +1,412 @@ +# VirtualList + +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. + +--- + +## 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.), +- 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 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 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. 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). | +| `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`** — 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. +- **`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** (in `_resolveSize` order, first match wins): + +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 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 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. + +**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 ? ( + /* 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} + + + ) : ( + /* 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. `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 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]` 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 `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, 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. + +--- + +## 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)`, 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. **`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 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 `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. **`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 focus re-enters and `handleChildFocused` runs — produces a no-op scroll. No animation, no flicker. + +### State and invariants + +- **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`. + +--- + +## 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` (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 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. + +`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. +**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`). 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. + +--- + +## What's not supported + +By design, this VL **does not**: + +- 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 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 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 LayoutManager only sees main-axis measurements + +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 _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 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. + +--- + +## 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. +- **`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]`** 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 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 new file mode 100644 index 0000000..98c5248 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualList.tsx @@ -0,0 +1,658 @@ +import type { ComponentType, Ref } from 'react'; +import { + type ForwardedRef, + forwardRef, + isValidElement, + type ReactElement, + useContext, + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, + useState, +} 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'; +import { + CellBoundsContext, + type VLPersistedState, + VLCellKeyContext, + VLStateCacheContext, +} from './VirtualListContext'; +import type { VirtualListProps, VirtualListRef } from './VirtualListTypes'; + +function renderListComponent( + component: VirtualListProps['ListHeaderComponent'], +): ReactElement | null { + if (!component) { + return null; + } + + if (isValidElement(component)) { + return component; + } + + const Component = component as ComponentType; + + return ; +} + +function VirtualListInner(props: VirtualListProps, ref: ForwardedRef) { + const { + data, + renderItem, + estimatedItemSize = 200, + horizontal = false, + numColumns = 1, + drawDistance = 250, + keyExtractor, + extraData, + contentContainerStyle, + style, + ListHeaderComponent, + listHeaderSize = 0, + ListFooterComponent, + listFooterSize = 0, + ListEmptyComponent, + ItemSeparatorComponent, + overrideItemLayout, + getItemType, + initialScrollIndex, + initialScrollIndexParams, + onEndReached, + onEndReachedThreshold = 0.5, + onScroll, + onViewableItemsChanged, + viewabilityConfig, + onLoad, + onLayout, + snapToAlignment = 'start', + animationDuration = 300, + autoFocus, + trapFocusUp, + trapFocusRight, + trapFocusDown, + trapFocusLeft, + } = props; + + const parentCellBounds = useContext(CellBoundsContext); + const isInFlex = useIsInFlex(); + + // 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; + const initialScrollOffset = initialRestoredState?.scrollOffset ?? 0; + // 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, + ); + // 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); + + const [ownStateCache] = useState>(() => new Map()); + + const [measuredSize, setMeasuredSize] = useState({ w: 0, h: 0 }); + const [, setLayoutVersion] = useState(0); + const [separatorSize, setSeparatorSize] = useState(0); + const separatorSizeRef = useRef(0); + // 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); + + 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: 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 measuredOuterMain = horizontal ? measuredSize.w : measuredSize.h; + const viewportSize = + 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 measuredOuterCross = horizontal ? measuredSize.h : measuredSize.w; + + let viewportCrossSize: number; + + // 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) { + 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, + cellCrossSize, + keyExtractor, + }); + + if (initialRestoredState?.measurements) { + lm.setMeasurements(initialRestoredState.measurements); + } + + lm.setOnChange(() => { + setLayoutVersion((v) => v + 1); + }); + + return lm; + }); + + useLayoutEffect(() => { + if ( + layoutManager.updateConfig({ + data, + estimatedItemSize, + numColumns, + overrideItemLayout, + extraData, + cellCrossSize, + keyExtractor, + separatorSize, + }) + ) { + setLayoutVersion((v) => v + 1); + } + }, [ + data, + estimatedItemSize, + numColumns, + overrideItemLayout, + extraData, + cellCrossSize, + keyExtractor, + separatorSize, + ]); + + 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]; + const getLayout = (i: number) => layoutManager.getLayout(i); + + const totalContentSize = + paddingStart + headerSize + layoutManager.totalSize + footerSize + paddingEnd; + const finalCross = + 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 = () => { + layoutManager.setBatching(true); + setIsScrollAnimating(true); + }; + const handleAnimationEnd = () => { + setIsScrollAnimating(false); + + if (layoutManager.setBatching(false)) { + setLayoutVersion((v) => v + 1); + } + }; + + // 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); + } + }; + + 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, + committedScrollOffset, + scrollToOffset, + scrollToIndex, + scrollToEnd, + handleChildFocused, + resetScroll, + } = useScrollHandler({ + layoutManager: layoutManager as LayoutManager, + horizontal, + viewportSize, + itemAreaOffset, + totalContentSize, + viewportCrossSize, + totalCrossSize: finalCross, + animationDuration, + snapToAlignment, + onScroll, + onEndReached, + onEndReachedThreshold, + paddingStart, + paddingEnd, + initialScrollOffset, + onAnimationStart: handleAnimationStart, + onAnimationEnd: handleAnimationEnd, + }); + + // 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; + } + + parentStateCache.set(cellKey, { + scrollOffset: committedScrollOffset, + focusedIndex, + measurements: layoutManager.getMeasurements(), + }); + }, [cellKey, committedScrollOffset, focusedIndex, parentStateCache, layoutManager]); + + // 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, { + scrollOffset: committedScrollOffset, + focusedIndex, + measurements: layoutManager.getMeasurements(), + }); + } + + // 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, + numColumns, + overrideItemLayout, + extraData, + cellCrossSize, + keyExtractor, + separatorSize, + }); + + 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(); + } + + setSkipNextFocus(true); + setPrevCellKey(cellKey); + } + + // 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); + 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); + + 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) { + for (let i = visibleRange.startIndex; i <= visibleRange.endIndex; i++) { + visibleIndices.push(i); + } + } + + useViewability({ + viewabilityConfig, + onViewableItemsChanged, + getLayout, + getData, + getKey, + visibleIndices, + scrollOffset: scrollInItemSpace, + viewportSize, + horizontal, + }); + + const getType = (index: number): string | number => + // oxlint-disable-next-line typescript/no-non-null-assertion -- index is within data bounds + getItemType?.(data[index]!, index, extraData) ?? 0; + + 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); + // 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), + scrollToOffset: (params) => scrollToOffset(params.offset, params.animated), + scrollToEnd: (params) => scrollToEnd(params?.animated), + getScrollOffset: () => scrollOffsetRef.current, + getVisibleRange: () => visibleRange, + })); + + const loadTimeRef = useRef(Date.now()); + const hasLoadedRef = useRef(false); + const prevLayoutRef = useRef({ w: 0, h: 0 }); + const scrollToIndexRef = useRef(scrollToIndex); + + useLayoutEffect(() => { + scrollToIndexRef.current = scrollToIndex; + }, [scrollToIndex]); + + // oxlint-disable-next-line react-hooks/exhaustive-deps -- mount-only — scrollToIndex accessed via ref + useEffect(() => { + if (initialScrollIndex != null && initialScrollIndex > 0) { + const viewOffset = initialScrollIndexParams?.viewOffset ?? 0; + + scrollToIndexRef.current(initialScrollIndex, false, 0, viewOffset); + } + }, []); + + useEffect(() => { + if (!hasLoadedRef.current && data.length > 0) { + hasLoadedRef.current = true; + onLoad?.({ elapsedTimeInMs: Date.now() - loadTimeRef.current }); + } + }, [data.length, onLoad]); + + useEffect(() => { + if (!onLayout) { + return; + } + + const contentW = horizontal ? totalContentSize : viewportCrossSize; + const contentH = horizontal ? viewportCrossSize : totalContentSize; + const prev = prevLayoutRef.current; + + if (prev.w !== contentW || prev.h !== contentH) { + prevLayoutRef.current = { w: contentW, h: contentH }; + onLayout({ w: contentW, h: contentH }); + } + }, [onLayout, horizontal, totalContentSize, viewportCrossSize]); + + 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)} + + + ); + } + + const scrollPosition = -committedScrollOffset; + // 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, + h: finalCross, + ...(isScrollAnimating ? null : { x: scrollPosition }), + } + : { + w: finalCross, + h: totalContentSize, + ...(isScrollAnimating ? null : { y: scrollPosition }), + }; + + const cells = visibleIndices.map((index) => { + const item = data[index]; + + if (item == null) { + return null; + } + + // oxlint-disable-next-line typescript/no-non-null-assertion -- layout exists for all visible indices + const layout = layoutManager.getLayout(index)!; + + // Skip cells that the LayoutManager has collapsed to zero (null/undef + // data or override.size === 0). Returning null here keeps them out of + // the React tree entirely. + if (layout.size === 0) { + return null; + } + + // oxlint-disable-next-line typescript/no-non-null-assertion -- slot was just assigned for this index + const slotKey = slotAssignments.get(index)!; + const mainPos = itemAreaOffset + layout.offset; + const crossPos = paddingCross + layout.crossOffset; + const isLastItem = + numColumns > 1 + ? layout.column >= numColumns - 1 || index >= data.length - 1 + : index >= data.length - 1; + + return ( + + ); + }); + + return ( + + + + + {ListHeaderComponent && ( + + {renderListComponent(ListHeaderComponent)} + + )} + + {cells} + + {ListFooterComponent && ( + + {renderListComponent(ListFooterComponent)} + + )} + + + + + ); +} + +export const VirtualList = forwardRef(VirtualListInner) as ( + props: VirtualListProps & { ref?: Ref }, +) => ReactElement | null; + +(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 new file mode 100644 index 0000000..d9e1504 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListCell.tsx @@ -0,0 +1,247 @@ +import type { ComponentType, ReactElement } 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 { + mainOffset: number; + crossOffset: number; + size: number; + crossSize: number; + renderItem: (info: VirtualListRenderItemInfo) => ReactElement; + item: T; + index: number; + /** Stable identity from VL's keyExtractor; keys measurements and provides VLCellKeyContext to descendants. */ + userKey: string; + shouldFocus: boolean; + extraData?: unknown; + horizontal: boolean; + isLastItem: boolean; + ItemSeparatorComponent?: ComponentType | null; + /** True when a flex ancestor exists; cells wrap in FlexRoot for layout + measurement. False means pinned/silent. */ + isInFlex: boolean; + onItemSizeChange?: (userKey: string, size: number) => void; + /** Distinct from `onItemSizeChange(_, 0)` (rejected) — this is the explicit empty-row path. */ + onItemEmpty?: (userKey: string) => void; + onContentCrossLayout?: (size: number) => void; + onSeparatorLayout?: (size: number) => void; + /** Mounted offscreen for state preservation; outer FG is disabled so spatial nav skips it. */ + pooled?: boolean; +} + +const VirtualListCellInner = ({ + mainOffset, + crossOffset, + size, + crossSize, + renderItem, + item, + index, + userKey, + shouldFocus, + extraData, + horizontal, + isLastItem, + ItemSeparatorComponent, + isInFlex, + onItemSizeChange, + onItemEmpty, + onContentCrossLayout, + onSeparatorLayout, + pooled = false, +}: VirtualListCellProps): ReactElement | null => { + 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); + const cellElementRef = useRef(null); + const prevShouldFocusRef = useRef(shouldFocus); + const focusManager = useFocusManager(); + + const renderedItem = renderItem({ item, index, extraData, shouldFocus }); + const isEmpty = renderedItem == null; + + // Imperative focus claim on shouldFocus false → true. Mount-time claims + // go through FocusGroup's autoFocus → useFocus.addElement instead. + // + // 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); + } + + prevShouldFocusRef.current = shouldFocus; + }, [shouldFocus, focusManager]); + + const handleResize = (event: { w: number; h: number }) => { + const main = horizontal ? event.w : event.h; + const cross = horizontal ? event.h : event.w; + + if (main > 0) { + onItemSizeChange?.(userKey, main); + } + + if (cross > 0) { + onContentCrossLayout?.(cross); + } + }; + + // 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 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; + } + + const rafId = requestAnimationFrame(() => { + const node = flexRootRef.current?.node; + + if (!node) { + return; + } + + const main = horizontal ? node.w : node.h; + const cross = horizontal ? node.h : node.w; + + if (main > 0) { + onItemSizeChange(userKey, main); + } + + if (cross > 0) { + onContentCrossLayout?.(cross); + } + }); + + return () => { + cancelAnimationFrame(rafId); + }; + }, [userKey, isInFlex, isEmpty, horizontal]); + + 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 + // collapses to zero main-axis size in layout. + if (isEmpty) { + return null; + } + + const innerContent = ( + + {renderedItem} + + ); + + // 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} + + ) : ( + innerContent + ); + + const handleSeparatorResize = (event: { w: number; h: number }) => { + const measured = horizontal ? event.w : event.h; + + if (measured > 0) { + onSeparatorLayout?.(measured); + } + }; + + let separatorEl: ReactElement | null = null; + + if (ItemSeparatorComponent && !isLastItem) { + const SeparatorComponent = ItemSeparatorComponent; + const separatorContent = isInFlex ? ( + + + + ) : ( + + ); + + separatorEl = ( + {separatorContent} + ); + } + + return ( + + {measuredContent} + {separatorEl} + + ); +}; + +function areCellPropsEqual( + prev: VirtualListCellProps, + next: VirtualListCellProps, +): boolean { + // 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 && + 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.isLastItem === next.isLastItem && + prev.ItemSeparatorComponent === next.ItemSeparatorComponent && + prev.isInFlex === next.isInFlex && + prev.pooled === next.pooled + ); +} + +export const VirtualListCell = memo(VirtualListCellInner, areCellPropsEqual) as (( + props: VirtualListCellProps, +) => ReactElement | null) & { displayName?: string }; + +VirtualListCell.displayName = 'VirtualListCell'; 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..1a4018b --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListContext.ts @@ -0,0 +1,52 @@ +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 | null>(null); diff --git a/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts b/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts new file mode 100644 index 0000000..daece40 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/VirtualListTypes.ts @@ -0,0 +1,151 @@ +import type { ComponentType, ReactElement } from 'react'; + +import type { LightningViewElementStyle } from '@plextv/react-lightning'; + +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 { + size?: number; + span?: number; +} + +export type OverrideItemLayoutFn = ( + layout: OverrideItemLayout, + item: T, + index: number, + maxColumns: number, + extraData?: unknown, +) => void; + +export interface ContentStyle { + paddingTop?: number; + paddingBottom?: number; + paddingLeft?: number; + paddingRight?: number; + padding?: number; + paddingVertical?: number; + paddingHorizontal?: number; + backgroundColor?: number; +} + +export interface ViewToken { + item: T; + key: string; + index: number; + isViewable: boolean; + timestamp: number; +} + +export interface ViewabilityConfig { + itemVisiblePercentThreshold?: number; + viewAreaCoveragePercentThreshold?: number; + minimumViewTime?: number; + waitForInteraction?: boolean; +} + +export interface ScrollEvent { + contentInset: { top: number; left: number; bottom: number; right: number }; + contentOffset: { x: number; y: number }; + contentSize: { width: number; height: number }; + layoutMeasurement: { width: number; height: number }; +} + +export interface VirtualListProps { + /** Array of data items to render. */ + data: ReadonlyArray; + /** Render function for each item. */ + renderItem: (info: VirtualListRenderItemInfo) => ReactElement; + /** Average or median item size. Used before items are measured. Default 200. */ + estimatedItemSize?: number; + /** Scroll horizontally instead of vertically. */ + horizontal?: boolean; + /** Number of columns for grid layout. Default 1. */ + numColumns?: number; + /** Pixels to render beyond the visible viewport. Default 250. */ + drawDistance?: number; + /** Extract a stable key for an item. Falls back to index. */ + keyExtractor?: (item: T, index: number) => string; + /** Changing this forces a re-render of all items. */ + extraData?: unknown; + + /** Padding and background for the list content area. */ + contentContainerStyle?: ContentStyle; + /** Style for the outer list container. Must include w and h. */ + style?: LightningViewElementStyle; + + /** Component rendered before the first item. */ + ListHeaderComponent?: ComponentType | ReactElement | null; + /** Size of the header along the scroll axis. */ + listHeaderSize?: number; + /** Component rendered after the last item. */ + ListFooterComponent?: ComponentType | ReactElement | null; + /** Size of the footer along the scroll axis. */ + listFooterSize?: number; + /** Component rendered when data is empty. */ + ListEmptyComponent?: ComponentType | ReactElement | null; + /** Component rendered between items. */ + ItemSeparatorComponent?: ComponentType | null; + + /** Override size or span per-item. Must be fast — called frequently. */ + overrideItemLayout?: OverrideItemLayoutFn; + /** Return a type for recycling pools. Items of same type reuse views. */ + getItemType?: (item: T, index: number, extraData?: unknown) => string | number; + + /** Scroll to this index on mount. */ + initialScrollIndex?: number; + /** Additional params for initialScrollIndex. */ + initialScrollIndexParams?: { viewOffset?: number }; + /** Called when the user scrolls near the end. */ + onEndReached?: () => void; + /** How close to the end (in viewport fractions) triggers onEndReached. Default 0.5. */ + onEndReachedThreshold?: number; + /** Called on every scroll position change. */ + onScroll?: (event: ScrollEvent) => void; + /** Called when viewable items change. */ + onViewableItemsChanged?: (info: { + viewableItems: ViewToken[]; + changed: ViewToken[]; + }) => void; + /** Configuration for viewability tracking. */ + viewabilityConfig?: ViewabilityConfig; + /** Called once when the list first renders items. */ + onLoad?: (info: { elapsedTimeInMs: number }) => void; + + /** Called when the content dimensions change (e.g. items measured, data changed). */ + onLayout?: (rect: { w: number; h: number }) => void; + + /** Snap scroll alignment when focusing items. Default 'start'. */ + snapToAlignment?: 'start' | 'center' | 'end'; + /** Duration of scroll animations in ms. Default 300. */ + animationDuration?: number; + + /** Auto-focus the first focusable child on mount. */ + autoFocus?: boolean; + trapFocusUp?: boolean; + trapFocusRight?: boolean; + trapFocusDown?: boolean; + trapFocusLeft?: boolean; +} + +export interface VirtualListRef { + scrollToIndex: (params: { + index: number; + animated?: boolean; + viewPosition?: number; + viewOffset?: number; + }) => void; + scrollToOffset: (params: { offset: number; animated?: boolean }) => void; + scrollToEnd: (params?: { animated?: boolean }) => void; + getScrollOffset: () => number; + getVisibleRange: () => { startIndex: number; endIndex: number }; +} diff --git a/packages/react-lightning-components/src/components/VirtualList/index.ts b/packages/react-lightning-components/src/components/VirtualList/index.ts new file mode 100644 index 0000000..cb0e1d2 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/index.ts @@ -0,0 +1,12 @@ +export { VirtualList } from './VirtualList'; +export type { + ContentStyle, + OverrideItemLayout, + OverrideItemLayoutFn, + ScrollEvent, + ViewabilityConfig, + ViewToken, + VirtualListProps, + VirtualListRef, + VirtualListRenderItemInfo, +} from './VirtualListTypes'; diff --git a/packages/react-lightning-components/src/components/VirtualList/parseContentStyle.ts b/packages/react-lightning-components/src/components/VirtualList/parseContentStyle.ts new file mode 100644 index 0000000..914b9eb --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/parseContentStyle.ts @@ -0,0 +1,27 @@ +import type { ContentStyle } from './VirtualListTypes'; + +export interface ParsedContentStyle { + top: number; + right: number; + bottom: number; + left: number; + backgroundColor?: number; +} + +export function parseContentStyle(style?: ContentStyle): ParsedContentStyle { + if (!style) { + return { top: 0, right: 0, bottom: 0, left: 0 }; + } + + const base = style.padding ?? 0; + const vertical = style.paddingVertical ?? base; + const horizontal = style.paddingHorizontal ?? base; + + return { + top: style.paddingTop ?? vertical, + right: style.paddingRight ?? horizontal, + bottom: style.paddingBottom ?? vertical, + left: style.paddingLeft ?? horizontal, + backgroundColor: style.backgroundColor, + }; +} diff --git a/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts new file mode 100644 index 0000000..abe6a9f --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/useScrollHandler.ts @@ -0,0 +1,283 @@ +import { type RefObject, useEffect, useRef, useState } from 'react'; + +import type { LightningElement } from '@plextv/react-lightning'; + +import type { LayoutManager } from './LayoutManager'; +import type { ScrollEvent } from './VirtualListTypes'; + +export interface UseScrollHandlerOptions { + layoutManager: LayoutManager; + horizontal: boolean; + viewportSize: 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). */ + totalContentSize: number; + /** Cross-axis viewport size (height for horizontal, width for vertical). */ + viewportCrossSize: number; + /** Cross-axis content size. */ + totalCrossSize: number; + animationDuration: number; + snapToAlignment: 'start' | 'center' | 'end'; + onScroll?: (event: ScrollEvent) => void; + onEndReached?: () => void; + onEndReachedThreshold: number; + /** Main-axis start padding (acts as scroll margin). */ + paddingStart: number; + /** Main-axis end padding (acts as scroll margin). */ + paddingEnd: number; + initialScrollOffset?: number; + /** Fires once when an animated scroll begins; VL flips LM into batching. */ + onAnimationStart?: () => void; + /** Fires once when the in-flight animation ends or `resetScroll` cancels it; VL drains the batch. */ + onAnimationEnd?: () => void; +} + +export interface UseScrollHandlerResult { + contentRef: RefObject; + /** Live scroll offset — updated on every scroll, including mid-animation. */ + scrollOffsetRef: RefObject; + /** Last scroll offset at which the visible range changed — safe to read during render. */ + committedScrollOffset: number; + maxScroll: number; + scrollToOffset: (offset: number, animated?: boolean) => void; + scrollToIndex: ( + index: number, + animated?: boolean, + viewPosition?: number, + viewOffset?: number, + ) => void; + scrollToEnd: (animated?: boolean) => void; + handleChildFocused: (child: LightningElement) => void; + /** Jump to an offset with no animation or onScroll/onEndReached side-effects (cellKey-restore path). */ + resetScroll: (offset: number) => void; +} + +export function useScrollHandler(options: UseScrollHandlerOptions): UseScrollHandlerResult { + const { + layoutManager, + horizontal, + viewportSize, + itemAreaOffset, + totalContentSize, + viewportCrossSize, + totalCrossSize, + animationDuration, + snapToAlignment, + onScroll, + onEndReached, + onEndReachedThreshold, + paddingStart, + paddingEnd, + initialScrollOffset = 0, + onAnimationStart, + onAnimationEnd, + } = options; + + const contentRef = useRef(null); + const scrollOffsetRef = useRef(initialScrollOffset); + const endReachedRef = useRef(false); + const animationIdRef = useRef(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); + + // 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) { + const value = -initialScrollOffset; + + if (horizontal) { + contentRef.current.node.x = value; + } else { + contentRef.current.node.y = value; + } + } + }, []); + + const maxScroll = Math.max(0, totalContentSize - viewportSize); + + function clamp(value: number): number { + return Math.max(0, Math.min(value, maxScroll)); + } + + function applyPosition(offset: number, animated: boolean): void { + const el = contentRef.current; + + if (!el) { + return; + } + + const value = -offset; + + if (animated && animationDuration > 0) { + 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' }); + + anim.once('stopped', () => { + if (animationIdRef.current !== thisAnimId) { + return; + } + + // Pin the position so reconciliation doesn't reset it + if (horizontal) { + el.node.x = value; + } else { + el.node.y = value; + } + + isAnimatingRef.current = false; + onAnimationEnd?.(); + }); + + anim.start(); + } else { + if (horizontal) { + el.node.x = value; + } else { + el.node.y = value; + } + } + } + + function scrollToOffset(offset: number, animated = true): void { + const clamped = clamp(offset); + + scrollOffsetRef.current = clamped; + + applyPosition(clamped, animated); + // 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) { + const distFromEnd = totalContentSize - clamped - viewportSize; + + if (distFromEnd <= viewportSize * onEndReachedThreshold) { + if (!endReachedRef.current) { + endReachedRef.current = true; + onEndReached(); + } + } else { + endReachedRef.current = false; + } + } + + onScroll?.({ + contentInset: { top: 0, left: 0, bottom: 0, right: 0 }, + contentOffset: horizontal ? { x: clamped, y: 0 } : { x: 0, y: clamped }, + contentSize: horizontal + ? { width: totalContentSize, height: totalCrossSize } + : { width: totalCrossSize, height: totalContentSize }, + layoutMeasurement: horizontal + ? { width: viewportSize, height: viewportCrossSize } + : { width: viewportCrossSize, height: viewportSize }, + }); + } + + function scrollToIndex(index: number, animated = true, viewPosition = 0, viewOffset = 0): void { + const layout = layoutManager.getLayout(index); + + if (!layout) { + return; + } + + const absOffset = itemAreaOffset + layout.offset; + const target = absOffset - viewPosition * (viewportSize - layout.size) + viewOffset; + + scrollToOffset(target, animated); + } + + function scrollToEnd(animated = true): void { + scrollToOffset(maxScroll, animated); + } + + function handleChildFocused(child: LightningElement): void { + const el = contentRef.current; + + if (!el) { + return; + } + + const pos = child.getRelativePosition(el); + const childOffset = horizontal ? pos.x : pos.y; + const childSize = horizontal ? child.node.w : child.node.h; + + let target: number; + + switch (snapToAlignment) { + case 'center': + target = childOffset + childSize / 2 - viewportSize / 2; + break; + case 'end': + target = childOffset + childSize - viewportSize + paddingEnd; + break; + default: + target = childOffset - paddingStart; + break; + } + + // Snap to edges to keep header/footer visible when near them + const footerAreaSize = totalContentSize - itemAreaOffset - layoutManager.totalSize; + + if (target > 0 && target <= itemAreaOffset) { + target = 0; + } else if (target < maxScroll && target >= maxScroll - footerAreaSize) { + target = maxScroll; + } + + 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, + committedScrollOffset, + maxScroll, + scrollToOffset, + scrollToIndex, + scrollToEnd, + handleChildFocused, + resetScroll, + }; +} diff --git a/packages/react-lightning-components/src/components/VirtualList/useViewability.ts b/packages/react-lightning-components/src/components/VirtualList/useViewability.ts new file mode 100644 index 0000000..0a25a66 --- /dev/null +++ b/packages/react-lightning-components/src/components/VirtualList/useViewability.ts @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; + +import type { ComputedLayout } from './LayoutManager'; +import { ViewabilityTracker } from './ViewabilityTracker'; +import type { ViewabilityConfig, ViewToken } from './VirtualListTypes'; + +export interface UseViewabilityOptions { + viewabilityConfig?: ViewabilityConfig; + onViewableItemsChanged?: (info: { + viewableItems: ViewToken[]; + changed: ViewToken[]; + }) => void; + getLayout: (index: number) => ComputedLayout | undefined; + getData: (index: number) => T | undefined; + getKey: (index: number) => string; + visibleIndices: number[]; + scrollOffset: number; + viewportSize: number; + horizontal: boolean; +} + +export function useViewability(options: UseViewabilityOptions): void { + const { + viewabilityConfig, + onViewableItemsChanged, + getLayout, + getData, + getKey, + visibleIndices, + scrollOffset, + viewportSize, + horizontal, + } = options; + + const [tracker] = useState( + () => + new ViewabilityTracker({ + viewabilityConfig, + onViewableItemsChanged, + getLayout, + getData, + getKey, + }), + ); + + useEffect(() => { + tracker.updateConfig({ + viewabilityConfig, + onViewableItemsChanged, + getLayout, + getData, + getKey, + }); + }, [viewabilityConfig, onViewableItemsChanged, getLayout, getData, getKey]); + + useEffect(() => { + tracker.update(visibleIndices, scrollOffset, viewportSize, horizontal); + }, [visibleIndices, scrollOffset, viewportSize, horizontal]); + + useEffect(() => { + return () => { + tracker.dispose(); + }; + }, []); +} 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 new file mode 100644 index 0000000..6a06546 --- /dev/null +++ b/packages/react-lightning-components/src/exports/lists/VirtualList.tsx @@ -0,0 +1,12 @@ +export type { + ContentStyle, + OverrideItemLayout, + OverrideItemLayoutFn, + ScrollEvent, + ViewabilityConfig, + ViewToken, + VirtualListProps, + VirtualListRef, + VirtualListRenderItemInfo, +} from '../../components/VirtualList'; +export { VirtualList as default } from '../../components/VirtualList'; diff --git a/packages/react-lightning-components/src/exports/text/StyledText.tsx b/packages/react-lightning-components/src/exports/text/StyledText.tsx index 381654a..c6588a4 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,19 @@ 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 +171,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/src/index.ts b/packages/react-lightning-components/src/index.ts index 4eb31e4..1b42e6e 100644 --- a/packages/react-lightning-components/src/index.ts +++ b/packages/react-lightning-components/src/index.ts @@ -1,4 +1,10 @@ export { default as Column } from './exports/layout/Column'; export { default as Row } from './exports/layout/Row'; +export { + default as VirtualList, + type VirtualListProps, + type VirtualListRef, + type VirtualListRenderItemInfo, +} from './exports/lists/VirtualList'; export { default as StyledText } from './exports/text/StyledText'; export { default as FPSMonitor } from './exports/util/FPSMonitor'; 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..50bf858 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,16 +24,16 @@ "./jsx": "./src/types/jsx.d.ts" }, "publishConfig": { - "provenance": true, "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" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -39,23 +42,24 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "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:" }, "volta": { "extends": "../../package.json" + }, + "inlinedDependencies": { + "type-fest": "5.5.0" } } 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..9d617dc 100644 --- a/packages/react-lightning/src/element/LightningViewElement.ts +++ b/packages/react-lightning/src/element/LightningViewElement.ts @@ -14,6 +14,8 @@ import type { } from '@lightningjs/renderer'; 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, @@ -60,10 +62,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 +70,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 +95,14 @@ 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; + private _resizeObserver: NodeResizeObserver | null = null; + private _isObservingResize = false; public get visible(): boolean { return this._visible; @@ -113,6 +112,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 +173,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; } @@ -207,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; } @@ -226,6 +243,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 +271,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 +295,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)); @@ -293,10 +314,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); @@ -321,21 +351,36 @@ 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); + + 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); @@ -354,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; } @@ -364,17 +410,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); @@ -394,10 +435,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; } @@ -433,6 +482,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) { @@ -559,12 +609,22 @@ 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; - 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 +654,7 @@ export class LightningViewElement< ).start(); } - public animateShader( - props: Partial, - ): IAnimationController { + public animateShader(props: Partial): IAnimationController { return this._createAnimation( { shaderProps: props, @@ -606,7 +664,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 = () => { @@ -615,11 +673,28 @@ 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 { + private _createNode({ data: _data, ...props }: Partial): RendererNode { const node = this.isTextElement ? this._renderer.createTextNode(props) : this._renderer.createNode(props); @@ -649,8 +724,14 @@ 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; + const previousOnResize = this.props.onResize; let changed = false; @@ -661,6 +742,10 @@ export class LightningViewElement< } } + if (previousOnResize !== this.props.onResize) { + this._reconcileResizeObserving(); + } + const lngProps = this._toLightningNodeProps({ ...this.props, ...transformedProps, @@ -671,24 +756,43 @@ 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; @@ -696,6 +800,109 @@ export class LightningViewElement< 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; + } + /** * This method is intended to handle changes that are important before * the loaded event is emitted to plugins and external channels. @@ -736,9 +943,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 +952,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 +961,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 { @@ -790,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; } @@ -807,7 +1002,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 +1022,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 +1037,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; @@ -848,17 +1045,20 @@ 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; } } else { - // biome-ignore lint/suspicious/noExplicitAny: TODO + // oxlint-disable-next-line typescript/no-explicit-any -- TODO (finalStyle as any)[key] = value; } } @@ -886,10 +1086,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 +1107,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..429218d 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,13 +73,15 @@ 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) { this._focusManager.focus(closestElement as T); + return false; } diff --git a/packages/react-lightning/src/focus/FocusManager.spec.ts b/packages/react-lightning/src/focus/FocusManager.spec.ts index 20e0772..f27c8b0 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,86 @@ 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('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)', () => { diff --git a/packages/react-lightning/src/focus/FocusManager.ts b/packages/react-lightning/src/focus/FocusManager.ts index f0b388c..1502ba1 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 { @@ -305,12 +292,50 @@ 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; - // 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 +370,18 @@ 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 +453,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 +470,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 +490,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 +502,7 @@ export class FocusManager< destinations, traps, hasFocusableChildren: false, + allowOffscreen, }; this.activeLayer.elements.set(element, node); @@ -491,21 +519,35 @@ 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); + } } }), ]); @@ -515,6 +557,7 @@ export class FocusManager< const { element } = node; const disposers = this._disposers.get(element); + if (disposers) { for (const dispose of disposers) { dispose(); @@ -524,42 +567,49 @@ 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; } - this._focusNode(focusNode); + visited.add(destination); + + 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 +621,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); @@ -615,6 +663,7 @@ export class FocusManager< if (childrenLength === 0) { parentNode.hasFocusableChildren = false; + return; } @@ -622,7 +671,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 +692,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 +757,55 @@ 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; + } + + // 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; + } - for (let i = layer.focusPath.length - 1; i >= divergenceIndex; i--) { - const removedFocus = layer.focusPath[i]; + // 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 +813,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..3156674 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,6 +61,7 @@ export function useFocus( autoFocus, focusRedirect, destinations, + allowOffscreen, }); } @@ -97,6 +96,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..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'; @@ -12,12 +13,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/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/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..e1d0d71 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( @@ -216,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/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..37d7ff3 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,23 @@ 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 +72,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..c273430 100644 --- a/packages/react-lightning/src/shim/resizeObserverShim.ts +++ b/packages/react-lightning/src/shim/resizeObserverShim.ts @@ -11,15 +11,14 @@ 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); + return; } + super.observe(target, options); } @@ -27,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); } @@ -39,37 +40,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..c90523b 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'; @@ -21,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; @@ -30,6 +32,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..793f3a1 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'; @@ -19,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; @@ -35,9 +32,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 +44,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..c652195 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>> = {}; @@ -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 */ }; @@ -73,10 +74,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..baa6454 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,110 @@ 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 }, + ]); + + 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(target, { focusable: false, focusableIntent: true }); + + const closest = findClosestElement( + source, + 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 }, + ]); + + 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(target, { focusable: false, focusableIntent: true }); + + const closest = findClosestElement( + source, + 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 }, + ]); + + 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(target, { focusableIntent: true }); + + const closest = findClosestElement( + source, + 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 }, + ]); + + 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(target, { focusable: false, focusableIntent: false }); + + const closest = findClosestElement( + source, + 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..247af1d 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,39 @@ 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 +193,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..1fd0145 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,41 @@ 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 +120,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 +128,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 +153,17 @@ 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/package.json b/packages/react-native-lightning-components/package.json index 2860eb7..033d471 100644 --- a/packages/react-native-lightning-components/package.json +++ b/packages/react-native-lightning-components/package.json @@ -1,16 +1,19 @@ { "name": "@plextv/react-native-lightning-components", - "description": "React components for react-native-lightning", "version": "0.4.0", - "author": "Plex Inc.", + "description": "React components for 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", @@ -19,36 +22,26 @@ ".": "./src/index.ts", "./layout/Column": "./src/exports/layout/Column.tsx", "./layout/Row": "./src/exports/layout/Row.tsx", - "./lists/CellContainer": "./src/exports/lists/CellContainer.tsx", - "./lists/FlashList": "./src/exports/lists/FlashList.tsx", "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "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" - }, - "./lists/CellContainer": { - "require": "./dist/exports/lists/CellContainer.cjs", - "import": "./dist/exports/lists/CellContainer.js" - }, - "./lists/FlashList": { - "require": "./dist/exports/lists/FlashList.cjs", - "import": "./dist/exports/lists/FlashList.js" + "import": "./dist/exports/layout/Row.js", + "require": "./dist/exports/layout/Row.cjs" }, "./package.json": "./package.json" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -57,27 +50,17 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "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:^", - "@shopify/flash-list": "^2.2.0", - "react": "^19.2.3", - "react-native": "^0.82.1" - }, - "peerDependenciesMeta": { - "@shopify/flash-list": { - "optional": true - } + "react": "catalog:", + "react-native": "catalog:" }, "volta": { "extends": "../../package.json" 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/src/exports/lists/CellContainer.tsx b/packages/react-native-lightning-components/src/exports/lists/CellContainer.tsx deleted file mode 100644 index ac011db..0000000 --- a/packages/react-native-lightning-components/src/exports/lists/CellContainer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { LightningElement, Rect } from '@plextv/react-lightning'; -import { convertCSSStyleToLightning } from '@plextv/react-lightning-plugin-css-transform'; -import { - createLayoutEvent, - type ViewProps, -} from '@plextv/react-native-lightning'; -import { - type ForwardRefExoticComponent, - forwardRef, - useCallback, - useMemo, -} from 'react'; - -type CellContainerProps = ViewProps & { - estimatedSize?: number; -}; - -const CellContainer: ForwardRefExoticComponent = forwardRef< - LightningElement, - CellContainerProps ->(({ style, estimatedSize, onLayout, ...props }, forwardedRef) => { - const lngStyle = useMemo(() => convertCSSStyleToLightning(style), [style]); - - const handleOnLayout = useCallback( - (rect: Rect) => { - onLayout?.(createLayoutEvent(rect)); - }, - [onLayout], - ); - - // We need to not set overflow: 'hidden' on the cell view, otherwise the - // FlashList will not render the items correctly. - return ( - - ); -}); - -CellContainer.displayName = 'LightningCellContainer'; - -export default CellContainer; diff --git a/packages/react-native-lightning-components/src/exports/lists/FlashList.tsx b/packages/react-native-lightning-components/src/exports/lists/FlashList.tsx deleted file mode 100644 index df3e8c8..0000000 --- a/packages/react-native-lightning-components/src/exports/lists/FlashList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import type { LightningElement } from '@plextv/react-lightning'; -import { ScrollView } from '@plextv/react-native-lightning'; -import { - type FlashListRef, - FlashList as ShopifyFlashList, - type FlashListProps as ShopifyFlashListProps, -} from '@shopify/flash-list'; -import { forwardRef, type ReactElement, type Ref, useMemo } from 'react'; -import CellContainer from './CellContainer'; - -type FlashList = FlashListRef; -type FlashListProps = ShopifyFlashListProps & { - estimatedItemSize?: number; - estimatedListSize?: { width: number; height: number }; -}; - -function FlashListImpl( - { CellRendererComponent, renderScrollComponent, ...props }: FlashListProps, - ref: Ref>, -) { - const CellComponent = useMemo( - () => - forwardRef((componentProps, cellRef) => { - const Component = CellRendererComponent ?? CellContainer; - - return ( - } - estimatedSize={props.estimatedItemSize} - /> - ); - }), - - [CellRendererComponent, props.estimatedItemSize], - ); - - return ( - ) - } - {...props} - /> - ); -} - -const FlashList = forwardRef(FlashListImpl) as ( - props: FlashListProps & { ref: Ref> }, -) => ReactElement; - -FlashListImpl.displayName = 'LightningFlashList'; - -export type { FlashListProps }; - -export default FlashList; diff --git a/packages/react-native-lightning-components/src/index.ts b/packages/react-native-lightning-components/src/index.ts index f141f05..0829fe5 100644 --- a/packages/react-native-lightning-components/src/index.ts +++ b/packages/react-native-lightning-components/src/index.ts @@ -1,4 +1,2 @@ export { default as Column } from './exports/layout/Column'; export { default as Row } from './exports/layout/Row'; -export { default as CellContainer } from './exports/lists/CellContainer'; -export { default as FlashList } from './exports/lists/FlashList'; 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 e2b12e9..9f3e97e 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,15 +23,15 @@ "./package.json": "./package.json" }, "publishConfig": { - "provenance": true, "access": "public", "exports": { ".": { - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./package.json": "./package.json" - } + }, + "provenance": true }, "scripts": { "build": "tsdown --config-loader unrun", @@ -37,33 +40,30 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { - "@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/node": "25.6.0", + "@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" - }, - "peerDependencyRules": { - "allowedVersions": { - "react": "^19" - } + "@plextv/react-lightning-components": "workspace:^", + "@plextv/react-lightning-plugin-css-transform": "workspace:^", + "@plextv/react-lightning-plugin-flexbox": "workspace:^", + "react": "catalog:", + "react-native": "catalog:" }, "volta": { "extends": "../../package.json" + }, + "inlinedDependencies": { + "tseep": "1.3.1", + "type-fest": "5.5.0" } } 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/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/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..5bcda76 100644 --- a/packages/react-native-lightning/src/exports/Pressable.tsx +++ b/packages/react-native-lightning/src/exports/Pressable.tsx @@ -1,39 +1,27 @@ -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 true; - }, - [...dependencies, handler], - ); + return false; + } + + return true; + }; } export const Pressable: ForwardRefExoticComponent = focusable< @@ -62,35 +50,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..e3e8b0a 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), ); } @@ -17,11 +15,12 @@ export function compose(style1: T, style2: T): T | NonNullable[] { if (style1 && style2) { return [style1, style2]; } + 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/exports/VirtualizedList.tsx b/packages/react-native-lightning/src/exports/VirtualizedList.tsx index 15208a7..270c2d1 100644 --- a/packages/react-native-lightning/src/exports/VirtualizedList.tsx +++ b/packages/react-native-lightning/src/exports/VirtualizedList.tsx @@ -1,34 +1,8 @@ -import { type FC, forwardRef, type Ref } from 'react'; -import { - VirtualizedList as RNVirtualizedList, - type VirtualizedListProps as RNVirtualizedListProps, -} from 'react-native'; -import { ScrollView } from './ScrollView'; +import type { VirtualListRef } from '@plextv/react-lightning-components/lists/VirtualList'; +import VirtualList from '@plextv/react-lightning-components/lists/VirtualList'; +import { type VirtualListProps } from '@plextv/react-lightning-components/lists/VirtualList'; -export type VirtualizedListProps = RNVirtualizedListProps; -export type VirtualizedList = RNVirtualizedList; +export type VirtualizedListProps = VirtualListProps; +export type VirtualizedList = VirtualListRef; -interface ForwardRef extends FC> { - ( - props: VirtualizedListProps & { ref: Ref> }, - ): ReturnType>>; -} - -export const VirtualizedList: ForwardRef = forwardRef( - ( - { renderScrollComponent, ...props }: VirtualizedListProps, - ref: Ref>, - ) => ( - ) - } - /> - ), -); - -VirtualizedList.displayName = 'LightningVirtualizedList'; +export const VirtualizedList: typeof VirtualList = VirtualList; 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..d454adf 100644 --- a/packages/react-native-lightning/src/index.ts +++ b/packages/react-native-lightning/src/index.ts @@ -8,26 +8,18 @@ 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'; +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'; 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 +38,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..de9de31 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 { @@ -113,6 +101,7 @@ export const reactNativePolyfillsPlugin = (): Plugin => { console.warn( '[react-native-lightning polyfills] FocusManager not found, cannot set destinations', ); + return; } } @@ -127,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/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..9a4c25f 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,15 +35,15 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "dependencies": { "@lightningjs/msdf-generator": "1.2.0", "crc-32": "1.2.2", - "glob": "13.0.0" + "glob": "13.0.6" }, "devDependencies": { "@repo/configs": "workspace:*" + }, + "peerDependencies": { + "vite": "catalog:" } } 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..0ed0b95 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,17 +35,17 @@ 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.'); } } 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 b6a0dc7..aa1d108 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'; @@ -22,15 +24,11 @@ export default async function generateFonts( if (files.length === 0) { console.log('No font files found'); + 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..b232a4e 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( @@ -18,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/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..66321d4 100644 --- a/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts +++ b/packages/vite-plugin-msdf-fontgen/src/sortByExtension.ts @@ -1,9 +1,8 @@ -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; + return extensions.indexOf(extA) - extensions.indexOf(extB); }; } 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..aa7ea31 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,16 +37,13 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], - "dependencies": { - "@vitejs/plugin-react": "5.1.2" - }, "devDependencies": { "@repo/configs": "workspace:*" }, "peerDependencies": { - "@plextv/react-native-lightning": "workspace:^" + "@plextv/react-native-lightning": "workspace:^", + "@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 9433902..3dccf2d 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,14 +38,12 @@ "check:types": "tsc --noEmit", "test:unit": "vitest run --passWithNoTests" }, - "files": [ - "dist" - ], "devDependencies": { "@repo/configs": "workspace:*" }, "peerDependencies": { "@plextv/react-lightning-plugin-reanimated": "workspace:^", - "react-native-reanimated": "^4.2.1" + "react-native-reanimated": "catalog:", + "vite": "catalog:" } } 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 ae04625..0a96958 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,22 +4,80 @@ settings: autoInstallPeers: true 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 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3 + '@types/react-reconciler': + specifier: 0.33.0 + version: 0.33.0 + '@vitejs/plugin-legacy': + specifier: 8.0.1 + version: 8.0.1 + '@vitejs/plugin-react': + specifier: ^6.0.0 + version: 6.0.1 + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 + react: + specifier: ^19.2.0 + 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 + tseep: + specifier: 1.3.1 + version: 1.3.1 + type-fest: + specifier: 5.5.0 + version: 5.5.0 + vite: + specifier: ^8.0.0 + version: 8.0.8 + 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) + 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 @@ -27,47 +85,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.45.0 + version: 0.45.0 + oxlint: + specifier: 1.60.0 + version: 1.60.0(oxlint-tsgolint@0.20.0) + oxlint-tsgolint: + 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: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: catalog:apps + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -81,17 +148,17 @@ importers: specifier: workspace:* version: link:../../packages/plugin-flexbox react: - specifier: 19.2.3 - version: 19.2.3 + specifier: catalog:apps + version: 19.2.5 react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) + specifier: catalog:apps + 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:* @@ -99,33 +166,39 @@ 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: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.8) + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.14) '@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)) + specifier: 'catalog:' + 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: 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)) - 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: '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)) + babel-plugin-react-compiler: + specifier: 'catalog:' + version: 1.0.0 apps/react-native-lightning-example: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: catalog:apps + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning '@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 @@ -139,20 +212,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.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: 19.2.3 - version: 19.2.3 + specifier: catalog:apps + version: 19.2.5 react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) + specifier: catalog:apps + version: 19.2.5(react@19.2.5) react-native: - specifier: 0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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: 4.2.1 - 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) + 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.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:* @@ -166,33 +242,42 @@ 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) + '@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: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.8) + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.14) '@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)) + specifier: 'catalog:' + 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(@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 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.2 + version: 6.0.2 apps/storybook: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: catalog:apps + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning '@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 @@ -209,29 +294,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))) - '@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)) + 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: 19.2.3 - version: 19.2.3 + specifier: catalog:apps + version: 19.2.5 + react-dom: + specifier: catalog:apps + version: 19.2.5(react@19.2.5) react-native: - specifier: 0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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: 4.2.1 - 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) + 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 + 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:* @@ -245,17 +333,33 @@ 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: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 + '@vitejs/plugin-react': + specifier: 'catalog:' + 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 - packages/configs: {} + packages/configs: + devDependencies: + '@rollup/plugin-babel': + specifier: 7.0.0 + 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 packages/plugin-css-transform: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: 'catalog:' + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -263,15 +367,15 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react-native: - specifier: ^0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 csstype: specifier: 3.2.3 version: 3.2.3 @@ -282,10 +386,10 @@ importers: specifier: workspace:^ version: link:../react-lightning react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: 'catalog:' + version: 19.2.5 tseep: - specifier: 1.3.1 + specifier: 'catalog:' version: 1.3.1 yoga-layout: specifier: 3.2.1 @@ -294,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 @@ -307,12 +414,15 @@ importers: '@repo/configs': specifier: workspace:* version: link:../configs + type-fest: + specifier: 'catalog:' + version: 5.5.0 packages/plugin-reanimated: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: 'catalog:' + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -326,46 +436,52 @@ importers: specifier: workspace:^ version: link:../react-native-lightning react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: 'catalog:' + version: 19.2.5 react-native: - specifier: ^0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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: ^4.2.1 - 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) + 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': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 + type-fest: + specifier: 'catalog:' + version: 5.5.0 packages/react-lightning: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: 'catalog:' + version: 3.0.1 react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: 'catalog:' + 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: 1.3.1 + specifier: 'catalog:' version: 1.3.1 devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 '@types/react-reconciler': - specifier: 0.32.3 - version: 0.32.3(@types/react@19.2.8) + specifier: 'catalog:' + version: 0.33.0(@types/react@19.2.14) + type-fest: + specifier: 'catalog:' + version: 5.5.0 packages/react-lightning-components: dependencies: @@ -376,55 +492,58 @@ importers: specifier: workspace:^ version: link:../plugin-flexbox react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: 'catalog:' + version: 19.2.5 devDependencies: '@repo/configs': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 packages/react-native-lightning: dependencies: '@lightningjs/renderer': - specifier: 3.0.0-beta20 - version: 3.0.0-beta20 + specifier: 'catalog:' + version: 3.0.1 '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning + '@plextv/react-lightning-components': + 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) + version: 2.0.0(@types/react@19.2.14)(react@19.2.5) react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: 'catalog:' + version: 19.2.5 react-native: - specifier: ^0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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 - 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: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 '@types/react-reconciler': - specifier: 0.32.3 - version: 0.32.3(@types/react@19.2.8) + specifier: 'catalog:' + version: 0.33.0(@types/react@19.2.14) packages/react-native-lightning-components: dependencies: @@ -434,31 +553,25 @@ 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 - '@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 + specifier: 'catalog:' + version: 19.2.5 react-native: - specifier: ^0.82.1 - version: 0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3) + 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': specifier: workspace:* version: link:../configs '@types/react': - specifier: 19.2.8 - version: 19.2.8 + specifier: 'catalog:' + version: 19.2.14 packages/vite-plugin-msdf-fontgen: dependencies: @@ -469,8 +582,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:' + 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:* @@ -481,9 +597,15 @@ 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: 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)) + 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:' + 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:* @@ -495,8 +617,11 @@ importers: specifier: workspace:^ version: link:../plugin-reanimated react-native-reanimated: - specifier: ^4.2.1 - 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) + 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:' + 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:* @@ -511,18 +636,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'} @@ -543,8 +688,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 @@ -594,10 +739,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'} @@ -615,6 +768,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'} @@ -645,29 +808,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 @@ -684,64 +849,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'} @@ -760,8 +883,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 @@ -784,12 +907,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'} @@ -802,12 +919,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'} @@ -838,8 +949,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 @@ -868,6 +979,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'} @@ -916,8 +1033,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 @@ -928,8 +1045,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 @@ -940,12 +1057,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'} @@ -976,12 +1087,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'} @@ -1012,6 +1117,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'} @@ -1024,8 +1135,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 @@ -1042,6 +1159,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'} @@ -1102,8 +1225,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 @@ -1131,65 +1254,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'} - '@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] + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} - '@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] + '@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.0.14': - resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} + '@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==} @@ -1197,12 +1279,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==} @@ -1210,8 +1292,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==} @@ -1222,14 +1304,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==} @@ -1243,20 +1325,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/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - - '@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==} @@ -1264,300 +1340,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'} @@ -1573,49 +1499,13 @@ 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/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jest/types@29.6.3': resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} @@ -1733,11 +1623,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 @@ -1765,8 +1655,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': @@ -1781,8 +1671,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==} @@ -1796,15 +1689,266 @@ 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.45.0': + resolution: {integrity: sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.20.0': + resolution: {integrity: sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==} + cpu: [arm64] + os: [darwin] + + '@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.20.0': + resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==} + cpu: [arm64] + os: [linux] + + '@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.20.0': + resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.20.0': + resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==} + cpu: [x64] + os: [win32] + + '@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.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.60.0': + resolution: {integrity: sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.60.0': + resolution: {integrity: sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} @@ -1821,72 +1965,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.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/debugger-shell@0.82.1': - resolution: {integrity: sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw==} - 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/dev-middleware@0.82.1': - resolution: {integrity: sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w==} - 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/gradle-plugin@0.82.1': - resolution: {integrity: sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q==} - 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/js-polyfills@0.82.1': - resolution: {integrity: sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA==} - engines: {node: '>= 20.19.4'} + '@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: '*' @@ -1894,168 +2058,130 @@ 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==} + '@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-android-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ==} + '@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: [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==} - 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==} - 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==} + '@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-arm64-gnu@1.0.0-beta.59': - resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==} + '@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-gnu@1.0.0-beta.60': - resolution: {integrity: sha512-sqI+Vdx1gmXJMsXN3Fsewm3wlt7RHvRs1uysSp//NLsCoh9ZFEUr4ZzGhWKOg6Rvf+njNu/vCsz96x7wssLejQ==} + '@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-arm64-musl@1.0.0-beta.59': - resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} 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==} - 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==} - 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==} + '@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-openharmony-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} - 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==} + '@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-arm64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-JBOm8/DC/CKnHyMHoJFdvzVHxUixid4dGkiTqGflxOxO43uSJMpl77pSPXvzwZ/VXwqblU2V0/PanyCBcRLowQ==} - 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==} + '@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/binding-win32-x64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-MKF0B823Efp+Ot8KsbwIuGhKH58pf+2rSM6VcqyNMlNBHheOM0Gf7JmEu+toc1jgN6fqjH7Et+8hAzsLVkIGfA==} - engines: {node: ^20.19.0 || >=22.12.0} - 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-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-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - '@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==} @@ -2191,13 +2317,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==} @@ -2205,73 +2324,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: @@ -2294,6 +2417,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==} @@ -2324,9 +2477,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==} @@ -2336,6 +2486,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==} @@ -2348,8 +2501,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==} @@ -2364,61 +2517,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/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/mocker@4.0.17': - resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} - 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 @@ -2428,26 +2574,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==} @@ -2464,12 +2610,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: @@ -2520,11 +2669,7 @@ packages: engines: {node: '>=14'} 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'} + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} arabic-persian-reshaper@1.0.1: resolution: {integrity: sha512-VYBjkhz6o4W1Xt4mD2LAReljJpLSw5CUZMqSBDIQRvFgUSlTKEYghapgBWvkeMWF4W+KF3Fm+/z8EywJU4PBeg==} @@ -2561,17 +2706,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==} @@ -2579,22 +2721,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 @@ -2603,28 +2731,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-syntax-hermes-parser@0.32.0: - resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.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-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + babel-plugin-syntax-hermes-parser@0.33.3: + resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==} + + 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==} @@ -2632,10 +2764,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'} @@ -2656,6 +2784,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'} @@ -2681,9 +2813,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==} @@ -2692,10 +2828,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'} @@ -2735,8 +2867,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==} @@ -2757,8 +2889,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: @@ -2775,9 +2907,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'} @@ -2814,11 +2943,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==} @@ -2877,12 +3006,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'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} del-cli@7.0.0: resolution: {integrity: sha512-fRl4pWJYu9WFQH8jXdQUYvcD0IMtij9WEc7qmB7xOyJEweNJNuE7iKmqNeoOT1DbBUjtRjxlw8Y63qKBI/NQ1g==} @@ -2921,6 +3058,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'} @@ -2948,9 +3089,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==} @@ -2963,9 +3101,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'} @@ -2996,18 +3131,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==} @@ -3025,10 +3150,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'} @@ -3087,9 +3208,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==} @@ -3136,10 +3254,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'} @@ -3147,10 +3261,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'} @@ -3190,13 +3300,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==} @@ -3204,17 +3317,13 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - 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==} - 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==} @@ -3236,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==} @@ -3258,21 +3364,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.33.3: + resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} - hermes-estree@0.32.0: - resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} + hermes-estree@0.35.0: + resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} - hermes-parser@0.32.0: - resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} + 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==} @@ -3325,10 +3437,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'} @@ -3365,6 +3473,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'} @@ -3386,6 +3499,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'} @@ -3422,6 +3540,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==} @@ -3431,46 +3553,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} @@ -3541,21 +3632,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==} @@ -3568,9 +3725,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==} @@ -3579,9 +3736,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} @@ -3629,75 +3783,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==} @@ -3717,9 +3871,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==} @@ -3728,15 +3882,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: @@ -3767,8 +3917,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: @@ -3792,16 +3942,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==} @@ -3831,14 +3977,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'} @@ -3847,6 +3993,25 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxfmt@0.45.0: + resolution: {integrity: sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint-tsgolint@0.20.0: + resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} + hasBin: true + + oxlint@1.60.0: + resolution: {integrity: sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.18.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -3855,18 +4020,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'} @@ -3879,9 +4036,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'} @@ -3921,10 +4075,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'} @@ -3936,13 +4086,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==} @@ -3974,14 +4120,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 @@ -4000,8 +4146,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} presentable-error@0.0.1: @@ -4077,10 +4223,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==} @@ -4091,18 +4237,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==} @@ -4110,21 +4256,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 @@ -4138,19 +4288,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' @@ -4159,8 +4305,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: @@ -4263,19 +4409,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': @@ -4287,13 +4428,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 @@ -4302,6 +4438,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==} @@ -4318,9 +4458,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==} @@ -4336,6 +4473,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'} @@ -4372,9 +4514,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'} @@ -4395,6 +4534,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'} @@ -4420,10 +4563,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==} @@ -4442,11 +4581,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 @@ -4462,16 +4601,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: @@ -4535,8 +4670,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 @@ -4556,10 +4691,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==} @@ -4582,16 +4713,28 @@ 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} + tinyrainbow@2.0.0: 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: @@ -4624,42 +4767,35 @@ 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'} - 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 @@ -4674,44 +4810,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'} @@ -4720,12 +4822,12 @@ 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: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -4738,11 +4840,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==} @@ -4760,10 +4862,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'} @@ -4780,12 +4878,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: @@ -4833,23 +4931,16 @@ 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==} - 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' @@ -4860,12 +4951,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: @@ -4881,20 +4974,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 @@ -4908,6 +5004,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: @@ -4957,14 +5057,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'} @@ -4972,21 +5072,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'} @@ -5011,6 +5096,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'} @@ -5044,8 +5133,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 @@ -5065,10 +5154,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==} @@ -5079,7 +5164,13 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@babel/code-frame@7.28.6': + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + 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 @@ -5087,6 +5178,8 @@ snapshots: '@babel/compat-data@7.28.6': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.6': dependencies: '@babel/code-frame': 7.28.6 @@ -5107,6 +5200,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 @@ -5115,6 +5228,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 @@ -5140,6 +5270,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 @@ -5147,7 +5290,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 @@ -5158,6 +5308,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': @@ -5179,7 +5340,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 @@ -5194,7 +5364,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 @@ -5207,6 +5386,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 @@ -5216,134 +5404,146 @@ 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/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@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 + '@babel/traverse': 7.29.0 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 + '@babel/traverse': 7.29.0 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)': @@ -5351,25 +5551,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)': @@ -5377,12 +5577,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 @@ -5395,9 +5609,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)': @@ -5405,7 +5628,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) @@ -5413,23 +5641,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 @@ -5441,21 +5669,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 @@ -5463,49 +5691,69 @@ 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 - '@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.29.0 + 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: @@ -5515,39 +5763,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 + '@babel/traverse': 7.29.0 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 @@ -5560,38 +5816,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)': @@ -5599,27 +5864,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/traverse': 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.29.0 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 @@ -5628,7 +5898,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 @@ -5636,17 +5911,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)': @@ -5657,6 +5932,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 @@ -5666,53 +5949,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-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-plugin-utils': 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-regexp-modifiers@7.28.6(@babel/core@7.28.6)': + '@babel/plugin-transform-runtime@7.29.0(@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-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-reserved-words@7.27.1(@babel/core@7.28.6)': + '@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-shorthand-properties@7.27.1(@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.28.6)': + '@babel/plugin-transform-spread@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-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)': @@ -5720,9 +6088,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)': @@ -5736,15 +6109,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)': @@ -5753,93 +6137,99 @@ 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 + '@babel/types': 7.29.0 esutils: 2.0.3 '@babel/preset-typescript@7.27.1(@babel/core@7.28.6)': @@ -5853,6 +6243,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': @@ -5873,49 +6274,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 - '@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 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/cli-win32-x64@2.3.11': - optional: true + '@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.0.14': + '@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 @@ -5942,30 +6330,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 @@ -5975,11 +6361,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 @@ -5996,12 +6383,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 @@ -6019,7 +6406,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 @@ -6031,11 +6418,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 @@ -6057,262 +6444,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)': + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': 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 + '@types/node': 25.6.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 - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.0.9 - jest-mock: 29.7.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 - '@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 @@ -6505,14 +6749,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: @@ -6545,7 +6788,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: @@ -6563,16 +6806,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 @@ -6588,11 +6831,138 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.107.0': {} + '@oxc-project/types@0.124.0': {} + + '@oxfmt/binding-android-arm-eabi@0.45.0': + optional: true + + '@oxfmt/binding-android-arm64@0.45.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.45.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.45.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.45.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.45.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.45.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.45.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.45.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.45.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.45.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.45.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.45.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.45.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.45.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.45.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.45.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.45.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.45.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.20.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.20.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.20.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.20.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.20.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.20.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.60.0': + optional: true + + '@oxlint/binding-android-arm64@1.60.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.60.0': + optional: true + + '@oxlint/binding-darwin-x64@1.60.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.60.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.60.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.60.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.60.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.60.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.60.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.60.0': + optional: true - '@oxc-project/types@0.108.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': {} @@ -6611,194 +6981,360 @@ 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.16 + 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.16 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.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 - semver: 7.7.3 + metro: 0.84.3 + metro-config: 0.84.3 + metro-core: 0.84.3 + semver: 7.7.4 + 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.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.4 + 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.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.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.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': 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': + '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.60': + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.59': + '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.60': + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.59': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.60': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@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-openharmony-arm64@1.0.0-beta.59': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': + '@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: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true + '@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/binding-wasm32-wasi@1.0.0-beta.60': + '@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: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': - optional: true + '@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-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.29.0)(@types/babel__core@7.20.5)(rollup@4.55.2)': + dependencies: + '@babel/core': 7.29.0 + '@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 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.55.2 @@ -6877,104 +7413,108 @@ 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': {} - '@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 @@ -6998,6 +7538,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 @@ -7037,10 +7595,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': @@ -7051,6 +7605,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': {} @@ -7059,68 +7615,70 @@ 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(@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: - '@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: + '@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': dependencies: @@ -7130,47 +7688,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 @@ -7178,7 +7729,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.0.17': {} + '@vitest/spy@4.1.4': {} '@vitest/utils@3.2.4': dependencies: @@ -7186,10 +7737,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: @@ -7213,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': @@ -7223,14 +7775,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: {} @@ -7264,11 +7818,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: @@ -7293,17 +7842,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 @@ -7311,99 +7859,90 @@ 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-syntax-hermes-parser@0.32.0: + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): dependencies: - hermes-parser: 0.32.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.6): + babel-plugin-react-compiler@1.0.0: 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/types': 7.29.0 - babel-preset-jest@29.6.3(@babel/core@7.28.6): + babel-plugin-syntax-hermes-parser@0.33.3: 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) + hermes-parser: 0.33.3 + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.28.6): + dependencies: + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - '@babel/core' + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + dependencies: + '@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 @@ -7432,6 +7971,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 @@ -7460,14 +8003,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: {} @@ -7497,21 +8042,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 @@ -7529,10 +8073,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: @@ -7552,8 +8096,6 @@ snapshots: color-name@1.1.4: {} - colorette@2.0.20: {} - commander@12.1.0: {} commander@14.0.2: {} @@ -7597,11 +8139,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: {} @@ -7649,9 +8191,16 @@ snapshots: deep-extend@0.6.0: {} - define-lazy-prop@2.0.0: {} + default-browser-id@5.0.1: {} - defu@6.1.4: {} + 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.7: {} del-cli@7.0.0: dependencies: @@ -7709,6 +8258,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7727,8 +8278,6 @@ snapshots: dts-resolver@2.1.3: {} - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -7737,8 +8286,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - empathic@2.0.0: {} encodeurl@1.0.2: {} @@ -7762,43 +8309,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: @@ -7835,8 +8346,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} esprima@4.0.1: {} @@ -7879,8 +8388,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 @@ -7905,9 +8412,9 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-type@16.5.4: dependencies: @@ -7938,12 +8445,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 @@ -7951,13 +8452,8 @@ snapshots: micromatch: 4.0.8 resolve-dir: 1.0.1 - flow-enums-runtime@0.0.6: {} - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - + flow-enums-runtime@0.0.6: {} + fresh@0.5.2: {} fs-extra@11.3.3: @@ -7991,12 +8487,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 @@ -8006,20 +8506,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: @@ -8066,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: {} @@ -8087,19 +8576,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: @@ -8147,8 +8642,6 @@ snapshots: import-without-cache@0.2.5: {} - imurmurhash@0.1.4: {} - indent-string@4.0.0: {} inflight@1.0.6: @@ -8178,6 +8671,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8192,6 +8687,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 @@ -8217,88 +8716,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 @@ -8315,7 +8755,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 @@ -8400,25 +8840,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: {} @@ -8427,7 +8911,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 @@ -8441,8 +8925,6 @@ snapshots: loupe@3.2.1: {} - lru-cache@10.4.3: {} - lru-cache@11.2.4: {} lru-cache@5.1.1: @@ -8479,50 +8961,51 @@ snapshots: merge2@1.4.1: {} - metro-babel-transformer@0.83.3: + 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.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 @@ -8536,87 +9019,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/core': 7.29.0 + '@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/core': 7.29.0 + '@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/core': 7.28.6 - '@babel/generator': 7.28.6 - '@babel/parser': 7.28.6 + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@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 @@ -8624,25 +9106,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 @@ -8659,11 +9141,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: {} @@ -8673,9 +9155,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: @@ -8685,13 +9167,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: {} @@ -8725,7 +9203,7 @@ snapshots: nanoid@3.3.11: {} - negotiator@0.6.3: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -8742,11 +9220,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 @@ -8776,14 +9252,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 @@ -8794,6 +9271,62 @@ snapshots: outdent@0.5.0: {} + oxfmt@0.45.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@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.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.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: p-map: 2.1.0 @@ -8802,32 +9335,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: @@ -8861,23 +9384,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: {} @@ -8895,9 +9411,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: @@ -8913,7 +9429,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -8987,9 +9503,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: @@ -9006,9 +9522,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: {} @@ -9017,20 +9533,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.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-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-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.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) - semver: 7.7.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-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.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.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: + 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.4 - 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 @@ -9039,71 +9568,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) - semver: 7.7.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@0.82.1(@babel/core@7.28.6)(@types/react@19.2.8)(react@19.2.3): + 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: - '@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.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-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 - semver: 7.7.3 + scheduler: 0.27.0 + semver: 7.7.4 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.16 + 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.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: + '@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-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.4 stacktrace-parser: 0.1.11 + tinyglobby: 0.2.16 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' @@ -9112,30 +9704,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: @@ -9253,63 +9843,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: @@ -9341,6 +9912,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: @@ -9354,8 +9928,6 @@ snapshots: sax@1.4.4: {} - scheduler@0.26.0: {} - scheduler@0.27.0: {} semver-compare@1.0.0: {} @@ -9364,6 +9936,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -9409,8 +9983,6 @@ snapshots: siginfo@2.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} simple-xml-to-json@1.2.3: {} @@ -9424,6 +9996,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: @@ -9444,10 +10021,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: {} @@ -9460,31 +10033,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: {} @@ -9494,21 +10067,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: {} @@ -9564,11 +10131,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: {} @@ -9583,12 +10150,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: @@ -9606,14 +10167,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 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + 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: {} @@ -9636,36 +10206,32 @@ 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 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' @@ -9684,56 +10250,36 @@ 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@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: {} @@ -9746,8 +10292,6 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} - unicorn-magic@0.1.0: {} - unicorn-magic@0.3.0: {} universalify@0.1.2: {} @@ -9756,14 +10300,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.4 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: {} @@ -9786,13 +10332,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: @@ -9802,72 +10348,51 @@ 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@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) + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - 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): - dependencies: + '@types/node': 25.6.0 esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.55.2 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.0.9 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: {} @@ -9907,18 +10432,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 @@ -9927,19 +10452,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: {} @@ -9961,7 +10481,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yargs-parser@20.2.9: {} @@ -9987,8 +10507,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 a0d4d82..5ecf2aa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,32 @@ packages: - apps/* - 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.0 + babel-plugin-react-compiler: 1.0.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: + 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 - esbuild 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(),