diff --git a/.changeset/bright-lions-march.md b/.changeset/bright-lions-march.md
new file mode 100644
index 0000000..925ed75
--- /dev/null
+++ b/.changeset/bright-lions-march.md
@@ -0,0 +1,6 @@
+---
+"@plextv/react-lightning-components": patch
+"@plextv/react-native-lightning-components": patch
+---
+
+feat: Add VirtualList component for fast virtualized lists with view recycling
diff --git a/.changeset/cold-pens-glow.md b/.changeset/cold-pens-glow.md
new file mode 100644
index 0000000..157c372
--- /dev/null
+++ b/.changeset/cold-pens-glow.md
@@ -0,0 +1,13 @@
+---
+"@plextv/react-lightning": patch
+"@plextv/react-lightning-plugin-css-transform": patch
+"@plextv/react-lightning-plugin-flexbox": patch
+"@plextv/react-lightning-plugin-flexbox-lite": patch
+"@plextv/react-lightning-plugin-reanimated": patch
+"@plextv/react-native-lightning": patch
+"@plextv/vite-plugin-msdf-fontgen": patch
+"@plextv/vite-plugin-react-native-lightning": patch
+"@plextv/vite-plugin-react-reanimated-lightning": patch
+---
+
+chore: Update dependencies and migrate from Biome to oxc
diff --git a/.changeset/gentle-foxes-sing.md b/.changeset/gentle-foxes-sing.md
new file mode 100644
index 0000000..7623732
--- /dev/null
+++ b/.changeset/gentle-foxes-sing.md
@@ -0,0 +1,5 @@
+---
+"@plextv/react-native-lightning": patch
+---
+
+feat: Export `NativeCanvas` for embedding a Lightning canvas inside a React Native Lightning tree.
diff --git a/.changeset/swift-hawks-dive.md b/.changeset/swift-hawks-dive.md
new file mode 100644
index 0000000..7704b52
--- /dev/null
+++ b/.changeset/swift-hawks-dive.md
@@ -0,0 +1,5 @@
+---
+"@plextv/react-lightning-plugin-flexbox": patch
+---
+
+refactor: Rework YogaManager, LightningManager, and the worker pipeline for faster prop translation and a non-flex fast path.
diff --git a/.changeset/warm-clouds-rise.md b/.changeset/warm-clouds-rise.md
new file mode 100644
index 0000000..c6763df
--- /dev/null
+++ b/.changeset/warm-clouds-rise.md
@@ -0,0 +1,5 @@
+---
+"@plextv/react-lightning": patch
+---
+
+feat: Add NodeResizeObserver, FocusManager.setFocusedChild, the `resized` element event, and a CanvasProps type export.
diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml
index 4f32613..1bd73db 100644
--- a/.github/workflows/deploy-storybook.yml
+++ b/.github/workflows/deploy-storybook.yml
@@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-node@v6.0.0
with:
node-version-file: package.json
- cache: "pnpm"
+ cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml
index d1d6189..8069f0d 100644
--- a/.github/workflows/release-packages.yml
+++ b/.github/workflows/release-packages.yml
@@ -3,7 +3,7 @@ name: Release Packages
on:
workflow_dispatch:
workflow_run:
- workflows: ["Run tests"]
+ workflows: ['Run tests']
branches: [main]
types:
- completed
@@ -34,8 +34,8 @@ jobs:
- uses: actions/setup-node@v6.0.0
with:
node-version-file: package.json
- cache: "pnpm"
- registry-url: "https://registry.npmjs.org"
+ cache: 'pnpm'
+ registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -46,8 +46,8 @@ jobs:
uses: changesets/action@v1.5.3
with:
version: pnpm run ci:version
- commit: "chore: Update versions"
- title: "chore: Update versions"
+ commit: 'chore: Update versions'
+ title: 'chore: Update versions'
publish: pnpm run ci:publish
env:
# When you use the repository's GITHUB_TOKEN to perform tasks, events
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6becc02..3308b8a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,7 +27,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: package.json
- cache: "pnpm"
+ cache: 'pnpm'
- run: pnpm install --frozen-lockfile
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..5652732
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "singleQuote": true,
+ "insertFinalNewline": true,
+ "trailingComma": "all",
+ "experimentalSortImports": {
+ "internalPattern": ["@plextv/", "@repo/"],
+ "partitionByNewline": false
+ },
+ "ignorePatterns": [".changeset", "**/public", "**/dist"]
+}
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 0000000..ec29658
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": [
+ "eslint",
+ "typescript",
+ "unicorn",
+ "oxc",
+ "react",
+ "react-perf",
+ "jsx-a11y",
+ "import"
+ ],
+ "rules": {
+ "typescript/consistent-type-imports": "error",
+ "typescript/no-non-null-assertion": "error",
+ "react-hooks/exhaustive-deps": "off",
+ "jsx-a11y/no-autofocus": "off",
+ "jsx-a11y/no-static-element-interactions": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/mouse-events-have-key-events": "off",
+ "jsx-a11y/click-events-have-key-events": "off"
+ },
+ "ignorePatterns": ["dist", ".changeset", "**/public"]
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index de51190..99e2f7d 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,5 +1,3 @@
{
- "recommendations": [
- "biomejs.biome"
- ]
+ "recommendations": ["oxc.oxc-vscode"]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 604f553..8ce25ad 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,38 +1,30 @@
{
"editor.codeActionsOnSave": {
- "quickfix.biome": "explicit",
- "source.organizeImports.biome": "explicit",
- "source.fixAll.biome": "explicit"
+ "source.fixAll.oxc": "explicit"
},
- "typescript.tsdk": "node_modules/typescript/lib",
- "cSpell.words": [
- "lightningjs",
- "threadx"
- ],
- "biome.enabled": true,
- "biome.lsp.bin": "./node_modules/.bin/biome",
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
+ "oxc.enable.oxfmt": true,
+ "oxc.enable.oxlint": true,
"json.schemas": [
{
- "fileMatch": [
- "manifest.json"
- ],
+ "fileMatch": ["manifest.json"],
"url": "https://json.schemastore.org/chrome-manifest.json"
}
],
"[javascript]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescriptreact]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascriptreact]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"vitest.disableWorkspaceWarning": true
-}
\ No newline at end of file
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..d31c2eb
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,282 @@
+# CLAUDE.md — react-lightning
+
+## Repository Overview
+
+**react-lightning** is a monorepo providing a React reconciler for [Lightning.js](https://lightningjs.io/), enabling developers to build Lightning.js apps using React and React Native patterns.
+
+- **Package manager:** pnpm@10.12.3 (workspace monorepo)
+- **Build orchestrator:** Turbo 2.7.5
+- **Node requirement:** >=22 (Volta pins: node 24.11.1, pnpm 10.12.3)
+
+---
+
+## Monorepo Structure
+
+```
+apps/
+ react-lightning-example/ # Main React + Lightning.js example app
+ react-native-lightning-example/ # React Native + Lightning example app
+ storybook/ # Component documentation (Storybook 9)
+
+packages/
+ react-lightning/ # Core React reconciler for Lightning.js
+ react-lightning-components/ # Pre-built UI components (Column, Row, VirtualList, etc.)
+ react-native-lightning/ # React Native API layer on Lightning
+ react-native-lightning-components/
+ configs/ # Shared tsconfig, vite, tsdown configs
+ plugin-flexbox/ # Yoga-based flexbox layout plugin
+ plugin-flexbox-lite/ # Lightweight flexbox alternative
+ plugin-css-transform/ # CSS property → Lightning property transforms
+ plugin-reanimated/ # Reanimated animation integration
+ vite-plugin-msdf-fontgen/ # Vite plugin for MSDF font generation
+ vite-plugin-react-native-lightning/
+ vite-plugin-react-reanimated-lightning/
+
+templates/
+ app-template/ # Starter template for new projects
+
+types/ # Global TypeScript type definitions
+scripts/ # Build and maintenance scripts
+```
+
+---
+
+## Key Commands
+
+### Development
+
+```bash
+pnpm dev # Run all dev servers
+pnpm dev --filter=@plextv/react-lightning-example # Run a specific app
+```
+
+### Testing
+
+```bash
+pnpm test # Run all unit tests (vitest)
+```
+
+### Linting & Formatting
+
+```bash
+pnpm lint # Lint with oxlint
+pnpm lint:format # Lint + format with oxlint and oxfmt
+```
+
+### Type Checking
+
+```bash
+pnpm check:types # TypeScript type checking across all packages
+```
+
+### Building
+
+```bash
+pnpm build # Build all packages (turbo)
+pnpm build:tsdown # Build library packages with tsdown
+pnpm build:vite # Build app/vite packages
+pnpm build:types # Generate type declarations only
+pnpm build:storybook # Build Storybook docs
+```
+
+### Maintenance
+
+```bash
+pnpm clean # Remove dist directories
+pnpm nuke # Deep clean: clean + remove node_modules + reinstall
+pnpm unused # Check for unused dependencies
+```
+
+### Versioning & Release
+
+```bash
+pnpm changeset # Create a changeset (required for releases)
+pnpm ci:version # Bump versions (CI only)
+pnpm ci:publish # Publish to npm (CI only)
+```
+
+---
+
+## Build System
+
+### Library packages (tsdown)
+
+Core packages (`react-lightning`, `react-lightning-components`, plugins, etc.) are bundled with **tsdown 0.19.0**.
+
+- Shared config: `packages/configs/tsdown.config.ts`
+- Output: ESM + CJS with `dist/es/` and `dist/cjs/` directories
+- Dual dev/prod builds: `index.development.js` and `index.production.js`
+- CJS entry auto-selects dev vs. prod based on `NODE_ENV`
+- Type declarations emitted to `dist/types/`
+- React Compiler applied via `@rollup/plugin-babel` + `babel-plugin-react-compiler`
+
+### App packages (Vite)
+
+Example apps and Storybook are built with **Vite 7.3.1**.
+
+- Shared config base: `packages/configs/`
+- Key plugins: `@vitejs/plugin-react`, `@vitejs/plugin-legacy` (Chrome 69+), `vite-tsconfig-paths`, `vite-plugin-msdf-fontgen`
+
+---
+
+## TypeScript
+
+- **Base config:** `packages/configs/tsconfig.json`
+- **React library config:** `packages/configs/tsconfig.react-library.json`
+- **Key settings:** `target: ES2022`, `module: ESNext`, `moduleResolution: Bundler`, `strict: true`, `isolatedModules: true`, `isolatedDeclarations: true`, `jsx: react-jsx`
+- All packages extend `@repo/configs/tsconfig.react-library.json`
+- Use `workspace:*` protocol for internal package dependencies
+
+---
+
+## Code Style
+
+- **Formatter:** oxfmt 0.35 — run `pnpm lint:format` to fix
+- **Linter:** oxlint 1.50 — run `pnpm lint` to check
+- **Indentation:** 2 spaces
+- **Quotes:** Single quotes in JS/TS
+- **No class components** — functional components + hooks only
+- **No default exports on hooks or utilities** — prefer named exports
+- Pre-commit hook via husky runs `oxlint`
+- `.npmrc` enforces exact versions (`save-exact=true`)
+
+---
+
+## Architecture
+
+### Core Layer: `@plextv/react-lightning`
+
+The reconciler bridges React's virtual DOM to Lightning.js node tree.
+
+**Element system** (`src/element/`):
+
+- `LightningViewElement` — container (like `
`)
+- `LightningImageElement` — image rendering
+- `LightningTextElement` — text rendering
+- All wrap Lightning's `INode` with React-specific prop handling
+
+**Plugin system:**
+Plugins extend renderer behavior via three hooks:
+
+```typescript
+type Plugin
= {
+ handledStyleProps?: Set;
+ init?(renderer, reconciler): Promise;
+ onCreateInstance?(instance, props, fiber): void;
+ transformProps?(instance, props): object | null; // return null to stop pipeline
+};
+```
+
+The `handledStyleProps` property defines the properties that the plugin will act on. This
+will allow us to skip plugin processing.
+
+Built-in plugins: `plugin-flexbox`, `plugin-css-transform`, `plugin-reanimated`
+
+**Focus management** (`src/focus/`):
+
+- Tree-based focus tracking with support for layers (modals)
+- `FocusManager`, `FocusGroup`, `focusable()` HOC
+- Hooks: `useFocus()`, `useFocusManager()`
+- Features: auto-focus, focus traps, focus redirection, custom navigation
+
+**Input handling** (`src/input/`):
+
+- `Keys` constants, `KeyPressHandler` component
+- Full event bubbling, capture, and `preventDefault` support
+
+**Renderer** (`src/render/`):
+
+- `createRoot()` — main entry point; accepts `RenderOptions` (renderer, fonts, shaders, plugins)
+- Lightning DevTools integration in dev builds
+- `import.meta.env.DEV` guards dev-only code
+
+### Component Library: `@plextv/react-lightning-components`
+
+Pre-built Lightning-optimized components:
+
+- **Layout:** `Column`, `Row` (flexbox via `plugin-flexbox`)
+- **Lists:** `VirtualList` (virtualized list with view recycling, optimized for low-powered devices)
+- **Text:** `StyledText`
+- **Dev:** `FPSMonitor`
+
+### React Native Layer: `@plextv/react-native-lightning`
+
+Provides a React Native-compatible API on top of Lightning:
+
+- RN-compatible components: `View`, `Text`, `Image`, `ScrollView`, etc.
+- Built on `react-native-web` polyfills + `its-fine` for context bridging
+- RN styling patterns supported
+
+### React Native Components: `@plextv/react-native-lightning-components`
+
+RN-style wrappers for Lightning components:
+
+- **Layout:** `Column`, `Row`
+
+---
+
+## Naming Conventions
+
+| Thing | Convention | Example |
+| ------------------ | ------------------------------ | --------------------------------------- |
+| React components | PascalCase | `LightningViewElement`, `FocusGroup` |
+| Hooks | `use` prefix + camelCase | `useFocus`, `useCombinedRef` |
+| Types / interfaces | PascalCase + descriptor suffix | `LightningElementProps`, `FocusNode` |
+| Plugin files | kebab-case directory | `plugin-flexbox/` |
+| Utility functions | camelCase | `bubbleEvent`, `traceWrap` |
+
+---
+
+## Testing
+
+- **Test runner:** vitest 4.0.17
+- **Config:** `vitest.workspace.ts` at root
+- Tests are colocated with source files (`.spec.ts` or `.test.ts` suffix)
+- `passWithNoTests: true` — packages without tests do not fail CI
+- Mock utilities live in `src/mocks/`
+
+---
+
+## CI/CD
+
+All workflows are in `.github/workflows/`:
+
+| Workflow | Trigger | What it does |
+| ---------------------- | ----------------------------------- | ---------------------------------------------- |
+| `test.yml` | PRs to `main`, pushes to `main` | Runs `pnpm lint` then `pnpm test` |
+| `release-packages.yml` | After `test.yml` succeeds on `main` | Changesets version bump + npm publish via OIDC |
+| `deploy-storybook.yml` | Pushes to `main` | Builds and deploys Storybook to GitHub Pages |
+
+CI uses `--frozen-lockfile` installs. Never modify `pnpm-lock.yaml` manually.
+
+---
+
+## Releases & Versioning
+
+- Releases are managed by **Changesets** (`@changesets/cli`)
+- Every PR that changes user-facing behavior needs a changeset: `pnpm changeset`
+- Changeset config: `.changeset/config.json`
+- Packages are published to npm under the `@plextv` scope
+- CI uses npm OIDC trust (no token required in secrets)
+
+---
+
+## Key Dependencies
+
+| Package | Version | Purpose |
+| ----------------------------- | ------------ | --------------------------------------- |
+| `@lightningjs/renderer` | 3.0.0-beta20 | Underlying Lightning.js renderer |
+| `react-reconciler` | 0.33.0 | React custom reconciler API |
+| `yoga-layout` | 3.2.1 | Flexbox layout engine (plugin-flexbox) |
+| `react-native-web` | 0.21.2 | RN polyfills (react-native-lightning) |
+| `its-fine` | 2.0.0 | Context bridge for concurrent renderers |
+| `tseep` | 1.3.1 | Typed event emitter |
+| `babel-plugin-react-compiler` | 1.0.0 | React Compiler for auto-memoization |
+
+---
+
+## Docs & Resources
+
+- Official docs: https://plexinc.github.io/react-lightning/
+- Storybook (deployed from `main`): GitHub Pages
+- GitHub: https://github.com/plexinc/react-lightning
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 27082ca..9a79b93 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,7 +69,6 @@ $ pnpm lint
Reviewers should use the following questions to evaluate the implementation for correctness/completeness and ensure all housekeeping items have been addressed prior to merging the code.
- Correctness/completeness
-
1. Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
1. Is the intention of the code captured in relevant tests?
- Does the description of each test accurately represent the assertions?
@@ -83,7 +82,6 @@ Reviewers should use the following questions to evaluate the implementation for
- easier to maintain (easier to change, harder to accidentally break)
- Housekeeping
-
1. Does the title and description of the PR reference the correct issue and does it use the correct conventional commit type (e.g., fix, feat, test, breaking change etc)?
1. If there are new TODOs, has a related issue been created?
1. Should any documentation be updated?
diff --git a/apps/react-lightning-example/index.html b/apps/react-lightning-example/index.html
index 331e0f5..5c52a8b 100644
--- a/apps/react-lightning-example/index.html
+++ b/apps/react-lightning-example/index.html
@@ -1,4 +1,4 @@
-
+
React-Lightning Sample App
diff --git a/apps/react-lightning-example/package.json b/apps/react-lightning-example/package.json
index 2ec5bcf..249f8db 100644
--- a/apps/react-lightning-example/package.json
+++ b/apps/react-lightning-example/package.json
@@ -1,19 +1,18 @@
{
"name": "@plextv/react-lightning-example",
- "description": "Sample implementation of @plextv/react-lightning in a React app",
"version": "0.4.0",
- "author": "Plex Inc.",
+ "private": true,
+ "description": "Sample implementation of @plextv/react-lightning in a React app",
+ "bugs": {
+ "url": "https://github.com/plexinc/react-lightning/issues/new"
+ },
"license": "MIT",
+ "author": "Plex Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/plexinc/react-lightning.git"
},
- "bugs": {
- "url": "https://github.com/plexinc/react-lightning/issues/new"
- },
- "packageManager": "pnpm@10.12.3",
"type": "module",
- "private": true,
"scripts": {
"build": "vite build",
"clean": "del ./dist",
@@ -23,26 +22,33 @@
"test:unit": "vitest run --passWithNoTests"
},
"dependencies": {
- "@lightningjs/renderer": "3.0.0-beta20",
+ "@lightningjs/renderer": "catalog:apps",
"@plextv/react-lightning": "workspace:*",
"@plextv/react-lightning-components": "workspace:*",
"@plextv/react-lightning-plugin-css-transform": "workspace:*",
"@plextv/react-lightning-plugin-flexbox": "workspace:*",
- "react": "19.2.3",
- "react-dom": "19.2.3",
- "react-router-dom": "7.12.0",
- "swr": "2.3.8"
+ "react": "catalog:apps",
+ "react-dom": "catalog:apps",
+ "react-router-dom": "7.14.1",
+ "swr": "2.4.1"
},
"devDependencies": {
"@plextv/vite-plugin-msdf-fontgen": "workspace:*",
"@repo/configs": "workspace:*",
- "@types/react": "19.2.8",
- "@types/react-dom": "19.2.3",
- "@vitejs/plugin-legacy": "7.2.1",
- "@vitejs/plugin-react": "5.1.2",
- "vite-tsconfig-paths": "6.0.4"
+ "@rolldown/plugin-babel": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-legacy": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "babel-plugin-react-compiler": "catalog:"
},
"volta": {
"extends": "../../package.json"
+ },
+ "packageManager": "pnpm@10.12.3",
+ "depcheck": {
+ "ignoreMatches": [
+ "babel-plugin-react-compiler"
+ ]
}
}
diff --git a/apps/react-lightning-example/src/api/useHubItemsData.ts b/apps/react-lightning-example/src/api/useHubItemsData.ts
index 929b938..451052f 100644
--- a/apps/react-lightning-example/src/api/useHubItemsData.ts
+++ b/apps/react-lightning-example/src/api/useHubItemsData.ts
@@ -1,4 +1,5 @@
import useSWR, { type SWRResponse } from 'swr';
+
import { getHeaders } from './getHeaders';
import { getToken } from './getToken';
import type { HubItemsRoot } from './types/HubItems';
@@ -7,8 +8,7 @@ const baseUrl = 'https://vod.provider.plex.tv';
const args = {
contentDirectoryID: 'movies',
- excludeElements:
- 'Actor,Collection,Country,Label,Mood,Part,Producer,Similar,Photo,Vast,Topic',
+ excludeElements: 'Actor,Collection,Country,Label,Mood,Part,Producer,Similar,Photo,Vast,Topic',
excludeFields: 'file,tagline',
includeDetails: '1',
};
diff --git a/apps/react-lightning-example/src/api/useHubsData.ts b/apps/react-lightning-example/src/api/useHubsData.ts
index 8cd4d7a..fe58629 100644
--- a/apps/react-lightning-example/src/api/useHubsData.ts
+++ b/apps/react-lightning-example/src/api/useHubsData.ts
@@ -1,4 +1,5 @@
import useSWR, { type SWRResponse } from 'swr';
+
import { getHeaders } from './getHeaders';
import { getToken } from './getToken';
import type { HubRoot } from './types/Hubs';
diff --git a/apps/react-lightning-example/src/components/AnimatedImage.tsx b/apps/react-lightning-example/src/components/AnimatedImage.tsx
index 7b8fdac..58cd5d1 100644
--- a/apps/react-lightning-example/src/components/AnimatedImage.tsx
+++ b/apps/react-lightning-example/src/components/AnimatedImage.tsx
@@ -1,6 +1,7 @@
-import type { LightningImageElement } from '@plextv/react-lightning';
import { type FC, useEffect, useMemo, useRef, useState } from 'react';
+import type { LightningImageElement } from '@plextv/react-lightning';
+
function randomInt(max: number): number {
return Math.round(Math.random() * max);
}
diff --git a/apps/react-lightning-example/src/components/Button.tsx b/apps/react-lightning-example/src/components/Button.tsx
index f5b5932..e3a9a73 100644
--- a/apps/react-lightning-example/src/components/Button.tsx
+++ b/apps/react-lightning-example/src/components/Button.tsx
@@ -1,3 +1,5 @@
+import { type FC, useCallback } from 'react';
+
import type {
KeyEvent,
LightningElement,
@@ -5,7 +7,6 @@ import type {
LightningViewElementProps,
} from '@plextv/react-lightning';
import { Keys, useFocus } from '@plextv/react-lightning';
-import { type FC, useCallback } from 'react';
const containerStyles: LightningElementStyle = {
w: 330,
@@ -47,12 +48,7 @@ const Button: FC = (props) => {
const color = focused ? 0xcccc44ff : 0xcccc44aa;
return (
-
+
{props.children}
);
diff --git a/apps/react-lightning-example/src/components/HubItem.tsx b/apps/react-lightning-example/src/components/HubItem.tsx
index 7a9843d..c26f657 100644
--- a/apps/react-lightning-example/src/components/HubItem.tsx
+++ b/apps/react-lightning-example/src/components/HubItem.tsx
@@ -1,3 +1,5 @@
+import type { FC } from 'react';
+
import {
type LightningViewElement,
type LightningViewElementProps,
@@ -5,7 +7,7 @@ import {
useFocus,
} from '@plextv/react-lightning';
import { Column } from '@plextv/react-lightning-components';
-import type { FC } from 'react';
+
import { getImageUrl } from '../api/getImageUrl';
import type { Metadata } from '../api/types/Metadata';
@@ -25,10 +27,7 @@ export const HubItem: FC = ({ metadata, style, ...rest }) => {
style={{ ...style, scale: focused ? 1.2 : 1 }}
transition={{ scale: { duration: 250 } }}
>
-
+
= (props) => {
const [horizontalOffset, setHorizontalOffset] = useState(0);
const handleFocus = useCallback((element: LightningElement) => {
- setHorizontalOffset(
- Math.min(0, -element.node.x - element.node.w / 2 + 1920 / 2),
- );
+ setHorizontalOffset(Math.min(0, -element.node.x - element.node.w / 2 + 1920 / 2));
}, []);
if (isLoading) {
diff --git a/apps/react-lightning-example/src/components/PosterCollection.tsx b/apps/react-lightning-example/src/components/PosterCollection.tsx
index b5e4a5d..f7ece62 100644
--- a/apps/react-lightning-example/src/components/PosterCollection.tsx
+++ b/apps/react-lightning-example/src/components/PosterCollection.tsx
@@ -1,4 +1,5 @@
import type { FC } from 'react';
+
import AnimatedImage from './AnimatedImage';
interface Props {
@@ -12,11 +13,7 @@ const PosterCollection: FC = ({ posterCount }) => {
posters.push();
}
- return (
-
- {posters}
-
- );
+ return {posters};
};
export { PosterCollection };
diff --git a/apps/react-lightning-example/src/components/ScrollItem.tsx b/apps/react-lightning-example/src/components/ScrollItem.tsx
new file mode 100644
index 0000000..36d1068
--- /dev/null
+++ b/apps/react-lightning-example/src/components/ScrollItem.tsx
@@ -0,0 +1,76 @@
+import { type ReactNode, useRef } from 'react';
+
+import { focusable } from '@plextv/react-lightning';
+
+export type ScrollItemProps = {
+ children: ReactNode;
+ index: number;
+ width?: number;
+ height?: number;
+ focused?: boolean;
+ horizontal?: boolean;
+ color: number;
+ altColor: number;
+};
+
+export const ScrollItem = focusable(
+ ({ color, altColor, index, focused, horizontal, width = 200, height = 75, children }, ref) => {
+ const isImage = useRef(Math.random() < 0.5).current;
+ const multiplier = index % 3 === 0 ? (horizontal ? 1.25 : 1.5) : 1;
+ const finalColor = index % 3 === 0 ? altColor : color;
+ const finalWidth = Math.round(horizontal ? width * multiplier : width);
+ const finalHeight = Math.round(horizontal ? height : height * multiplier);
+ const imageUrl = isImage
+ ? `https://picsum.photos/${horizontal ? finalWidth : finalWidth + 50}/${horizontal ? finalHeight + 25 : finalHeight}?seed=${index}`
+ : null;
+
+ return (
+
+ {imageUrl ? (
+
+ ) : (
+
+ {children}
+
+ )}
+
+ );
+ },
+);
+
+ScrollItem.displayName = 'ScrollItem';
diff --git a/apps/react-lightning-example/src/index.tsx b/apps/react-lightning-example/src/index.tsx
index f4e012c..1879f8a 100644
--- a/apps/react-lightning-example/src/index.tsx
+++ b/apps/react-lightning-example/src/index.tsx
@@ -1,17 +1,21 @@
-import { Canvas, type RenderOptions } from '@plextv/react-lightning';
-import { plugin as cssTransformPlugin } from '@plextv/react-lightning-plugin-css-transform';
-import { plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox';
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+
+import { Canvas, type RenderOptions } from '@plextv/react-lightning';
+import { plugin as cssTransformPlugin } from '@plextv/react-lightning-plugin-css-transform';
+import { FlexRoot, plugin as flexPlugin } from '@plextv/react-lightning-plugin-flexbox';
+
import { keyMap } from './keyMap';
import { AnimationPage } from './pages/AnimationPage';
import { BrowsePage } from './pages/BrowsePage';
import { LayoutPage } from './pages/LayoutPage';
+import { NestedListPage } from './pages/NestedListPage';
import { Page60 } from './pages/Page60';
import { PosterPage } from './pages/PosterPage';
import { ShaderPage } from './pages/ShaderPage';
import { TexturePage } from './pages/TexturePage';
import { TransformsPage } from './pages/TransformsPage';
+import { VirtualListPage } from './pages/VirtualListPage';
import { MyCustomShader } from './shaders/MyCustomShader';
import { MyCustomTexture } from './shaders/MyCustomTexture';
@@ -24,6 +28,10 @@ const router = createBrowserRouter([
path: '/flex-test',
element: ,
},
+ {
+ path: '/virtual-list',
+ element: ,
+ },
{
path: '/poster',
element: ,
@@ -44,6 +52,10 @@ const router = createBrowserRouter([
path: '/transforms',
element: ,
},
+ {
+ path: '/nested-list',
+ element: ,
+ },
{
path: '/page60',
element: ,
@@ -83,7 +95,9 @@ const options: RenderOptions = {
const App = () => (
);
diff --git a/apps/react-lightning-example/src/pages/AnimationPage.tsx b/apps/react-lightning-example/src/pages/AnimationPage.tsx
index a1e32a1..6390ddd 100644
--- a/apps/react-lightning-example/src/pages/AnimationPage.tsx
+++ b/apps/react-lightning-example/src/pages/AnimationPage.tsx
@@ -1,5 +1,7 @@
-import { Column } from '@plextv/react-lightning-components';
import { type FC, useCallback, useState } from 'react';
+
+import { Column } from '@plextv/react-lightning-components';
+
import Button from '../components/Button';
import { PosterCollection } from '../components/PosterCollection';
@@ -16,9 +18,7 @@ export const AnimationPage: FC = () => {
return (
<>
-
- Poster Count: {numPosters}
-
+ Poster Count: {numPosters}
diff --git a/apps/react-lightning-example/src/pages/BrowsePage.tsx b/apps/react-lightning-example/src/pages/BrowsePage.tsx
index d00c2a2..d947b47 100644
--- a/apps/react-lightning-example/src/pages/BrowsePage.tsx
+++ b/apps/react-lightning-example/src/pages/BrowsePage.tsx
@@ -1,6 +1,8 @@
+import { type FC, useCallback, useState } from 'react';
+
import type { LightningElement } from '@plextv/react-lightning';
import { Column } from '@plextv/react-lightning-components';
-import { type FC, useCallback, useState } from 'react';
+
import { useHubsData } from '../api/useHubsData';
import { HubRow } from '../components/HubRow';
@@ -9,9 +11,7 @@ export const BrowsePage: FC = () => {
const [verticalOffset, setVerticalOffset] = useState(0);
const handleFocus = useCallback((element: LightningElement) => {
- setVerticalOffset(
- Math.min(0, -element.node.y - element.node.h / 2 + 1080 / 2),
- );
+ setVerticalOffset(Math.min(0, -element.node.y - element.node.h / 2 + 1080 / 2));
}, []);
if (isLoading) {
diff --git a/apps/react-lightning-example/src/pages/LayoutPage.tsx b/apps/react-lightning-example/src/pages/LayoutPage.tsx
index 471455e..07cc3c6 100644
--- a/apps/react-lightning-example/src/pages/LayoutPage.tsx
+++ b/apps/react-lightning-example/src/pages/LayoutPage.tsx
@@ -1,8 +1,9 @@
-import { focusable, type LightningImageElement } from '@plextv/react-lightning';
-import { Column, Row } from '@plextv/react-lightning-components';
import type { FC, ForwardedRef } from 'react';
import { useEffect, useMemo } from 'react';
+import { focusable, type LightningImageElement } from '@plextv/react-lightning';
+import { Column, Row } from '@plextv/react-lightning-components';
+
const RandomImage = focusable<{ autoFocus?: boolean }>(({ focused }, ref) => {
const seed = useMemo(() => Math.random() * 10000, []);
diff --git a/apps/react-lightning-example/src/pages/NestedListPage.tsx b/apps/react-lightning-example/src/pages/NestedListPage.tsx
new file mode 100644
index 0000000..e6d3391
--- /dev/null
+++ b/apps/react-lightning-example/src/pages/NestedListPage.tsx
@@ -0,0 +1,115 @@
+import { useEffect, useState } from 'react';
+
+import { focusable } from '@plextv/react-lightning';
+import VirtualList from '@plextv/react-lightning-components/lists/VirtualList';
+
+type RowData = {
+ id: number;
+ label: string;
+ items: string[];
+};
+
+const COLORS = [0x4fafafff, 0xafaf4fff, 0x9f5fdfff, 0xdf5f9fff, 0x5fdf5fff];
+const ROWS: RowData[] = Array.from({ length: 30 }, (_, i) => ({
+ id: i,
+ label: `Row ${i}`,
+ items: i === 2 ? [] : Array.from({ length: 40 }, (_, j) => `R${i} Item ${j}`),
+}));
+
+const SMALL_ROW_IDS = new Set([3, 23]);
+const isSmallRow = (id: number) => SMALL_ROW_IDS.has(id);
+
+const InnerItem = focusable<{
+ focused?: boolean;
+ label: string;
+ color: number;
+ small?: boolean;
+ autoFocus?: boolean;
+}>(
+ ({ focused, label, color, small }, ref) => {
+ const timeoutDuration = Math.random() * 1000 + 6;
+
+ const [w, setW] = useState(0);
+ const [h, setH] = useState(0);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setW(small ? 130 : 160);
+ setH(small ? 50 : 90);
+ }, timeoutDuration);
+ }, [small]);
+
+ return (
+
+
+ {label}
+
+
+ );
+ },
+ 'InnerItem',
+ (props) => ({ autoFocus: props.autoFocus ?? false }),
+);
+
+const RowRenderer = ({ item }: { item: RowData }) => {
+ const color = COLORS?.[item.id % COLORS.length] ?? 0xff4f4fff;
+ const small = isSmallRow(item.id);
+ const itemHeight = small ? 50 : 90;
+
+ return (
+
+ {item.label}
+
+ `${item.id}-${index}`}
+ renderItem={({ item: label, shouldFocus }) => (
+
+ )}
+ />
+
+ );
+};
+
+export const NestedListPage = () => (
+
+
+ Nested VirtualList — Scroll Persistence
+
+
+ Scroll inner rows, then scroll outer list away and back. Position should be preserved.
+
+ {
+ if (row.items.length === 0) {
+ layout.size = 0;
+ }
+ }}
+ style={{ w: 960, h: 480, y: 55 }}
+ keyExtractor={(item) => String(item.id)}
+ renderItem={({ item }) => }
+ />
+
+);
diff --git a/apps/react-lightning-example/src/pages/Page60.tsx b/apps/react-lightning-example/src/pages/Page60.tsx
index b1f77cb..cefe213 100644
--- a/apps/react-lightning-example/src/pages/Page60.tsx
+++ b/apps/react-lightning-example/src/pages/Page60.tsx
@@ -1,6 +1,8 @@
+import { type FC, useCallback, useState } from 'react';
+
import { type LightningElement, useFocus } from '@plextv/react-lightning';
import { Column, Row } from '@plextv/react-lightning-components';
-import { type FC, useCallback, useState } from 'react';
+
import Button from '../components/Button';
const TEXT_WIDTH = 1076;
diff --git a/apps/react-lightning-example/src/pages/PosterPage.tsx b/apps/react-lightning-example/src/pages/PosterPage.tsx
index 955294d..fe15f7e 100644
--- a/apps/react-lightning-example/src/pages/PosterPage.tsx
+++ b/apps/react-lightning-example/src/pages/PosterPage.tsx
@@ -1,5 +1,7 @@
-import { Column, Row } from '@plextv/react-lightning-components';
import type { FC } from 'react';
+
+import { Column, Row } from '@plextv/react-lightning-components';
+
import Button from '../components/Button';
export const PosterPage: FC = () => {
diff --git a/apps/react-lightning-example/src/pages/ShaderPage.tsx b/apps/react-lightning-example/src/pages/ShaderPage.tsx
index fa9ebcd..9f77184 100644
--- a/apps/react-lightning-example/src/pages/ShaderPage.tsx
+++ b/apps/react-lightning-example/src/pages/ShaderPage.tsx
@@ -1,6 +1,7 @@
-import { Column } from '@plextv/react-lightning-components';
import type { FC } from 'react';
+import { Column } from '@plextv/react-lightning-components';
+
export const ShaderPage: FC = () => {
return (
{
return (
{
((props) => {
+ return (
+
+
+
+
+ );
+});
+
+const Header = () => (
+
+ VirtualList Header
+
+);
+
+const HorizontalHeader = () => (
+
+
+ Header
+
+
+);
+
+const HorizontalFooter = () => (
+
+
+ Footer
+
+
+);
+
+const Footer = () => (
+
+ End of List
+
+);
+
+const Separator = () => ;
+
+const Separator2 = () => ;
+
+export const VirtualListPage = () => {
+ const items = Array.from({ length: 40 }, (_, i) => `Item ${i}`);
+ const horizontalItems = Array.from({ length: 40 }, (_, i) => `H-${i}`);
+
+ return (
+
+ {
+ layout.size = Math.round(index % 3 === 0 ? 75 * 1.25 : 75);
+ }}
+ renderItem={({ index, item }) => (
+
+ {item}
+
+ )}
+ />
+
+ {
+ layout.size = Math.round(index % 3 === 0 ? 50 * 1.5 : 50);
+ }}
+ renderItem={({ index, item }) => (
+
+ {item}
+
+ )}
+ />
+
+ );
+};
diff --git a/apps/react-lightning-example/src/shaders/MyCustomTexture.ts b/apps/react-lightning-example/src/shaders/MyCustomTexture.ts
index c412315..d70c373 100644
--- a/apps/react-lightning-example/src/shaders/MyCustomTexture.ts
+++ b/apps/react-lightning-example/src/shaders/MyCustomTexture.ts
@@ -1,8 +1,4 @@
-import {
- type CoreTextureManager,
- Texture,
- type TextureData,
-} from '@lightningjs/renderer';
+import { type CoreTextureManager, Texture, type TextureData } from '@lightningjs/renderer';
/**
* Augment the EffectMap interface to include the CustomEffect
@@ -68,9 +64,7 @@ export class MyCustomTexture extends Texture {
return false; // <-- Don't cache at all
}
- static override resolveDefaults(
- props: MyCustomTextureProps,
- ): Required {
+ static override resolveDefaults(props: MyCustomTextureProps): Required {
return {
percent: props.percent ?? 20,
w: props.w,
diff --git a/apps/react-lightning-example/tsconfig.json b/apps/react-lightning-example/tsconfig.json
index 5001905..fea3f73 100644
--- a/apps/react-lightning-example/tsconfig.json
+++ b/apps/react-lightning-example/tsconfig.json
@@ -2,7 +2,6 @@
"extends": "@repo/configs/tsconfig.react-library.json",
"compilerOptions": {
"isolatedDeclarations": false,
- "outDir": "dist",
"types": ["node", "vite/client"]
},
"include": ["src"],
diff --git a/apps/react-lightning-example/vite.config.mjs b/apps/react-lightning-example/vite.config.mjs
index 8a4cd7d..35c79e7 100644
--- a/apps/react-lightning-example/vite.config.mjs
+++ b/apps/react-lightning-example/vite.config.mjs
@@ -1,17 +1,20 @@
-import fontGen from '@plextv/vite-plugin-msdf-fontgen';
+import babel from '@rolldown/plugin-babel';
import legacy from '@vitejs/plugin-legacy';
-import react from '@vitejs/plugin-react';
-import tsconfigPaths from 'vite-tsconfig-paths';
+import react, { reactCompilerPreset } from '@vitejs/plugin-react';
+
+import fontGen from '@plextv/vite-plugin-msdf-fontgen';
/**
* @type {import('vite').InlineConfig}
*/
const config = {
plugins: [
- tsconfigPaths({
- skip: (dir) => dir.includes('app-template'),
- }),
react(),
+ // React Compiler. @vitejs/plugin-react v6 dropped its built-in Babel
+ // pipeline in favour of oxc, so the legacy `react({ babel: { plugins: [...] } })`
+ // config is silently ignored. The compiler runs through @rolldown/plugin-babel
+ // with the preset exposed by @vitejs/plugin-react.
+ babel({ presets: [reactCompilerPreset()] }),
fontGen({
inputs: [
{
diff --git a/apps/react-native-lightning-example/package.json b/apps/react-native-lightning-example/package.json
index 4439a15..bc07e2f 100644
--- a/apps/react-native-lightning-example/package.json
+++ b/apps/react-native-lightning-example/package.json
@@ -1,19 +1,18 @@
{
"name": "@plextv/react-native-lightning-example",
- "description": "Sample implementation of @plextv/react-native-lightning in a React Native app",
"version": "0.4.0",
- "author": "Plex Inc.",
+ "private": true,
+ "description": "Sample implementation of @plextv/react-native-lightning in a React Native app",
+ "bugs": {
+ "url": "https://github.com/plexinc/react-lightning/issues/new"
+ },
"license": "MIT",
+ "author": "Plex Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/plexinc/react-lightning.git"
},
- "bugs": {
- "url": "https://github.com/plexinc/react-lightning/issues/new"
- },
- "packageManager": "pnpm@10.12.3",
"type": "module",
- "private": true,
"scripts": {
"build": "vite build",
"clean": "del ./dist",
@@ -23,38 +22,44 @@
"test:unit": "vitest run --passWithNoTests"
},
"dependencies": {
- "@lightningjs/renderer": "3.0.0-beta20",
+ "@lightningjs/renderer": "catalog:apps",
"@plextv/react-lightning": "workspace:*",
"@plextv/react-lightning-components": "workspace:*",
+ "@plextv/react-lightning-plugin-css-transform": "workspace:*",
"@plextv/react-lightning-plugin-flexbox": "workspace:*",
"@plextv/react-lightning-plugin-reanimated": "workspace:*",
"@plextv/react-native-lightning": "workspace:*",
"@plextv/react-native-lightning-components": "workspace:*",
- "@react-navigation/native": "7.1.28",
- "react": "19.2.3",
- "react-dom": "19.2.3",
- "react-native": "0.82.1",
- "react-native-reanimated": "4.2.1"
+ "@react-navigation/native": "7.2.2",
+ "react": "catalog:apps",
+ "react-dom": "catalog:apps",
+ "react-native": "catalog:apps",
+ "react-native-reanimated": "catalog:apps",
+ "react-native-worklets": "0.8.1"
},
"devDependencies": {
"@plextv/vite-plugin-msdf-fontgen": "workspace:*",
"@plextv/vite-plugin-react-native-lightning": "workspace:*",
"@plextv/vite-plugin-react-reanimated-lightning": "workspace:*",
"@repo/configs": "workspace:*",
- "@shopify/flash-list": "2.2.0",
- "@types/react": "19.2.8",
- "@types/react-dom": "19.2.3",
- "@vitejs/plugin-legacy": "7.2.1",
- "typescript": "5.9.3"
+ "@rolldown/plugin-babel": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-legacy": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "babel-plugin-react-compiler": "catalog:",
+ "typescript": "6.0.2"
},
"volta": {
"extends": "../../package.json"
},
+ "packageManager": "pnpm@10.12.3",
"//": "unsure why react-dom needed to be ignored...",
"depcheck": {
"ignoreMatches": [
"typescript",
- "react-dom"
+ "react-dom",
+ "babel-plugin-react-compiler"
]
}
}
diff --git a/apps/react-native-lightning-example/src/ErrorBoundary.tsx b/apps/react-native-lightning-example/src/ErrorBoundary.tsx
index 1a26c9f..f5991c7 100644
--- a/apps/react-native-lightning-example/src/ErrorBoundary.tsx
+++ b/apps/react-native-lightning-example/src/ErrorBoundary.tsx
@@ -1,3 +1,4 @@
+import type { ErrorInfo } from 'react';
import { Component, type ReactNode } from 'react';
interface Props {
@@ -20,7 +21,7 @@ class ErrorBoundary extends Component {
return { hasError: true };
}
- public componentDidCatch(error: Error, info: React.ErrorInfo): void {
+ public componentDidCatch(error: Error, info: ErrorInfo): void {
console.error(error, info.componentStack);
}
diff --git a/apps/react-native-lightning-example/src/components/ScrollItem.tsx b/apps/react-native-lightning-example/src/components/ScrollItem.tsx
index a955236..2a325da 100644
--- a/apps/react-native-lightning-example/src/components/ScrollItem.tsx
+++ b/apps/react-native-lightning-example/src/components/ScrollItem.tsx
@@ -1,8 +1,9 @@
-import { focusable } from '@plextv/react-lightning';
-import { View } from '@plextv/react-native-lightning';
import { type ReactNode, useEffect, useMemo } from 'react';
import { type ColorValue, Text } from 'react-native';
+import { focusable } from '@plextv/react-lightning';
+import { View } from '@plextv/react-native-lightning';
+
export type ScrollItemProps = {
children: ReactNode;
index: number;
@@ -14,10 +15,7 @@ export type ScrollItemProps = {
};
const ScrollItem = focusable(
- (
- { color, altColor, image, index, horizontal, focused, children, onFocused },
- ref,
- ) => {
+ ({ color, altColor, image, index, horizontal, focused, children, onFocused }, ref) => {
useEffect(() => {
if (focused) {
onFocused(index);
diff --git a/apps/react-native-lightning-example/src/index.tsx b/apps/react-native-lightning-example/src/index.tsx
index 3b8d157..32a4a68 100644
--- a/apps/react-native-lightning-example/src/index.tsx
+++ b/apps/react-native-lightning-example/src/index.tsx
@@ -1,7 +1,3 @@
-import { Canvas } from '@plextv/react-lightning';
-import { Column, Row } from '@plextv/react-lightning-components';
-import '@plextv/react-lightning-plugin-flexbox/jsx';
-import { getReactNativePlugins } from '@plextv/react-native-lightning';
import type { LinkingOptions } from '@react-navigation/native';
import {
createNavigatorFactory,
@@ -11,24 +7,26 @@ import {
useNavigation,
useNavigationBuilder,
} from '@react-navigation/native';
+
+import '@plextv/react-lightning-plugin-flexbox/jsx';
import { createRoot } from 'react-dom/client';
import { Button } from 'react-native';
+
+import { Column, Row } from '@plextv/react-lightning-components';
+import { getReactNativePlugins, NativeCanvas } from '@plextv/react-native-lightning';
+
import { ErrorBoundary } from './ErrorBoundary';
import { keyMap } from './keyMap';
import { AnimationBuilderTest } from './pages/AnimationBuilderTest';
import { AnimationTest } from './pages/AnimationTest';
import { ComponentTest } from './pages/ComponentTest';
-import { FlashListTest } from './pages/FlashListTest';
import { LayoutTest } from './pages/LayoutTest';
import { LibraryTest } from './pages/LibraryTest';
import { SimpleTest } from './pages/SimpleTest';
import { VirtualizedListTest } from './pages/VirtualizedListTest';
function CustomNavigator(props: Parameters[1]) {
- const { state, descriptors, NavigationContent } = useNavigationBuilder(
- StackRouter,
- props,
- );
+ const { state, descriptors, NavigationContent } = useNavigationBuilder(StackRouter, props);
const focusedRoute = state.routes[state.index];
@@ -60,7 +58,6 @@ const screens = {
Components: 'components',
NestedLayouts: 'nestedLayouts',
VirtualizedList: 'virtualizedList',
- FlashList: 'flashList',
};
const linking: LinkingOptions
-
+
{
>
-
+
-
-
+
@@ -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 49c08df..51c287f 100644
--- a/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/AnimationBuilderTest.tsx
@@ -1,5 +1,3 @@
-import { Button } from '@plextv/react-native-lightning';
-import { Column, Row } from '@plextv/react-native-lightning-components';
import { type FC, useMemo, useState } from 'react';
import Animated, {
type EntryOrExitLayoutType,
@@ -23,6 +21,9 @@ import Animated, {
SlideOutUp as SlideOutUpBuilder,
} from 'react-native-reanimated';
+import { Button } from '@plextv/react-native-lightning';
+import { Column, Row } from '@plextv/react-native-lightning-components';
+
type AnimatedProps = {
visible: boolean;
entering?: EntryOrExitLayoutType;
diff --git a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
index 64f511c..2a9635c 100644
--- a/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
+++ b/apps/react-native-lightning-example/src/pages/AnimationTest.tsx
@@ -55,11 +55,7 @@ const AnimationTest: FC = () => {
/>
-
+
>
);
diff --git a/apps/react-native-lightning-example/src/pages/ComponentTest.tsx b/apps/react-native-lightning-example/src/pages/ComponentTest.tsx
index facd31a..c008043 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 33a2cac..0000000
--- 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 0d08253..14098f9 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 8cb3a11..a32d687 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 235b40e..20b6aa2 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 42e3367..8c59086 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 2e49e8e..5b4ce0b 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 996e397..9677c64 100644
--- a/apps/react-native-lightning-example/tsconfig.json
+++ b/apps/react-native-lightning-example/tsconfig.json
@@ -2,8 +2,7 @@
"extends": "@repo/configs/tsconfig.react-native-library.json",
"compilerOptions": {
"jsx": "react-native",
- "isolatedDeclarations": false,
- "outDir": "dist"
+ "isolatedDeclarations": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
diff --git a/apps/react-native-lightning-example/vite.config.mjs b/apps/react-native-lightning-example/vite.config.mjs
index bec1630..287b525 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 4c99540..19a54d6 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 af63bc9..1fc3251 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 02c2aa7..3d1761b 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 e71a5a9..f351ace 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 38d7605..b40fb7e 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 fc7c3e3..56ad31a 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 73b3a9e..f7572f4 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 ffcbc98..af5aeec 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 53d621a..42e909b 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 780dd8e..a0eec25 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 afc543d..8ba1d5a 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 beec158..2f4cd10 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 6655a07..c4e4a4d 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 9efdd14..66f9b8e 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 1e31f21..c237291 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 ea66e2f..227795a 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 6b11eac..2c805c7 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 1440eaa..96c4c27 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 a8c1381..deef517 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 91f5657..e79571b 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 887f229..03f6271 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 5b6ed1f..bcc0c51 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 ce550e9..a22684c 100644
--- a/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/FocusRedirection.stories.tsx
@@ -1,11 +1,13 @@
+import type { Meta, StoryFn } from '@storybook/react-vite';
+import { forwardRef, type ReactNode, useState } from 'react';
+
import {
type LightningElement,
type LightningElementStyle,
useCombinedRef,
useFocus,
} from '@plextv/react-lightning';
-import type { Meta, StoryFn } from '@storybook/react-vite';
-import { forwardRef, type ReactNode, useState } from 'react';
+
import { FocusableImage } from '../../../components/FocusableImage';
export default {
@@ -52,37 +54,19 @@ export const FocusRedirect: StoryFn = () => {
return (
<>
-
-
+
+
-
+
-
+
-
-
+
+
{
FocusRedirect.args = {
label: 'Focus Redirection Example',
- description:
- 'This example demonstrates how to redirect focus between elements.',
+ description: 'This example demonstrates how to redirect focus between elements.',
};
diff --git a/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx
index ef08809..3495078 100644
--- a/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/FocusTree.stories.tsx
@@ -1,5 +1,7 @@
-import { Column, Row } from '@plextv/react-lightning-components';
import type { Meta } from '@storybook/react-vite';
+
+import { Column, Row } from '@plextv/react-lightning-components';
+
import Button from '../../../components/Button';
export default {
@@ -23,17 +25,9 @@ export const SimpleFocusTree = () => {
return (
-
+
-
+
Top-B
@@ -42,11 +36,7 @@ export const SimpleFocusTree = () => {
-
+
diff --git a/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx b/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx
index 7204228..068f951 100644
--- a/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/focus/TrapFocus.stories.tsx
@@ -1,10 +1,9 @@
+import type { Meta } from '@storybook/react-vite';
+
import { FocusGroup } from '@plextv/react-lightning';
import { Column, Row } from '@plextv/react-lightning-components';
-import type { Meta } from '@storybook/react-vite';
-import {
- FocusableImage,
- type FocusableImageProps,
-} from '../../../components/FocusableImage';
+
+import { FocusableImage, type FocusableImageProps } from '../../../components/FocusableImage';
export default {
title: 'react-lightning/Examples/Focus/TrapFocus',
@@ -66,14 +65,7 @@ export const TrapFocus = () => {
<>
-
+
diff --git a/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx b/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx
index 3083866..84f1dd5 100644
--- a/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/render/RenderToTexture.stories.tsx
@@ -40,10 +40,7 @@ const content = (
);
export const WithRtt = ({ rtt }: Props) => (
-
+
{content}
);
@@ -53,10 +50,7 @@ WithRtt.args = {
};
export const WithoutRtt = ({ rtt }: Props) => (
-
+
{content}
);
diff --git a/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx b/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx
index cfc7054..2ab2e25 100644
--- a/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx
+++ b/apps/storybook/src/react-lightning/examples/text/Text.stories.tsx
@@ -1,10 +1,9 @@
import type { ITextNode } from '@lightningjs/renderer';
-import type { LightningTextElementProps } from '@plextv/react-lightning';
import type { Meta } from '@storybook/react-vite';
-import {
- DefaultStoryHeight,
- DefaultStoryWidth,
-} from '../../../helpers/constants';
+
+import type { LightningTextElementProps } from '@plextv/react-lightning';
+
+import { DefaultStoryHeight, DefaultStoryWidth } from '../../../helpers/constants';
type Props = {
text: string;
diff --git a/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx b/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx
index c607646..a210f14 100644
--- a/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx
+++ b/apps/storybook/src/react-native-lightning-components/layout/Column.stories.tsx
@@ -1,5 +1,7 @@
-import { Column } from '@plextv/react-native-lightning-components';
import type { Meta } from '@storybook/react-vite';
+
+import { Column } from '@plextv/react-native-lightning-components';
+
import {
ColorPalette,
DefaultStoryHeight,
@@ -50,9 +52,7 @@ export default {
tags: ['reactNative'],
} as Meta;
-export const FlexStart = ({
- justifyContent = FlexTypes.FLEX_START,
-}: ColumnLayoutProps) => {
+export const FlexStart = ({ justifyContent = FlexTypes.FLEX_START }: ColumnLayoutProps) => {
return (
{
+export const FlexEnd = ({ justifyContent = FlexTypes.FLEX_END }: ColumnLayoutProps) => {
return (
{
+export const SpaceEvenly = ({ justifyContent = FlexTypes.SPACE_EVENLY }: ColumnLayoutProps) => {
return (
{
+export const SpaceBetween = ({ justifyContent = FlexTypes.SPACE_BETWEEN }: ColumnLayoutProps) => {
return (
{
+export const SpaceAround = ({ justifyContent = FlexTypes.SPACE_AROUND }: ColumnLayoutProps) => {
return (
;
// The rest of the story definitions
-export const FlexStart = ({
- justifyContent = FlexTypes.FLEX_START,
-}: RowLayoutProps) => {
+export const FlexStart = ({ justifyContent = FlexTypes.FLEX_START }: RowLayoutProps) => {
return (
{
+export const FlexEnd = ({ justifyContent = FlexTypes.FLEX_END }: RowLayoutProps) => {
return (
{
+export const SpaceEvenly = ({ justifyContent = FlexTypes.SPACE_EVENLY }: RowLayoutProps) => {
return (
{
+export const SpaceBetween = ({ justifyContent = FlexTypes.SPACE_BETWEEN }: RowLayoutProps) => {
return (
{
+export const SpaceAround = ({ justifyContent = FlexTypes.SPACE_AROUND }: RowLayoutProps) => {
return (
;
-
-export const FlashListExample = () => {
- const buttons = new Array(50).fill(null).map((_, i) => `Flash Button ${i}`);
- const verticalRef = useRef>(null);
- const horizontalRef = useRef>(null);
-
- const handleVerticalFocus = useCallback((index: number) => {
- verticalRef.current?.scrollToIndex({ index, viewPosition: 0.5 });
- }, []);
-
- const handleHorizontalFocus = useCallback((index: number) => {
- horizontalRef.current?.scrollToIndex({ index, viewPosition: 0.5 });
- }, []);
-
- return (
-
-
- (
-
- {item}
-
- )}
- drawDistance={50}
- />
-
-
-
- (
-
- {item}
-
- )}
- drawDistance={50}
- />
-
-
- );
-};
diff --git a/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx b/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx
index 0333e1b..8cc8467 100644
--- a/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx
+++ b/apps/storybook/src/react-native-lightning/lists/VirtualizedList.stories.tsx
@@ -1,6 +1,7 @@
import type { Meta } from '@storybook/react-vite';
-import { createRef, useCallback } from 'react';
-import { View, VirtualizedList } from 'react-native';
+import { createRef } from 'react';
+import { VirtualizedList } from 'react-native';
+
import ScrollItem from '../../components/ScrollItem';
export default {
@@ -9,49 +10,27 @@ export default {
tags: ['reactNative'],
} as Meta;
-const getItem = (_: string[], index: number) => `Button ${index}`;
-const ITEM_WIDTH = 100;
-const ITEM_HEIGHT = 50;
-
export const VirtualizedListTest = () => {
- const ref = createRef>();
-
- const handleFocus = useCallback(
- (index: number) => {
- ref.current?.scrollToIndex({ index, viewPosition: 0.5 });
- },
- [ref.current],
- );
+ const ref = createRef>();
+ const data = Array.from({ length: 5000 }, (_, i) => ({
+ text: `Button ${i}`,
+ isImage: Math.random() < 0.5,
+ }));
return (
-
- 5000}
- getItemLayout={(_, index) => ({
- index,
- length: ITEM_HEIGHT,
- offset: index * ITEM_HEIGHT,
- })}
- keyExtractor={(item) => item}
- windowSize={2}
- renderItem={({ index, item }) => (
-
- {item}
-
- )}
- />
-
+
+ ref={ref}
+ data={data}
+ removeClippedSubviews={true}
+ snapToAlignment="center"
+ initialNumToRender={20}
+ keyExtractor={(item) => item.text}
+ windowSize={2}
+ renderItem={({ index, item }) => (
+
+ {item.text}
+
+ )}
+ />
);
};
diff --git a/apps/storybook/vite.config.mjs b/apps/storybook/vite.config.mjs
index 55b0f49..c32f0f4 100644
--- a/apps/storybook/vite.config.mjs
+++ b/apps/storybook/vite.config.mjs
@@ -1,7 +1,10 @@
+import babel from '@rolldown/plugin-babel';
+import { reactCompilerPreset } from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
import fontGen from '@plextv/vite-plugin-msdf-fontgen';
import reactNativeLightningPlugin from '@plextv/vite-plugin-react-native-lightning';
import reactReanimatedLightningPlugin from '@plextv/vite-plugin-react-reanimated-lightning';
-import { defineConfig } from 'vite';
/**
* @type {import('vite').InlineConfig}
@@ -10,14 +13,16 @@ const config = defineConfig((env) => ({
base: './',
define: {
- __DEV__: JSON.stringify(
- (env.mode ?? process.env.NODE_ENV) !== 'production',
- ),
+ __DEV__: JSON.stringify((env.mode ?? process.env.NODE_ENV) !== 'production'),
'process.env.NODE_ENV': JSON.stringify(env.mode),
},
plugins: [
reactNativeLightningPlugin(),
+ // React Compiler. @vitejs/plugin-react v6 uses oxc and ignores any
+ // `babel` option, so the compiler runs through @rolldown/plugin-babel
+ // with the preset exported by @vitejs/plugin-react.
+ babel({ presets: [reactCompilerPreset()] }),
reactReanimatedLightningPlugin(),
fontGen({
inputs: [
@@ -31,6 +36,13 @@ const config = defineConfig((env) => ({
}),
],
+ optimizeDeps: {
+ // plugin-flexbox uses a ?worker&inline Vite import that rolldown's
+ // dep optimizer can't resolve. Exclude it so Vite handles it via its
+ // normal transform pipeline instead.
+ exclude: ['@plextv/react-lightning-plugin-flexbox'],
+ },
+
server: {
port: 3333,
host: true,
diff --git a/biome.json b/biome.json
deleted file mode 100644
index 998503f..0000000
--- a/biome.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
- "root": true,
- "files": {
- "includes": [
- "**/*.ts",
- "**/*.tsx",
- "**/*.js",
- "**/*.jsx",
- "**/*.json",
- "!.vscode",
- "!.changeset",
- "!**/public"
- ],
- "ignoreUnknown": true
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true,
- "correctness": {
- "noUnusedImports": {
- "level": "error",
- "fix": "safe"
- }
- },
- "a11y": {
- "noStaticElementInteractions": "off",
- "useAltText": "off",
- "useKeyWithMouseEvents": "off",
- "useKeyWithClickEvents": "off"
- }
- }
- },
- "assist": {
- "enabled": true
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "space",
- "formatWithErrors": true
- },
- "javascript": {
- "linter": {
- "enabled": true
- },
- "formatter": {
- "indentStyle": "space",
- "quoteStyle": "single"
- }
- },
- "vcs": {
- "enabled": true,
- "clientKind": "git",
- "defaultBranch": "main",
- "useIgnoreFile": true
- }
-}
diff --git a/package.json b/package.json
index 2e9ef03..fa1dc5e 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,16 @@
{
"name": "react-lightning",
+ "private": true,
"description": "React reconciler for rendering React apps with Lightning.js",
- "author": "Plex Inc.",
+ "bugs": {
+ "url": "https://github.com/plexinc/react-lightning/issues/new"
+ },
"license": "MIT",
+ "author": "Plex Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/plexinc/react-lightning.git"
},
- "bugs": {
- "url": "https://github.com/plexinc/react-lightning/issues/new"
- },
- "private": true,
"type": "module",
"scripts": {
"build": "turbo build",
@@ -23,8 +23,8 @@
"ci:publish": "pnpm run build && pnpm exec changeset publish",
"ci:version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile",
"dev": "turbo dev",
- "lint": "biome check",
- "lint:format": "biome check --write",
+ "lint": "oxlint",
+ "lint:format": "oxlint --fix --fix-suggestions && oxfmt",
"nuke": "pnpm run clean && pnpx npkill -x -y -D && pnpm install",
"test": "pnpm run test:unit",
"test:unit": "turbo test:unit",
@@ -32,26 +32,27 @@
"prepare": "husky"
},
"devDependencies": {
- "@biomejs/biome": "2.3.11",
- "@changesets/cli": "2.29.8",
+ "@changesets/cli": "2.30.0",
"@repo/configs": "workspace:*",
- "@types/node": "25.0.9",
+ "@types/node": "25.6.0",
"del-cli": "7.0.0",
"depcheck": "1.4.7",
- "glob": "13.0.0",
+ "glob": "13.0.6",
"husky": "9.1.7",
- "listr2": "10.0.0",
- "tsdown": "0.19.0",
+ "listr2": "10.2.1",
+ "oxfmt": "0.45.0",
+ "oxlint": "1.60.0",
+ "oxlint-tsgolint": "0.20.0",
+ "tsdown": "0.21.8",
"tsx": "4.21.0",
- "turbo": "2.7.5",
- "type-fest": "5.4.1",
- "typescript": "5.9.3",
- "vite": "7.3.1",
+ "turbo": "2.9.6",
+ "type-fest": "5.5.0",
+ "typescript": "6.0.2",
+ "vite": "8.0.8",
"vite-plugin-externalize-deps": "0.10.0",
- "vitest": "4.0.17",
- "yaml": "2.8.2"
+ "vitest": "4.1.4",
+ "yaml": "2.8.3"
},
- "packageManager": "pnpm@10.12.3",
"engines": {
"node": ">=22"
},
@@ -59,6 +60,7 @@
"node": "24.11.1",
"pnpm": "10.12.3"
},
+ "packageManager": "pnpm@10.12.3",
"depcheck": {
"ignoreMatches": [
"del-cli",
diff --git a/packages/configs/package.json b/packages/configs/package.json
index 67f1c61..7138774 100644
--- a/packages/configs/package.json
+++ b/packages/configs/package.json
@@ -2,8 +2,8 @@
"name": "@repo/configs",
"version": "0.0.2",
"private": true,
- "author": "Plex Inc.",
"license": "MIT",
+ "author": "Plex Inc.",
"type": "module",
"exports": {
"./tsconfig.json": "./tsconfig.json",
@@ -15,7 +15,16 @@
"./vite.config": "./vite.config.mjs"
},
"scripts": {},
+ "devDependencies": {
+ "@rollup/plugin-babel": "7.0.0",
+ "babel-plugin-react-compiler": "catalog:"
+ },
"volta": {
"extends": "../../package.json"
+ },
+ "depcheck": {
+ "ignoreMatches": [
+ "babel-plugin-react-compiler"
+ ]
}
}
diff --git a/packages/configs/tsdown.config.ts b/packages/configs/tsdown.config.ts
index 55169b3..fb1cd54 100644
--- a/packages/configs/tsdown.config.ts
+++ b/packages/configs/tsdown.config.ts
@@ -1,3 +1,4 @@
+import pluginBabel from '@rollup/plugin-babel';
import { defineConfig, type UserConfig } from 'tsdown';
const config: UserConfig = defineConfig({
@@ -20,6 +21,17 @@ const config: UserConfig = defineConfig({
resolveNewUrlToAsset: true,
},
},
+ plugins: [
+ pluginBabel({
+ babelHelpers: 'bundled',
+ parserOpts: {
+ sourceType: 'module',
+ plugins: ['jsx', 'typescript'],
+ },
+ plugins: ['babel-plugin-react-compiler'],
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
+ }),
+ ],
});
export default config;
diff --git a/packages/configs/tsdown.node.config.ts b/packages/configs/tsdown.node.config.ts
index b618c35..06bafb0 100644
--- a/packages/configs/tsdown.node.config.ts
+++ b/packages/configs/tsdown.node.config.ts
@@ -1,4 +1,5 @@
import { defineConfig, type UserConfig } from 'tsdown';
+
// @ts-expect-error: Needed for unrun to resolve this module correctly
import baseConfig from './tsdown.config.ts';
@@ -7,7 +8,9 @@ const config: UserConfig = defineConfig({
format: 'esm',
target: 'node22',
platform: 'node',
- external: [/^node:.*/],
+ deps: {
+ neverBundle: [/^node:.*/],
+ },
exports: {
devExports: false,
},
diff --git a/packages/configs/tsdown.withExports.config.ts b/packages/configs/tsdown.withExports.config.ts
index 66d8c91..bb40d6e 100644
--- a/packages/configs/tsdown.withExports.config.ts
+++ b/packages/configs/tsdown.withExports.config.ts
@@ -1,4 +1,5 @@
import { defineConfig, type UserConfig } from 'tsdown';
+
// @ts-expect-error: Needed for unrun to resolve this module correctly
import baseConfig from './tsdown.config.ts';
@@ -11,9 +12,7 @@ const config: UserConfig = defineConfig({
// Remove 'exports/' prefix from export paths
return Object.entries(pkg).reduce(
(acc, [key, value]) => {
- const newKey = key.startsWith('./exports/')
- ? key.replace('./exports/', './')
- : key;
+ const newKey = key.startsWith('./exports/') ? key.replace('./exports/', './') : key;
acc[newKey] = value;
diff --git a/packages/plugin-css-transform/package.json b/packages/plugin-css-transform/package.json
index a827314..7636101 100644
--- a/packages/plugin-css-transform/package.json
+++ b/packages/plugin-css-transform/package.json
@@ -1,16 +1,19 @@
{
"name": "@plextv/react-lightning-plugin-css-transform",
- "description": "Transforms CSS properties to lightning properties. Requires @plextv/react-lightning-plugin-flexbox",
"version": "0.4.0",
- "author": "Plex Inc.",
+ "description": "Transforms CSS properties to lightning properties. Requires @plextv/react-lightning-plugin-flexbox",
+ "bugs": {
+ "url": "https://github.com/plexinc/react-lightning/issues/new"
+ },
"license": "MIT",
+ "author": "Plex Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/plexinc/react-lightning.git"
},
- "bugs": {
- "url": "https://github.com/plexinc/react-lightning/issues/new"
- },
+ "files": [
+ "dist"
+ ],
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -21,16 +24,16 @@
"./jsx": "./src/types/jsx.d.ts"
},
"publishConfig": {
- "provenance": true,
"access": "public",
"exports": {
".": {
- "require": "./dist/index.cjs",
- "import": "./dist/index.js"
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
},
"./package.json": "./package.json",
"./jsx": "./dist/types/jsx.d.ts"
- }
+ },
+ "provenance": true
},
"scripts": {
"build": "tsdown --config-loader unrun",
@@ -39,21 +42,21 @@
"check:types": "tsc --noEmit",
"test:unit": "vitest run --passWithNoTests"
},
- "files": [
- "dist"
- ],
"devDependencies": {
"@repo/configs": "workspace:*",
- "@types/react": "19.2.8",
+ "@types/react": "catalog:",
"csstype": "3.2.3"
},
"peerDependencies": {
- "@lightningjs/renderer": "3.0.0-beta20",
+ "@lightningjs/renderer": "catalog:",
"@plextv/react-lightning": "workspace:^",
"@plextv/react-lightning-plugin-flexbox": "workspace:^",
- "react-native": "^0.82.1"
+ "react-native": "catalog:"
},
"volta": {
"extends": "../../package.json"
+ },
+ "inlinedDependencies": {
+ "csstype": "3.2.3"
}
}
diff --git a/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts b/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts
index 27c79e6..c4f2557 100644
--- a/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts
+++ b/packages/plugin-css-transform/src/convertCSSStyleToLightning.ts
@@ -1,7 +1,5 @@
-import type {
- LightningElementStyle,
- LightningTextElementStyle,
-} from '@plextv/react-lightning';
+import type { LightningElementStyle, LightningTextElementStyle } from '@plextv/react-lightning';
+
import type { AllStyleProps } from './types/ReactStyle';
import { flattenStyles } from './utils/flattenStyles';
import { htmlColorToLightningColor } from './utils/htmlColorToLightningColor';
@@ -49,8 +47,7 @@ export function convertCSSStyleToLightning(
}
if (shadowColor != null) {
- (finalStyle as LightningTextElementStyle).shadowColor =
- htmlColorToLightningColor(shadowColor);
+ (finalStyle as LightningTextElementStyle).shadowColor = htmlColorToLightningColor(shadowColor);
}
if (border != null || borderWidth != null || borderColor != null) {
@@ -103,9 +100,7 @@ export function convertCSSStyleToLightning(
if (otherStyles.top != null) {
finalStyle.y =
- typeof otherStyles.top === 'number'
- ? otherStyles.top
- : Number.parseInt(otherStyles.top, 10);
+ typeof otherStyles.top === 'number' ? otherStyles.top : Number.parseInt(otherStyles.top, 10);
}
if (fontWeight != null) {
@@ -116,8 +111,7 @@ export function convertCSSStyleToLightning(
}
if (transform != null) {
- const { scaleX, scaleY, rotation, ...translateTransforms } =
- parseTransform(transform);
+ const { scaleX, scaleY, rotation, ...translateTransforms } = parseTransform(transform);
if (scaleX != null) {
finalStyle.scaleX = scaleX;
@@ -135,11 +129,7 @@ export function convertCSSStyleToLightning(
}
// Disabled for now as some components set overflow to hidden while not having their size correctly calculated
- if (
- overflow === 'hidden' ||
- overflowX === 'hidden' ||
- overflowY === 'hidden'
- ) {
+ if (overflow === 'hidden' || overflowX === 'hidden' || overflowY === 'hidden') {
finalStyle.clipping = true;
}
diff --git a/packages/plugin-css-transform/src/index.ts b/packages/plugin-css-transform/src/index.ts
index 9c3941b..9d545c2 100644
--- a/packages/plugin-css-transform/src/index.ts
+++ b/packages/plugin-css-transform/src/index.ts
@@ -1,4 +1,5 @@
import type { Plugin } from '@plextv/react-lightning';
+
import { convertCSSStyleToLightning } from './convertCSSStyleToLightning';
import type { AllStyleProps } from './types/ReactStyle';
@@ -9,8 +10,31 @@ export { flattenStyles } from './utils/flattenStyles';
export { htmlColorToLightningColor } from './utils/htmlColorToLightningColor';
export { parseTransform } from './utils/parseTransform';
+const CSS_HANDLED_STYLE_PROPS: ReadonlySet = new Set([
+ 'backgroundColor',
+ 'color',
+ 'border',
+ 'borderWidth',
+ 'borderColor',
+ 'shadowColor',
+ 'opacity',
+ 'overflow',
+ 'overflowX',
+ 'overflowY',
+ 'tintColor',
+ 'fontWeight',
+ 'transform',
+ 'width',
+ 'height',
+ 'left',
+ 'top',
+ 'display',
+]);
+
export function plugin(): Plugin {
return {
+ handledStyleProps: CSS_HANDLED_STYLE_PROPS,
+
transformProps(_instance, props) {
if (!('style' in props)) {
return props;
diff --git a/packages/plugin-css-transform/src/types/Node.ts b/packages/plugin-css-transform/src/types/Node.ts
index 31bad68..f4fa833 100644
--- a/packages/plugin-css-transform/src/types/Node.ts
+++ b/packages/plugin-css-transform/src/types/Node.ts
@@ -1,4 +1,5 @@
import type { INodeProps } from '@lightningjs/renderer';
+
import type { LightningElement, RendererNode } from '@plextv/react-lightning';
export type RendererNodeWithCore = RendererNode & {
diff --git a/packages/plugin-css-transform/src/types/ReactStyle.ts b/packages/plugin-css-transform/src/types/ReactStyle.ts
index eef916d..b88f0ba 100644
--- a/packages/plugin-css-transform/src/types/ReactStyle.ts
+++ b/packages/plugin-css-transform/src/types/ReactStyle.ts
@@ -1,10 +1,11 @@
+import type { StandardProperties as CSSProperties } from 'csstype';
+import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
+
import type {
LightningImageElementStyle,
LightningTextElementStyle,
LightningViewElementStyle,
} from '@plextv/react-lightning';
-import type { StandardProperties as CSSProperties } from 'csstype';
-import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
export type AllStyles = Partial<
ViewStyle &
diff --git a/packages/plugin-css-transform/src/types/jsx.d.ts b/packages/plugin-css-transform/src/types/jsx.d.ts
index 31a86e5..df207c9 100644
--- a/packages/plugin-css-transform/src/types/jsx.d.ts
+++ b/packages/plugin-css-transform/src/types/jsx.d.ts
@@ -3,6 +3,7 @@ import type {
LightningTextElementProps,
LightningViewElementProps,
} from '@plextv/react-lightning';
+
import type { AllStyleProps } from './ReactStyle';
declare module 'react' {
diff --git a/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts b/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts
index 6f7b526..dc7cbfc 100644
--- a/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts
+++ b/packages/plugin-css-transform/src/utils/convertCSSTransformToLightning.ts
@@ -1,4 +1,5 @@
import type { Transform } from '@plextv/react-lightning-plugin-flexbox';
+
import { convertRotationValue } from './convertRotationValue';
function getValue(
@@ -42,19 +43,13 @@ function getXYValue(
export function convertCSSTransformToLightning(
transformType: string,
- transformValue:
- | string
- | number
- | number[]
- | Record,
+ transformValue: string | number | number[] | Record,
): Transform {
const transformResult: Transform = {};
if (typeof transformValue === 'object') {
for (const key in transformValue) {
- const value = (
- transformValue as Record
- )[key];
+ const value = (transformValue as Record)[key];
if (value) {
const result = convertCSSTransformToLightning(key, value);
@@ -97,11 +92,7 @@ export function convertCSSTransformToLightning(
break;
case 'rotate':
case 'rotation':
- transformResult.rotation = getValue(
- transformValue,
- 0,
- convertRotationValue,
- );
+ transformResult.rotation = getValue(transformValue, 0, convertRotationValue);
break;
}
diff --git a/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts b/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts
index f6497c1..f4aa208 100644
--- a/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts
+++ b/packages/plugin-css-transform/src/utils/convertRotationValue.spec.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
+
import { convertRotationValue } from './convertRotationValue';
describe('convertRotationValue', () => {
diff --git a/packages/plugin-css-transform/src/utils/flattenStyles.ts b/packages/plugin-css-transform/src/utils/flattenStyles.ts
index 51649ce..4a9f68d 100644
--- a/packages/plugin-css-transform/src/utils/flattenStyles.ts
+++ b/packages/plugin-css-transform/src/utils/flattenStyles.ts
@@ -1,4 +1,5 @@
import type { StyleProp } from 'react-native';
+
import type { AllStyleProps, AllStyles } from '../types/ReactStyle';
export function flattenStyles(styles: AllStyleProps): T {
@@ -11,6 +12,7 @@ export function flattenStyles(styles: AllStyleProps): T {
return JSON.parse(styles);
} catch (err) {
console.warn('There was an error parsing the style: ', styles, '\n', err);
+
return {} as T;
}
}
diff --git a/packages/plugin-css-transform/src/utils/fromCssUnit.ts b/packages/plugin-css-transform/src/utils/fromCssUnit.ts
index 6c97f1d..35332d4 100644
--- a/packages/plugin-css-transform/src/utils/fromCssUnit.ts
+++ b/packages/plugin-css-transform/src/utils/fromCssUnit.ts
@@ -1,6 +1,7 @@
-import type { DimensionValue } from '@plextv/react-lightning-plugin-flexbox';
import type { Animated } from 'react-native';
+import type { DimensionValue } from '@plextv/react-lightning-plugin-flexbox';
+
const unitRegex = /^(\d+)(px|vw|vh|%)?$/i;
/**
@@ -16,6 +17,7 @@ export function fromCssUnit(
if (typeof value === 'object') {
// TODO: Support animated nodes
console.warn('[fromCssUnit] Unsupported css unit:', value);
+
return;
}
diff --git a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts
index e2e9b71..53865b1 100644
--- a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts
+++ b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.test.ts
@@ -1,5 +1,6 @@
import type { ColorValue } from 'react-native';
import { describe, expect, it } from 'vitest';
+
import { htmlColorToLightningColor } from './htmlColorToLightningColor';
describe('htmlColorToLightningColor', () => {
diff --git a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts
index e83ce71..59e97d3 100644
--- a/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts
+++ b/packages/plugin-css-transform/src/utils/htmlColorToLightningColor.ts
@@ -1,23 +1,18 @@
import type { ColorValue } from 'react-native';
+
import { htmlColorCodes } from './htmlColorCodes';
const hexRgbRegex = /^#?([a-f0-9]{6})$/i;
const hexShortRgbRegex = /^#?([a-f0-9]{3})$/i;
-const rgbRegex =
- /^rgba?\(([0-9.]+)[, ]+([0-9.]+)[, ]+([0-9.]+)[, ]*([0-9.]+)?\)$/i;
+const rgbRegex = /^rgba?\(([0-9.]+)[, ]+([0-9.]+)[, ]+([0-9.]+)[, ]*([0-9.]+)?\)$/i;
-function withAlphaOverride(
- color: number,
- overrideAlpha?: number | string,
-): number {
+function withAlphaOverride(color: number, overrideAlpha?: number | string): number {
if (overrideAlpha == null) {
return color;
}
const alphaInt =
- typeof overrideAlpha === 'string'
- ? Number.parseInt(overrideAlpha, 16)
- : overrideAlpha;
+ typeof overrideAlpha === 'string' ? Number.parseInt(overrideAlpha, 16) : overrideAlpha;
// Create a bitmask for the alpha value
const alphaMask = 0xffffff00 | alphaInt;
@@ -48,13 +43,7 @@ export function htmlColorToLightningColor(
const rgbResult = rgbRegex.exec(colorLower);
if (rgbResult) {
- const parts = rgbResult.slice() as [
- string,
- string,
- string,
- string,
- string?,
- ];
+ const parts = rgbResult.slice() as [string, string, string, string, string?];
const rgbColor =
((Number.parseInt(parts[1], 10) << 24) >>> 0) +
@@ -68,10 +57,7 @@ export function htmlColorToLightningColor(
const hexRgbResult = hexRgbRegex.exec(colorLower);
if (hexRgbResult?.[1]) {
- return withAlphaOverride(
- Number.parseInt(`${hexRgbResult[1]}ff`, 16),
- overrideAlpha,
- );
+ return withAlphaOverride(Number.parseInt(`${hexRgbResult[1]}ff`, 16), overrideAlpha);
}
const hexShortRgbResult = hexShortRgbRegex.exec(colorLower);
@@ -83,7 +69,5 @@ export function htmlColorToLightningColor(
return withAlphaOverride(Number.parseInt(rgbText, 16), overrideAlpha);
}
- throw new Error(
- `Invalid hex value specified for conversion: ${color.toString()}`,
- );
+ throw new Error(`Invalid hex value specified for conversion: ${color.toString()}`);
}
diff --git a/packages/plugin-css-transform/src/utils/parseTransform.ts b/packages/plugin-css-transform/src/utils/parseTransform.ts
index 5bbc972..e4b0d25 100644
--- a/packages/plugin-css-transform/src/utils/parseTransform.ts
+++ b/packages/plugin-css-transform/src/utils/parseTransform.ts
@@ -1,11 +1,10 @@
import type { Transform } from '@plextv/react-lightning-plugin-flexbox';
+
import { convertCSSTransformToLightning } from './convertCSSTransformToLightning';
const transformPartRegex = /(\w+)\(([^)]+)\)/g;
-export function parseTransform(
- transform?: string | object | Array