diff --git a/.changeset/bright-lions-march.md b/.changeset/bright-lions-march.md
new file mode 100644
index 00000000..925ed756
--- /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 00000000..157c3724
--- /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 00000000..7623732c
--- /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 00000000..7704b52e
--- /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 00000000..c6763df5
--- /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 4f32613d..1bd73dbc 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 d1d61895..8069f0dc 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 6becc024..3308b8a8 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 00000000..56527329
--- /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 00000000..ec296584
--- /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 de51190c..99e2f7dd 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 604f5537..8ce25ad7 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 00000000..d31c2eb8
--- /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 27082cae..9a79b93f 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 331e0f5c..5c52a8b2 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 2ec5bcfb..249f8dbb 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 929b9386..451052fe 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 8cd4d7a5..fe586296 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 7b8fdac1..58cd5d16 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 f5b59323..e3a9a73a 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 7a9843d5..c26f6578 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 b5e4a5d2..f7ece62f 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 00000000..36d1068a
--- /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 f4e012c2..1879f8a5 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 a1e32a1f..6390ddd9 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 d00c2a26..d947b472 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 471455e4..07cc3c68 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 00000000..e6d33915
--- /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 b1f77cb9..cefe2137 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 955294d0..fe15f7e5 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 fa9ebcd8..9f771846 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 c4123155..d70c3737 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 50019057..fea3f73b 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 8a4cd7df..35c79e7d 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 4439a15c..bc07e2f5 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 1a26c9f1..f5991c7b 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 a9552369..2a325da8 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 3b8d1579..32a4a689 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
-
+
{
>
-
+
-
-
+
@@ -162,7 +144,7 @@ const MainApp = () => {
const App = () => {
return (
-
+
);
};
diff --git a/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx b/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx
index 49c08df2..51c287f7 100644
--- a/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx
@@ -1,5 +1,3 @@
-import { Button } from '@plextv/react-native-lightning';
-import { Column, Row } from '@plextv/react-native-lightning-components';
import { type FC, useMemo, useState } from 'react';
import Animated, {
type EntryOrExitLayoutType,
@@ -23,6 +21,9 @@ import Animated, {
SlideOutUp as SlideOutUpBuilder,
} from 'react-native-reanimated';
+import { Button } from '@plextv/react-native-lightning';
+import { Column, Row } from '@plextv/react-native-lightning-components';
+
type AnimatedProps = {
visible: boolean;
entering?: EntryOrExitLayoutType;
diff --git a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
index 64f511ca..2a9635cb 100644
--- a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
@@ -55,11 +55,7 @@ const AnimationTest: FC = () => {
/>
-
+
>
);
diff --git a/apps/react-native-lightning-example/src/pages/ComponentTest.tsx b/apps/react-native-lightning-example/src/pages/ComponentTest.tsx
index facd31ac..c008043d 100644
--- a/apps/react-native-lightning-example/src/pages/ComponentTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/ComponentTest.tsx
@@ -1,6 +1,3 @@
-import { FocusGroup, type LightningElement } from '@plextv/react-lightning';
-import { Column, Row } from '@plextv/react-lightning-components';
-import { ScrollView } from '@plextv/react-native-lightning';
import { type FC, type RefObject, useCallback, useRef } from 'react';
import {
ActivityIndicator,
@@ -12,6 +9,10 @@ import {
View,
} from 'react-native';
+import { FocusGroup, type LightningElement } from '@plextv/react-lightning';
+import { Column, Row } from '@plextv/react-lightning-components';
+import { ScrollView } from '@plextv/react-native-lightning';
+
const ComponentTest: FC = () => {
const ref = useRef(null);
@@ -48,20 +49,17 @@ const ComponentTest: FC = () => {
ellipsizeMode="tail"
numberOfLines={3}
>
- This is a text component that overflows. Lorem ipsum dolor sit
- amet, consectetur adipiscing elit. Donec tellus libero, maximus
- sit amet lorem quis, vehicula vestibulum magna. Donec eget
- consequat erat. Donec lorem erat, lacinia a ultricies a,
- consectetur vitae ipsum. Integer at viverra eros. Nullam nec
- lectus et enim faucibus molestie. Etiam porttitor pharetra mauris,
- quis sodales quam molestie sed. Cras consectetur interdum purus,
- ut malesuada nisl ultricies sit amet. Aliquam cursus orci ipsum, a
- vestibulum tortor pellentesque in. Donec non urna facilisis nisl
- laoreet mollis nec vitae dolor. Phasellus dictum ac quam ac
- laoreet. Morbi pellentesque varius odio, vel pretium orci pretium
- eget. Nam ultrices lorem urna, in mattis quam commodo a. Morbi
- fermentum, tellus at condimentum rhoncus, quam neque lacinia nibh,
- quis tincidunt orci metus sed metus.
+ This is a text component that overflows. Lorem ipsum dolor sit amet, consectetur
+ adipiscing elit. Donec tellus libero, maximus sit amet lorem quis, vehicula vestibulum
+ magna. Donec eget consequat erat. Donec lorem erat, lacinia a ultricies a, consectetur
+ vitae ipsum. Integer at viverra eros. Nullam nec lectus et enim faucibus molestie.
+ Etiam porttitor pharetra mauris, quis sodales quam molestie sed. Cras consectetur
+ interdum purus, ut malesuada nisl ultricies sit amet. Aliquam cursus orci ipsum, a
+ vestibulum tortor pellentesque in. Donec non urna facilisis nisl laoreet mollis nec
+ vitae dolor. Phasellus dictum ac quam ac laoreet. Morbi pellentesque varius odio, vel
+ pretium orci pretium eget. Nam ultrices lorem urna, in mattis quam commodo a. Morbi
+ fermentum, tellus at condimentum rhoncus, quam neque lacinia nibh, quis tincidunt orci
+ metus sed metus.
@@ -73,9 +71,7 @@ const ComponentTest: FC = () => {
height: 50,
}}
>
-
- This is a View with a red background
-
+ This is a View with a red background
{
height: 50,
}}
>
-
- This is a TouchableWithoutFeedback with a purple background
-
+ This is a TouchableWithoutFeedback with a purple background
-
+
diff --git a/apps/react-native-lightning-example/src/pages/FlashListTest.tsx b/apps/react-native-lightning-example/src/pages/FlashListTest.tsx
deleted file mode 100644
index 33a2cace..00000000
--- a/apps/react-native-lightning-example/src/pages/FlashListTest.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import Row from '@plextv/react-lightning-components/layout/Row';
-import FlashList from '@plextv/react-native-lightning-components/lists/FlashList';
-import { type FC, useCallback, useRef } from 'react';
-import { View } from 'react-native';
-import ScrollItem from '../components/ScrollItem';
-
-export const FlashListTest: FC = () => {
- 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={75}
- snapToAlignment="center"
- />
-
-
-
- (
-
- {item}
-
- )}
- drawDistance={75}
- snapToAlignment="center"
- />
-
-
- );
-};
diff --git a/apps/react-native-lightning-example/src/pages/LayoutTest.tsx b/apps/react-native-lightning-example/src/pages/LayoutTest.tsx
index 0d08253a..14098f97 100644
--- a/apps/react-native-lightning-example/src/pages/LayoutTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/LayoutTest.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 { 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-native-lightning-example/src/pages/LibraryTest.tsx b/apps/react-native-lightning-example/src/pages/LibraryTest.tsx
index 8cb3a116..a32d6878 100644
--- a/apps/react-native-lightning-example/src/pages/LibraryTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/LibraryTest.tsx
@@ -1,8 +1,11 @@
+import { type FC, useCallback, useMemo, useRef } from 'react';
+import { Image, Text } from 'react-native';
+
import type { LightningElement } from '@plextv/react-lightning';
import { useFocus } from '@plextv/react-lightning';
-import { Column, FlashList } from '@plextv/react-native-lightning-components';
-import { type FC, useCallback, useEffect, useMemo, useRef } from 'react';
-import { Image, Text } from 'react-native';
+import type { VirtualListRef } from '@plextv/react-lightning-components/lists/VirtualList';
+import VirtualList from '@plextv/react-lightning-components/lists/VirtualList';
+import { Column } from '@plextv/react-native-lightning-components';
const ITEM_COUNT = 150;
@@ -16,10 +19,6 @@ interface Props {
const Poster: FC = ({ title, subtitle, seed, onFocus }) => {
const { focused, ref } = useFocus();
- useEffect(() => {
- console.log(`Rendering poster ${seed}`);
- });
-
return (
{
- const ref = useRef>(null);
+ const ref = useRef(null);
- const handleFocus = useCallback((element: PosterItem) => {
- ref.current?.scrollToItem({ item: element, viewPosition: 0.5 });
- }, []);
+ const handleFocus = useCallback(
+ (item: PosterItem) => {
+ const index = items.indexOf(item);
+ if (index >= 0) {
+ ref.current?.scrollToIndex({ index, viewPosition: 0.5 });
+ }
+ },
+ [items],
+ );
const renderItem = useCallback(
({ item }: { item: PosterItem }) => (
@@ -84,14 +89,15 @@ const LibraryView = ({ items }: { items: PosterItem[] }) => {
);
return (
-
- snapToAlignment="center"
+
ref={ref}
+ snapToAlignment="center"
drawDistance={100}
numColumns={6}
- centerContent={true}
- estimatedItemSize={500}
- estimatedListSize={{ height: 1080, width: 1670 }}
+ estimatedItemSize={400}
+ ItemSeparatorComponent={() => }
+ contentContainerStyle={{ paddingHorizontal: 25 }}
+ style={{ w: 1670, h: 1080 }}
renderItem={renderItem}
data={items}
/>
diff --git a/apps/react-native-lightning-example/src/pages/SimpleTest.tsx b/apps/react-native-lightning-example/src/pages/SimpleTest.tsx
index 235b40e7..20b6aa20 100644
--- a/apps/react-native-lightning-example/src/pages/SimpleTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/SimpleTest.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 '../../../react-lightning-example/src/api/useHubsData';
import { HubRow } from '../../../react-lightning-example/src/components/HubRow';
@@ -9,9 +11,7 @@ export const SimpleTest: 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-native-lightning-example/src/pages/VirtualizedListTest.tsx b/apps/react-native-lightning-example/src/pages/VirtualizedListTest.tsx
index 42e3367e..8c590866 100644
--- a/apps/react-native-lightning-example/src/pages/VirtualizedListTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/VirtualizedListTest.tsx
@@ -1,11 +1,14 @@
import { createRef, type FC, useCallback } from 'react';
import { VirtualizedList } from 'react-native';
-import ScrollItem from '../components/ScrollItem';
-const getItem = (_data: string[], index: number) => `Button ${index}`;
+import ScrollItem from '../components/ScrollItem';
export const VirtualizedListTest: FC = () => {
- const ref = createRef>();
+ const ref = createRef>();
+ const data = Array.from({ length: 5000 }, (_, i) => ({
+ text: `Button ${i}`,
+ isImage: Math.random() < 0.5,
+ }));
const handleFocus = useCallback(
(index: number) => {
@@ -15,29 +18,23 @@ export const VirtualizedListTest: FC = () => {
);
return (
-
ref={ref}
+ data={data}
removeClippedSubviews={true}
snapToAlignment="center"
initialNumToRender={20}
- getItem={getItem}
- getItemCount={() => 5000}
- getItemLayout={(_, index) => ({
- index,
- length: 75,
- offset: index * 75,
- })}
- keyExtractor={(item) => item}
+ keyExtractor={(item) => item.text}
windowSize={2}
renderItem={({ index, item }) => (
- {item}
+ {item.text}
)}
/>
diff --git a/apps/react-native-lightning-example/src/polyfills/Atomics.ts b/apps/react-native-lightning-example/src/polyfills/Atomics.ts
index 2e49e8e8..5b4ce0ba 100644
--- a/apps/react-native-lightning-example/src/polyfills/Atomics.ts
+++ b/apps/react-native-lightning-example/src/polyfills/Atomics.ts
@@ -61,9 +61,7 @@
function allocHelper() {
if (helpers.length > 0) return helpers.pop() as Worker;
- const h = new Worker(
- `data:application/javascript,${encodeURIComponent(helperCode)}`,
- );
+ const h = new Worker(`data:application/javascript,${encodeURIComponent(helperCode)}`);
return h;
}
@@ -94,11 +92,11 @@
const index = index_ | 0;
const value = value_ | 0;
- const timeout =
- timeout_ === undefined ? Number.POSITIVE_INFINITY : Number(timeout_);
+ const timeout = timeout_ === undefined ? Number.POSITIVE_INFINITY : Number(timeout_);
// Range checking for the index.
+ // oxlint-disable-next-line no-unused-expressions -- intentional range check; throws RangeError if index is out of bounds
ia[index];
// Optimization, avoid the helper thread in this common case.
diff --git a/apps/react-native-lightning-example/tsconfig.json b/apps/react-native-lightning-example/tsconfig.json
index 996e3977..9677c64e 100644
--- a/apps/react-native-lightning-example/tsconfig.json
+++ b/apps/react-native-lightning-example/tsconfig.json
@@ -2,8 +2,7 @@
"extends": "@repo/configs/tsconfig.react-native-library.json",
"compilerOptions": {
"jsx": "react-native",
- "isolatedDeclarations": false,
- "outDir": "dist"
+ "isolatedDeclarations": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
diff --git a/apps/react-native-lightning-example/vite.config.mjs b/apps/react-native-lightning-example/vite.config.mjs
index bec16303..287b5252 100644
--- a/apps/react-native-lightning-example/vite.config.mjs
+++ b/apps/react-native-lightning-example/vite.config.mjs
@@ -1,13 +1,20 @@
+import babel from '@rolldown/plugin-babel';
+import legacy from '@vitejs/plugin-legacy';
+import { reactCompilerPreset } from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
import fontGen from '@plextv/vite-plugin-msdf-fontgen';
import reactNativeLightningPlugin from '@plextv/vite-plugin-react-native-lightning';
import reactReanimatedLightningPlugin from '@plextv/vite-plugin-react-reanimated-lightning';
-import legacy from '@vitejs/plugin-legacy';
-import { defineConfig } from 'vite';
const config = defineConfig((env) => ({
base: './',
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,9 +38,7 @@ const config = defineConfig((env) => ({
hmr: false,
},
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),
},
}));
diff --git a/apps/storybook/.storybook/manager.ts b/apps/storybook/.storybook/manager.ts
index 4c995402..19a54d6e 100644
--- a/apps/storybook/.storybook/manager.ts
+++ b/apps/storybook/.storybook/manager.ts
@@ -1,4 +1,5 @@
import { addons } from 'storybook/manager-api';
+
import theme from './theme';
addons.setConfig({
diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx
index af63bc9a..1fc3251f 100644
--- a/apps/storybook/.storybook/preview.tsx
+++ b/apps/storybook/.storybook/preview.tsx
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/react-vite';
+
import { StorybookDecorator } from '../src/components/StorybookDecorator';
import theme from './theme';
diff --git a/apps/storybook/.storybook/theme.ts b/apps/storybook/.storybook/theme.ts
index 02c2aa7d..3d1761b6 100644
--- a/apps/storybook/.storybook/theme.ts
+++ b/apps/storybook/.storybook/theme.ts
@@ -4,8 +4,7 @@ export default create({
base: 'dark',
brandTitle: 'Plex Theme',
brandUrl: 'https://plex.tv',
- brandImage:
- 'https://www.plex.tv/wp-content/themes/plex/assets/img/plex-logo.svg',
+ brandImage: 'https://www.plex.tv/wp-content/themes/plex/assets/img/plex-logo.svg',
brandTarget: '_top',
// Colors
diff --git a/apps/storybook/package.json b/apps/storybook/package.json
index e71a5a9b..f351acee 100644
--- a/apps/storybook/package.json
+++ b/apps/storybook/package.json
@@ -1,56 +1,62 @@
{
"name": "@plextv/react-lightning-storybook",
- "description": "Documentation for react-lightning",
"version": "0.4.0",
- "author": "Plex Inc.",
+ "private": true,
+ "description": "Documentation 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"
- },
- "packageManager": "pnpm@10.12.3",
"type": "module",
- "private": true,
"scripts": {
"build": "storybook build --output-dir ./dist",
"clean": "del ./dist",
"dev": "storybook dev -p 6006 --no-open"
},
"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-flexbox-lite": "workspace:*",
"@plextv/react-lightning-plugin-reanimated": "workspace:*",
"@plextv/react-native-lightning": "workspace:*",
"@plextv/react-native-lightning-components": "workspace:*",
- "@storybook/addon-docs": "9.1.17",
- "@storybook/addon-links": "9.1.17",
- "@storybook/builder-vite": "9.1.17",
- "@storybook/react-vite": "9.1.17",
- "react": "19.2.3",
- "react-native": "0.82.1",
- "react-native-reanimated": "4.2.1",
- "storybook": "9.1.17"
+ "@storybook/addon-docs": "10.3.5",
+ "@storybook/addon-links": "10.3.5",
+ "@storybook/react-vite": "10.3.5",
+ "react": "catalog:apps",
+ "react-dom": "catalog:apps",
+ "react-native": "catalog:apps",
+ "react-native-reanimated": "catalog:apps",
+ "react-native-worklets": "0.8.1",
+ "storybook": "10.3.5"
},
"devDependencies": {
"@plextv/vite-plugin-msdf-fontgen": "workspace:*",
"@plextv/vite-plugin-react-native-lightning": "workspace:*",
"@plextv/vite-plugin-react-reanimated-lightning": "workspace:*",
"@repo/configs": "workspace:*",
- "@types/react": "19.2.8"
+ "@rolldown/plugin-babel": "catalog:",
+ "@types/react": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "babel-plugin-react-compiler": "catalog:"
},
"volta": {
"extends": "../../package.json"
},
+ "packageManager": "pnpm@10.12.3",
"depcheck": {
"ignoreMatches": [
"@storybook/**",
- "**/storybook"
+ "**/storybook",
+ "babel-plugin-react-compiler"
]
}
}
diff --git a/apps/storybook/src/components/Button.tsx b/apps/storybook/src/components/Button.tsx
index 38d7605a..b40fb7e4 100644
--- a/apps/storybook/src/components/Button.tsx
+++ b/apps/storybook/src/components/Button.tsx
@@ -1,10 +1,11 @@
+import { useCallback } from 'react';
+
import type {
KeyEvent,
LightningElement,
LightningViewElementProps,
} from '@plextv/react-lightning';
import { Keys, useFocus } from '@plextv/react-lightning';
-import { useCallback } from 'react';
const containerStyles = {
w: 170,
diff --git a/apps/storybook/src/components/FocusableImage.tsx b/apps/storybook/src/components/FocusableImage.tsx
index fc7c3e30..56ad31a3 100644
--- a/apps/storybook/src/components/FocusableImage.tsx
+++ b/apps/storybook/src/components/FocusableImage.tsx
@@ -1,10 +1,7 @@
-import {
- type LightningElement,
- useCombinedRef,
- useFocus,
-} from '@plextv/react-lightning';
import { forwardRef } from 'react';
+import { type LightningElement, useCombinedRef, useFocus } from '@plextv/react-lightning';
+
export type FocusableImageProps = {
disable?: boolean;
hidden?: boolean;
diff --git a/apps/storybook/src/components/ScrollItem.tsx b/apps/storybook/src/components/ScrollItem.tsx
index 73b3a9e9..f7572f49 100644
--- a/apps/storybook/src/components/ScrollItem.tsx
+++ b/apps/storybook/src/components/ScrollItem.tsx
@@ -1,75 +1,50 @@
+import { type ReactNode, useRef } from 'react';
+
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';
export type ScrollItemProps = {
children: ReactNode;
index: number;
width?: number;
height?: number;
+ focused?: boolean;
horizontal?: boolean;
- color: ColorValue;
- altColor: ColorValue;
- image?: boolean;
- onFocused: (index: number) => void;
+ color: number;
+ altColor: number;
};
-const ScrollItem = focusable(
- (
- {
- color,
- altColor,
- image,
- index,
- horizontal,
- width = 200,
- height = 75,
- focused,
- children,
- onFocused,
- },
- ref,
- ) => {
- useEffect(() => {
- if (focused) {
- onFocused(index);
- }
- }, [index, focused, onFocused]);
-
- const multiplier = index % 3 === 0 ? 1.25 : 1;
+const ScrollItem = focusable(
+ ({ color, altColor, index, focused, horizontal, width = 200, height = 75, children }, ref) => {
+ const isImage = useRef(Math.random() > 0.5).current;
const finalColor = index % 3 === 0 ? altColor : color;
- const finalWidth = Math.round(horizontal ? height * multiplier : width);
- const finalHeight = Math.round(horizontal ? width : height * multiplier);
- const imageSrc = useMemo(
- () =>
- `https://picsum.photos/${finalWidth}/${finalHeight}?seed=${Math.random()}`,
- [finalWidth, finalHeight],
- );
+ const finalWidth = Math.round(horizontal ? width : width);
+ const finalHeight = Math.round(horizontal ? height : height);
+ const imageUrl = isImage
+ ? `https://picsum.photos/${finalWidth}/${finalHeight}?seed=${index}`
+ : null;
return (
-
- {image ? (
+ {imageUrl ? (
(
}}
/>
) : (
-
{children}
-
+
)}
-
+
);
},
);
diff --git a/apps/storybook/src/components/StorybookDecorator.tsx b/apps/storybook/src/components/StorybookDecorator.tsx
index ffcbc98b..af5aeec1 100644
--- a/apps/storybook/src/components/StorybookDecorator.tsx
+++ b/apps/storybook/src/components/StorybookDecorator.tsx
@@ -1,7 +1,9 @@
+import { type JSX, useMemo } from 'react';
+
import { Canvas, type RenderOptions } from '@plextv/react-lightning';
-import { plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox';
+import { FlexRoot, plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox';
import { getReactNativePlugins } from '@plextv/react-native-lightning';
-import { type JSX, useMemo } from 'react';
+
import { keyMap } from '../../keyMap';
import { DefaultStoryHeight, DefaultStoryWidth } from '../helpers/constants';
@@ -11,11 +13,7 @@ type Props = {
canvasOptions?: Partial;
};
-export function StorybookDecorator({
- story: Story,
- tags,
- canvasOptions,
-}: Props) {
+export function StorybookDecorator({ story: Story, tags, canvasOptions }: Props) {
const options: RenderOptions = useMemo(
() => ({
fpsUpdateInterval: 1000,
@@ -26,12 +24,8 @@ export function StorybookDecorator({
{
type: 'sdf',
fontFamily: 'sans-serif',
- atlasUrl:
- import.meta.env.BASE_URL +
- 'fonts/ChocolateClassicalSans-Regular.msdf.png',
- atlasDataUrl:
- import.meta.env.BASE_URL +
- 'fonts/ChocolateClassicalSans-Regular.msdf.json',
+ atlasUrl: import.meta.env.BASE_URL + 'fonts/ChocolateClassicalSans-Regular.msdf.png',
+ atlasDataUrl: import.meta.env.BASE_URL + 'fonts/ChocolateClassicalSans-Regular.msdf.json',
},
],
shaders: [
@@ -52,7 +46,9 @@ export function StorybookDecorator({
return (
);
}
diff --git a/apps/storybook/src/getting-started/QuickStart.mdx b/apps/storybook/src/getting-started/QuickStart.mdx
index 53d621a4..42e909bd 100644
--- a/apps/storybook/src/getting-started/QuickStart.mdx
+++ b/apps/storybook/src/getting-started/QuickStart.mdx
@@ -72,14 +72,14 @@ A key map will be required for adding keyboard and remote support.
import { Keys } from '@plextv/react-lightning';
const keyMap = {
- 37: Keys.Left,
- 38: Keys.Up,
- 39: Keys.Right,
- 40: Keys.Down,
-
- 8: Keys.Back, // Backspace
- 27: Keys.Back, // Esc
- 13: Keys.Enter, // Enter
+37: Keys.Left,
+38: Keys.Up,
+39: Keys.Right,
+40: Keys.Down,
+
+8: Keys.Back, // Backspace
+27: Keys.Back, // Esc
+13: Keys.Enter, // Enter
};
export { keyMap };
@@ -99,13 +99,14 @@ import { keyMap } from './keyMap';
const appElement = document.getElementById('app');
const options: RenderOptions = {
- // ...
+// ...
};
const App = () => (
-
+
+
);
const root = await createRoot(appElement, options);
diff --git a/apps/storybook/src/helpers/constants.ts b/apps/storybook/src/helpers/constants.ts
index 780dd8e0..a0eec250 100644
--- a/apps/storybook/src/helpers/constants.ts
+++ b/apps/storybook/src/helpers/constants.ts
@@ -7,8 +7,8 @@ export enum FlexTypes {
}
export const ColorPalette = [
- 0xfe9e03ff, 0xfd5501ff, 0xfe5400ff, 0xfd0900ff, 0xfd0701ff, 0xd1014cff,
- 0xd1019eff, 0x8d1286ff, 0x64138bff, 0x2b3e9aff, 0x283f9aff, 0x0671f8ff,
+ 0xfe9e03ff, 0xfd5501ff, 0xfe5400ff, 0xfd0900ff, 0xfd0701ff, 0xd1014cff, 0xd1019eff, 0x8d1286ff,
+ 0x64138bff, 0x2b3e9aff, 0x283f9aff, 0x0671f8ff,
];
export const DefaultStoryWidth = 800;
diff --git a/apps/storybook/src/plugin-flexbox-lite/FlexboxLite.stories.tsx b/apps/storybook/src/plugin-flexbox-lite/FlexboxLite.stories.tsx
index afc543d2..8ba1d5a5 100644
--- a/apps/storybook/src/plugin-flexbox-lite/FlexboxLite.stories.tsx
+++ b/apps/storybook/src/plugin-flexbox-lite/FlexboxLite.stories.tsx
@@ -1,6 +1,8 @@
+import type { Meta } from '@storybook/react-vite';
+
import { Row } from '@plextv/react-lightning-components';
import flexboxLitePlugin from '@plextv/react-lightning-plugin-flexbox-lite';
-import type { Meta } from '@storybook/react-vite';
+
import { StorybookDecorator } from '../components/StorybookDecorator';
import {
ColorPalette,
@@ -35,10 +37,7 @@ export default {
title: 'Plugins/react-lightning-plugin-flexbox-lite/FlexBoxLite',
decorators: [
(Story) => (
-
+
),
],
argTypes: {
diff --git a/apps/storybook/src/plugin-reanimated/AnimationSampler.tsx b/apps/storybook/src/plugin-reanimated/AnimationSampler.tsx
index beec158d..2f4cd10d 100644
--- a/apps/storybook/src/plugin-reanimated/AnimationSampler.tsx
+++ b/apps/storybook/src/plugin-reanimated/AnimationSampler.tsx
@@ -1,8 +1,8 @@
-import Animated, {
- type ReanimatedAnimation,
-} from '@plextv/react-lightning-plugin-reanimated';
-import { Row } from '@plextv/react-native-lightning-components';
import { type FC, useMemo, useState } from 'react';
+
+import Animated, { type ReanimatedAnimation } from '@plextv/react-lightning-plugin-reanimated';
+import { Row } from '@plextv/react-native-lightning-components';
+
import Button from '../components/Button';
type AnimatedProps = {
diff --git a/apps/storybook/src/plugin-reanimated/Fade.stories.tsx b/apps/storybook/src/plugin-reanimated/Fade.stories.tsx
index 6655a073..c4e4a4d5 100644
--- a/apps/storybook/src/plugin-reanimated/Fade.stories.tsx
+++ b/apps/storybook/src/plugin-reanimated/Fade.stories.tsx
@@ -1,4 +1,3 @@
-import { Column } from '@plextv/react-native-lightning-components';
import type { Meta } from '@storybook/react-vite';
import {
FadeIn,
@@ -12,6 +11,9 @@ import {
FadeOutRight,
FadeOutUp,
} from 'react-native-reanimated';
+
+import { Column } from '@plextv/react-native-lightning-components';
+
import { AnimationSampler } from './AnimationSampler';
export default {
diff --git a/apps/storybook/src/plugin-reanimated/Slide.stories.tsx b/apps/storybook/src/plugin-reanimated/Slide.stories.tsx
index 9efdd14b..66f9b8ed 100644
--- a/apps/storybook/src/plugin-reanimated/Slide.stories.tsx
+++ b/apps/storybook/src/plugin-reanimated/Slide.stories.tsx
@@ -1,4 +1,3 @@
-import { Column } from '@plextv/react-native-lightning-components';
import type { Meta } from '@storybook/react-vite';
import {
SlideInDown,
@@ -10,6 +9,9 @@ import {
SlideOutRight,
SlideOutUp,
} from 'react-native-reanimated';
+
+import { Column } from '@plextv/react-native-lightning-components';
+
import { AnimationSampler } from './AnimationSampler';
export default {
diff --git a/apps/storybook/src/react-lightning-components/layout/Column.stories.tsx b/apps/storybook/src/react-lightning-components/layout/Column.stories.tsx
index 1e31f21d..c237291b 100644
--- a/apps/storybook/src/react-lightning-components/layout/Column.stories.tsx
+++ b/apps/storybook/src/react-lightning-components/layout/Column.stories.tsx
@@ -1,5 +1,7 @@
-import { Column } from '@plextv/react-lightning-components';
import type { Meta } from '@storybook/react-vite';
+
+import { Column } from '@plextv/react-lightning-components';
+
import {
ColorPalette,
DefaultStoryHeight,
@@ -49,9 +51,7 @@ export default {
},
} 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 (
;
+
+// ---------------------------------------------------------------------------
+// Shared helpers
+// ---------------------------------------------------------------------------
+
+const makeItems = (count: number) => Array.from({ length: count }, (_, i) => `Item ${i}`);
+
+const COLORS = {
+ teal: 0x4fafafff,
+ tealAlt: 0xafaf4fff,
+ yellow: 0xafaf4fff,
+ yellowAlt: 0xaf4fafff,
+ purple: 0x9f5fdfff,
+ purpleAlt: 0xdf5f9fff,
+ dark: 0x333333ff,
+ mid: 0x555555ff,
+ bg: 0x111111ff,
+};
+
+const Label = ({ text, w = 500, h = 30 }: { text: string; w?: number; h?: number }) => (
+
+ {text}
+
+);
+
+// ---------------------------------------------------------------------------
+// Vertical — The simplest possible list.
+// ---------------------------------------------------------------------------
+
+export const Vertical = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// Horizontal — Scrolling along the x-axis.
+// ---------------------------------------------------------------------------
+
+export const Horizontal = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// Grid — Multi-column layout using numColumns.
+// ---------------------------------------------------------------------------
+
+export const Grid = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// HeaderFooterSeparator — ListHeaderComponent, ListFooterComponent,
+// and ItemSeparatorComponent slots.
+// ---------------------------------------------------------------------------
+
+const HeaderBanner = () => ;
+const FooterBanner = () => ;
+const ItemDivider = () => ;
+
+export const HeaderFooterSeparator = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// EmptyList — Rendered when data is an empty array.
+// ---------------------------------------------------------------------------
+
+const EmptyState = () => (
+
+ Nothing here yet
+
+);
+
+export const EmptyList = () => (
+ }
+ />
+);
+
+// ---------------------------------------------------------------------------
+// ContentPadding — contentContainerStyle with padding and backgroundColor.
+// ---------------------------------------------------------------------------
+
+export const ContentPadding = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// SnapAlignment — Controls where focused items align within the viewport.
+// Three sub-stories: start (default), center, and end.
+// ---------------------------------------------------------------------------
+
+export const SnapStart = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+export const SnapCenter = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+export const SnapEnd = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// OverrideItemLayout — Per-item size and span customization. Here every
+// third item is taller, and the first item spans 2 columns in a grid.
+// ---------------------------------------------------------------------------
+
+export const OverrideItemLayout = () => (
+ {
+ if (index === 0) {
+ layout.span = 2;
+ layout.size = 150;
+ } else if (index % 3 === 0) {
+ layout.size = 140;
+ }
+ }}
+ renderItem={({ index, item }) => (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// InfiniteScroll — onEndReached appends more data when the user scrolls
+// near the bottom. onEndReachedThreshold controls the trigger distance.
+// ---------------------------------------------------------------------------
+
+export const InfiniteScroll = () => {
+ const [items, setItems] = useState(() => makeItems(20));
+ const loadingRef = useRef(false);
+
+ const handleEndReached = () => {
+ if (loadingRef.current) {
+ return;
+ }
+
+ loadingRef.current = true;
+
+ const next = Array.from({ length: 10 }, (_, i) => `Item ${items.length + i}`);
+
+ setItems((prev) => [...prev, ...next]);
+ loadingRef.current = false;
+ };
+
+ return (
+ }
+ listFooterSize={30}
+ renderItem={({ index, item }) => (
+
+ {item}
+
+ )}
+ />
+ );
+};
+
+// ---------------------------------------------------------------------------
+// ItemTypes — getItemType enables view recycling across different item
+// types. Items of the same type reuse each other's views, reducing
+// mount/unmount overhead.
+// ---------------------------------------------------------------------------
+
+type TypedItem = { id: number; type: 'text' | 'image' };
+
+const typedItems: TypedItem[] = Array.from({ length: 60 }, (_, i) => ({
+ id: i,
+ type: i % 3 === 0 ? 'image' : 'text',
+}));
+
+const TextCell = focusable<{ focused?: boolean; label: string }>(({ focused, label }, ref) => (
+
+ {label}
+
+));
+
+const ImageCell = focusable<{ focused?: boolean; index: number }>(({ focused, index }, ref) => (
+
+
+
+));
+
+export const ItemTypes = () => (
+ String(item.id)}
+ getItemType={(item) => item.type}
+ overrideItemLayout={(layout, item) => {
+ layout.size = item.type === 'image' ? 100 : 50;
+ }}
+ renderItem={({ item, index }) =>
+ item.type === 'image' ? (
+
+ ) : (
+
+ )
+ }
+ />
+);
+
+// ---------------------------------------------------------------------------
+// InitialScrollIndex — The list starts scrolled to a specific item.
+// ---------------------------------------------------------------------------
+
+export const InitialScrollIndex = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// ImperativeScrolling — Using the ref to call scrollToIndex, scrollToEnd,
+// and scrollToOffset programmatically.
+// ---------------------------------------------------------------------------
+
+const NavButton = focusable<{
+ focused?: boolean;
+ label: string;
+ onEnter: () => void;
+}>(({ focused, label, onEnter }, ref) => (
+ {
+ if (ev.remoteKey === 'Enter') {
+ onEnter();
+ }
+
+ return true;
+ }}
+ style={{
+ w: 140,
+ h: 40,
+ borderRadius: 8,
+ color: focused ? 0xcccc44ff : 0xcccc44aa,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ }}
+ >
+ {label}
+
+));
+
+export const ImperativeScrolling = () => {
+ const listRef = useRef(null);
+
+ return (
+
+
+ listRef.current?.scrollToIndex({ index: 0 })} />
+ listRef.current?.scrollToIndex({ index: 50, viewPosition: 0.5 })}
+ />
+ listRef.current?.scrollToEnd()} />
+
+ (
+
+ {item}
+
+ )}
+ />
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// DrawDistance — Controls how many pixels beyond the viewport are
+// pre-rendered. Lower values save memory; higher values prevent flashes
+// during fast scrolling.
+// ---------------------------------------------------------------------------
+
+export const DrawDistance = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
+
+// ---------------------------------------------------------------------------
+// AnimationDuration — Controls how fast scroll animations play.
+// A slower animation (600ms) makes the scroll behavior visible.
+// ---------------------------------------------------------------------------
+
+export const SlowAnimation = () => (
+ (
+
+ {item}
+
+ )}
+ />
+);
diff --git a/apps/storybook/src/react-lightning-components/text/StyledText.stories.tsx b/apps/storybook/src/react-lightning-components/text/StyledText.stories.tsx
index ea66e2fd..227795a4 100644
--- a/apps/storybook/src/react-lightning-components/text/StyledText.stories.tsx
+++ b/apps/storybook/src/react-lightning-components/text/StyledText.stories.tsx
@@ -1,5 +1,7 @@
+import React, { useState } from 'react';
+
import { StyledText } from '@plextv/react-lightning-components';
-import React from 'react';
+
import Button from '../../components/Button';
const LANGUAGE = {
@@ -89,12 +91,7 @@ export const TextWithTags = () => {
return (
-
+
);
};
@@ -113,8 +110,7 @@ export const MultiplePlaceholders = () => {
question: { fontStyle: 'italic' as const, color: 0xccdddd99 },
};
- const text =
- 'Hello {friendName}, your answer is {answer} for the question: {question}.';
+ const text = 'Hello {friendName}, your answer is {answer} for the question: {question}.';
return (
{
answer: 'C',
};
- const [isCorrect, setIsCorrect] = React.useState(false);
+ const [isCorrect, setIsCorrect] = useState(false);
const dynamicStyles = {
answer: {
@@ -177,14 +173,13 @@ export const MultilingualSupport = () => {
},
};
const languages = [LANGUAGE.EN, LANGUAGE.DE, LANGUAGE.IT];
- const [currentLanguage, setCurrentLanguage] = React.useState(LANGUAGE.EN);
+ const [currentLanguage, setCurrentLanguage] = useState(LANGUAGE.EN);
const firstLineText = PhoneFriendFirstLine[currentLanguage] as string;
const secondLineText = PhoneFriendSecondLine[currentLanguage] as string;
const changeLanguage = () => {
- const nextIndex =
- (languages.indexOf(currentLanguage) + 1) % languages.length;
+ const nextIndex = (languages.indexOf(currentLanguage) + 1) % languages.length;
const newLanguage = languages[nextIndex] as string;
setCurrentLanguage(newLanguage);
};
diff --git a/apps/storybook/src/react-lightning/Components.mdx b/apps/storybook/src/react-lightning/Components.mdx
index 6b11eacf..2c805c7b 100644
--- a/apps/storybook/src/react-lightning/Components.mdx
+++ b/apps/storybook/src/react-lightning/Components.mdx
@@ -1,5 +1,5 @@
import { Canvas, Meta, Source, Story, Controls } from '@storybook/addon-docs/blocks';
-import * as ComponentsStories from './Components.stories'
+import * as ComponentsStories from './Components.stories';
@@ -15,7 +15,7 @@ Similar to React, components in react-lightning are built using primitives.
There are three primitive components in react-lightning, all prefixed with
`lng-`. React primitives like `div`, `span`, and `img` are not used in react-lightning.
-- *lng-view*
+- _lng-view_
- `lng-view` is a container component that can hold other components, similar to
a `div` element in HTML.
- lng-text
@@ -31,7 +31,6 @@ There are three primitive components in react-lightning, all prefixed with
withToolbar
/>
-
## Styling
Styling in react-lightning is done using the `style` prop. The `style` prop is
@@ -40,9 +39,4 @@ can see a full list of the supported props from the [Lightning.js
source](https://github.com/lightning-js/renderer/blob/6f8e057b4928d40b7433382267b3133ebdcbb8a0/src/core/CoreNode.ts#L214),
with a few exceptions.
-
+
diff --git a/apps/storybook/src/react-lightning/Components.stories.tsx b/apps/storybook/src/react-lightning/Components.stories.tsx
index 1440eaac..96c4c274 100644
--- a/apps/storybook/src/react-lightning/Components.stories.tsx
+++ b/apps/storybook/src/react-lightning/Components.stories.tsx
@@ -15,9 +15,7 @@ export const Primitives = () => (
export const Styling = () => (
-
+
I've got some style
diff --git a/apps/storybook/src/react-lightning/Focus.mdx b/apps/storybook/src/react-lightning/Focus.mdx
index a8c1381d..deef5174 100644
--- a/apps/storybook/src/react-lightning/Focus.mdx
+++ b/apps/storybook/src/react-lightning/Focus.mdx
@@ -1,5 +1,5 @@
import { Canvas, Meta, Source } from '@storybook/addon-docs/blocks';
-import { SimpleFocusTree } from './examples/focus/FocusTree.stories'
+import { SimpleFocusTree } from './examples/focus/FocusTree.stories';
@@ -20,8 +20,4 @@ element to focus.
### Example
-
+
diff --git a/apps/storybook/src/react-lightning/examples/focus/AutoFocus.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/AutoFocus.stories.tsx
index 91f5657f..e79571b9 100644
--- a/apps/storybook/src/react-lightning/examples/focus/AutoFocus.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/AutoFocus.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 { FocusableImage } from '../../../components/FocusableImage';
export default {
diff --git a/apps/storybook/src/react-lightning/examples/focus/FocusGroup.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/FocusGroup.stories.tsx
index 887f2290..03f6271c 100644
--- a/apps/storybook/src/react-lightning/examples/focus/FocusGroup.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/FocusGroup.stories.tsx
@@ -1,6 +1,8 @@
-import { Column, Row } from '@plextv/react-lightning-components';
import type { Meta } from '@storybook/react-vite';
import { useCallback, useMemo, useState } from 'react';
+
+import { Column, Row } from '@plextv/react-lightning-components';
+
import Button from '../../../components/Button';
import { FocusableImage } from '../../../components/FocusableImage';
@@ -49,10 +51,7 @@ export const DynamicFocusDisabling = () => {
{row.map((state, index) => (
-
+
))}
diff --git a/apps/storybook/src/react-lightning/examples/focus/FocusLayers.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/FocusLayers.stories.tsx
index 5b6ed1fe..bcc0c513 100644
--- a/apps/storybook/src/react-lightning/examples/focus/FocusLayers.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/FocusLayers.stories.tsx
@@ -1,3 +1,6 @@
+import type { Meta } from '@storybook/react-vite';
+import { useEffect, useRef, useState } from 'react';
+
import {
FocusGroup,
type KeyEvent,
@@ -5,8 +8,7 @@ import {
useFocusManager,
} from '@plextv/react-lightning';
import { Column, Row } from '@plextv/react-lightning-components';
-import type { Meta } from '@storybook/react-vite';
-import { useEffect, useRef, useState } from 'react';
+
import Button from '../../../components/Button';
export default {
@@ -79,9 +81,7 @@ export const ModalExample = () => {
- {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 ce550e9a..a22684c5 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 ef08809b..3495078b 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 72042280..068f951e 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 30838663..84f1dd5b 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 cfc70546..2ab2e254 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 c6076462..a210f148 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 0333e1b3..8cc84676 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 55b0f496..c32f0f41 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 998503f7..00000000
--- 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 2e9ef039..fa1dc5ed 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 67f1c61e..71387749 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 55169b3f..fb1cd544 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 b618c35a..06bafb0c 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 66d8c917..bb40d6e5 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 a8273147..76361017 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 27c79e63..c4f25579 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 9c3941b7..9d545c20 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 31bad689..f4fa833a 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 eef916df..b88f0baf 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 31a86e59..df207c95 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 6f7b5268..dc7cbfcd 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 f6497c17..f4aa2088 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 51649cee..4a9f68db 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 6c97f1d9..35332d40 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 e2e9b71f..53865b12 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 e83ce712..59e97d3f 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 5bbc9722..e4b0d25d 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